Compare commits

..
Author SHA1 Message Date
Ricki Hirner 0c991288d5 Bump version to 4.3.6.1 2023-09-07 00:29:39 +02:00
Ricki Hirner f1a1d0efd8 Fix bug where sync isn't possible as soon there's any connection without INTERNET (bitfireAT/davx5#370)
Fix bug where sync wasn't possible as soon there's any connection without INTERNET (bitfireAT/davx5#369)
2023-09-07 00:29:23 +02:00
Ricki Hirner 56deab70ee Version bump to 4.3.6 2023-09-06 16:50:09 +02:00
Sunik KupferandRicki Hirner 5640250359 Wait an appropriate delay before SyncWorker retries after soft errors (bitfireAT/davx5#337)
* Use appropriate delayUntil value for retrying syncs on 503s.

Crop server suggested retryAfter value to self defined min/max values and use a reasonable default value if non-existent.

* Add tests for getDelayUntil

* Wait appropriate delay, before retrying sync after a soft error happened

* Increase max and default sync delays after soft errors

* Increase initial backoff time for SyncWorker retries

* Minor getDelayUntil changes

* Minor changes

- store delayUntil in seconds
- pass duration instead of timestamp to Thread.sleep
- other minor changes

* Use Instant instead of Long timestamps

* Correct calculation of blocking duration

* Indicate soft error occurred on 503 server message

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-09-05 20:30:52 +02:00
Ricki Hirner b670979f12 Fetch translations from Transifex 2023-09-05 20:30:30 +02:00
Ricki Hirner 251cf1b2e9 Update dependencies (including vcard4android), bump version to 4.3.6-rc.1 2023-09-05 10:42:33 +02:00
Ricki HirnerandArnau Mora 4ad54cd28b Update dependencies (including cert4android and vcard4android) (bitfireAT/davx5#360)
* Update dependencies (including cert4android and vcard4android)

* Migrated to new version of ical4android

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

* Increased compileSdk

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

* Upgraded browser

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

* Only use `appInForeground` for `customCertsUi`

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

* Removed unnecessary variable and fixed trust manager

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

* Cleaned up trust manager factory

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

* Update dependencies (including cert4android and vcard4android)

* Migrated to new version of ical4android

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

* Increased compileSdk

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

* Upgraded browser

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

* Only use `appInForeground` for `customCertsUi`

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

* Removed unnecessary variable and fixed trust manager

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

* Cleaned up trust manager factory

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

* Minor changes

* Fixed build for SDK 34

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

* Migrated certificate trusting

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

* NetworkConfigProvider: handle invalid trusted certificate

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Arnau Mora <arnyminerz@proton.me>
2023-09-05 10:41:54 +02:00
Sunik KupferandRicki Hirner 1c419cd75c Add setting to ignore VPNs at connection detection (bitfireAT/davx5#356)
* Add setting to ignore VPNs at connection detection

* Minor changes

- move methods to ConnectionUtils to keep SyncWorker class compact
- always use "ignore VPNs" as Boolean
- other minor changes

* Show ignore VPNs setting only below api lvl 23

* Change strings

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-09-05 10:38:38 +02:00
Arnau Mora 62cca2939a Upgrade AppIntro to 7.0.0-beta02 (bitfireAT/davx5#357)
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2023-09-05 10:37:16 +02:00
Arnau Mora 795ae49da4 SyncWorker "soft error (max retries reached)" notification confusing for users (bitfireAT/davx5#353)
* Sync error notification dismiss

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

* FIXME

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

* Added click intent

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

* Delayed error info

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

* Added tag for max retries

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

* Reduced priority

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

* Removed max retries tag

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

* Using account name as tag

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

* Added authority to notification tag

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

* Added account type to notification tag

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

* Changed priority to min

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

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2023-09-05 10:37:08 +02:00
Arnau Mora bd7b2714d2 Upgrade AGP to 8.1.1 (bitfireAT/davx5#355)
* Upgrade AGP to 8.1.1

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

* Upgrade Kotlin and dependencies

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

* Downgrade browser

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

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2023-09-05 10:37:00 +02:00
Ricki Hirner 273deecbe4 Update dependencies 2023-08-16 22:59:20 +02:00
a375a16edf Added tooltip to sync collections FAB (bitfireAT/davx5#340)
* Added tooltip to sync collections fab

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

* Greyscale tint for collections sync FAB

* Always use TooltipCompat for FAB tooltips

Closes bitfireAT/davx5#339

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Sunik Kupfer <kupfer@bitfire.at>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-08-15 18:23:27 +02:00
Sunik KupferandRicki Hirner 4ce33e949b Use AccountManager.setAndVerifyUserData extension method, instead of setUserData (bitfireAT/davx5#344)
Hopefully fixes bitfireAT/davx5#308

* Add new setAndVerifyUserData extension function to AccountManager
* Use new setAndVerifyUserData extension function instead of insecure setUserData
* Update KDoc [skip CI]

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-08-15 18:23:00 +02:00
Ricki Hirner fcb9d7f560 Fetch translations from Transifex 2023-08-07 20:45:55 +02:00
Ricki Hirner 3709c2ab32 Version bump to 4.3.5.2 2023-08-07 20:44:00 +02:00
Ricki Hirner 8aeb2d5064 Fix onPerformSync crash on InterruptedException (bitfireAT/davx5#343)
- use Kotlin and CompletableDeferred instead of Java synchronization
- signal cancellation by completing CompletableDeferred instead of Thread.currentThread.interrupt()
2023-08-07 20:43:51 +02:00
Arnau MoraandRicki Hirner f806122b00 Upgrade AGP to 8.1 and configure per app language (bitfireAT/davx5#338)
* Upgraded AGP

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

* Enabled automatic locale config generation

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

* Added fallback language

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

* Added legacy service

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

* Added `Accept-Language` header to custom tabs

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

* Nextcloud Login Flow/Google OAuth: also send language tag for default locale

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-07-28 10:53:57 +02:00
Ricki Hirner 4f192c253d Use KSP for Room (bitfireAT/davx5#333) 2023-07-27 18:20:44 +02:00
Ricki Hirner 7038bbf70a Google OAuth: fix manifest merging so that intent-filter for OAuth callback is included again (#367) 2023-07-27 18:13:57 +02:00
Ricki Hirner b7e00fac40 Fetch translations from Transifex 2023-07-22 12:23:44 +02:00
Ricki Hirner 13d9ce089b Bump version to 4.3.5.1 2023-07-22 12:22:28 +02:00
Ricki Hirner d74e8da5c1 Update dependencies 2023-07-22 12:22:26 +02:00
Arnau MoraandRicki Hirner a2b633edc0 Updated Google login button (bitfireAT/davx5#332)
* Updated Google login button

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

* Moved Google G logo file

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

* Changed background color

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

* Amend warning sign [skip CI]

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-07-22 12:22:22 +02:00
Ricki Hirner e6125dd644 Update README, add Mastodon button [skip CI] 2023-07-16 12:14:48 +02:00
Ricki Hirner 162c9effe0 Update to latest dav4jvm (uses Java 8 Time API) (bitfireAT/davx5#329)
Fixes WebDAV file timestamps always being null (see bitfireAT/dav4jvm#22)
2023-07-14 23:11:44 +02:00
c62874a34b Make "Refresh collection list" more visible (bitfireAT/davx5#266)
* Added refresh collections fab

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

* Added listener for clicks on refresh collections fab

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

* Adjusted sizing

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

* Updated tooltip and description for collections sync

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

* Removed Snackbar

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

* Added warning for null serviceId

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

* Changed refresh collections service id fetching method

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

* Tooltip updates on refresh collections list

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

* Migrate to ViewPager2; show "Refresh collections" for WebCal, too

* Added refresh collections fab

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

* Added listener for clicks on refresh collections fab

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

* Adjusted sizing

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

* Updated tooltip and description for collections sync

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

* Removed Snackbar

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

* Added warning for null serviceId

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

* Changed refresh collections service id fetching method

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

* Tooltip updates on refresh collections list

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

* Migrate to ViewPager2; show "Refresh collections" for WebCal, too

* Changed collections refresh action update method

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

* Use lambda syntax for observers

---------

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
Co-authored-by: Sunik Kupfer <kupfer@bitfire.at>
2023-07-14 23:11:32 +02:00
Ricki Hirner 36e377001f Clean up unused resources
- also fixes some wrong resources
2023-07-11 11:47:16 +02:00
Ricki Hirner 400d46fce2 Amend comments for repository identity 2023-07-10 15:22:24 +02:00
Ricki Hirner ec34da1ace Google Login: add Limited Use policy (bitfireAT/davx5#327) 2023-07-10 15:14:21 +02:00
Ricki Hirner bb6ed7ec64 Fetch translations from Transifex 2023-07-10 15:13:23 +02:00
Ricki Hirner 042b2371fb Version bump to 4.3.5 2023-07-10 15:12:04 +02:00
Ricki Hirner 5272943a05 Google: use new API base URL (bitfireAT/davx5#324)
Google: use new API base URL (finds secondary calendars and homesets)
2023-07-07 11:30:22 +02:00
Sunik KupferandRicki Hirner 318939e970 Ensure internet connection before syncing (bitfireAT/davx5#305)
* Comment out failing test

* Check internet connection before syncing for API 23+

* [Skip CI] Amend comments

* Comment out whole test class

* Comment out whole test class

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-07-06 23:45:08 +02:00
Ricki Hirner 1478306e54 Comment out failing test 2023-07-06 23:45:06 +02:00
Sunik KupferandRicki Hirner c7ff42c03f [Google OAuth] fix Re-authentication crash (bitfireAT/davx5#319)
* Hide/Show auth settings instead of removing them prevents NPE

* Provide original OAuth login email as default value for GoogleLoginFragment started from account settings

* Account settings: explicitly trim empty user name to null

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-07-06 23:45:01 +02:00
Ricki Hirner 17ccb305f9 Configuration detection: allow null Credentials (anonymous login) (bitfireAT/davx5#323) 2023-07-06 23:44:11 +02:00
Sunik Kupfer 37a7ebb9c0 Fix test again (bitfireAT/davx5#321) 2023-07-06 23:44:08 +02:00
Sunik Kupfer 1c403d171c Fix test (bitfireAT/davx5#320) 2023-07-06 23:44:00 +02:00
Arnau MoraandRicki Hirner 81273f028e Added known base urls (bitfireAT/davx5#315)
* Added known base urls

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

* Change order

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-07-03 12:07:57 +02:00
Sunik KupferandRicki Hirner fac68680ee Block sync adapter's onPerformSync until SyncWorker finishes (bitfireAT/davx5#278)
* Block sync framework until SyncWorker finishes

* Bump version code for 4.3.3 (previous version code was never released publicly)

* Fetch translations from Transifex

* Release internal version automatically [skip ci]

* Update periodic sync workers when "Sync only on WiFi" flag is changed (#282)

* Update periodic sync workers when "sync only on WiFi" flag is changed
* Remove BootCompletedReceiver which was only needed to repair sync intervals (not required with WorkManager anymore)

* Bump version code to 403030006 (stays 4.3.3)

* Use unique worker name, Java notify/wait and observeForever

* Remove observer when sync finished

* Catch and ignore, but log interruption exceptions

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-07-03 12:07:39 +02:00
Ricki Hirner 46488f1618 Version bump to 4.3.5-alpha.1; update Compose; move GoogleLoginFragment to fix build 2023-06-30 13:51:09 +02:00
Sunik KupferandRicki Hirner 0f92b0fb05 291 clean up oauth mess (bitfireAT/davx5#307)
* Move GoogleOAuth members into GoogleLoginFragment

* Require login flow capable browser and notify user if missing

* Receive AppAuth redirects only in standard and gplay flavor

* Set davx5 as user-agent for AppAuth connection builder

* Re-authentication in Account settings

* Catch unauthorized exceptions at collection refresh and notify user to re-authenticate

* Suggest email address on account creation

* Set contact groups default setting as per-contact categories for oauth logins

* Add authentication to debug info, minor other changes

* Better error handling; don't pre-set group type

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-06-30 12:53:16 +02:00
Ricki Hirner 6a2c366358 Update dependencies; CI: don't checkout submodules 2023-06-28 22:42:24 +02:00
Arnau MoraandRicki Hirner 4ba50b969a Migrated to ical4android library (bitfireAT/davx5#302)
* Migrated to ical4android library

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

* Manually excluded Groovy

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

* Updated ical4android

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

* Update dependencies

* Added more exclusions

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

* Also update Room [skip CI]

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-06-28 22:30:53 +02:00
Michael Biebl 473dca62fa Shorten version hash of vcard4android library (#345)
Looks better in AboutLibraries and is more consistent with cert4android
and dav4jvm.
2023-06-28 12:53:43 +02:00
Arnau MoraandRicki Hirner 11c8866614 Security: check/fix DebugInfoActivity: Uncontrolled data used in path expression (bitfireAT/davx5#267)
* Passing file name to `EXTRA_LOG_FILE` instead of path. Replaces illegal chars

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

* Removed log file extra

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

* Removed log file extra

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

* Added more error logging

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

* Update KDoc, remove strings

* Moved ViewModel initialization to Factory

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

* Changed ViewModel factory

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

* Updated annotations

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

* Handle EXTRA_LOGS correctly

---------

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-06-28 12:52:57 +02:00
Arnau Mora 3ba463dbd4 Removed Twitter from navigation drawer (bitfireAT/davx5#312)
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
2023-06-26 08:53:19 +02:00
Arnau Mora 837fcc7805 Removed Twitter links (#351) 2023-06-25 14:11:24 +02:00
Arnau MoraandRicki Hirner f820e92d15 Integrate vcard4android as an android library (bitfireAT/davx5#300)
* Migrated to external dependency for vcard4android

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

* Removed vcard4android submodule

* Update vcard4android version, remove from settings.gradle

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-06-14 14:21:10 +02:00
Ricki Hirner 1b521e3c17 Bump version code 2023-06-13 21:01:10 +02:00
Ricki Hirner 50cb223bb6 DavResourceFinder: make input arguments explicit (bitfireAT/davx5#298) 2023-06-13 21:00:56 +02:00
Ricki Hirner 764c382af3 Fetch translations from Transifex 2023-06-13 18:47:08 +02:00
Ricki Hirner aed721409f Bump version to 4.3.4.1 2023-06-13 18:43:06 +02:00
Ricki Hirner 24a3954d75 Google OAuth: fix endless loop in Fragment (bitfireAT/davx5#295)
Fix endless loop
2023-06-13 18:40:49 +02:00
Sunik KupferandRicki Hirner ba36d01e11 [Google OAuth] Support custom Client IDs (bitfireAT/davx5#294)
* Support custom Client IDs

* Refactoring

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-06-13 18:39:40 +02:00
Ricki Hirner ff4af11bcf Minor HttpClient change 2023-06-11 14:30:48 +02:00
Ricki Hirner 66c93414c0 Fetch translations from Transifex 2023-06-11 14:30:24 +02:00
Ricki Hirner 278171dbac Bump version to 4.3.4 2023-06-11 14:29:28 +02:00
Ricki Hirner 5b38943205 Support OAuth for Google (bitfireAT/davx5#289)
* Proof of concept (without auth state storage)
* Implement Bearer authentication, create network package
* Properly create/dispose AuthService
* Use proper ActivityResultContract
* Integrate into default login activity
* Change client ID to davx5integration@gmail.com
* Google Login: adapt login view
* Fix tests
* Don't allow empty Google account
* Move strings to resources
2023-06-11 14:17:50 +02:00
Sunik KupferandRicki Hirner 1b3fde0854 280 fix problematic toasts (bitfireAT/davx5#283)
* When renaming show a toast when account with new name exists already

* Replace toasts with snackbars

* Apply code styling hints

* Move lambdas out of parentheses

* Inject application instead of context

* Replace Toast with Snackbar

* Shorten messages shown in snackbars

* Show Toast in UI instead of model; use AndroidViewModel

* Move account name check out of lock; don't close activity if not successful

* Duplicate account name check: only check for our account type

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-06-09 17:26:11 +02:00
Sunik KupferandRicki Hirner caf7be5e11 New setting to preselect only personal collections (bitfireAT/davx5#276)
* Add new preselect_collections option and deprecate sync_all_collections option

* Optimize imports

* At refresh, decide on whether a collection should be preselected

* Add preselect_collections_blacklist setting and restriction

* Adhere to preselect_collections_blacklist setting

* Add preselect_collections values

* Also check for empty regex string and use new setting values

* Add unit tests

* Remove sync_all_collections setting and restriction

* Blacklist nextclouds recently contacted addressbook in restriction and setting by default

* Improve kdoc

* KDoc, changed setting names, minor code optimizations

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-06-06 21:03:50 +02:00
Ricki Hirner 3b2246e74b Bump version code to 403030006 (stays 4.3.3) 2023-06-01 19:12:47 +02:00
Ricki Hirner 12902a051f Update periodic sync workers when "Sync only on WiFi" flag is changed (bitfireAT/davx5#282)
* Update periodic sync workers when "sync only on WiFi" flag is changed
* Remove BootCompletedReceiver which was only needed to repair sync intervals (not required with WorkManager anymore)
2023-06-01 19:12:41 +02:00
Ricki Hirner bed762ab7d Fetch translations from Transifex 2023-06-01 12:31:23 +02:00
Ricki Hirner b56dd96fd8 Bump version code for 4.3.3 (previous version code was never released publicly) 2023-06-01 12:30:11 +02:00
jgirardet cb5f722a87 Add mailo.com to known base urls (#329)
* Add mailo.com to known base urls

* fix order
2023-05-31 09:51:04 +02:00
Ricki Hirner dceb1493eb Remove cert4android from settings.gradle, remove obsolete adb-log.sh, update ical4android 2023-05-30 17:09:37 +02:00
Arnau MoraandRicki Hirner 17718a7ed8 Integrate cert4android as an android library (bitfireAT/davx5#274)
* Removed `cert4android` submodule

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

* Added cert4android dependency

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

* Update dependencies, fix invalid style

---------

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-05-30 11:10:06 +02:00
Arnau Mora 6d278b7e04 Made collection url selectable (bitfireAT/davx5#275)
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
2023-05-23 17:02:33 +02:00
Sunik Kupfer ae303e3e8d Refactor queryHomeSets method for clarity (bitfireAT/davx5#270)
* Make it clear that the first call of recursive method queryHomeSets is for the current-user-principal home set

* Update whitespace in kdoc

* Update kdoc
2023-05-23 17:02:26 +02:00
Ricki Hirner 38cfd200b1 Fetch translations from Transifex 2023-05-22 08:27:33 +02:00
Ricki Hirner 968db83366 Version bump to 4.3.3 2023-05-22 08:26:46 +02:00
Ricki Hirner 8923f9b1ad Fix workflow concurrency 2023-05-17 22:15:49 +02:00
Ricki Hirner e60723042b Cancel running actions for previous commits if there are new pushes on the same branch/tag 2023-05-17 18:24:15 +02:00
Ricki Hirner 5fee2375a5 Update dependencies, bump version to 4.3.3-rc.1 2023-05-17 18:13:27 +02:00
Ricki Hirner 7f62148c75 Disable configuration cache for release builds (closes bitfireAT/davx5#263) (bitfireAT/davx5#269) 2023-05-17 18:13:27 +02:00
Sunik KupferandRicki Hirner a072cf0404 Add last synced time to collection properties (bitfireAT/davx5#260)
* Add last synced time to collection properties

* Show last synced time for every to the collection relevant authority

* Try finding the application name for given package name

* Resolve authority to package name before finding application label

* Use AndroidViewModel instead of ViewModel; change model LiveData to val

* Rewrite to Compose, use relative time description

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-05-17 18:13:27 +02:00
Ricki Hirner 5aff37a9a6 Add style guide 2023-05-17 18:13:27 +02:00
Ricki Hirner 2f1757bb78 Version bump to 4.3.3-beta.2
- minor workflow changes
2023-05-17 18:13:27 +02:00
Ricki Hirner 34cc3ac670 Update ical4android (increase minimum jtx Board version) and vcard4android (fixes NoYearOfEra bug) 2023-05-17 18:13:27 +02:00
Ricki Hirner 55ee89529f Update dependencies, including cert4android and vcard4android 2023-05-17 18:07:17 +02:00
Ricki Hirner 83b5b939e1 Add issue template file so that we can enable issues for ourselves 2023-05-12 11:25:37 +02:00
Ricki Hirner b543dc9332 Fetch translations from Transifex 2023-05-12 10:50:46 +02:00
Ricki Hirner 28466ace46 Version bump to 4.3.3-beta.1 2023-05-12 10:49:38 +02:00
Sunik KupferandRicki Hirner 63f51c4b42 Account settings migration 13 to 14 (bitfireAT/davx5#262)
* AccountSettings: prevent redundant calls, move migrations to separate class

* Don't create AccountSettings while migrating

* Double thread sleep time to wait for sync framework to get disabled, and update logging

* Remove manual testing helper

* Fix tests

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-05-12 10:23:47 +02:00
Ricki Hirner 8ac8971293 Update dav4jvm (WebDAV timestamps), ical4android (jtx geofence x radius), Github workflow actions 2023-05-12 00:14:05 +02:00
Arnau MoraandRicki Hirner 9748b75671 Fixed toast threading (bitfireAT/davx5#256)
* Fixed toast threading

* Added `AndroidViewModel.context`

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

* Moved to `AndroidViewModel`

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

* Changed error invocation

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

* Added back constructor inject

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

* Save Application with val instead of util method

* Use LiveData for error message

---------

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-05-11 18:41:07 +02:00
Sunik KupferandRicki Hirner 2ebb40b152 Implement proper network check before manual sync (bitfireAT/davx5#259)
* Implement proper network check before manual sync

Do a proper connectivity check, to show a message to the user, about the sync being scheduled for when connectivity is available, before enqueueing the SyncWorker.

* Update the sync status in accounts adapter to include pending syncs

This is relevant for when a manual sync is triggered without connectivity.

* Replace Toast by Snackbar

* Replace ViewModel by AndroidViewModel

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-05-11 18:40:29 +02:00
Ricki Hirner 5cbf14fa27 Update ical4android to fix TZ with negative DST (Google Europe/Dublin) bug 2023-05-10 13:21:12 +02:00
Ricki Hirner 2ff4b35bdb Use configuration cache to improve build speed (closes bitfireAT/davx5#248) (bitfireAT/davx5#255)
- disable per-app language preference for now (will be enabled again with bitfireAT/davx5#245)
- disable Jetifier (doesn't seem to be explicitly required for in-app rating API anymore)
- remove Locator dependency
- update ical4android
2023-05-10 13:20:57 +02:00
Ricki Hirner 7b2ffeb98e Update dependencies 2023-05-09 12:36:45 +02:00
Ricki Hirner 9917f7131a Comment out AccountSettingsTest for now because they cause strange failures 2023-05-07 08:03:21 +02:00
Ricki Hirner 1c474df606 Version bump to 4.3.3-alpha.1 2023-05-07 08:03:21 +02:00
Ricki Hirner c3cd970581 Use Jetpack Compose for AboutLibraries (bitfireAT/davx5#246)
- add JetPack Compose dependencies
- use existing XML theme as Compose MdcTheme
- update AboutLibraries to newest version (uses Compose)
- embed AboutLibraries Compose into existing Fragment layout
2023-05-07 08:03:21 +02:00
Ricki Hirner 0bed00c05a Handle InvalidAccountException in SyncAdapterService 2023-05-07 08:03:21 +02:00
Sunik KupferandRicki Hirner f9d6bb153c Workmanager scheduled sync (bitfireAT/davx5#216)
Closes bitfireAT/davx5#14

* extract WorkManager util functions and kdoc
* add PeriodicSyncWorker
* refactor and some kdoc
* use PeriodicSyncWorker and add todos
* allow SyncAdapter arguments to be passed to SyncWorker and have re-synchronization use SyncWorker
* handle sync cancellation in AccountSetting migration
* handle sync cancellation when account is renamed
* remove sync frameworks global sync setting and sync state awareness for the GUI
* Observe sync worker status in AccountListFragment
* Create setup for stepwise refactor of the sync adapters. This should keep the app from failing to build.
* Create new abstract BaseSyncer class for generic sync code. Adapt consumers and tests to use the new class.
* Move calender sync code to new CalenderSyncer class which is independent from the sync framework
* In CalendarSyncAdapterService pass sync requests from Sync Adapter Framework to SyncWorker
* Use CalenderSyncer in SyncWorker
* Move contacts sync code to new ContactsSyncer class.
* Move address book sync code to new AddressBookSyncer class
* Move jtx sync code to new JtxSyncer class
* Move tasks sync code to new TaskSyncer class
* Remove duplicate code in inherited sync adapters.
* Remove refactoring helper interface, duplicate generic sync code and some linting.
* Adapt tests for new Syncer class and move to the new package
* Remove remaining duplicate code in SyncAdapterService, add todos and edit kdoc.
* Move all the single line sync adapter services into one file.
* Remove concurrent sync runner code and its test, as we now use WorkManagers one time work requests.
* Remove SAF manual sync flag usage where unnecessary.
* Drop ability to prioritise collections for sync, as not used and hindering removal of sync adapter arguments.
* Pass simple string array to SyncWorker instead of bundle to simplify code.
* Restructure work query code.
* Get debug info from account settings and WorkManager.
* Write tests for PeriodicSyncWorker
* Test account creation will set a default sync interval for CardDAV and CalDAV
* Throw an exception if accountManager returns null for sync interval value
* Do proper interval check and add tests for AccountSettings
* Use work manager query to determine whether work is in a specific state
* [WIP] Add test to check that task provider is configured correctly on account creation
* Edit test checking that task sync is configured correctly on account creation with/without installed task app(s), by mocking TaskUtil
* Edit test such that it does not require a flaky flag
* Remove periodic sync when tasks app is uninstalled
* Bring back content triggered syncs
* Rename enqueueSyncWorker method to enqueue only for clarity
* Enable SyncAdapterFramework to cancel running SyncWorker
* Add test ensuring that SyncWorker.onStopped() interrupts the running sync thread
* Add retry policy sync on soft errors
* Check users sync conditions before enqueueing SyncWorker
* Add test for whether user sync conditions are treated correctly and kdoc
* Rename ambiguous shorthand "SAF" to "SyncFramework", as SAF usually means StorageAccessFramework
* Migration: Disable sync framework periodic syncs when interval is changed for specified authority
* Add Workers info to debug info
* Use WorkInfo.runAttemptCount to fail work after 20 soft errors
* Notify user if retry limit for soft errors has been reached
* Remove left over concurrency sync run tests prevention
* Migration: Continue to remove periodic sync framework syncs until user migration to PeriodicSyncWorker syncs is complete
* Kdoc and small changes
* Migration: Change to hard migration strategy
* Drop repairSyncIntervals method in favor of hard migration strategy
* Improve debug info of workers
* Remove sync framework periodic syncs, created by enabling content triggered syncs
* Change minimum sync interval to 15 min; minor other changes
* Fix tests
* Implement requested changes and update kdoc
* Add network connectivity restrictions to PeriodicSyncWorker
* Minor changes
* Move back sync classes to syncadapter package for now (can be separated later)
* Add KDoc
* Changes from review
* Rename test methods
* Add back global sync status warning

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-05-07 08:03:21 +02:00
Ricki Hirner 299fb27af4 Fetch translations from Transifex 2023-05-02 14:06:43 +02:00
Ricki Hirner 9c51dadce8 Version bump to 4.3.2 2023-05-02 13:57:31 +02:00
Ricki Hirner ae3f5c8bab Fix ProGuard rules for R8 (AGP 8.0) 2023-05-01 11:00:37 +02:00
Ricki Hirner aa063adaba Upgrade gradle, AGP, dependencies 2023-04-29 22:50:18 +02:00
Sunik KupferandRicki Hirner c5b02d2bde Log warning if principal URL does not provide CalDAV/CardDAV service (bitfireAT/davx5#242)
* Log warning if principal URL does not provide CalDAV/CardDAV service

* Unified log messages for identical cases

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-04-20 14:01:30 +02:00
Arnau Mora 61b6cd5e2e Disabled GPS feature requirement (bitfireAT/davx5#241)
* Disabled GPS feature requirement

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Updated GPS feature comment

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

---------

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>
2023-04-18 11:11:04 +02:00
Ricki Hirner aecabb9845 Update dependencies 2023-04-13 11:25:51 +02:00
Michael Biebl 16573e9ea6 Sync interval failure (#235)
* Fix log message

* Log a warning if repairing the sync interval failed
2023-04-12 11:41:24 +02:00
Ricki Hirner e2bfda71bc Use large runners for CI Android tests (#306) 2023-04-12 11:39:32 +02:00
Ricki Hirner 133d155b94 Update ical4android; CI: use gradle/gradle-build-action for caching 2023-03-31 16:12:20 +02:00
Sunik Kupfer 874ad7b1c6 Handle error responses when querying principals for display names (closes bitfireAT/#237) (bitfireAT/#238)
* Add test with inaccessible principal

* Catch and log exceptions where server resource is inaccessible

* Catch and log every HttpException
2023-03-31 13:29:20 +02:00
Sunik KupferandRicki Hirner fc17d0ed69 SettingsActivity resync now uses SyncWorker (bitfireAT/davx5#226)
* Alter the SyncWorker sync request to allow passing sync arguments along

* Request sync in SettingsActivity via SyncWorker instead of ContentResolver

* Don't use catch-all arguments as worker arguments

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-03-28 16:59:36 +02:00
Ricki Hirner 2f8340a646 SyncManager: fix logging string that always talked about address data even for events/tasks 2023-03-24 11:52:19 +01:00
Ricki Hirner 6050396a45 Update Transformations to new syntax (androidx.lifecycle:lifecycle-viewmodel-ktx:2.6) 2023-03-24 10:46:42 +01:00
Ricki Hirner 2503d92dca Update dependencies, including ical4android (now ignores invalid properties)
Closes bitfireAT/davx5#165
2023-03-23 17:09:37 +01:00
Ricki Hirner 983a142670 Add CodeQL security scanning (#296)
* Create .github/workflows/codeql.yml

* Build explicitly instead of using autobuild (because it doesn't work)

* Checkout submodules and cache Java dependencies
2023-03-23 11:29:29 +01:00
Arnau MoraandRicki Hirner a3a8317b2f Fix fetching list of translators (bitfireAT/davx5#232)
* Changed `password` with `token`

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Fetch script: don't use --minimum-perc 20 because it's in .tx/config

---------

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-03-21 15:46:58 +01:00
Ricki Hirner 880abf024c Fetch translations from Transifex 2023-03-20 13:10:34 +01:00
Ricki Hirner 83936b97ea Bump version to 4.3.1.1 2023-03-20 12:58:10 +01:00
Ricki Hirner 50dbd66d3a Version bump to 4.3.1 2023-03-13 18:01:20 +01:00
Arnau MoraandRicki Hirner 17fb1168cc Generate locales with Locator (bitfireAT/davx5#218)
* Added locator plugin

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Removed old generator

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Updated source sets

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Added locator submodule

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Removed locator submodule

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Moving scripts

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Update dependencies

* Added `LocatorPlugin`

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Fixed locales generation

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Removed buildSrc

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Using plugin from Jitpack

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Using plugin from Gradle

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Removed comment

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Remove jitpack

---------

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-03-13 18:01:13 +01:00
Ricki Hirner 89a698e7b2 Bump version to 4.3.1-beta.1 2023-03-02 13:11:09 +01:00
Ricki Hirner 6dd0af2d7a Update Kotlin and gradle plugin 2023-03-02 13:11:08 +01:00
Patrick Lang 29a97c85ff Fixed language selector icon for night mode (bitfireAT/davx5#223)
Update ic_language.xml

Replaced tint by  android:tint="?attr/colorControlNormal">
2023-03-02 13:11:01 +01:00
Sunik KupferandRicki Hirner 9fee782968 Check sync workers properly (closes bitfireAT/davx5#217) (bitfireAT/davx5#220)
* check sync workers properly

* Move utils around

* Merge live data, instead of observing in view, to recalculate account list.

* Update LiveData with same value

* Remove unused context property.

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-02-28 10:28:52 +01:00
Sunik KupferandPatrick Lang fe7a928064 Provide owner url in jtx collection (bitfireAT/davx5#221)
* provide owner url in jtx collection

* Updated contract, added owner displayname to collections sync

* Update ical4android

---------

Co-authored-by: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com>
2023-02-28 10:28:46 +01:00
Ricki Hirner 54c6117ba2 Update dependencies 2023-02-28 10:28:29 +01:00
Arnau Mora fffb029362 Fixed missing languages and wrong display (bitfireAT/davx5#214)
* Displaying language with `displayName` instead of `displayLanguage`

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Added languages from `davdroid`

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Fixed wrong locale tags

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Using Map instead of list for language loading

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Typo

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Changed language loading logic

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Improved docs

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Moved method from `LangUtils`

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Using regex

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Added test for `resourceQualifierToLanguageTag`

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

---------

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>
2023-02-28 10:28:19 +01:00
Sunik KupferandRicki Hirner b83d116f40 show real name of davowner (bitfireAT/davx5#208)
* add principal table with dao

* add principal table with dao

* collection saves ownerId instead of owner URL

* save and refresh principals

* show display name of collection owner in GUI

* show only the owner name (preferably) or respective url

* remove principals which do not own any collections

* Don't mock AppDatabase

* ensure we are really dealing with a principal and save it even without its display name

* ensure owner label is hidden when neither owner-displayname nor  owner-url are available

* save principal urls without trailing slash

* use a custom query to find principals without collections

* Some changes

- insertOrUpdateByUrl
- don't explicitly set id=0 when not necessary,
- make it work when there are already entries with trailing slahes
- added TODOs

* Small changes
- Update principal only if display name changed
- Rename methods
- Kdoc
- Tests

* stop using simple methods with vague names

* rename method insertOrUpdateOrGet to insertOrUpdate and leave existing kdoc for explanation

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-02-20 13:29:56 +01:00
Ricki Hirner 26961d9749 Update dependencies 2023-02-20 13:29:52 +01:00
Patrick Lang 7b34f8148f jtx-board-2-3-new-recur-handling (bitfireAT/davx5#196)
* adapted JtxSyncManager, added new method in LocalJtxCollection

* minor code improvement

* inserting recurring entry as well, as the series might not have been processed yet

* version bump (preliminarily)

* version bump (preliminarily)
2023-02-20 13:29:44 +01:00
Ricki Hirner 4dd79606f2 F-Droid changelog for 4.3 2023-02-08 14:04:40 +01:00
Ricki Hirner f13164af41 Fetch translations from Transifex, tx config changes 2023-02-08 12:39:40 +01:00
Ricki Hirner 43b6b8b3db Version bump to 4.3 2023-02-08 12:33:16 +01:00
Ricki Hirner d00966063e Update ical4android 2023-02-08 12:33:15 +01:00
Arnau MoraandRicki Hirner 6990479d18 Enabled automatic popup of the custom certificate dialog (bitfireAT/davx5#207)
* Enabled automatic popup of the custom certificate dialog

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Use chaining instead of .let

---------

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-02-08 12:32:29 +01:00
Ricki Hirner 1dd93ae43a Collections fragment: cache paging data (bitfireAT/davx5#206)
Closes bitfireAT/davx5#205
2023-02-07 17:14:21 +01:00
Michael Biebl 179602df46 Drop duplicated .ForegroundService (#266)
Was added by mistake in cfee0f3461.
2023-02-07 17:13:04 +01:00
Ricki Hirner 412acbcc7d Version bump to 4.3-rc.2 2023-02-03 21:08:24 +01:00
Ricki Hirner dc207682a5 CollectionsFragment: Replace Kotlin flow by LiveData (closes bitfireAT/davx5#201) (bitfireAT/davx5#204) 2023-02-03 21:08:14 +01:00
Ricki Hirner c8c1124303 Fix release workflow 2023-02-03 21:07:35 +01:00
Ricki Hirner 00235e12a4 Fetch translations from Transifex 2023-02-01 14:02:59 +01:00
Ricki Hirner e283f5f34b Version bump to 4.3-rc.1 2023-02-01 13:27:56 +01:00
Ricki Hirner 2349468b0b Update dependencies, including ical4android 2023-02-01 13:27:55 +01:00
Sunik Kupfer cf4b7891e3 fix collections vanishing and remove useless home set reference (bitfireAT/davx5#199) 2023-02-01 13:10:35 +01:00
Sunik KupferandRicki Hirner ca62efa2aa remove DaoTools (for syncing) (bitfireAT/davx5#190)
* add test fixtures

* Use direct DB access instead of DaoTools

* minor changes and kdoc

* minor changes and kdoc

* remove obsolete DaoTools, SyncableDao and IdEntity classes

* KDoc

* use hashmap instead of list and added kdoc

* always load from database

* add a test and make test structur more flexible for future tests

* minor KDoc

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-01-31 13:40:39 +01:00
Ricki Hirner 2661dbba34 Use Worker for account cleanup (bitfireAT/davx5#197)
* [WIP] Use Worker for account cleanup

* Remove AccountsUpdatedListener Hilt module

* Use 15 seconds, provide Hilt module for App creation
2023-01-28 21:09:58 +01:00
Ricki Hirner 954f4dcaa3 Debug info: count jtx Board tasks correctly (bitfireAT/davx5#195)
* Reformat code

* Use sync-adapter URI to count jtx Board entries

* Debug info: restrict entry counts per withSyncAdapter URI and not over WHERE clause
2023-01-28 21:09:49 +01:00
Ricki Hirner 6c8976f6ff Version bump to 4.3-beta.1 2023-01-20 11:12:33 +01:00
Ricki Hirner 29b2b2372f Catch SecurityException of notifications 2023-01-20 11:12:32 +01:00
Ricki Hirner 5156ff1380 Update dependencies 2023-01-20 11:12:32 +01:00
Sunik KupferandRicki Hirner 62617a2889 168 refactor service detection part 2 (bitfireAT/davx5#174)
* [WIP] refactor

* [WIP] refactor

* save, update and delete homesets one by one

* save, update and delete collections one by one

* cleaner code

* prevent jumps in row ids

* [WIP] small changes and kdoc

* remove duplicate code and add kdoc

* improve kdoc

* remove redundancy based on service type in resource finder

* tests setup

* handle cancellation

* add tests

* Don't use IdEntity for service detection anymore

* HomeSetDao: getByUrl requires a service (there may be two accounts with the same homeset URLs, for instance with different credentials)

* Deprecate DaoTools

* Minor changes

* Add TODO

* use self explanatory variables instead of a pair

* update kdoc

* add unfinished tests

* add test for updating a collection

* add test for preserving collection flags

* mark collections as homeless if not rediscovered in its homeset

* proper implementation of update and delete of homeless collections with test

* minor changes and kdoc

* Tests: adapt mock server 404

* KDoc

* get collections by service and url, deprecate getByUrl()

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-01-19 23:32:15 +01:00
Patrick Lang df2922f873 Added intent-filter for AccountActivity (bitfireAT/davx5#187)
* Added intent-filter for AccountActivity

* removed intent filter for account activity
2023-01-19 23:31:57 +01:00
Flo EdelmannandRicki Hirner 861bcf30b4 Add dav.mailbox.org to known base URLs (#251)
* Add dav.mailbox.org to known base URLs

* Replace mailbox.org by dav.mailbox.org

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-01-11 23:51:35 +01:00
Arnau MoraandRicki Hirner bee7cf20ac 169 webdav handle weak etags correctly (bitfireAT/davx5#185)
* Added `GetETag.NAME` fetching when `PROPFIND`ing

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* `GetETag.eTag` is only taken if tag is not weak

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Using `GetETag` instead of `QuotedStringUtils` and ignoring weak eTags

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Updated `dav4jvm`

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Fixed checks

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Use new dav4jvm where weak can't be null

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2023-01-09 17:57:04 +01:00
Sunik Kupfer 5434291c6c fixes data saver warning showing below android 7 (bitfireAT/davx5#180)
post false to live boolean dataSaverEnabled even if below api lvl 24
2023-01-09 17:56:56 +01:00
Bernhard Stockmann 8dc9f2e415 Update README.md 2023-01-04 14:55:11 +01:00
Arnau Mora 53bd4866c3 Added Mastodon link to navigation drawer (bitfireAT/davx5#179)
* Added mastodon icon

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Added mastodon link to drawer

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Added mastodon link action

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Mastodon icon tint

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Added Mastodon icon to main

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>
2023-01-01 23:26:54 +01:00
Michael Biebl 1f4398b21e Use consistent margins between different permission switches 2022-12-22 21:25:12 +01:00
Ricki Hirner 12bb09ecb8 Minor CI changes 2022-12-22 21:22:56 +01:00
Ricki Hirner 4c71d21d7b WorkManager address book sync: don't run calendar/tasks sync on address book accounts; vcard4android: set N:LastName for vCard3 groups 2022-12-22 21:22:56 +01:00
1f818ee3b4 Added language selection (bitfireAT/davx5#137)
* added language selection

* PermissionsFragment: Hide notification permission switch on Android < 13

* Retrieve locales with a function

* Added locales flavoring

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Moved locales functions to `locales.gradle`

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Added `locales_config.xml` generation

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Now gets generated automatically

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Added `android:localeConfig`

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Updated submodules

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

* Using `logger` instead of `println`

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>

Signed-off-by: Arnau Mora <arnyminer.z@gmail.com>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
Co-authored-by: Arnau Mora <arnyminer.z@gmail.com>
2022-12-22 21:22:55 +01:00
Michael Biebl 0521f4bcf0 Trivial indentation fixes (#226) 2022-12-22 21:22:55 +01:00
Ricki Hirner 2730d14cb9 Version bump to 4.3-alpha.1 2022-12-22 21:22:55 +01:00
Sunik KupferandRicki Hirner cfee0f3461 work manager manual sync (bitfireAT/davx5#109)
* rabase with dev branch

* added test for checking whether manual work-manager sync queues worker

* overwrite getForegroundInfo to show a "sync running" notification, to run expedited work on Android <12

* basic error state with sensible feedback from syncframework

* remove integer state flags and pass SyncResult as string

* Manual sync cancellation

* rabase with dev branch

* Minor changes

- add Jtx Board sync adapter to sync worker
- use new notification ID for sync worker

* status bar reflects sync of SyncWorker and sync framework correctly

* [WIP] custom hilt SyncComponent

* fix autoclose cast not available below api24

* SyncScope implementation using WeakReference

* Remove unnecessary logging call

* AddressBooksSyncAdapter.sync uses SyncWorker instead of ContentResolve to call requestSync

* add some code documentation

* move all utility objects into one package

* Also check SyncWorker state for accounts list sync status bar

* clean up imports

* Remove duplicate copyright notices

* Minor changes

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-12-15 16:04:39 +01:00
Michael Biebl 262592a3d9 Remove duplicate copyright header (#218) 2022-12-11 17:03:27 +01:00
Ricki Hirner 18a3c0e2fd Fetch Translations from Transifex 2022-12-10 13:20:31 +01:00
Ricki Hirner 5374afb7ae Version bump to 4.2.6 2022-12-10 13:17:59 +01:00
Álvaro BreyandSunik Kupfer 9ceee1b6c0 LoginSetupActivity: allow http/https in intent filter (#211)
* LoginSetupActivity: allow http/https in intent filter

Initially introduced in https://github.com/bitfireAT/davx5-ose/commit/054f1ece612eceef962b356b7ad8b2b7125a6393,
this `intent-filter` breaks login from the Nextcloud app, as we send the server URI as `http`/`https`.

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* Update intent-filter for login flow on api lvl 33 (closes bitfireAT/davx5#172)

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>
Co-authored-by: Sunik Kupfer <kupfer@bitfire.at>
2022-12-08 17:48:13 +01:00
Sunik KupferandRicki Hirner e6ecc7a9b0 Remove forced wifi-only sync when Data Saver is active (bitfireAT/davx5#171)
* Remove forced wifi-only sync when Data Saver is active

* move data saver warning to AppWarningsManager

* Minor changes

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-12-08 11:56:47 +01:00
Sunik KupferandRicki Hirner 1b17124b80 move warning notification logic into a warning class (bitfireAT/davx5#170)
* move warning notification logic into a warning class

* Move Warnings to UI package

* Minor changes

* Move "global sync disabled" warning to AccountListFragment, too

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-12-08 11:52:56 +01:00
Sunik KupferandRicki Hirner 02885947da Refactor DavService to WorkManager (bitfireAT/#164)
* SyncManager: remove retry intent
* refactor DavService to RefreshCollectionsWorker now using WorkManager
* move DavResourceFinder to new service detection package
* Optimize imports

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-12-08 11:52:38 +01:00
Ricki Hirner 3f2fcda6d3 Add Github sponsoring 2022-11-30 10:25:15 +01:00
Sunik Kupfer ba46c4793a Update F-Droid changelog 2022-11-29 15:13:24 +01:00
Sunik Kupfer 405ef4d0b8 Version bump to 4.2.5 2022-11-29 15:09:24 +01:00
Sunik Kupfer 5e099561c1 Fetch translations from Transifex 2022-11-29 15:07:22 +01:00
Ricki Hirner dd9ac77705 Add referrer with utm_source to task app installation Intents 2022-11-29 15:04:34 +01:00
Ricki Hirner 369d5e49f1 Update dependencies, version bump to 4.2.5-rc.1 2022-11-24 18:14:26 +01:00
Sunik KupferandRicki Hirner 5222c13a23 Support read-only task lists for Tasks.org (bitfireAT/davx5#160)
* [WIP] save task list access level to android task list for tasks.org to use
* Disrecommend using OpenTasks in selection dialog
* Update string, use matching ical4android

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-11-24 17:33:37 +01:00
Ricki Hirner b17b38ff50 Update dependencies, including ical4android 2022-11-09 13:16:59 +01:00
Ricki Hirner 3b74ad5257 PermissionsFragment: Hide notification permission switch on Android < 13 2022-11-04 17:53:30 +01:00
Clément GIBAUD e4a5c47887 Add framagenda to pre-set URLs (#192) 2022-11-04 16:31:24 +01:00
Sunik KupferandRicki Hirner 8915a3c103 android 13 compatibility (bitfireAT/davx5#155)
* check for notify permission before notifying

* [WIP] add notification permission request

* Revert "check for notify permission before notifying"

This reverts commit ed8e046d73163bef5684622bd877f08b9ee781c6.

* add notification permission request

* [WIP] add notifications disabled card in accounts view

* add notifications disabled card in accounts view

* reorder permissions

* use dp instead of mm for image max height in permissions view

* stop using array to bundle notification permissions

* Use new permissions contract for PermissionsFragment

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-11-01 12:51:20 +01:00
Ricki Hirner 43ae3fcd1f Update libraries 2022-10-29 16:42:05 +02:00
Ricki Hirner 796eddb975 Update dav4jvm, Kotlin 2022-10-28 13:34:32 +02:00
Ricki Hirner 57a77bd5f5 Actually update vcard4android (better read-only contacts support) 2022-10-28 13:15:38 +02:00
Sunik Kupfer d32b21a465 Update F-Droid changelog 2022-10-25 11:22:34 +02:00
Sunik Kupfer 31a61f508d Version bump to 4.2.4 2022-10-25 11:16:53 +02:00
Ricki Hirner a8e21bed8b Force downloading all contacts after resetting read-only contacts when collection supports Collection Sync 2022-10-21 13:41:22 +02:00
Ricki Hirner 7b70cf45fc Fetch translations from Transifex 2022-10-21 13:41:22 +02:00
Ricki Hirner 66282c10e6 Version bump to 4.2.4-rc.1 2022-10-21 13:41:22 +02:00
Ricki Hirner a493f0a9a9 Update dependencies 2022-10-21 13:41:22 +02:00
Ricki Hirner 951c29e66a Force sync after resetting read-only contacts (bitfireAT/davx5#154)
* Force sync after resetting read-only contacts

* Rename variable for clarity
2022-10-21 13:41:21 +02:00
Sunik KupferandRicki Hirner 0f124cf5b5 LocalAddressBook: update read-only flag of groups, too (bitfireAT/davx5#153)
* LocalAddressBook: update read-only flag of groups, too

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-10-21 13:41:21 +02:00
Sunik Kupfer 9bd564b2c6 fix crash when accounts vanish (closes bitfireAT/davx5#140) (bitfireAT/davx5#148) 2022-10-21 13:41:21 +02:00
Ricki Hirner bec9f8bcfb Fetch translations from Transifex (including new translations) 2022-10-14 23:06:56 +02:00
Ricki HirnerandSunik Kupfer d2614bd87e Create an option to pre-select the read-only setting for address books (bitfireAT/davx5#141)
* [WIP] add managed restriction to force read-only addressbooks
* Honor app-wide read-only address book setting when syncing address books
* reflect status of force read-only address books setting in the GUI

Co-authored-by: Sunik Kupfer <kupfer@bitfire.at>
2022-10-12 17:35:12 +02:00
Ricki Hirner e14460111e Use Material switches for preferences 2022-10-11 12:48:16 +02:00
Ricki Hirner 31fb7ec190 CalDAV/CardDAV UI: replace checkbox by switch (bitfireAT/davx5#143)
Closes bitfireAT/davx5#142
2022-10-11 10:15:01 +02:00
Ricki Hirner 99710ca94e Update Manual URL 2022-10-08 18:58:31 +02:00
Ricki Hirner c635dc8aa3 Add pre-set base URLs 2022-10-03 10:51:33 +02:00
Ricki Hirner ce3f3b7cc4 Update vcard4android (disable LABEL generation); version bump to 4.2.4-beta.1 2022-10-03 10:51:12 +02:00
Ricki Hirner 2b16cd6365 Lint 2022-09-27 13:03:08 +02:00
Ricki Hirner 8a7f51b705 Update dependencies, compile SDK level, build tools, gradle plugin 2022-09-27 13:03:08 +02:00
Ricki Hirner f5b08b60bf Update ical4j (handle NPE when time zone is known to Android but not to ical4j) 2022-09-27 13:03:06 +02:00
Ricki Hirner fc819eb001 Update README
- closes #153
- closes #152
2022-09-27 11:33:07 +02:00
Ricki Hirner fbbf2a6b07 Add calendar and contacts provider to manifest/queries (for increased compatibility with apps like Huawei calendar) 2022-09-23 13:32:22 +02:00
Ricki Hirner 8ff90954cd Don't run AccountsUpdatedListener while account is being renamed
Don't run AccountsUpdatedListener while account is being renamed (closes bitfireAT/davx5#135)
2022-09-22 20:55:08 +02:00
Sandelinos a7fcc087bc Add monochrome icon (#151) 2022-09-20 20:32:58 +02:00
Ricki Hirner e2c2e566b9 Bump version to 4.2.3.4 2022-09-20 18:50:24 +02:00
Ricki Hirner d4eaa0718a Fetch translations from Transifex 2022-09-19 16:36:52 +02:00
Ricki Hirner e3c44f2e50 Update ical4android (enable relaxed valiation of email addresses); bump version to 4.2.3.4-rc.2 2022-09-19 16:35:02 +02:00
Ricki Hirner c517c9c3b5 Bump version to 4.2.3.4-rc1 2022-09-19 16:35:01 +02:00
Ricki Hirner 757c01c186 Update dependencies (including ical4android to address Kiev/Kyiv bug)
See https://github.com/bitfireAT/davx5-ose/discussions/133
2022-09-19 16:35:01 +02:00
Ricki Hirner 97eb45ec12 Fetch translations from Transifex 2022-09-08 22:04:35 +02:00
Ricki Hirner d7102f94bd Version bump to 4.2.3.3 2022-09-08 22:03:43 +02:00
Ricki Hirner cf76cc16ce Update ical4android 2022-09-08 22:03:43 +02:00
Sunik Kupfer 78e4cbe06d roll back commons-io to 2.8.0 for API levels <26 (closes bitfireAT/davx5#130) 2022-09-07 12:23:12 +02:00
Ricki Hirner f47adbef4a Wrap GrantPermissionRule into InitCalendarProviderRule
Closes bitfireAT/davx5#127
2022-08-31 17:16:56 +02:00
Sunik KupferandRicki Hirner 94c9f869c9 Generate new filename if invalid for upload (closes bitfireAT/davx5#110)
* use random UUID instead of UID as upload file name if UID would be dangerous

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-08-31 17:16:54 +02:00
Ricki Hirner ee993a8557 Rename "standard" flavor to "ose" 2022-08-31 17:12:25 +02:00
Ricki Hirner 03deb4191a Bump version to 4.2.3.2 2022-08-29 15:07:51 +02:00
Ricki Hirner b5c16ddbd4 Fetch translations from Transifex 2022-08-29 15:07:50 +02:00
Ricki Hirner f054ab43b9 Update ical4android 2022-08-29 15:07:49 +02:00
Ricki Hirner aeb4541eb9 Version bump to 4.2.3.1 2022-08-24 16:56:19 +02:00
Ricki Hirner 7144b963e4 Version bump to 4.2.3.1-rc1 2022-08-23 22:00:08 +02:00
Ricki Hirner f38e7b2dab Update hilt and ical4android (ical4android had dependency that caused JSONObject problem in release builds) 2022-08-23 21:51:54 +02:00
Ricki Hirner 62912dc83a Add F-Droid changelog 2022-08-23 12:01:17 +02:00
Ricki Hirner acc3ca1b4c Version bump to 4.2.3 2022-08-23 11:36:32 +02:00
Ricki Hirner fec8eae9d0 Update dependencies, including ical4android 2022-08-19 12:39:32 +02:00
Ricki Hirner 885035fadf Update dependencies 2022-08-19 12:39:32 +02:00
Sunik KupferandRicki Hirner e5604118fd confirmation dialog for removing webdav mount (closes bitfireAT/davx5#47) (#119)
* confirmation dialog for removing webdav mount

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-08-19 12:39:32 +02:00
Ricki Hirner 20e79e49b3 Rename notification for external file logging 2022-08-15 12:41:50 +02:00
Ricki Hirner b3e0cef0a6 Fix Android10ResolverTest when IPv6 is the primary address type 2022-08-15 12:41:49 +02:00
Michael Biebl 7bb97ffa31 Remove unused colors (#110)
They are a left over from
commit 12802f8697 and the incomplete
removal in commit 054f1ece61
2022-08-10 17:09:35 +02:00
Ricki Hirner fbb7c6d161 Update dav4jvm, gradle plugin 2022-08-08 20:52:03 +02:00
Ricki Hirner 4f143318da Update dependencies and ical4android 2022-07-28 15:12:34 +02:00
Ricki Hirner 8aefab65f6 Update dependencies 2022-07-28 15:01:23 +02:00
Ricki Hirner 38c40d9466 Add authors 2022-06-21 14:45:14 +02:00
Sunik Kupfer 5daae1b74d Update F-Droid changelog 2022-06-21 12:50:25 +02:00
Sunik Kupfer 8c22b4ed3b Version bump to 4.2.2 2022-06-21 12:35:05 +02:00
Sunik Kupfer dc94791637 etch translations from Transifex 2022-06-21 12:09:04 +02:00
Sunik KupferandRicki Hirner d2c0a2bb44 use findActivity() provided by hilt (closes bitfireAT/davx5#106) (#107)
* use FragmentComponentManagers findActivity method provided by hilt (closes bitfireAT/davx5#106)

* WebCalFragment CalendarViewHolder: Pass Fragment instead of FragmentManager so that requireActivity() can be used

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-06-21 12:05:43 +02:00
Patrick LangandRicki Hirner c90e0bf085 Removed update related to from JtxSyncManager (#92)
* Removed update related to from JtxSyncManager

updateRelatedTo is now done implicitely in the sync content provider for every inserted or updated icalobject/relatedto

* Update ical4android

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-06-21 12:04:12 +02:00
Sunik Kupfer 2e6ce88c58 non-flaky tests (#103)
* [WIP] initialization code to make tests non-flaky

* init code as junit rule and remove flaky annotations

* remove exception for flaky tests in Github test workflow

* ensure correct class rule execution order
2022-06-21 12:02:28 +02:00
Patrick Lang 5b0788b2e9 Don't set account visibility if it was already visible (#102) 2022-06-21 11:55:59 +02:00
Ricki Hirner f813392c72 Version bump to 4.2.1.2-alpha.3 2022-05-30 14:49:47 +02:00
Ricki Hirner bd7d6c7b4d AccountSettings: handle address book accounts (#101)
* AccountSettings: handle address book accounts (fixes: address books are not synced because of IllegalArgumentException)

* Tests (not finished)
2022-05-30 14:49:47 +02:00
Ricki Hirner 0e93a47d6d Version bump to 4.2.1.2-alpha.2 2022-05-29 15:03:37 +02:00
Ricki Hirner 99325e8930 Sync adapter: improve HttpClient and cancellation (#100)
* Sync adapter: share single HttpClient between all SyncManagers (should fix bitfireAT/davx5#99)
* HttpClient: use producer for CertManager
* Call setAccountVisibility only when necessary
* Fix tests
2022-05-29 15:03:36 +02:00
Ricki Hirner 0a11a0bcfc Don't set up periodic syncs for contacts sync of the address book acc… (#97)
Don't set up periodic syncs for contacts sync of the address book accounts (fixes bitfireAT/davx5#96)
2022-05-29 15:02:17 +02:00
Ricki Hirner f3c79367ef Version bump to 4.2.1.2-alpha.1 2022-05-29 15:01:57 +02:00
Ricki Hirner 25ea4fc2ca Create SECURITY.md 2022-05-28 20:06:35 +02:00
Ricki Hirner 4dfebe4355 Hilt: use intro OpenSourceFragment 2022-05-28 19:19:43 +02:00
Ricki Hirner b51d18a659 Use Hilt instead of Koin (#93)
* Use Hilt for DI
* Use Hilt for settings providers
* Use Hilt for login credentials fragment
* Use Hilt instead of ServiceLoader for intro fragments
* Rewrite from Koin to Hilt
* Tests
* Use more Hilt modules for service loading (account drawer, intro fragments)
* Lint
* WebDAV provider: don't listen to changes in onCreate (causes problems with Hilt and tests)
2022-05-28 11:05:35 +02:00
Ricki Hirner 07ba158e66 Fix network security policy with tests 2022-05-27 14:36:25 +02:00
Patrick Lang 338d0d4691 Moved jtx Board in TaskProvider selection to first position (#94) 2022-05-27 14:35:59 +02:00
Bernhard Stockmann 7f58e3e4a8 Update images, add video URL (#49)
* Update images, add video URL

* Video without ab_channel
2022-05-26 12:05:11 +02:00
Michael Biebl b71452195e Remove duplicate copyright header (#46) 2022-05-23 11:31:47 +02:00
Ricki Hirner cf3be750d0 Fetch translations from Transifex 2022-05-22 23:50:22 +02:00
Ricki Hirner 212392e78c Version bump to 4.2.1.1 2022-05-22 23:49:45 +02:00
Sunik Kupfer ddae93c225 Add mocck dependency for tests 2022-05-22 23:48:00 +02:00
Ricki Hirner 1f4a25e4a4 Use Koin for all tests; CI: run tests for all flavors (#91)
Closes bitfireAT/davx5#89
2022-05-22 18:56:47 +02:00
Ricki Hirner 84f55bc39a Fix Android10Resolver test (requires Android 10 instead of 8) 2022-05-12 10:46:49 +02:00
Ricki Hirner 8384730aff Refactor some models to use proper constructors instead of custom initialize() methods 2022-05-12 10:46:49 +02:00
Ricki Hirner 8ca3d857d3 Use Koin Dependency Injection for Singletons (#84) 2022-05-12 10:46:48 +02:00
Ricki Hirner 12cc2cfcde Update submodules 2022-05-12 10:39:28 +02:00
Ricki Hirner 7d424183a8 Update gradle plugin, Kotlin, AndroidX Material 2022-05-12 10:39:26 +02:00
Ricki Hirner d3c10c36b0 CI: create Github release for new tags 2022-05-05 15:21:57 +02:00
Ricki Hirner f37f3e5104 Bump version to 4.2.1 2022-05-05 14:25:38 +02:00
Ricki Hirner 541f51f77e Version bump to 4.2.1-rc.2 2022-04-29 16:03:59 +02:00
Ricki Hirner 9bc4b0a837 Fetch translations from Transifex 2022-04-25 14:52:53 +02:00
Ricki Hirner f31c3cab3d Move EarnBadgesActivity strings to gplay 2022-04-25 14:50:35 +02:00
Ricki Hirner 776adedfbf Version bump to 4.2.1-rc.1 2022-04-25 14:25:36 +02:00
Ricki Hirner 054f1ece61 Setup intent filter (#80)
* Add intent filter for caldav(s)://, carddav(s):// and davx5:// schemes
* Define intent-filter only for standard and gplay flavors
* Merge manifest; remove Espresso tests (further tests should be added)
* Lint

Closes bitfireAT/davx5#77
2022-04-25 14:25:02 +02:00
Ricki Hirner eaea4936d7 ical4android: don't crash on RDATEs with PERIOD 2022-04-25 14:20:51 +02:00
Ricki Hirner 6b98088922 Update dav4jvm 2022-04-24 10:38:17 +02:00
Ricki Hirner 5d61487a85 Rename at.bitfire.davdroid.model to at.bitfire.davdroid.db to make clear it contains database models (and not view models) 2022-04-24 10:38:17 +02:00
Ricki Hirner d4b82d37be Version bump to 4.2.1-beta.2 2022-04-21 21:18:32 +02:00
Ricki Hirner ffea72bc7c Update dav4jvm for improved service compatibility 2022-04-21 21:18:08 +02:00
Ricki Hirner f4b157a101 Server handles only tasks not jouranls (#79)
* Service detection: update properties of already existing collections which are not in homesets
* Trigger service detection after creating a collection because the server may change properties
* jtx sync: query tasks/journals only when supported

Should fix bitfireAT/davx5#78
2022-04-20 17:57:11 +02:00
Ricki Hirner c283691399 jtx Board sync: show whether VTODO and/or VJOURNAL components are supported by a collection; update ical4android 2022-04-20 17:57:09 +02:00
Ricki Hirner e2c48df68f Version bump to 4.2.1-beta.1 2022-04-15 12:22:42 +02:00
Ricki Hirner 40941f9bce Move file, update gradle and plugin 2022-04-15 12:12:17 +02:00
Sunik KupferandRicki Hirner 12802f8697 Mirror changes in AccountsDrawerHandler
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-04-15 12:01:09 +02:00
Michael Biebl 3c0a26bb0c Preferences (#25)
* Set InputType for proxy_host and proxy_port programmatically

Since AndroidX it is no longer sufficient to simply specify
android:inputType in settings_app.xml.

* Drop custom IntEditTextPreference.kt

Doesn't appear to be necessary anymore.
2022-04-11 11:24:24 +02:00
Ricki Hirner 08dfa3f048 Fetch translations from Transifex 2022-03-23 18:09:47 +01:00
Ricki Hirner cd4aa0891b ical4android fix; raise SDK level to 32; bump version to 4.2.0.3 2022-03-23 18:09:02 +01:00
Patrick Lang c8723d0f64 propagate privWriteContent as read only collection (#71) 2022-03-23 18:08:34 +01:00
Ricki Hirner 5ee6e57b03 Fetch translations from Transifex 2022-03-22 13:10:56 +01:00
Ricki Hirner 48bd99e0b6 Version bump to 4.2.0.2 2022-03-22 13:05:23 +01:00
Ricki Hirner ab806e458a vcard4android: contact photo improvements 2022-03-22 12:55:42 +01:00
Ricki Hirner 2c785684a2 Navigation drawer: update Community 2022-03-20 20:26:45 +01:00
Ricki Hirner 522fc8763f Fetch translations from Transifex 2022-03-20 20:26:17 +01:00
Ricki Hirner df08cf6074 Version bump to 4.2.0.1 2022-03-20 20:25:32 +01:00
Ricki Hirner 763da2221c Contact photos: always output JPEG (vcard4android); sync: query max-resource-size (at the moment only for debugging) 2022-03-20 20:25:14 +01:00
Ricki Hirner 850b1f54a8 vcard4android: better handling of (invalid) photos; reduce RAM usage by reducing sync threads a bit 2022-03-19 18:44:53 +01:00
Ricki Hirner f24c591283 F-Droid changelogs 2022-03-18 11:05:04 +01:00
Ricki Hirner b8d83cf46f Fetch translations from Transifex 2022-03-18 10:36:21 +01:00
Ricki Hirner ec7d59a46b Version bump to 4.2 2022-03-18 10:36:05 +01:00
Ricki Hirner 66ebe4f224 Update dav4jvm (fixes bitfireAT/davx5#69) 2022-03-18 10:35:44 +01:00
Ricki Hirner abb60a4331 Fetch translations from Transifex 2022-03-16 14:43:44 +01:00
Ricki Hirner 4950a2390d Version bump to 4.2-rc.1 2022-03-16 14:43:43 +01:00
Ricki Hirner 49ba32e64f Update dependencies 2022-03-16 14:43:43 +01:00
Ricki Hirner da5ff1ea39 Move OnAccountsUpdateListener to App (may fix bitfireAT/davx5#45) 2022-03-16 14:43:43 +01:00
Ricki Hirner da4b5cf143 DebugInfo: provide IntentBuilder that truncates large logs/local resource dumps (should fix bitfireAT/davx5#69) 2022-03-16 14:43:42 +01:00
Ricki Hirner 248dd6fdb3 Update dependencies (including vcard4android with new photo handling) 2022-03-16 14:43:42 +01:00
Ricki Hirner 979123b9ef Add option to select detected collections for synchronization automatically 2022-03-16 14:43:37 +01:00
Sunik KupferandRicki Hirner 7367477ebe Add option to use socks proxy for orbot (closes bitfireAT/davx5#28) (#64)
* Add option to use SOCKS proxy for Orbot
* Fix tests, add constants
* Add option for "no proxy", add migration
* Improve UI

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-03-09 12:51:31 +01:00
Ricki Hirner 8fb9b3a86e Update dependencies; bump version to 4.2.0-beta.1 2022-03-06 11:47:39 +01:00
Sunik KupferandRicki Hirner a7779ca6c3 billing and play dependencies (bitfireAT/davx5#62) (#63)
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-03-06 11:47:29 +01:00
Ricki Hirner 73a7b718e2 Update dependencies; version bump to 4.2.0-alpha.3 2022-03-06 11:44:12 +01:00
Patrick LangandRicki Hirner 4084dc36a2 Updated queryCapabilities to use the same code as in TasksSyncManager, added try-catch block for sync-method (#60)
* Updated queryCapabilities to use the same code as in TasksSyncManager
Might solve #59

* Added try-catch block for sync-method
updated min version for JtxBoard in TaskProvider.kt
Should solve #59

* removed syncResult.databaseError = true

* Changed wrong file before

* Created TasksSyncUtils to put notifyProviderTooOld method

* Cleaned up imports

* Added Version check in JtxSyncAdapterService.kt, required TaskProvider.checkVersion to be public

* Minor changes

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-03-06 11:41:33 +01:00
Ricki Hirner 7534fe689b Update ical4android 2022-03-06 11:41:33 +01:00
Ricki Hirner 955c09d518 Update gradle plugin and build tools 2022-03-06 11:41:28 +01:00
Ricki Hirner 7493d1eda8 Version bump to 4.2.0-alpha.2 2022-02-24 11:15:42 +01:00
Ricki Hirner ff3b5f50af Update ical4android to fix problem with Jtx Board relations 2022-02-24 11:15:26 +01:00
Ricki Hirner 1554c6ae1c Version bump to 4.2.0-alpha.1; update dependencies 2022-02-09 13:43:57 +01:00
Patrick LangandRicki Hirner 16311708f8 Jtx Board synchronization (#56)
Support tasks and notes synchronization with Jtx Board

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-02-09 13:43:17 +01:00
Ricki Hirner 6ab291bdb2 Update gradle plugin 2022-02-09 13:42:37 +01:00
Ricki Hirner 19be3c34bd Version bump to 4.1.1; add F-Droid changelog 2022-02-01 11:00:55 +01:00
Ricki Hirner 64bd373611 Fetch translations from Transifex 2022-02-01 10:46:14 +01:00
Ricki Hirner b2a5a1b1f3 Version bump to 4.1.1-rc.1 2022-01-28 17:06:26 +01:00
Ricki Hirner 6af033a351 Avoid UninitializedPropertyAccessException when accessing collectionURL for logging the sync 2022-01-27 14:11:20 +01:00
Ricki Hirner 1532fab49e Use asSyncAdapter() for calendar provider URIs 2022-01-27 12:18:09 +01:00
Ricki Hirner 62af6f45ab Update gradle plugin 2022-01-27 12:18:09 +01:00
Ricki Hirner 2ec33002c8 Version bump to 4.1.1-beta1 2022-01-24 11:23:51 +01:00
Ricki Hirner 81aa95223f Fetch translations from Transifex 2022-01-24 11:14:50 +01:00
Ricki Hirner c1125a5bd0 Update dependencies 2022-01-24 11:14:22 +01:00
Sunik KupferandRicki Hirner 7593229623 Delete dirty events without instances before syncing
Delete dirty events without instances before syncing (resolves bitfireAT/davx5#34)

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-01-22 10:55:32 +01:00
Ricki Hirner 87725a3b5d Show a warning when disk space is low, which will stop synchronization (resolves bitfireAT/davx5#44) 2022-01-17 18:22:45 +01:00
Ricki Hirner 708c80fa0a Update cert4android and vcard4android (only copyright) 2022-01-13 14:18:23 +01:00
Ricki Hirner 6c34737cbc Fix that foreground service notification was always shown (fix for bitfireAT/davx5#32) 2022-01-13 14:18:23 +01:00
Sunik KupferandRicki Hirner 989cb1fd99 Fix foreground service start not allowed exception (resolves bitfireAT/davx5#32)
* Make "Battery optimization whitelisting" a global DAVx5 setting
* Make "Keep in foreground" setting dependent of "Battery optimization whitelisting" setting
* if foreground service is enabled, remind user to enable battery optimization whitelisting too, before starting the foreground service

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2022-01-13 14:18:23 +01:00
Sunik Kupfer c9b88b0c73 Update ical4j (Closes bitfireAT/davx5#37) (#43) 2022-01-13 14:18:23 +01:00
Ricki Hirner 161157a37c Fetch translations from Transifex 2022-01-05 16:20:12 +01:00
sunkup 9ac05f4f51 Add warning to avoid apostrophes in account names
Add warning to avoid apostrophes in account names. Resolves bitfireAT/davx5#5
2022-01-05 16:18:45 +01:00
sunkup ed08cc8d15 update dependencies (#33) 2022-01-04 12:38:01 +01:00
Ricki Hirner 197432a245 README: Add Twitter and Website 2021-12-30 19:22:59 +01:00
Ricki Hirner 32d215b3a4 Add F-Droid changelog 2021-12-26 13:41:42 +01:00
Ricki Hirner d08a1ec737 Version bump to 4.1; update vcard4android 2021-12-20 17:48:13 +01:00
Ricki Hirner ccd738b4ff Update vcard4android: always add FN (also for vCard4) 2021-12-20 17:47:57 +01:00
Ricki Hirner 985dcf09d3 Fetch translations from Transifex 2021-12-16 13:37:18 +01:00
Ricki Hirner 2b940bfcd7 Version bump to 4.1-rc.1 2021-12-16 13:36:55 +01:00
Ricki Hirner 22e4c75064 WebDAV mounts: add empty view manual link (resolves bitfireAT/davx5#29); update dependencies 2021-12-16 13:36:38 +01:00
Ricki Hirner c7fd893b31 Update copyright (resolves bitfireAT/davx5#26) 2021-12-15 17:55:00 +01:00
Ricki Hirner ac7e717cde WebDAV mounts activity: add Help icon 2021-12-15 17:48:30 +01:00
Ricki Hirner 0009429b68 Update dependencies 2021-12-12 16:50:53 +01:00
Ricki Hirner 4b49df77ed Update room, version bump to 4.1-alpha.3 2021-12-03 16:57:09 +01:00
Ricki Hirner 3c6d1172cf Optimize singletons and companion objects (resolves bitfireAT/davx5#25) 2021-12-03 16:57:09 +01:00
Ricki Hirner 6960a48021 Change static singletons to cached singletons with weak references to save memory when the objects are not needed anymore 2021-12-03 15:46:29 +01:00
Ricki Hirner 54d1b5ec2e Refactor WebDAV cache 2021-12-03 15:46:29 +01:00
Ricki Hirner 99ecfb1ca3 WebDAV mounts: add empty view; add navigation drawer entry for Managed DAVx5 (closes bitfireAT/davx5#23) 2021-12-03 15:46:09 +01:00
Ricki Hirner f1a441c27d WebDAV: accept compliance class 2 or 3 (closes bitfireAT/davx5#21); fix division by zero in progress
- WebDAV service detection: accept compliance class 2 or 3, because they both imply class 1 (there are servers which don't show class 1 although they MUST)
- WebDAV random access: fix division by zero when file size reported by HEAD is zero
2021-12-03 15:45:31 +01:00
Ricki Hirner 56c7d2d73f Remove dokka because the generated documentation isn't used by anybody; fix dav4jvm reference 2021-11-27 18:07:12 +01:00
Ricki Hirner be98bef4ab Sync adapter: add tests, move check of simultaneous running syncs to new ConcurrentUtils 2021-11-23 14:34:25 +01:00
Ricki Hirner 1747cb2896 Update dependencies 2021-11-23 14:31:34 +01:00
Ricki Hirner c82d5cb1b4 WebcalModel: get rid of extra WorkerThread + Handler (were not stopped when fragment was closed) 2021-11-22 19:48:28 +01:00
Ricki Hirner f1de88a0f1 Android10Resolver: add test 2021-11-19 22:45:52 +01:00
Ricki Hirner a280de73a3 Update cert4android and vcard4android (only README changes); Android10Resolver: use empty JVM object instead of UUID as handle 2021-11-19 14:41:32 +01:00
Michael Biebl 489e09ea08 Drop AboutInfo override for dav4jvm
With the recent move to GitHub, the AboutInfo override for dav4jvm was
broken as it still used com_gitlab_bitfireAT__dav4jvm whereas it is now:
```
$ ./gradlew findLibraries | grep dav4jvm
bitfireAT/dav4jvm (f1d9354a41) -> com_github_bitfireAT__dav4jvm
Manually requested license: mpl_2_0
```

Since the autogenerated dav4jvm AboutInfo looks fine (see attached
screenshot), remove the override.
2021-11-19 11:58:08 +01:00
Michael Biebl 3403992c8b Fix cert4android project URL
Follow-up for commit 331c102992
2021-11-19 11:52:40 +01:00
Ricki Hirner 9418b4f2a5 Temporarily disable jCard because of nextcloud/server#29693, version bump to 4.1-alpha.3 2021-11-13 13:56:42 +01:00
Ricki Hirner 26b4824124 Small Android10Resolver improvements 2021-11-13 13:56:19 +01:00
Ricki Hirner 2eeb1be833 DiskCacheTest: fixes another possible reason why tests may fail?
(trim deletes the files in modification time order, so if two files have the same
modification time, the order is not well-defined)
2021-11-13 11:09:50 +01:00
Ricki Hirner db77f43931 Bump version to 4.1-alpha.2 2021-11-12 22:23:36 +01:00
Ricki Hirner d96e311e8b Improve SRV record resolving (resolves bitfireAT/davx5#18)
- Android 8 to 10: use all DNS servers; prioritize resolvers of active connections
  (before: use only DNS servers of "active" connection, which may be a VPN connection without servers)
- Android 10+: use new DnsResolver API so that we don't need to know DNS servers anymore (also supports DoT etc.)
2021-11-12 22:23:09 +01:00
Ricki Hirner 952a10fb3d WebDAV cache: fix problem that sometimes the tests fail 2021-11-12 01:13:03 +01:00
Ricki Hirner 09384948b5 Github issues: add link to forum; add sponsorships 2021-11-10 13:02:48 +01:00
Ricki Hirner 0a17169a8b Update gradle and dependencies 2021-11-08 09:56:14 +01:00
Ricki Hirner 331c102992 Move to Github 2021-11-07 21:18:08 +01:00
Ricki Hirner 5de0718210 Update dependencies (including dav4jvm on Github instead of Gitlab) 2021-11-06 10:17:55 +01:00
Ricki Hirner 2504a81024 jCard: check MIME type from server; use MediaType for MIME types when possible 2021-11-05 01:38:59 +01:00
Ricki Hirner ede9e1cf55 CI: use container from ghcr.io 2021-11-04 10:30:35 +01:00
Michael Biebl cd1b14889b Properly cancel NOTIFY_REFRESH_COLLECTIONS error notifications
When displaying the notification, we use serviceId as tag, so use
serviceId as well when cancelling it.
2021-10-22 12:56:36 +00:00
Ricki Hirner d8fa5d4979 Version bump to 4.1-alpha.1 2021-10-22 14:52:05 +02:00
Ricki Hirner 563b131211 Support jCard (closes #11) 2021-10-22 14:51:41 +02:00
Ricki Hirner 201719af1a Debug info: show available/total storage memory (closes #10) 2021-10-21 07:10:47 +02:00
Ricki Hirner 290e73229b Update dependencies 2021-10-17 16:06:15 +02:00
Ricki Hirner 265d899d2c WebDAV: always return MIME type; change cached file size/modification time after successful upload 2021-10-15 12:38:18 +02:00
Ricki Hirner 7d4a5863b3 Update gradle plugin 2021-10-14 18:16:06 +02:00
Ricki Hirner 265d75b290 Add F-Droid release notes 2021-10-11 16:57:50 +02:00
Ricki Hirner 918c2647da Version bump to 4.0 2021-10-10 13:53:45 +02:00
Ricki Hirner 1bf66770ef Version bump to 4.0-beta3 2021-10-07 21:50:19 +02:00
Ricki Hirner 75bd04e882 CardDAV KIND/MEMBER: always download group vCards after uploading them to keep groups in sync with server 2021-10-07 21:50:04 +02:00
Ricki Hirner 566d10ef8d Add time zone to debug info 2021-10-06 13:57:22 +02:00
Ricki Hirner a10b1153ea Improve group assignment 2021-10-05 12:29:10 +02:00
Ricki Hirner 5b6a0a8114 Comments, version bump to 4.0-beta3 2021-10-05 10:19:39 +02:00
Ricki Hirner e8fb6becf9 Fix Room crash and group processing performance problem 2021-10-05 10:08:14 +02:00
Ricki Hirner f7eea633fb WebDAV: fix random access without DocumentState; add tests 2021-10-04 19:09:47 +02:00
Ricki Hirner e22928eeea Fetch translations from Transifex 2021-10-04 17:42:43 +02:00
Ricki Hirner be63957507 Version bump to 4.0-beta2 2021-10-04 17:42:43 +02:00
Ricki Hirner 764102ae6e vCard4-groups: apply pending memberships after a contact has been downloaded again 2021-10-04 14:35:31 +02:00
Ricki Hirner 459cb538d7 GroupMembershipHandler: ignore memberships in groups that don't exist (anymore) 2021-10-04 11:20:19 +02:00
Ricki Hirner 9ee2930312 WebDAV: Add permanent notification to hopefully avoid being killed because of "excessive cpu usage" 2021-10-04 11:08:50 +02:00
Ricki Hirner 5161fad251 WebDAV: optimize cache 2021-10-03 12:51:33 +02:00
Ricki Hirner a42517e1ed Improve WebDAV UI 2021-10-02 18:54:43 +02:00
Ricki Hirner 231e54db4f Lint 2021-09-29 13:40:36 +02:00
Ricki Hirner 6e502ae8dc Squashed commit of the following:
commit 56f5f5a6cbad595534148a160678972734ca6cf2
Author: Ricki Hirner <hirner@bitfire.at>
Date:   Wed Sep 29 10:59:23 2021 +0200

    Version bump to 4.0-beta1

commit 2a4d7501d9d6cd572269de21e82c204c5a2a57e8
Author: Ricki Hirner <hirner@bitfire.at>
Date:   Tue Sep 28 21:45:49 2021 +0200

    Fix UiUtils.launchUri once again

commit 293accaf8f26842d198e9d5c4fc9029cb6ac71cc
Author: Ricki Hirner <hirner@bitfire.at>
Date:   Tue Sep 28 13:47:06 2021 +0200

    Adjust gradle daemon heap size

commit 7763a51c9bbc7baee36ef71648889f7fb4fe965d
Author: Ricki Hirner <hirner@bitfire.at>
Date:   Tue Sep 28 12:21:37 2021 +0200

    Query and show quota

commit 95f4dbfd8ffadd930aab9fc36dc3865bfee54fb7
Author: Ricki Hirner <hirner@bitfire.at>
Date:   Mon Sep 27 22:16:48 2021 +0200

    Persist cookies over WebDAV sessions

commit 17c9327eb3ef3d9384b28b70aa9637718d34e8a2
Author: Ricki Hirner <hirner@bitfire.at>
Date:   Sun Sep 26 19:54:57 2021 +0200

    Add WebDAV caches

    * add random access page cache
    * add thumbnail cache
    * add HEAD response cache

commit 0e5237f8b178ab66099999f72e657c59c9302d71
Author: Ricki Hirner <hirner@bitfire.at>
Date:   Tue Aug 31 13:25:36 2021 +0200

    Add WebDAV Access over Storage Access Framework
2021-09-29 11:04:44 +02:00
Ricki Hirner da7d1e1ec6 UiUtils: fix wrong "Please install a browser" message 2021-09-29 11:04:00 +02:00
Ricki Hirner 1a13fbbff4 ical4android: fix attendee problem; version bump to 3.4.1-beta1 2021-09-11 15:02:03 +02:00
Ricki Hirner 425a88f6f7 Fetch translations from Transifex 2021-09-09 19:31:34 +02:00
Ricki Hirner 3afc7c8dbc Version bump to 3.4.0.1 2021-09-09 19:31:17 +02:00
Ricki Hirner a6854f2461 Merge branch 'dev-3.x-ose' of gitlab.com:bitfireAT/davx5-ose into dev-3.x-ose 2021-09-09 16:35:19 +02:00
Ricki Hirner fc574f5853 lint 2021-09-09 16:34:52 +02:00
rhi 12a85cbf60 Merge branch 'dev-3.x-ose' into 'dev-3.x-ose'
add app icon to Fastlane

See merge request bitfireAT/davx5-ose!62
2021-09-08 07:56:14 +00:00
Ricki Hirner e0e69d3ffd Collection list: migrate to Paging3 2021-09-07 23:04:28 +02:00
Hans-Christoph Steiner 17430ab9d1 add app icon to Fastlane 2021-09-07 17:25:07 +02:00
Ricki Hirner 3e77b3ab1b SyncManager syncRemote: use main dispatcher for outer coroutine 2021-09-01 18:03:02 +02:00
Ricki Hirner 700ced1701 Simplify checking of duplicated sync calls (don't use WeakReference but locks instead) 2021-09-01 18:03:02 +02:00
Ricki Hirner d8a70ca30c ProGuard rules: XmlPullParser 2021-08-31 15:08:17 +02:00
Ricki Hirner 57f11f84c9 Update Gitlab CI 2021-08-31 14:24:03 +02:00
Ricki Hirner 660daf6573 Fetch translations from Transifex 2021-08-31 13:47:54 +02:00
Ricki Hirner ad657dfad7 Version bump to 3.4 2021-08-31 13:47:19 +02:00
Ricki Hirner bc8d9b5b8c Update Gitlab CI 2021-08-26 18:22:20 +02:00
Ricki Hirner 925bc6a7b0 Update dav4jvm; group strategy: passing LocalAddressBook instead of ContactSyncManager is enough 2021-08-26 18:22:20 +02:00
Ricki Hirner 514dfdae80 Location permission for SSID restriction: make clear that "precise" location is required in Android 12+ 2021-08-26 12:47:20 +02:00
Ricki Hirner 12a08b4a0b Update dependencies 2021-08-25 17:54:05 +02:00
Ricki Hirner 52c1a69ac6 Version bump to 3.4-beta3 2021-08-23 12:02:14 +02:00
Ricki Hirner 87fdb06fc9 Version bump to 3.4-beta2 2021-08-23 11:08:17 +02:00
Ricki Hirner 450368bd0e Use ThreadPoolExecutor with dynamic number of threads as global sync work dispatcher 2021-08-23 11:08:01 +02:00
Ricki Hirner bf18d02715 Raise SDK level to 31 (Android 12); explicitly make components with intent-filters (not) exported 2021-08-23 11:08:01 +02:00
Ricki Hirner 43086f3cb9 Fetch translations from Transifex 2021-08-22 20:45:42 +02:00
Ricki Hirner 8973054e85 Version bump to 3.4-beta1 2021-08-22 20:44:48 +02:00
Ricki Hirner 8069657238 Fix tests 2021-08-22 18:50:36 +02:00
Ricki Hirner 9ecb02a059 Update SyncManager test 2021-08-22 15:12:33 +02:00
Ricki Hirner 5307333fcf CardDAV: add tests for new data handlers/builders (also relevant for contact groups) 2021-08-22 14:40:39 +02:00
Ricki Hirner 2700ac51da Refactor group handling 2021-08-22 14:40:39 +02:00
Ricki Hirner 7356a4152f Manage worker threads in Sync*Service, not in SyncManager 2021-08-22 14:40:39 +02:00
Ricki Hirner 1c356c1e17 Use strategies to handle contact groups (decouple from ContactsSyncManager) 2021-08-22 14:40:39 +02:00
Ricki Hirner 5feb9d658f Fetch translations from Transifex 2021-07-31 11:33:37 +02:00
Ricki Hirner 1259a8ae0d Version bump to 3.3.13-beta1 2021-07-31 11:33:01 +02:00
Ricki Hirner 6dd6833446 Better support for vCard contact dates and other properties (great rewrite of vcard4android) 2021-07-31 11:32:39 +02:00
Ricki Hirner bf1d3ef624 Remove onClick from layout XMLs 2021-07-29 13:08:50 +02:00
Ricki Hirner 9d77b54d89 Update gradle 2021-07-29 11:16:46 +02:00
Ricki Hirner e580dceb65 AboutActivity: update copyright string to mention contributors 2021-07-26 12:51:28 +02:00
Ricki Hirner 6a9f31d3f0 Use new Flexbox repo 2021-07-25 15:52:54 +02:00
Ricki Hirner 12c1d99a2e Update dependencies, including ical4android 2021-07-25 11:41:01 +02:00
Ricki Hirner d8ab4ffddd Debug info: attach logcat by default; ical4android: don't crash on too large strings when dumping objects 2021-07-22 21:24:31 +02:00
Ricki Hirner 265d086301 Use ./fetch-translations instead of "tx pull" again (also fetches translators) 2021-07-22 16:02:07 +02:00
Ricki Hirner 3a1844d5b5 Transifex: use "tx pull" instead of scripts/fetch-translations.sh from now on 2021-07-20 13:19:49 +02:00
Ricki Hirner 189d7a31d8 Version bump to 3.3.12 2021-07-18 11:35:04 +02:00
Ricki Hirner 5e49d9c191 Version bump to 3.3.12-rc1 2021-07-16 01:51:55 +02:00
Ricki Hirner f7a176513a Improve AutoComplete fields, especially in AppCompatDelegate-set dark mode 2021-07-16 01:50:37 +02:00
Ricki Hirner 0da5a967b9 Fetch translations from Transifex 2021-07-14 16:32:01 +02:00
Ricki Hirner b39321d033 Update cert4android translations 2021-07-14 16:28:44 +02:00
Ricki Hirner a84b8a5a65 Version bump to 3.3.12 2021-07-14 16:28:43 +02:00
Ricki Hirner dbd0dd0a9b Lint cleanup 2021-07-12 13:40:13 +02:00
Ricki Hirner 267e73723c Version bump to 3.3.12-beta3 2021-07-12 12:49:16 +02:00
Ricki Hirner 42be478ab6 Update test dependencies 2021-07-12 12:42:56 +02:00
Ricki Hirner ed76789417 AccountsActivity: use model for data/view separation 2021-07-12 12:36:08 +02:00
Ricki Hirner ee48c41259 Fetch translations from Transifex 2021-07-08 14:38:49 +02:00
Ricki Hirner 137a916b20 Version bump to 3.3.12-beta2 2021-07-08 14:31:48 +02:00
Ricki Hirner a7abb34fbe Update dependencies 2021-07-08 14:18:55 +02:00
Ricki Hirner 47d317bce9 Create address book/calendar: set error on TextInputLayout instead of TextInputEditText, optimize layout 2021-07-08 11:16:58 +02:00
Ricki Hirner b918b49ec2 Provide auto-completion for some common base URLs 2021-07-06 20:51:22 +02:00
Ricki Hirner 235b9b1ac6 Change remaining Spinners to AutoCompleteTextInput fields; update dependencies 2021-07-06 20:51:22 +02:00
Ricki Hirner 663957b7dc Rewrite HomeSetAdapter so that it uses a Material-style text field instead of an old-style Spinner that doesn't work with dark mode anymore 2021-07-02 00:32:15 +02:00
Ricki Hirner 23235991d2 Log last sync time for each collection 2021-07-02 00:32:15 +02:00
Ricki Hirner b94935db50 SyncManager: remove unused code 2021-06-29 19:30:32 +02:00
Ricki Hirner 6cd2af292d Fetch translations from Transifex 2021-06-29 19:11:37 +02:00
Ricki Hirner 51ee39c4f0 Version bump to 3.3.12-beta1 2021-06-29 19:11:37 +02:00
Ricki Hirner ed722d574e SyncManager: don't create worker threads when not necessary; shut Executor down after using it 2021-06-29 19:11:37 +02:00
Ricki Hirner 2d388eb3ff Debug info activity: create separate card for archive, provide UI feedback for archive generation 2021-06-25 18:08:47 +02:00
Ricki Hirner 7b4ccb3fd4 Set UI theme directly in App.onCreate() and not in an asynchronous thread to avoid rare occurrences of IllegalStateException 2021-06-25 15:38:58 +02:00
Ricki Hirner d67f57dd13 Update dependencies 2021-06-25 15:28:45 +02:00
Ricki Hirner 36a8ccf740 Debug info .zip: change MIME type from application/zip to */* 2021-06-21 11:56:02 +02:00
Ricki Hirner 616833c9dc Remove deprecated kotlin-android-extensions in favor of view bindings 2021-06-06 20:46:57 +02:00
Ricki Hirner 520f96f017 Update dependencies 2021-06-06 20:41:52 +02:00
Ricki Hirner 0b4fdaffe2 Debug info: use max compression for .zip 2021-06-06 12:12:37 +02:00
Ricki Hirner 0da6b5b032 Changelog for F-Droid 2021-05-26 12:34:37 +02:00
Ricki Hirner ad4caecf7d Version bump to 3.3.11 2021-05-26 12:13:39 +02:00
Ricki Hirner d87789690b Catch DB errors while renaming account 2021-05-23 14:32:03 +02:00
Ricki Hirner 5f5da4681e Fetch translations from Transifex 2021-05-23 14:04:49 +02:00
Ricki Hirner b87c881f12 Version bump to 3.3.11-rc1 2021-05-23 14:03:42 +02:00
Ricki Hirner bd45588865 Debug info: show app standby bucket; update AndroidX dependencies; minor night mode updates 2021-05-23 14:03:19 +02:00
Ricki Hirner be99cb0a18 Debug info: show app standby bucket; update AndroidX dependencies 2021-05-23 14:03:00 +02:00
Ricki Hirner 3545cf30cf Remove comments which are not understood by Transifex 2021-05-11 15:04:29 +02:00
Ricki Hirner 6178f39639 Minor amendments for dark theme 2021-05-08 21:31:43 +02:00
Ricki Hirner c8eae2c90e Version bump to 3.3.11-beta2 2021-05-07 23:06:06 +02:00
Ricki Hirner 78fb37516f Update ical4j 2021-05-07 23:05:36 +02:00
Ricki Hirner f74c613b53 Dark theme, animation for WelcomeFragment 2021-05-07 22:54:42 +02:00
Ricki Hirner 9ce481b37a Don't use jCenter anymore 2021-05-04 21:02:19 +02:00
Ricki Hirner d6dc42eb91 Update gradle plugin and version 2021-05-04 21:02:19 +02:00
Ricki Hirner 20034a8f32 Fix Espresso tests 2021-05-04 21:02:19 +02:00
Ricki Hirner 8b43c6f280 Merge branch 'dev-3.x_EspressoExperimental-ose' into 'dev-3.x-ose'
Dev 3.x espresso experimental ose

See merge request bitfireAT/davx5-ose!59
2021-05-04 17:20:44 +00:00
Patrick Lang a5c51bc05f Dev 3.x espresso experimental ose 2021-05-04 17:20:44 +00:00
Ricki Hirner 6f398701cc Update dependencies 2021-05-04 15:56:10 +02:00
Ricki Hirner fc1b5d6a33 Merge branch 'dev-3.x_RTLSupportEnabled-ose' into dev-3.x-ose 2021-05-04 12:02:01 +02:00
Ricki Hirner b44ed202a3 ical4android: assume 1 day/1 hour for Android events without dtEnd and duration (should not occur, but occurs) 2021-04-21 15:14:34 +02:00
Ricki Hirner 90b61d541e Add Vietnamese translations (thanks!); fetch translations from Transifex 2021-04-19 18:54:22 +02:00
Ricki Hirner 42832f22c9 Version bump to 3.3.11-beta1 2021-04-19 18:52:52 +02:00
Ricki Hirner dffe5d176b Android 11: fix problems when viewing URLs in some default browsers; vcard4android: don't convert TYPEs into explicit custom labels 2021-04-19 18:52:20 +02:00
Ricki Hirner 7122e344b7 F-Droid changelog for 3.3.10 2021-04-15 17:43:53 +02:00
Ricki Hirner 26900c5d4f Version bump to 3.3.10 2021-04-15 17:37:08 +02:00
Patrick Lang f7c0ef231b Update .gitlab-ci.yml file 2021-04-13 18:06:48 +00:00
Patrick Lang 5c430664a9 Update .gitlab-ci.yml file, added CURL: - curl -XPOST -d 'email=gitlab%40bitfire.at&password=$DAVTEST_TOKEN&action=Request+access' 'https://davtest.dev001.net/access/' 2021-04-13 17:43:05 +00:00
Ricki Hirner d9d593a4d6 Fetch translations from Transifex 2021-04-12 23:03:04 +02:00
Ricki Hirner 6790d95b21 Update build tools, ical4android, cert4android 2021-04-12 22:59:55 +02:00
Ricki Hirner db2c207669 Version bump to 3.3.10-rc1 2021-04-09 20:54:26 +02:00
Ricki Hirner 71d6402ff2 Update dependencies (including ical4j to 3.0.24) 2021-04-09 20:54:06 +02:00
Ricki Hirner e19ae992ea Client certificates: fix occasional HTTP 400 errors
* don't catch and ignore exceptions when adding client certificates for authentication
* remove useless KeyStore usage
2021-04-06 13:37:42 +02:00
Ricki Hirner ef63165783 Update dependencies 2021-03-20 13:03:31 +01:00
Ricki Hirner 9048af4af5 Update ical4android 2021-03-20 12:38:58 +01:00
Ricki Hirner 512754d014 Typo 2021-03-12 12:12:52 +01:00
Ricki Hirner 8fdf57ea28 Version bump to 3.3.10-beta2 2021-03-08 19:20:38 +01:00
Ricki Hirner a1506e1514 Update Kotlin, ical4android 2021-03-08 19:20:13 +01:00
Ricki Hirner 3049f7304d Update AboutLibraries 2021-03-08 18:50:54 +01:00
Ricki Hirner b1d230353e debug info: wording; update JUnit 2021-03-08 18:45:12 +01:00
Ricki Hirner f9a6107f38 Debug info: allow sharing only after debug info is generated 2021-03-02 12:31:19 +01:00
Ricki Hirner 266490bf14 Update libraries 2021-03-02 11:53:01 +01:00
Ricki Hirner c037b7e152 Version bump to 3.3.10-beta1 2021-02-12 17:59:28 +01:00
Ricki Hirner 83f257f07a Events: support URL as extended property; rewrite deprecated unknown properties to current format 2021-02-12 17:47:11 +01:00
Ricki Hirner 1d35f80afb Fetch translations from Transifex 2021-02-09 11:02:45 +01:00
Ricki Hirner 46b78c043b Version bump to 3.3.9 2021-02-09 11:00:27 +01:00
Ricki Hirner 0687b8599e Version bump to 3.3.9-beta4 2021-02-07 13:12:51 +01:00
Ricki Hirner 77bd44044e Fix foreground notification crash 2021-02-07 12:56:11 +01:00
Ricki Hirner e95366adcc Fix translations 2021-02-07 12:01:26 +01:00
Ricki Hirner a1d689eb1b Fetch translations from Transifex 2021-02-07 12:01:26 +01:00
Ricki Hirner 2d5b879b34 Update kotlin and cert4android 2021-02-07 12:01:26 +01:00
Ricki Hirner 7667ecb437 Version bump to 3.3.9-beta3 2021-02-01 14:58:25 +01:00
Ricki Hirner 6942ad9867 Debug info activity: remove string 2021-02-01 14:40:09 +01:00
Ricki Hirner 8aba0c80bc New option: keep in foreground (may help when the device prevents automatic synchronization) 2021-02-01 14:40:09 +01:00
Ricki Hirner 652b0ba92c Version bump to 3.3.9-beta2 2021-01-31 13:45:57 +01:00
Ricki Hirner a92593ea77 Update okhttp 2021-01-31 13:45:41 +01:00
Ricki Hirner 3fb4e305ad Debug info: show number of events, contacts, tasks for each account (to know whether sync is working) 2021-01-31 13:45:40 +01:00
Ricki Hirner e96f8a627e Version bump to 3.3.9-beta1 2021-01-16 14:18:11 +01:00
Ricki Hirner eebd21a02e AccountActivity: don't show "Create addressbook/calendar" when there's no bind privilege on any homeset 2021-01-16 12:53:32 +01:00
Ricki Hirner 8323ea1872 AccountActivity: finish when account has been removed externally 2021-01-16 12:53:32 +01:00
Ricki Hirner 2f1ee9994d *DAV service detection: save all information of homesets again 2021-01-16 12:53:32 +01:00
Ricki Hirner e157322a1f Correctly show dav4jvm in About/Libraries 2021-01-16 12:53:32 +01:00
Ricki Hirner 40a35ac7a0 Version bump to 3.3.8 2021-01-11 20:10:34 +01:00
Ricki Hirner d0385ea8ce Fetch translations from Transifex 2021-01-11 20:08:37 +01:00
Ricki Hirner 4a89bb762e Catch some exceptions 2021-01-11 20:05:18 +01:00
Ricki Hirner 51c96a50c0 Update vcard4android 2021-01-06 10:25:51 +01:00
Ricki Hirner 16464ed025 Fetch translations from Transifex 2021-01-02 15:27:43 +01:00
Ricki Hirner 5567e79d2d Update libraries and dependencies 2021-01-02 15:11:45 +01:00
Ricki Hirner ececb08bb6 Debug info: include interface name and DNS servers (because DNS-related problems are reported often) 2020-12-23 16:49:21 +01:00
Ricki Hirner 07fccd86d1 Version bump to 3.3.8-beta1 2020-12-14 19:12:18 +01:00
Ricki Hirner 4e870123ff Merge branch 'dev-3.x_DontShowUnspportedCollections-ose' into dev-3.x-ose 2020-12-14 19:06:31 +01:00
Patrick e2e852c2e0 Exclude unsupported calendar collections
Excludes calendar collections that have both VEVENT and VTODO not supported from being displayed in the collections overview.
2020-12-14 19:04:31 +01:00
Ricki Hirner 46ee17f04b Make default sync interval a setting 2020-12-14 18:52:06 +01:00
Ricki Hirner 21fffe0b65 Update dependencies 2020-12-14 18:51:52 +01:00
Ricki Hirner 35a6ddb89c Bump version code 2020-11-28 14:32:48 +01:00
Ricki Hirner be48945c03 Support resizeable activites (multi-window mode) 2020-11-28 14:26:38 +01:00
Ricki Hirner 1a7b1f9420 Update ical4android and version code 2020-11-28 13:38:25 +01:00
Ricki Hirner 9e1dc44048 Version bump to 3.3.7 2020-11-28 08:58:47 +01:00
Ricki Hirner 30348b40a4 Fetch translations from Transifex 2020-11-27 21:47:02 +01:00
Ricki Hirner 7af2b8bd00 Version bump to 3.3.7-beta3 2020-11-27 21:46:16 +01:00
Ricki Hirner 90c503fc16 add "show only personal" as setting 2020-11-27 21:45:28 +01:00
Ricki Hirner 6411dfef3a Update ical4android and vcard4android; add HTTP/2 ping every 45 secs to avoid cancellation 2020-11-26 11:39:18 +01:00
Ricki Hirner 6b84350df5 Minor rewriting 2020-11-24 12:14:34 +01:00
Ricki Hirner f1d06a305f Fetch translations from Transifex 2020-11-23 11:48:42 +01:00
Ricki Hirner 38eb9693d2 Version bump to 3.3.7-beta2 2020-11-23 11:39:00 +01:00
Ricki Hirner c7c547e391 Update ical4android, vcard4android 2020-11-23 00:31:34 +01:00
Ricki Hirner 0226414458 Save sync intervals and check/repair them at app start and reboot 2020-11-19 17:51:24 +01:00
Ricki Hirner 7a95bfdb94 Update AboutLibraries 2020-11-17 23:36:12 +01:00
Ricki Hirner 491763025b Merge branch 'dev-3.x_NoCertificatesFoundToast-ose' into dev-3.x-ose 2020-11-16 09:22:11 +01:00
Patrick 2d6b9eef4d Shows a toast when no certificate was found
Currently the certificate selection would popup und disappear immedeately. This quick fix would show a Toast for User Feedback
2020-11-16 09:20:30 +01:00
Ricki Hirner 0a06d0ea11 Account settings: always allow client certificate selection 2020-11-16 09:05:14 +01:00
Ricki Hirner 737af880bd Add migration for AppDatabase v8 2020-11-15 22:41:54 +01:00
Ricki Hirner 19474a1e17 Version bump to 3.3.7-beta1 2020-11-15 21:38:04 +01:00
Ricki Hirner 326267afce DAV:owner: resolve relative paths, only show in UI when it's set 2020-11-15 21:32:28 +01:00
Ricki Hirner 8ff0a192b8 Update submodules 2020-11-15 21:13:21 +01:00
Ricki Hirner e9a62eddf5 DAV:owner small rewrite 2020-11-15 21:11:39 +01:00
Patrick Lang ced1ed9abf Added Owner
- Extend Collection to store owner
- Show Owner in collection_properties.xml
2020-11-15 20:04:32 +00:00
Ricki Hirner a1898888cb Fix tests 2020-11-15 20:47:31 +01:00
Ricki Hirner 7e5a7dff20 Add an account setting for "Show only personal collections" 2020-11-15 20:35:46 +01:00
Ricki Hirner 5c44332e69 Add account view option: show only personal collections 2020-11-15 20:35:46 +01:00
Ricki Hirner 4fb6e39148 Associate each collection to its homeset 2020-11-15 20:35:46 +01:00
Ricki Hirner 2c28ffad2e Update dependencies 2020-11-14 13:09:48 +01:00
Patrick 674455cc8c Merge branch 'dev-3.x-ose' into dev-3.x_RTLSupportEnabled-ose 2020-11-11 19:36:28 +01:00
Ricki Hirner 540d1ddb74 Merge branch 'infomaniak/android-infomaniak-sync-fix-string-localization' into dev-3.x-ose 2020-11-10 22:04:09 +01:00
Joris Bodin fb4f870742 Extract string for remove auto reset permission 2020-11-10 22:03:46 +01:00
Ricki Hirner 1a8fca415d Update ez-vcard 2020-11-10 13:34:07 +01:00
Ricki Hirner b69ff2380a Wifi permission activity: add "Allow all the time" string for Android 10 2020-11-07 12:31:12 +01:00
Ricki Hirner 260c5f928d Version bump to 3.3.6 2020-11-07 12:31:12 +01:00
Patrick d199560add Merge branch 'dev-3.x-ose' into dev-3.x_RTLSupportEnabled-ose 2020-11-06 21:03:26 +01:00
Ricki Hirner 775a8abc60 cert4android: fetch translations from Transifex 2020-11-05 12:10:14 +01:00
Ricki Hirner 74f2bf69d6 Fetch translations from Transifex 2020-11-05 12:09:19 +01:00
Ricki Hirner c583d92205 Version bump to 3.3.6-beta2 2020-11-05 12:06:36 +01:00
Ricki Hirner 64cb191c6d Try to work around problems with package visibility: use package instead of provider in visibility declaration 2020-11-05 12:06:22 +01:00
Ricki Hirner b84ef186d8 Merge branch 'mbiebl/davx5-ose-share-icon-dark' into dev-3.x-ose 2020-11-03 13:14:48 +01:00
Ricki Hirner bf08231bbd Don't sync Webcal-only calendars/tasks lists over CalDAV (may cause duplicate calendars)
(If a calendar is both CalDAV- and Webcal-capable, i.e. resourcetype is collection
and subscribed, it will be shown as CalDAV calendar.)
2020-11-03 12:58:48 +01:00
Patrick 907c1cb1ba Fixed layout gravity in login_credentials_fragment 2020-10-31 14:12:22 +01:00
Patrick 203fa7b1cd RTL Support enabled
+ Checked all Screens
+ checked drawables and mirrored if applicable
+ checked screens without translations to still fulfill RTL layout
2020-10-31 13:03:33 +01:00
Ricki Hirner 1973fc4f29 Add login layout ID for pre-launch tests; version bump to 3.3.6-beta1 2020-10-30 18:23:32 +01:00
Ricki Hirner e6cc326166 Check whether WiFi permissions are declared 2020-10-30 12:52:42 +01:00
Ricki Hirner 1f47798cf7 Minor refactoring: About, styles 2020-10-29 20:19:23 +01:00
Ricki Hirner 1ed91ff11a Use Western digits where Western digits are required; show locale in debug info 2020-10-26 18:28:32 +01:00
Ricki Hirner 0782af6e91 cert4android: use MD5 fingerprint instead of CN as key to store custom certs 2020-10-25 20:26:08 +01:00
Ricki Hirner 99f59286c7 Really disable calendar sync for an account when there is no CalDAV service; update ez-vcard 2020-10-25 17:56:55 +01:00
Ricki Hirner ecdf2607f9 Update ez-vcard 2020-10-25 15:53:40 +01:00
Ricki Hirner c4440bfc4f Always enable verbose logs in debug builds; show SyncResult when a sync is complete 2020-10-25 14:19:05 +01:00
Ricki Hirner 96aa2fe3ce Don't ask for active location services on Android <9 / location permissions on Android 8.0 2020-10-25 10:28:29 +01:00
Ricki Hirner ca9044cf76 Fetch translations from Transifex 2020-10-24 14:02:00 +02:00
Ricki Hirner 88bbb3c8bf Update ical4android; version bump to 3.3.5 2020-10-24 13:52:42 +02:00
Ricki Hirner b920cdb6f8 Add ICSx5 to visible packages so that it can be detected under Android 11 2020-10-23 13:11:45 +02:00
Ricki Hirner baedab5228 Account settings: make password a password field 2020-10-23 00:33:52 +02:00
Ricki Hirner 8e0d85615f When an account is created, open it immediately after setup 2020-10-22 18:21:50 +02:00
Ricki Hirner dc1d5b6016 Don't require location permissions for SSID access on Android <8 2020-10-21 13:21:22 +02:00
Ricki Hirner b75064ee3c Version bump to 3.3.4.1 2020-10-20 12:20:04 +02:00
Ricki Hirner f39a0c2459 Fetch translations from Transifex 2020-10-20 12:19:25 +02:00
Ricki Hirner 93dd713c41 Sync algorithm: when uploading a locally modified resource results in 404/410, try as fresh upload again 2020-10-20 12:10:24 +02:00
Ricki Hirner 817e2e834d Fetch translations from Transifex 2020-10-19 23:17:03 +02:00
Ricki Hirner 5e071c7a61 Fix libraries; version bump to 3.3.4 2020-10-19 23:16:11 +02:00
Ricki Hirner 26a7c72ad7 Update ical4j to 3.0.20 2020-10-19 20:38:43 +02:00
Michael Biebl 9323464dd1 Use dark variant of share icon in Debug activity
We typically provide a dark variant for icons that are used on a light
background. The current share icon sticks out a bit, so add a dark
variant of that icon and use that in the Debug activity.
2020-10-19 16:14:10 +02:00
Ricki Hirner 244dd7e3e7 Version bump to 3.3.4-beta1 2020-10-18 21:20:47 +02:00
Ricki Hirner f365570952 Advanced login: small fixes 2020-10-18 12:32:56 +02:00
Patrick Lang 4f1d546174 Login with advanced options
Adds a new radiobutton for login with advanced options
2020-10-17 18:58:08 +00:00
Michael Biebl 998da1bebf Typo fixes 2020-10-17 11:45:32 +02:00
Ricki Hirner 3e2bb92a15 F-Droid Changelog 2020-10-16 14:05:15 +02:00
Ricki Hirner b77586e31c Fetch translations from Transifex 2020-10-16 13:06:27 +02:00
Ricki Hirner 204e0444f9 Version bump to 3.3.3 2020-10-16 13:02:38 +02:00
Ricki Hirner ebed7e63c2 Nextcloud Login Flow v2: better error handling 2020-10-16 13:02:20 +02:00
Ricki Hirner 0d9c78ee07 Debug info: add Share action hint 2020-10-16 12:40:48 +02:00
Ricki Hirner 6666b28f31 Version bump to 3.3.3-beta4 2020-10-15 18:18:28 +02:00
Ricki Hirner acbd93ce7e Update gradle plugin to 4.1; update dependencies 2020-10-15 18:08:39 +02:00
Ricki Hirner 61904434ff Fetch translations from Transifex 2020-10-14 13:26:49 +02:00
Ricki Hirner 33f5cf8099 Version bump to 3.3.3-beta3 2020-10-14 13:25:36 +02:00
Ricki Hirner d52e94ebe8 Fix bug where OpenTasks permissions where hidden in UI when tasks.org was not installed 2020-10-13 20:34:09 +02:00
Ricki Hirner 72faa6a456 Update dependencies 2020-10-13 20:33:36 +02:00
Ricki Hirner 3204000411 Nextcloud: use Login Flow v2 for better browsing experience and to support things like Webauthn 2020-10-05 15:45:32 +02:00
Ricki Hirner 63572ad62a Debug info: show all permissions 2020-10-04 10:45:49 +02:00
Ricki Hirner 394e319959 Version bump to 3.3.3-beta2 2020-10-03 14:22:36 +02:00
Ricki Hirner bd154a6315 Fetch translations from Transifex 2020-10-03 14:21:23 +02:00
Ricki Hirner 35b92f4469 Background location: use mandatory wording for explanation [https://support.google.com/googleplay/android-developer/answer/9799150?hl=en#prominent_disclosure] 2020-10-03 14:11:43 +02:00
Ricki Hirner eaf5b37c01 Version bump to 3.3.3-beta1 2020-10-02 13:12:24 +02:00
Ricki Hirner 2656e7e58d Fetch translations from Transifex 2020-10-02 13:04:41 +02:00
Ricki Hirner 7e98563038 Improve logging notification/debug info; fix account settings UI WiFi permission info 2020-10-02 13:03:33 +02:00
Ricki Hirner 4a83df50b6 Error "Expected 207, got 200": show HTTP request/body in debug info 2020-10-01 12:23:12 +02:00
Ricki Hirner cea038f29b Small compatibility fix/lint 2020-10-01 11:35:47 +02:00
Ricki Hirner 0bb1aa0243 Android 11: declare package visibility; adapt debug info 2020-10-01 11:05:17 +02:00
Ricki Hirner de7ca2cf16 Refactor navigation from/to account settings activity 2020-10-01 11:05:17 +02:00
Ricki Hirner f057b41c0f Android 11: handle new background location permissions for WiFi SSID restriction 2020-10-01 11:05:17 +02:00
Ricki Hirner ab5f834ded Android 11: increase target SDK level; PermissionsFragment: ask to keep permissions on Android >= 11 2020-10-01 11:05:15 +02:00
Ricki Hirner 31c9f0f745 F-droid release notes 2020-09-29 15:57:48 +02:00
Ricki Hirner d9f8185604 Version bump to 3.3.2 2020-09-29 15:23:53 +02:00
Ricki Hirner da38fab28d Version bump to 3.3.2-beta5 2020-09-27 16:55:42 +02:00
Ricki Hirner 511ae6ad4e Fetch translations from Transifex 2020-09-27 16:55:12 +02:00
Ricki Hirner 1883bc8f64 Sync intervals: remove endless loop, add tests 2020-09-27 16:48:30 +02:00
Ricki Hirner def9222148 Cache task provider in the UI 2020-09-25 12:46:41 +02:00
Ricki Hirner 9c2b81017f Collections fragment: use current task provider's status only 2020-09-25 12:46:41 +02:00
Ricki Hirner 8a8ae586c1 Version bump to 3.3.2-beta4 2020-09-21 11:14:22 +02:00
Ricki Hirner 517e1daea9 Fix tasks sync interval setting for tasks.org 2020-09-20 22:30:01 +02:00
Ricki Hirner 559665a481 Bump version to 3.3.2-beta3 2020-09-20 14:23:02 +02:00
Ricki Hirner 65f5f3cf62 Fetch translations from Transifex 2020-09-20 14:22:29 +02:00
Ricki Hirner 92bf8d55a1 Fix permissions UI 2020-09-20 14:21:05 +02:00
Ricki Hirner 093671a43d Dependencies; lint 2020-09-19 14:50:39 +02:00
Ricki Hirner 36263e0044 Fetch translations from Transifex 2020-09-18 16:54:19 +02:00
Ricki Hirner 1ea156750c Update cert4android/vcard4android, bump version to 3.3.2-beta2 2020-09-18 16:52:39 +02:00
Ricki Hirner 799eb05097 Small TasksWatcher fix; update ical4android and build tools 2020-09-17 19:38:43 +02:00
Ricki Hirner 1a92067262 Version bump to 3.3.2-beta1 2020-09-15 21:59:37 +02:00
Ricki Hirner a9d652684e tasks.org support 2020-09-15 21:59:18 +02:00
Ricki Hirner 1cd17a88f7 Update strings 2020-09-14 10:15:40 +02:00
Ricki Hirner 5a9dc79595 Move translation pt-rBR to pt 2020-09-13 18:09:26 +02:00
Ricki Hirner 62a4420d73 improve HTTP requests in debug info; cert4android update 2020-09-09 21:49:45 +02:00
Ricki Hirner ff8de74f21 Add F-Droid Changelog 2020-09-09 12:54:21 +02:00
Ricki Hirner ba973a7320 Fetch translations from Transifex 2020-09-09 12:06:12 +02:00
Ricki Hirner 5bfc343629 Version bump to 3.3.1; minor dependency updates 2020-09-09 12:00:02 +02:00
Ricki Hirner d6df81586b Fetch translations from Transifex 2020-08-31 12:43:17 +02:00
Ricki Hirner c502834530 Version bump to 3.3.1-beta1 2020-08-31 12:41:53 +02:00
Ricki Hirner 638f9f9264 Adaptation for Google Contacts in Android 11: export address book authenticator so that Google Contacts shows the address book accounts 2020-08-31 12:41:34 +02:00
Ricki Hirner ed38d38f20 Scripts etc. 2020-08-31 12:41:33 +02:00
Ricki Hirner 838855b9e8 Add F-Droid changelog for 3.3 2020-08-23 18:02:32 +02:00
Ricki Hirner 99502d827d Fetch translations from Transifex 2020-08-22 13:21:15 +02:00
Ricki Hirner 0ee9ed4999 Version bump to 3.3 2020-08-22 13:17:36 +02:00
Ricki Hirner b2a6bddfa5 Update dependencies (including ical4android) 2020-08-22 13:17:04 +02:00
Ricki Hirner 6898e05b7f Version bump to 3.3-beta5 2020-08-15 13:36:17 +02:00
Ricki Hirner 83eca1ea3d Update app settings dynamically when they are changed 2020-08-15 12:37:58 +02:00
Ricki Hirner 4e42aa1375 Use plain-text debug info instead of HTML, again
* use UTF-8
* use plain-text tables
2020-08-15 12:19:58 +02:00
Ricki Hirner 949049f414 Fetch translations from Transifex 2020-08-12 12:12:11 +02:00
Ricki Hirner 5346095d66 Version bump to 3.3-beta4 2020-08-12 12:11:23 +02:00
Ricki Hirner 7358461ecc Add settings to debug info 2020-08-12 12:11:09 +02:00
Ricki Hirner 22c399e6fd Fixes for Android <25 2020-08-10 15:32:30 +02:00
Ricki Hirner 6b2828646e Version bump to 3.3-beta3 2020-08-10 15:09:36 +02:00
Ricki Hirner 6ce830442d Rewrite DebugInfoActivity 2020-08-10 15:09:15 +02:00
Ricki Hirner 0129f4bcf8 Fix Sync all shortcut 2020-08-10 15:09:14 +02:00
Ricki Hirner d4942cbf14 Some lint 2020-08-07 12:23:50 +02:00
Ricki Hirner f18a8cb9ab Update AppIntro 2020-08-07 11:44:32 +02:00
Ricki Hirner f4194a7f95 Update dependencies 2020-08-07 11:44:21 +02:00
Ricki Hirner a79ec19c6a Provide "Sync all accounts" option
* show sync status in accounts overview
* add "sync all accounts" in accounts overview
* add "sync all accounts" shortcut
2020-08-02 17:22:12 +02:00
Ricki Hirner a14169d19d Bump version code 2020-08-02 17:14:36 +02:00
Ricki Hirner 28dc549744 Fix isSyncable problems
- set isSyncable(AddressBooks)=0 at account creation when there is no CardDAV service
- OpenTasksWatcher: enumerate real accounts and not services from DB -> isSyncable(tasks) will be set to 0 correctly for accounts without CalDAV service
- SyncAdapter base class: set autoInitialize to true, although it should not be needed regularly
2020-08-02 17:14:24 +02:00
Ricki Hirner 3069d79e8b Version bump to 3.2.1.4 2020-08-01 00:05:15 +02:00
Ricki Hirner 2e9f4c4a95 Update ical4android, vcard4android 2020-08-01 00:04:53 +02:00
Ricki Hirner a833d002df Don't grey out WiFi SSID setting when data saver is active; update dependencies 2020-08-01 00:04:53 +02:00
Ricki Hirner 87a52cceef Sync interval settings: wait with UI update until settings have arrived in sync framework 2020-08-01 00:04:53 +02:00
Ricki Hirner 349c1beab5 Fetch translations from Transifex 2020-07-25 11:55:20 +02:00
Ricki Hirner 8835220116 Update vcard4android, version bump to 3.2.1.3 2020-07-25 11:50:04 +02:00
Ricki Hirner 6982304ea3 Nextcloud login flow: decode parameters in nc://login URL 2020-07-24 18:39:20 +02:00
Ricki Hirner 4aee529457 Rename account: handle exceptions (for instance, when the desired name already exists) 2020-07-22 13:16:43 +02:00
Ricki Hirner 6e84ea52c8 Fetch translations from Transifex 2020-07-22 11:13:05 +02:00
Ricki Hirner ab3122e731 Version bump to 3.2.1.2 2020-07-22 11:12:18 +02:00
Ricki Hirner 3325e6a9bc Update dependencies 2020-07-21 15:54:43 +02:00
Ricki Hirner b120a797f2 Sort accounts in AccountsActivity by name 2020-07-20 19:17:12 +02:00
Ricki Hirner eca4ac177d Version bump to 3.2.1.2-beta1 2020-07-15 13:10:19 +02:00
Ricki Hirner c5960b6b40 Regenerate UID if it was dropped from an existing resource 2020-07-15 13:09:43 +02:00
Ricki Hirner 0f44b63262 Version bump to 3.2.1.1 2020-07-11 22:26:48 +02:00
Ricki Hirner 01ae43bdcc Fix crash in ical4android, update okhttp 2020-07-11 22:26:28 +02:00
Ricki Hirner c6d4a07b05 Fix SyncManager tests on Android 5 2020-07-10 18:15:02 +02:00
Ricki Hirner 8104ed4aed SyncManager tests 2020-07-10 17:21:49 +02:00
Ricki Hirner 4f0236e14b Fetch translations from Transifex 2020-07-09 14:59:56 +02:00
Ricki Hirner 86c49f2afa Version bump to 3.2.1 2020-07-09 14:59:04 +02:00
Ricki Hirner ab489dbb44 Sync fix 2020-07-09 14:58:48 +02:00
Ricki Hirner e0d0ef7370 Version bump to 3.2 2020-07-07 17:08:07 +02:00
Ricki Hirner 047a115171 Minor settings provider change 2020-07-07 16:50:17 +02:00
Ricki Hirner fd72d4611f Fetch translations from Transifex 2020-07-07 16:49:36 +02:00
Ricki Hirner 519dda024b Update ical4android 2020-07-07 16:20:36 +02:00
Ricki Hirner c746aa4ddf Lint 2020-07-06 12:26:18 +02:00
Ricki Hirner ec6b2dd16a Version bump to 3.2-beta3 2020-07-06 11:19:56 +02:00
Ricki Hirner 1e57b83a66 Sync algorithm: small refactoring 2020-07-06 11:14:12 +02:00
Ricki Hirner 32b0c8355b Merge branch 'verify-user-data' into 'dev-3.x-ose'
When creating an addressbook account, verify the stored user data

See merge request bitfireAT/davx5-ose!33
2020-07-05 13:34:16 +00:00
Ricki Hirner 8e2bee7d36 Improve debug info: software information 2020-07-05 15:30:47 +02:00
Ricki Hirner 3c202d35d5 Set address book accounts isSyncable=1 at creation (and not only after first sync) 2020-07-05 15:30:47 +02:00
Alex Baker d82341f310 Clear url, username, and name errors 2020-07-05 15:30:47 +02:00
Michael Biebl 77c00414c1 Intro: Use consistent padding among screens
The battery optimization screen used some additional padding which made
it look out of place when swiping through the intro screens.
Instead of applying app:cardUseCompatPadding="true" to the other
screens, we remove it from intro_battery_optimizations.xml.

While at it, fix the indentation of the TextView element.
2020-07-05 15:30:47 +02:00
Ricki Hirner 4b26ab1f34 rename useLocal and useRemote for better readability 2020-07-05 15:30:47 +02:00
Ricki Hirner 9582e08d0e Sync algorithm: use resource name (and not ETag) to determine whether a dirty local record is new or modified 2020-07-05 15:30:47 +02:00
Ricki Hirner 0cdb2d1ff3 Fetch translations from Transifex 2020-07-03 16:49:54 +02:00
Ricki Hirner 31b75cffa3 Version bump to 3.2-beta2 2020-07-03 16:41:35 +02:00
Ricki Hirner 71111172cb Task improvements 2020-07-03 16:41:17 +02:00
Ricki Hirner 357e7cca24 Login: show "username/password wrong" when 401 is encountered during resource detection; update ical4android 2020-07-03 11:34:14 +02:00
Ricki Hirner a652d5bccf Improve Schedule-Tag handling 2020-07-03 11:34:14 +02:00
Ricki Hirner c794e1ffbb Resource detection: evaluate priority/weight of multiple SRV records 2020-07-03 11:34:14 +02:00
Ricki Hirner 9350818029 Events scheduling: support schedule-tag 2020-07-03 11:34:14 +02:00
Ricki Hirner 9f0db15bca Fetch translations from Transifex 2020-06-29 23:15:13 +02:00
Ricki Hirner 73e2d554fb Update dependencies; bump version to 3.1-beta2 2020-06-29 23:14:20 +02:00
Ricki Hirner fd44aea3ce Minor group-scheduling improvements; ical4android: always generate DTEND instead of DURATION 2020-06-29 23:13:52 +02:00
Ricki Hirner c0cf9194b7 Shorten PRODID; don't minify debug builds anymore; ical4android update 2020-06-22 16:25:03 +02:00
Ricki Hirner ca3b308018 Show CalDAV/CardDAV account settings only when CalDAV/CardDAV is present; update ical4android 2020-06-19 13:46:54 +02:00
Ricki Hirner c9ccbb73ac Update dependencies 2020-06-18 17:44:03 +02:00
Ricki Hirner 9ee65a229d Always set "Sync over WiFi only" when data saver is active 2020-06-16 17:10:23 +02:00
Michael Biebl 27e5504de8 When creating an addressbook account, verify the stored user data
Apparently even with newer Android versions we sometimes fail to store
the user data. If we fail to save the main account, synchronization will fail.
2020-06-14 23:41:15 +02:00
Ricki Hirner d3e851cb58 Version bump to 3.1.1 2020-06-14 16:39:37 +02:00
Ricki Hirner 453200ab1e Version bump to 3.1.1-beta2 2020-06-12 13:39:47 +02:00
Ricki Hirner c51677b001 ical4android update 2020-06-12 13:39:32 +02:00
Ricki Hirner aeb0dd70f8 Use some Kotlin extensions 2020-06-11 23:22:46 +02:00
Ricki Hirner 47874bf7eb Fetch translations from Transifex 2020-06-11 18:32:10 +02:00
Ricki Hirner e0f90bb311 Version bump to 3.1.1-beta1 2020-06-11 18:31:26 +02:00
Ricki Hirner 055fdeaf4c Use fragment-ktx viewModels(); begin removing I- prefix from interface names 2020-06-11 18:26:19 +02:00
Ricki Hirner cc092daf72 R8: keep all ez-vcard properties/parameters in release builds 2020-06-11 14:38:49 +02:00
Ricki Hirner 9acbb49a27 Fix UI crash 2020-06-11 00:07:08 +02:00
Ricki Hirner c536a8a50c Refactor settings management 2020-06-11 00:07:07 +02:00
Ricki Hirner d3bed790ab Improve singletons and settings management 2020-06-11 00:03:43 +02:00
Ricki Hirner 2a24e05161 LocalAddressBook.findAll: don't return address book accounts without associated main account 2020-06-05 12:58:55 +02:00
Ricki Hirner 9e08e73fc6 AccountActivity: use constructor instead of initialize() to avoid non-initialized lateinit properties 2020-06-05 12:49:23 +02:00
Ricki Hirner 80df1eecaa Add F-Droid changelog 2020-05-31 21:05:40 +02:00
Ricki Hirner 4e062bc621 Version bump to 3.1 2020-05-31 20:52:14 +02:00
Ricki Hirner 02a1899109 Fetch translations from Transifex 2020-05-31 20:51:36 +02:00
Ricki Hirner 59180cda06 Version bump to 3.1-rc4 2020-05-31 17:36:48 +02:00
Ricki Hirner 74cb4963e4 Update libraries; use Kotlin for JDK7 2020-05-31 16:32:48 +02:00
Ricki Hirner 0e4e655d51 Link PermissionsActivity from app settings 2020-05-31 16:32:48 +02:00
Ricki Hirner 76020736bb Don't use R8 shrinking for debug builds/tests 2020-05-31 01:03:33 +02:00
Ricki Hirner 1e14bbfe42 Version bump to 3.1-rc3 2020-05-30 23:53:42 +02:00
Ricki Hirner 6f5363ef7a Workaround: Android loses initial account data sometimes 2020-05-30 23:53:42 +02:00
Ricki Hirner cd85c4b05a Completely remove customCerts build flag; shrink resources for release builds 2020-05-30 23:53:42 +02:00
Ricki Hirner 4bef2420df Fix account name selection 2020-05-30 20:29:04 +02:00
Ricki Hirner 9f052f6231 Documentation: link to JDK8 2020-05-30 14:19:59 +02:00
Ricki Hirner 877d06abba Version bump to 3.1-rc2 2020-05-30 14:19:58 +02:00
Ricki Hirner 822720e597 Enable R8 desugaring also for debug builds 2020-05-30 14:19:02 +02:00
Ricki Hirner 24c583b784 Fetch translations from Transifex 2020-05-29 00:27:08 +02:00
Ricki Hirner c3f6791baa Version bump to 3.1-rc1 2020-05-29 00:27:08 +02:00
Ricki Hirner a09f1575c2 Update dependencies 2020-05-29 00:27:08 +02:00
Ricki Hirner 63898da6e3 Fix some TextInputLayouts 2020-05-27 16:20:47 +02:00
Ricki Hirner 558ced82da Show all detected email addresses as account name suggestion 2020-05-27 16:20:47 +02:00
Ricki Hirner b59f6c8b7e Update logger tags 2020-05-27 16:20:47 +02:00
Ricki Hirner 85beb90b2f Fix exception in OpenTasksWatcher when account is not available (anymore) 2020-05-27 16:20:47 +02:00
Ricki Hirner e732aa7ebd Version bump to 3.1-beta7 2020-05-27 16:20:47 +02:00
Ricki Hirner eb38791d61 Drop customCerts build flag (alwase use cert4android and thus Conscrypt) 2020-05-27 16:20:47 +02:00
Ricki Hirner 447e7c3bdf Update dependencies 2020-05-27 16:20:47 +02:00
Ricki Hirner a81eb8be51 Nextcloud login fragment: always use fragment view for Snackbar 2020-05-27 16:20:47 +02:00
Ricki Hirner c0e2488ef8 Rename account: handle situation when account doesn't exist (anymore) 2020-05-27 16:20:47 +02:00
Ricki Hirner 34adafeac2 Account settings activity: handle situation when account is not available (anymore) 2020-05-27 16:20:47 +02:00
Ricki Hirner cad2b15a81 Synchronize possibly simultaneous calls to ServiceLoader 2020-05-27 16:20:47 +02:00
Ricki Hirner 0670732cc8 Version bump to 3.1-beta6 2020-05-27 16:20:47 +02:00
Ricki Hirner 3089d42c9f Update dependencies (ical4android: minify VTIMEZONEs) 2020-05-27 16:20:47 +02:00
Ricki Hirner b6bdf2dfaa Update dependencies, remove obsolete version checks 2020-05-27 16:20:47 +02:00
Ricki Hirner 5a657ba5e0 Update okhttp, use new version constant for User-Agent 2020-05-27 16:20:47 +02:00
Ricki Hirner 9055b34a1d Fetch translations from Transifex 2020-05-27 16:20:47 +02:00
Ricki Hirner 7bf1d5618a Version bump to 3.1-beta5 2020-05-27 16:20:47 +02:00
Ricki Hirner f7d001ae1d ical4j: fix XParameter type cast problem with ATTENDEE EMAILs 2020-05-27 16:20:47 +02:00
Ricki Hirner 8cf8dadb8f Update copyright 2020-05-27 16:20:47 +02:00
Michael Biebl d5d9f276a3 Clear all errors when a different login method is chosen
See https://forums.bitfire.at/topic/2275/error-message-not-updated-when-switching-to-different-login-method/
2020-05-27 16:20:47 +02:00
Ricki Hirner a1509f8ce4 Update cert4android, add comment 2020-05-27 16:20:47 +02:00
Michael Biebl af227737f5 Override Brotli meta data in About activity
This is a workaround until
https://github.com/mikepenz/AboutLibraries/issues/490 has been resolved.
2020-05-27 16:20:47 +02:00
Ricki Hirner 65ca1c37e8 Update dependencies 2020-05-27 16:20:47 +02:00
Ricki Hirner 1c2bfc582c Version bump to 3.1-beta4 2020-05-27 16:20:47 +02:00
Ricki Hirner dbc7ca7cc3 Fetch translations from Transifex 2020-05-27 16:20:47 +02:00
Ricki Hirner 6ceca3793c Update dependencies 2020-05-27 16:20:46 +02:00
Ricki Hirner 2ff46e5219 Add app settings to debug info 2020-05-27 16:20:46 +02:00
Ricki Hirner 00cf40a694 Version bump to 3.1-beta3 2020-05-27 16:20:46 +02:00
Ricki Hirner 3445d0df54 Fetch translations from Transifex 2020-05-27 16:20:46 +02:00
Ricki Hirner 1b0720d798 Handle exceptions when event/task SEQUENCE is increased; update ical4android 2020-05-27 16:20:46 +02:00
Ricki Hirner fa1ed29659 Show Donate in Navigation drawer only in ose 2020-05-27 16:20:46 +02:00
Ricki Hirner f01b57bb66 Version bump to 3.1-beta2 2020-05-27 16:20:46 +02:00
Ricki Hirner 94fc3b7cdd Make sure Thread.getContextClassLoader is set while syncing (for ical4j)
* use dav4jvm version that doesn't depend on ServiceLoader anymore
2020-05-27 16:20:46 +02:00
Ricki Hirner ffa40e5e13 Use coroutines instead of threads, when possible 2020-05-27 16:20:46 +02:00
Ricki Hirner 17846c2413 Permissions activity: add App settings button 2020-05-27 16:20:46 +02:00
Ricki Hirner cc6194fdf6 Sync algorithm: use Kotlin coroutines instead of thread-pool executors 2020-05-27 16:20:46 +02:00
Ricki Hirner ebcec6a5a4 Update dependencies; version bump to 3.1.0-beta1 2020-05-27 16:20:46 +02:00
Ricki Hirner 4977a1ab58 New permissions model 2020-05-27 16:20:46 +02:00
Ricki Hirner 7ef847ea35 Update gradle plugin 2020-05-27 16:20:46 +02:00
Ricki Hirner 5e0e26a5e5 Sync cancellation: show in logs, cancel whole thread group 2020-05-27 16:20:46 +02:00
Ricki Hirner dcdc659882 Version bump to 3.0.1-beta1 2020-05-27 16:20:46 +02:00
Ricki Hirner 96adb926c6 Use ical4j 3.x 2020-05-27 16:20:46 +02:00
Ricki Hirner 14bdaaa189 minor change in evil manufacturer warning in intro fragment 2020-05-27 16:15:19 +02:00
Ricki Hirner 86bb432625 Fix intro fragment checkbox/button interaction 2020-05-27 16:15:16 +02:00
Ricki Hirner 97d657e501 Fix crash because of empty drawable in -ose version 2020-04-27 12:04:09 +02:00
Ricki Hirner 4067afb20d Fix build 2020-04-24 21:04:43 +02:00
Ricki Hirner 843f1e099b Merge branch 'dev-3.x-ose' into master-ose 2020-04-22 13:16:02 +02:00
Ricki Hirner 4ed9d8a6a8 F-Droid changelog for 3.0 2020-04-22 13:15:19 +02:00
Ricki Hirner 5b60ccaa2e Merge branch 'dev-3.x-ose' into master-ose 2020-04-22 11:53:27 +02:00
Ricki Hirner 9bdfcb5dc1 Update AboutLibraries, bump version to 3.0 2020-04-22 11:49:28 +02:00
Ricki Hirner fd92267c4d Replace stock images by images from undraw.co 2020-04-21 19:19:20 +02:00
Ricki Hirner 3c100274ad Fetch translations from Transifex 2020-04-21 18:49:46 +02:00
Ricki Hirner ede9f2a472 cert4android: update strings 2020-04-21 18:49:46 +02:00
Ricki Hirner 884dbdbd3d Adapt WebView progress bar; don't use okhttp BOM 2020-04-21 18:49:46 +02:00
Ricki Hirner e267e620ba Fix build; bump version to 3.0-beta2 2020-04-21 18:49:46 +02:00
Ricki Hirner f32006254f Nextcloud Login flow: show progress and errors (including TLS errors) 2020-04-21 18:49:46 +02:00
Ricki Hirner 2f92002fc3 Update dependencies; clean up ProGuard/R8 rules 2020-04-21 18:49:46 +02:00
Ricki Hirner a7c814bafd Use new AboutLibraries gradle plugin 2020-04-21 18:49:46 +02:00
Ricki Hirner e124ce2f70 Update gradle plugin 2020-04-21 18:49:46 +02:00
Ricki Hirner 17c9537fc5 Adapt login styles 2020-04-21 18:49:46 +02:00
Ricki Hirner ad4c7c97c2 Don't cancel notifications of other sync threads (lets important notifications disappear sometimes) 2020-04-21 18:49:46 +02:00
Ricki Hirner 27675de7ec Version bump to 3.0-beta1 2020-04-21 18:49:46 +02:00
Ricki Hirner 204ca9120d Update About activity 2020-04-21 18:49:46 +02:00
Ricki Hirner 061df6a014 Enable Brotli (in addition to gzip) compression 2020-04-21 18:49:46 +02:00
Ricki Hirner aab676b7e4 Code cleanup 2020-04-21 18:49:46 +02:00
Ricki Hirner 7fd2a99edb Keep TLS 1.0 and 1.1 for now; update dependencies 2020-04-21 18:49:46 +02:00
Ricki Hirner b7893398bd Fix some vector icons 2020-04-21 18:49:46 +02:00
Ricki Hirner 07f2c85ae1 Remove bitmap notification icons 2020-04-21 18:49:45 +02:00
Ricki Hirner 08cefe9f80 Fix tests 2020-04-21 18:49:45 +02:00
Ricki Hirner 0b9aeb05ab Update okhttp to 4.5.0 and dav4jvm to 2.0 2020-04-21 18:49:45 +02:00
Ricki Hirner 1d7e4e8d33 Revert "Update libraries (including dnsjava 2.x -> 3.x)"
This reverts commit bcd7700dc7d2d67a6c3a25c8f9a18694e496a29c.
2020-04-21 18:49:45 +02:00
Ricki Hirner 621c32da2a Remove vectorDrawables support library and multidex 2020-04-21 18:49:45 +02:00
Ricki Hirner dcb3dd577a Require Android 5 (SDK level 21); update gradle
Recent okhttp versions require Android 5. Also, a lot of workarounds
(like non-vector graphics for notification icons) which are cumbersome
to maintain have accumulated.

So, DAVx5 will require Android 5 (SDK level 21) from the next release,
which will probably tagged as 3.0.

2.6.x (currently 2.6.5) will be the last branch which supports Android 4.4.
Maybe there will never be a version after 2.6.5, which will still be offered for
Android 4.4 devices. If there's big need for a maintenance release (like severe security
problems), there may be further 2.6.x releases (branched from 2.6.5), but
development and new features will only go to the master branch.
2020-04-21 18:49:45 +02:00
Ricki Hirner a07d849c35 Version bump to 2.7.0-beta2 2020-04-21 18:49:45 +02:00
Ricki Hirner cea8d54556 lint; remove soldupe for now 2020-04-21 18:49:45 +02:00
Ricki Hirner 6189eef55f Update libraries (including dnsjava 2.x -> 3.x)
* dnsjava 3.x is now compatible with Android by default; removed compatibility code
2020-04-21 18:49:45 +02:00
Ricki Hirner 07dc7592b7 Minor code cleanup 2020-04-21 18:49:45 +02:00
Ricki Hirner da42ad63a2 Intro optimizations 2020-04-21 18:49:45 +02:00
Ricki Hirner a2eabbdcee battery intro: capitalize manufacturer and re-check on activity resume; update dependencies 2020-04-21 18:49:45 +02:00
Ricki Hirner ad185865c4 Version bump to 2.7.0-beta1 2020-04-21 18:49:45 +02:00
Ricki Hirner b0a5bfccad Introduce IntroActivity instead of startup fragments 2020-04-21 18:49:45 +02:00
Ricki Hirner 4303132a38 Fetch translations from Transifex 2020-04-21 18:48:47 +02:00
Ricki Hirner a0da93f910 cert4android: update strings 2020-04-21 18:48:47 +02:00
Ricki Hirner 3c38f063c5 Adapt WebView progress bar; don't use okhttp BOM 2020-04-21 18:48:47 +02:00
Ricki Hirner c18ef6304b Fix build; bump version to 3.0-beta2 2020-04-21 18:48:47 +02:00
Ricki Hirner 2542aba3f0 Nextcloud Login flow: show progress and errors (including TLS errors) 2020-04-21 18:48:47 +02:00
Ricki Hirner 75ef28bd1d Update dependencies; clean up ProGuard/R8 rules 2020-04-21 18:48:47 +02:00
Ricki Hirner 3aa729051f Use new AboutLibraries gradle plugin 2020-04-21 18:48:47 +02:00
Ricki Hirner 49fafb40e3 Update gradle plugin 2020-04-21 18:48:47 +02:00
Ricki Hirner d1dcf889f3 Adapt login styles 2020-04-21 18:48:47 +02:00
Ricki Hirner 91f9baf042 Don't cancel notifications of other sync threads (lets important notifications disappear sometimes) 2020-04-21 18:48:47 +02:00
Ricki Hirner 453e703bc0 Version bump to 3.0-beta1 2020-04-21 18:48:47 +02:00
Ricki Hirner 0d5b3c2816 Update About activity 2020-04-21 18:48:47 +02:00
Ricki Hirner 86744db3b3 Enable Brotli (in addition to gzip) compression 2020-04-21 18:48:47 +02:00
Ricki Hirner 2965ae4b1e Code cleanup 2020-04-21 18:48:47 +02:00
Ricki Hirner 3a1edbafbf Keep TLS 1.0 and 1.1 for now; update dependencies 2020-04-21 18:48:47 +02:00
Ricki Hirner 7f5f3e492b Fix some vector icons 2020-04-21 18:48:47 +02:00
Ricki Hirner e7dce774c8 Remove bitmap notification icons 2020-04-21 18:48:47 +02:00
Ricki Hirner 25b97e96e1 Fix tests 2020-04-21 18:48:47 +02:00
Ricki Hirner 0d66d29380 Update okhttp to 4.5.0 and dav4jvm to 2.0 2020-04-21 18:48:47 +02:00
Ricki Hirner 029c4737bd Revert "Update libraries (including dnsjava 2.x -> 3.x)"
This reverts commit bcd7700dc7d2d67a6c3a25c8f9a18694e496a29c.
2020-04-21 18:48:47 +02:00
Ricki Hirner 5e5d59889f Remove vectorDrawables support library and multidex 2020-04-21 18:48:47 +02:00
Ricki Hirner bc9aaf04fe Require Android 5 (SDK level 21); update gradle
Recent okhttp versions require Android 5. Also, a lot of workarounds
(like non-vector graphics for notification icons) which are cumbersome
to maintain have accumulated.

So, DAVx5 will require Android 5 (SDK level 21) from the next release,
which will probably tagged as 3.0.

2.6.x (currently 2.6.5) will be the last branch which supports Android 4.4.
Maybe there will never be a version after 2.6.5, which will still be offered for
Android 4.4 devices. If there's big need for a maintenance release (like severe security
problems), there may be further 2.6.x releases (branched from 2.6.5), but
development and new features will only go to the master branch.
2020-04-21 18:48:47 +02:00
Ricki Hirner 3ba2922bde Version bump to 2.7.0-beta2 2020-04-21 18:48:47 +02:00
Ricki Hirner 6277f35db4 lint; remove soldupe for now 2020-04-21 18:48:47 +02:00
Ricki Hirner b0c53fb852 Update libraries (including dnsjava 2.x -> 3.x)
* dnsjava 3.x is now compatible with Android by default; removed compatibility code
2020-04-21 18:48:47 +02:00
Ricki Hirner ce0e623912 Minor code cleanup 2020-04-21 18:48:47 +02:00
Ricki Hirner d1709df0b6 Intro optimizations 2020-04-21 18:48:47 +02:00
Ricki Hirner 5469dee1f2 battery intro: capitalize manufacturer and re-check on activity resume; update dependencies 2020-04-21 18:48:47 +02:00
Ricki Hirner 489f7ac639 Version bump to 2.7.0-beta1 2020-04-21 18:48:47 +02:00
Ricki Hirner 58ca99198f Introduce IntroActivity instead of startup fragments 2020-04-21 18:48:47 +02:00
Ricki Hirner 8a46fcedba Do full resync if "past event time limit" is changed from number to null 2020-03-29 17:57:25 +02:00
Ricki Hirner a0816c11d2 Show languages in About 2020-03-07 18:51:02 +01:00
Ricki Hirner c06950751b Version bump to 2.6.5 2020-03-07 12:30:52 +01:00
Ricki Hirner cfc0130bec Version bump to 2.6.5-beta1; update dependencies 2020-03-06 12:08:36 +01:00
Alex Baker 213851856e Clear password error on text changed 2020-03-06 12:04:48 +01:00
Ricki Hirner b7e60cd143 Only use multi-get for tasks sync, too 2020-03-06 12:04:48 +01:00
Ricki Hirner 37a299d0f7 Fetch translations from Transifex 2020-03-06 00:23:42 +01:00
Ricki Hirner 41d33fac44 Version bump to 2.6.5 2020-03-06 00:22:36 +01:00
Ricki Hirner 3dc2aa65df Sync algorithm bug fixes/improvements
- Collection sync: don't save new sync state before downloading is finished
- throw exception when waiting for completion times out
- always use multi-get, even for single vCards/iCalendars
2020-03-06 00:06:07 +01:00
Ricki Hirner 43f4d9c05d Merge branch 'textlayout_errors' into 'master-ose'
Move errors from EditTexts to TextInputLayouts

See merge request bitfireAT/davx5-ose!28
2020-03-03 21:38:09 +00:00
Ricki Hirner 6c0b555ec9 lint 2020-03-03 18:59:33 +01:00
Ricki Hirner 8918382003 Use requireView() instead of view!! and requireActivity() instead of activity!! 2020-03-03 16:28:32 +01:00
Ricki Hirner e30c41828f Update gradle plugin, okhttp 2020-03-03 14:46:00 +01:00
Ricki Hirner 224c92cc87 Update gradle version and Android plugin; dependencies 2020-03-03 14:46:00 +01:00
Ricki Hirner 4498f9bf03 Update okhttp to 3.12.9 2020-03-03 14:45:52 +01:00
Alex Baker b3c7f1f9ef Move errors from EditTexts to TextInputLayouts 2020-02-27 08:57:07 -06:00
Ricki Hirner 5622d743c3 Version bump to 2.6.4 2020-02-20 18:31:01 +01:00
Ricki Hirner ae4c6e94c8 Fetch translations from Transifex 2020-02-20 18:15:50 +01:00
Ricki Hirner 7bdb5c5a97 Update cert4android 2020-02-20 18:13:57 +01:00
Ricki Hirner 62c0dfbaee Update support libraries 2020-02-20 18:10:57 +01:00
Ricki Hirner 53f35d5ee3 Event sync: delete exceptions from events when events are mass-deleted, too 2020-02-20 18:10:57 +01:00
Ricki Hirner c9dab31067 Webcal calendars: use UrlUtils.equals to find matching calendar; update dependencies 2020-01-24 20:22:02 +01:00
Ricki Hirner ac3e9fb825 Fetch translations from Transifex 2020-01-23 18:39:07 +01:00
Ricki Hirner 8325fdcf86 Version bump to 2.6.4-beta1 2020-01-23 18:37:41 +01:00
Ricki Hirner 3f2090ecc7 Update ical4android (alarm handling) and AboutLibraries 2020-01-20 22:17:19 +01:00
Ricki Hirner e236b07184 Use okhttp version defined by DAVx5 (dav4jvm version may be older) 2020-01-16 19:57:38 +01:00
Ricki Hirner 3905679576 Version bump to 3.6.2.1 2020-01-14 21:11:58 +01:00
Ricki Hirner 2f33374649 Update dependencies (including okhttp 3.12.8) 2020-01-14 21:08:49 +01:00
Ricki Hirner 54222d3328 Version bump to 2.6.3; add F-Droid changelog 2020-01-07 14:47:55 +01:00
Ricki Hirner 31339f3014 Don't create default reminder for full-day events 2020-01-07 09:47:32 +01:00
Ricki Hirner 6f1d513e54 Version bump to 2.6.3-beta6 2020-01-06 20:59:44 +01:00
Ricki Hirner fef8ce2366 Improve resource detection: detect address books/calendars when they are identical with their home-set 2020-01-06 16:25:39 +01:00
Ricki Hirner c9ca84f14b Account activity: make space for FAB at end of collection list 2020-01-06 14:40:30 +01:00
Ricki Hirner 53698adda2 Version bump to 2.6.3-beta5 2020-01-06 13:50:29 +01:00
Ricki Hirner 27c89e762e Fetch translations from Transifex 2020-01-06 13:49:35 +01:00
Ricki Hirner 92394bb6af Take default reminder from settings provider, if available 2020-01-06 13:39:46 +01:00
Ricki Hirner eba5280a58 Update ical4android, okhttp 2020-01-05 13:58:16 +01:00
Ricki Hirner c3157f1256 Bump version to 2.6.3-beta4 2019-12-30 17:35:50 +01:00
Ricki Hirner 5c795950af Fetch translations from Transifex 2019-12-30 17:28:28 +01:00
Ricki Hirner 15f5152a7d UiUtils.launchUri: show toast if no browser is installed 2019-12-28 15:33:59 +01:00
Ricki Hirner 03f54f5402 Add link to Privacy policy to Accounts drawer 2019-12-28 15:16:03 +01:00
Ricki Hirner 41ba92cc4c Default alarms: don't take app-wide Settings into account 2019-12-27 23:22:27 +01:00
Ricki Hirner c267c92a87 Rewrite account settings to use ViewModel 2019-12-27 19:00:33 +01:00
Ricki Hirner 3111e54d1a Rename "default alarm" to "default reminder" 2019-12-27 19:00:19 +01:00
Ricki Hirner ebd20866b3 Version bump to 2.6.3-beta3 2019-12-26 17:51:07 +01:00
Ricki Hirner a789246925 Account settings: always call sync adapter for re-sync 2019-12-26 17:49:12 +01:00
Ricki Hirner 11452caa13 cert4android: use new Material theme 2019-12-26 17:26:29 +01:00
Ricki Hirner 7577eb10e8 Rename SyncAdapterService.SYNC_EXTRAS_RELOAD_ALL to SYNC_EXTRAS_FULL_RESYNC 2019-12-26 17:26:29 +01:00
Ricki Hirner 4b579ca419 Introduce default alarm setting
* move .ui.AccountSettingsActivity to .ui.account.SettingsActivity
* add setting for default alarms
* add sync extra: SyncAdapterService.SYNC_EXTRAS_RELOAD_ALL (forces full re-synchronization of all members)
* LocalCollection: add forgetETags() which resets the ETags of members
* account settings: automatic reloading of members when certain settings are modified
2019-12-26 17:26:29 +01:00
Ricki Hirner f9a8de29e2 Fix vcard4android; bump version to 2.6.3-beta2 2019-12-23 22:42:07 +01:00
Ricki Hirner d28f5115fb vCard sync: improve compatibility (for instance with Samsung "Edge panel"); version bump to 2.6.3-beta1 2019-12-22 17:24:52 +01:00
Ricki Hirner 2a40432494 Fetch translations from Transifex 2019-12-22 11:03:28 +01:00
Ricki Hirner 06d79087b0 Version bump to 2.6.2 2019-12-22 11:01:32 +01:00
Ricki Hirner 3e6243901e Version bump to 2.6.2-beta5 2019-12-20 17:14:52 +01:00
Ricki Hirner 054955b89e Use String for SYNC_EXTRAS_PRIORITY_COLLECTIONS becuase LongArray mustn't be used for sync extras 2019-12-20 12:14:39 +01:00
Ricki Hirner c28001f0f1 Version bump to 2.6.2-beta4 2019-12-17 14:01:27 +01:00
Ricki Hirner c4d7e6857b CardDAV: handle (non-standard) TYPE=other for phone numbers and addresses, too (for better compatibility) 2019-12-17 13:34:01 +01:00
Ricki Hirner 4f237d82fb Update theme 2019-12-17 12:55:24 +01:00
Ricki Hirner 05cb460cf5 Version bump to 2.6.2-beta3 2019-12-15 22:22:26 +01:00
Ricki Hirner 2a31ebb2a3 Increase vCard compatibility 2019-12-15 22:14:51 +01:00
Ricki Hirner 372ad8b704 SYNC_EXTRAS_PRIORITY_COLLECTIONS: use LongArray instead of String 2019-12-13 11:06:55 +01:00
Ricki Hirner 296f55651e Introduce SYNC_EXTRAS_PRIORITY_COLLECTIONS; ical4android: map event categories to special extended property 2019-12-12 20:15:30 +01:00
Ricki Hirner fe5c5737ef Update gradle plugin, version bump to 2.6.2-beta2 2019-12-11 12:48:31 +01:00
Ricki Hirner 32fe98a196 ProGuard: keep enum classes 2019-12-07 23:48:48 +01:00
Ricki Hirner de687eaf52 Version bump to 2.6.2-beta1 2019-12-06 12:50:37 +01:00
Ricki Hirner 2062802cb6 Improve usage of Material theme 2019-12-03 18:52:35 +01:00
Ricki Hirner b280941b2b Don't handle uncaught exceptions in debug builds 2019-12-03 18:52:33 +01:00
Ricki Hirner 049145d0ea use Material theme; update AboutLibraries dependency 2019-12-03 18:51:47 +01:00
Ricki Hirner c1162fce8e Library updates; show permissions in debug info 2019-11-30 23:50:42 +01:00
Ricki Hirner 40fd412064 Fetch translations from Transifex 2019-11-25 22:58:38 +01:00
Ricki Hirner c13243f3ae Version bump to 2.6.1.1 2019-11-25 22:58:38 +01:00
Ricki Hirner bda2d64ca4 ical4android: generate DTSTAMP again 2019-11-25 22:57:16 +01:00
Ricki Hirner f974ca2ffa ical4android: unify VEVENT/VTODO parsing 2019-11-24 15:35:42 +01:00
Ricki Hirner d6cd0faeeb ical4android: better compatibility with Outlook timezone IDs 2019-11-24 14:38:44 +01:00
Ricki Hirner 33345db0f0 Managed DAVx5: don't show OpenTasks startup dialog 2019-11-23 14:52:33 +01:00
Ricki Hirner 6e2d7b10d7 Fetch translations from Transifex 2019-11-22 18:04:10 +01:00
Ricki Hirner b9fb983b97 Version bump to 2.6.1 2019-11-22 18:02:55 +01:00
Ricki Hirner fe738636a2 Events: ignore empty strings when processing locally stored events; tasks: clear parent_id when it's not set anymore after a remote update 2019-11-16 18:57:41 +01:00
577 changed files with 39229 additions and 11288 deletions

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
github: bitfireAT
liberapay: DAVx5
custom: 'https://www.davx5.com/donate'
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: DAVx⁵ Community Support
url: https://github.com/bitfireAT/davx5-ose/discussions
about: Ask and answer questions (including feature requests and bug reports) here.
+55
View File
@@ -0,0 +1,55 @@
name: "CodeQL"
on:
push:
branches: [ "dev-ose", main-ose ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "dev-ose" ]
schedule:
- cron: '22 10 * * 1'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'java' ]
steps:
- name: Checkout repository
uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: 17
- uses: gradle/gradle-build-action@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# 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 --no-daemon app:assembleOseDebug
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
+40
View File
@@ -0,0 +1,40 @@
name: Create release
on:
push:
tags:
- v*
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Create release
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- uses: gradle/gradle-build-action@v2
- name: Prepare keystore
run: echo ${{ secrets.android_keystore_base64 }} | base64 -d >$GITHUB_WORKSPACE/keystore.jks
- name: Build signed package
# --no-configuration-cache is only required for AboutLibraries (bitfireAT/davx5#263, mikepenz/AboutLibraries#857)
# Remove it as soon as AboutLibraries is compatbile with the gradle configuration cache.
run: ./gradlew --no-configuration-cache --no-daemon app:assembleRelease
env:
ANDROID_KEYSTORE: ${{ github.workspace }}/keystore.jks
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.android_keystore_password }}
ANDROID_KEY_ALIAS: ${{ secrets.android_key_alias }}
ANDROID_KEY_PASSWORD: ${{ secrets.android_key_password }}
- name: Create Github release
uses: softprops/action-gh-release@v1
with:
prerelease: ${{ contains(github.ref_name, '-alpha') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-rc') }}
files: app/build/outputs/apk/ose/release/*.apk
fail_on_unmatched_files: true
+85
View File
@@ -0,0 +1,85 @@
name: Development tests
on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Tests without emulator
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- uses: gradle/gradle-build-action@v2
- name: Run lint and unit tests
run: ./gradlew app:check
- name: Archive results
uses: actions/upload-artifact@v2
with:
name: test-results
path: |
app/build/outputs/lint*
app/build/reports
test_on_emulator:
name: Tests with emulator
runs-on: ubuntu-latest-4-cores
strategy:
matrix:
api-level: [ 31 ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- uses: gradle/gradle-build-action@v2
- 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 and APKs
uses: actions/cache@v3
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.api-level }}
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86_64
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."
- name: Run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86_64
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew app:connectedCheck
- name: Archive results
if: always()
uses: actions/upload-artifact@v2
with:
name: test-results
path: |
app/build/reports
-31
View File
@@ -1,31 +0,0 @@
image: registry.gitlab.com/bitfireat/docker-android-emulator:latest
before_script:
- git submodule update --init --recursive
- export GRADLE_USER_HOME=`pwd`/.gradle; chmod +x gradlew
cache:
paths:
- .gradle/
test:
tags:
- privileged
script:
- start-emulator.sh
- ./gradlew app:check app:connectedCheck
artifacts:
paths:
- app/build/outputs/lint-results-debug.html
- app/build/reports
- build/reports
pages:
script:
- ./gradlew app:dokka
- mkdir public && mv app/build/dokka public
artifacts:
paths:
- public
only:
- master-ose
-9
View File
@@ -1,9 +0,0 @@
[submodule "ical4android"]
path = ical4android
url = https://gitlab.com/bitfireAT/ical4android.git
[submodule "vcard4android"]
path = vcard4android
url = https://gitlab.com/bitfireAT/vcard4android.git
[submodule "cert4android"]
path = cert4android
url = https://gitlab.com/bitfireAT/cert4android.git
+30
View File
@@ -0,0 +1,30 @@
[main]
host = https://www.transifex.com
lang_map = ar_SA: ar, en_GB: en-rGB, es_MX: es-rMX, fa_IR: fa-rIR, fi_FI: fi, fr_FR: fr-rFR, nb_NO: nb, pt_BR: pt-rBR, sk_SK: sk, sl_SI: sl, sv_SE: sv-rSE, 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
+11
View File
@@ -0,0 +1,11 @@
# This is the list of significant contributors to DAVx5.
#
# This does not necessarily list everyone who has contributed work.
# To see the full list of contributors, see the revision history in
# source control.
Ricki Hirner (bitfire.at)
Bernhard Stockmann (bitfire.at)
Sunik Kupfer (bitfire.at)
Patrick Lang (techbee.at)
+103 -33
View File
@@ -1,47 +1,117 @@
Contributing to DAVx⁵
=====================
**Thank you for your interest in contributing to DAVx⁵!**
Because you're reading this, you're probably interested in
contributing to the DAVx⁵ code. [Other ways to contribute:
see here.](https://www.davx5.com/donate#c306)
To contribute:
# Licensing
1. It's good idea to have a look at the [DAVx⁵ Roadmap](https://gitlab.com/bitfireAT/davx5-ose/wikis/Roadmap)
to see whether the change is already planned. Maybe there's even a link to a
corresponding forum thread there.
1. Determine which project the changes shall go to. There's
the DAVx⁵ main project (this repo), and the [related
libraries](README.md).
1. Please post to the [DAVx⁵ development forum](https://www.davx5.com/forums)
before doing actual work (unless you do it only for yourself, of course).
This will help to coordinate activities and you'll also get hints
about where to start and possible pitfalls.
1. Fork the repository.
1. Do the changes in your repository.
1. Submit a pull request to the original project.
1. Post in the forum again (to make sure the pull request is being notified).
All work in this repository is [licensed under the GPLv3](LICENSE).
We (bitfire.at, initial and main contributors) are also asking you to give us
permission to use your contribution for related non-open source projects
like [Managed DAVx⁵](https://www.davx5.com/organizations/managed-davx5).
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.
Questions, discussion
=====================
# Copyright
We're happy to see questions, discussions etc. in the
[DAVx⁵ development forum](https://www.davx5.com/forums)!
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.
Licensing
=========
# Style guide
All code has to be licensed under the GPL.
Please adhere to the [Kotlin style guide](https://developer.android.com/kotlin/style-guide) and
the following hints to make the source code uniform.
We (bitfire.at, initial developers) are also asking you to double-license the
code so that we can also use it for related non-open source projects like
[Managed DAVx⁵](https://www.davx5.com/organizations/managed-davx5).
**Have a look at similar files and copy their style if you're not certain.**
Please find more about this in the Contributor's License Agreement (CLA)
we'll send to you if you want to contribute.
Sample file (pay attention to blank lines and other formatting):
```
<Copyright header, see above>
class MyClass(int arg1) : SuperClass() {
companion object {
const val CONSTANT_STRING = "Constant String";
fun staticMethod() { // Use static methods when you don't need the object context.
// …
}
}
var someProperty: String = "12345"
var someRelatedProperty: Int = 12345
init {
// constructor
}
/**
* Use KDoc to document important methods. Don't use it dogmatically, but writing proper documentation
* (not just the method name with spaces) helps you to re-think what the method shall really do.
*/
fun aFun1() { // Group methods by some logic (for instance, the order in which they will be called)
} // and alphabetically within a group.
fun anotherFun() {
// …
}
fun somethingCompletelyDifferent() { // two blank lines to separate groups
}
fun helperForSomethingCompletelyDifferent() {
someCall(arg1, arg2, arg3, arg4) // function calls: stick to one line unless it becomes confusing
}
class Model( // two blank lines before inner classes
someArgument: SomeLongClass, // arguments in multiple lines when they're too long for one line
anotherArgument: AnotherLongType,
thirdArgument: AnotherLongTypeName
) : ViewModel() {
fun abc() {
}
}
}
```
In general, use one blank line to separate things within one group of things, and two blank lines
to separate groups. In rare cases, when methods are tightly coupled and are only helpers for another
method, they may follow the calling method without separating blank lines.
## Tests
Test classes should be in the appropriate directory (see existing tests) and in the same package as the
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).
+25 -16
View File
@@ -1,37 +1,46 @@
[![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)
![DAVx⁵ logo](app/src/main/res/mipmap-xxxhdpi/ic_launcher.png)
DAVx⁵
========
Please see the [DAVx⁵ Web site](https://www.davx5.com) for
comprehensive information about DAVx⁵.
comprehensive information about DAVx⁵, including a list of services it has been tested with.
DAVx⁵ is licensed under the [GPLv3 License](LICENSE).
News and updates: [@davx5app](https://twitter.com/davx5app) on Twitter
News and updates:
Help, discussion, feature requests, bug reports and "issues": [DAVx⁵ forums](https://www.davx5.com/forums)
* [@davx5app@fosstodon.org](https://fosstodon.org/@davx5app) on Mastodon
**If you want to support DAVx⁵, please consider [donating to DAVx⁵](https://www.davx5.com/donate)
or [purchasing it](https://www.davx5.com/download).**
Generated KDoc: https://bitfireAT.gitlab.io/davx5-ose/dokka/app/
**Help, feature requests, bug reports: [DAVx⁵ discussions](https://github.com/bitfireAT/davx5-ose/discussions)**
Parts of DAVx⁵ have been outsourced into these libraries:
* [cert4android](https://gitlab.com/bitfireAT/cert4android) custom certificate management
* [dav4jvm](https://gitlab.com/bitfireAT/dav4jvm) WebDAV/CalDav/CardDAV framework
* [ical4android](https://gitlab.com/bitfireAT/ical4android) iCalendar processing and Calendar Provider access
* [vcard4android](https://gitlab.com/bitfireAT/vcard4android) vCard processing and Contacts Provider access
* [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
**If you want to support DAVx⁵, please consider [donating to DAVx⁵](https://www.davx5.com/donate)
or [purchasing it](https://www.davx5.com/download).**
USED THIRD-PARTY LIBRARIES
==========================
Those libraries are used by DAVx⁵ (alphabetically):
The most important libraries which are used by DAVx⁵ (alphabetically):
* [Color Picker](https://github.com/jaredrummler/ColorPicker) [Apache License, Version 2.0](https://github.com/jaredrummler/ColorPicker/LICENSE)
* [dnsjava](http://www.xbill.org/dnsjava/) [BSD License](http://www.xbill.org/dnsjava/dnsjava-current/LICENSE)
* [ez-vcard](https://github.com/mangstadt/ez-vcard) [New BSD License](http://opensource.org/licenses/BSD-3-Clause)
* [iCal4j](https://github.com/ical4j/ical4j) [New BSD License](http://sourceforge.net/p/ical4j/ical4j/ci/default/tree/LICENSE)
* [dnsjava](https://github.com/dnsjava/dnsjava) [BSD License](https://github.com/dnsjava/dnsjava/blob/master/LICENSE)
* [ez-vcard](https://github.com/mangstadt/ez-vcard) [New BSD License](https://github.com/mangstadt/ez-vcard/blob/master/LICENSE)
* [iCal4j](https://github.com/ical4j/ical4j) [New BSD License](https://github.com/ical4j/ical4j/blob/develop/LICENSE.txt)
* [okhttp](https://square.github.io/okhttp) [Apache License, Version 2.0](https://square.github.io/okhttp/#license)
See _About / Libraries_ in the app for all used libraries and their licenses.
+5
View File
@@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Please report security vulnerabilities using our [secure support form](https://www.davx5.com/support) or via email to support-en@davx5.com.
+157 -89
View File
@@ -1,143 +1,211 @@
/*
* Copyright (c) Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'org.jetbrains.dokka'
plugins {
id 'com.android.application'
id 'com.google.devtools.ksp'
id 'com.mikepenz.aboutlibraries.plugin'
id 'dagger.hilt.android.plugin'
id 'kotlin-android'
id 'kotlin-kapt' // remove as soon as Hilt supports KSP [https://issuetracker.google.com/179057202]
}
// Android configuration
android {
compileSdkVersion 29
buildToolsVersion '29.0.2'
compileSdk 34
defaultConfig {
applicationId "at.bitfire.davdroid"
versionCode 312
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
buildConfigField "boolean", "customCerts", "true"
versionCode 403060100
versionName '4.3.6.1'
minSdkVersion 19 // Android 4.4
targetSdkVersion 29 // Android 10.0
multiDexEnabled true // >64k methods for Android 4.4
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
setProperty "archivesBaseName", "davx5-ose-" + getVersionName()
minSdkVersion 21 // Android 5
targetSdkVersion 33 // Android 13
buildConfigField "String", "userAgent", "\"DAVx5\""
// when using this, make sure that notification icons are real bitmaps
vectorDrawables.useSupportLibrary = true
testInstrumentationRunner "at.bitfire.davdroid.CustomTestRunner"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
// enable because ical4android requires desugaring
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
dataBinding.enabled = true
buildFeatures {
buildConfig = true
compose = true
viewBinding = true
dataBinding = true
}
composeOptions {
// Keep this in sync with Kotlin version:
// https://developer.android.com/jetpack/androidx/releases/compose-kotlin
kotlinCompilerExtensionVersion = "1.5.1"
}
flavorDimensions "distribution"
// Java namespace for our classes (not to be confused with Android package ID)
namespace 'at.bitfire.davdroid'
flavorDimensions = [ "distribution" ]
productFlavors {
standard {
versionName "2.6.1-beta4-ose"
ose {
dimension "distribution"
versionNameSuffix "-ose"
}
}
sourceSets {
androidTest {
assets.srcDirs += files("$projectDir/schemas".toString())
}
}
signingConfigs {
bitfire {
storeFile file(System.getenv("ANDROID_KEYSTORE") ?: "/dev/null")
storePassword System.getenv("ANDROID_KEYSTORE_PASSWORD")
keyAlias System.getenv("ANDROID_KEY_ALIAS")
keyPassword System.getenv("ANDROID_KEY_PASSWORD")
}
}
buildTypes {
debug {
minifyEnabled false
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules-release.pro'
shrinkResources true
signingConfig signingConfigs.bitfire
}
}
lintOptions {
disable 'GoogleAppIndexingWarning' // we don't need Google indexing, thanks
disable 'ImpliedQuantity', 'MissingQuantity' // quantities from Transifex may vary
disable 'MissingTranslation', 'ExtraTranslation' // translations from Transifex are not always up to date
disable 'RtlEnabled'
disable 'RtlHardcoded'
disable 'Typos'
lint {
disable 'GoogleAppIndexingWarning', 'ImpliedQuantity', 'MissingQuantity', 'MissingTranslation', 'ExtraTranslation', 'RtlEnabled', 'RtlHardcoded', 'Typos', 'NullSafeMutableLiveData'
}
packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/LICENSE'
resources {
excludes += ['META-INF/*.md']
}
}
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
androidResources {
generateLocaleConfig true
}
}
dokka.configuration {
sourceLink { url = "https://gitlab.com/bitfireAT/davx5-ose/tree/master-ose/" }
jdkVersion = 7
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
externalDocumentationLink {
url = new URL("https://bitfireat.gitlab.io/cert4android/dokka/cert4android/")
packageListUrl = new URL("https://bitfireat.gitlab.io/cert4android/dokka/cert4android/package-list")
}
externalDocumentationLink {
url = new URL("https://bitfireat.gitlab.io/dav4jvm/dokka/dav4jvm/")
packageListUrl = new URL("https://bitfireat.gitlab.io/dav4jvm/dokka/dav4jvm/package-list")
}
externalDocumentationLink {
url = new URL("https://bitfireat.gitlab.io/ical4android/dokka/ical4android/")
packageListUrl = new URL("https://bitfireat.gitlab.io/ical4android/dokka/ical4android/package-list")
}
externalDocumentationLink {
url = new URL("https://bitfireat.gitlab.io/vcard4android/dokka/vcard4android/")
packageListUrl = new URL("https://bitfireat.gitlab.io/vcard4android/dokka/vcard4android/package-list")
}
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'
}
}
dependencies {
implementation project(':cert4android')
implementation project(':ical4android')
implementation project(':vcard4android')
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
implementation 'androidx.multidex:multidex:2.0.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}"
implementation "com.google.dagger:hilt-android:${versions.hilt}"
kapt "com.google.dagger:hilt-android-compiler:${versions.hilt}" // replace by KSP when ready [https://issuetracker.google.com/179057202]
implementation 'androidx.appcompat:appcompat:1.1.0'
// support libs
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.browser:browser:1.6.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.fragment:fragment-ktx:1.1.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'
implementation 'androidx.paging:paging-runtime-ktx:2.1.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
implementation 'com.google.android:flexbox:1.1.0'
implementation 'com.google.android.material:material:1.1.0-beta02'
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.fragment:fragment-ktx:1.6.1'
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.paging:paging-runtime-ktx:3.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation "androidx.work:work-runtime-ktx:${versions.workManager}"
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'com.google.android.material:material:1.9.0'
def room_version = '2.2.1'
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// Jetpack Compose
implementation platform("androidx.compose:compose-bom:${versions.composeBom}")
implementation 'androidx.compose.material:material'
implementation 'androidx.compose.runtime:runtime-livedata'
debugImplementation 'androidx.compose.ui:ui-tooling'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'com.google.accompanist:accompanist-themeadapter-material:0.30.1'
implementation 'com.gitlab.bitfireAT:dav4jvm:214d9178'
// Jetpack Room
implementation "androidx.room:room-runtime:${versions.room}"
implementation "androidx.room:room-ktx:${versions.room}"
implementation "androidx.room:room-paging:${versions.room}"
ksp "androidx.room:room-compiler:${versions.room}"
androidTestImplementation "androidx.room:room-testing:${versions.room}"
// own libraries
implementation "com.github.bitfireAT:cert4android:${versions.cert4android}"
implementation "com.github.bitfireAT:ical4android:${versions.ical4android}"
implementation "com.github.bitfireAT:vcard4android:${versions.vcard4android}"
// third-party libs
implementation 'com.jaredrummler:colorpicker:1.1.0'
implementation 'com.mikepenz:aboutlibraries:6.2.3'
implementation "com.github.AppIntro:AppIntro:${versions.appIntro}"
implementation("com.github.bitfireAT:dav4jvm:${versions.dav4jvm}") {
exclude group: 'junit'
}
implementation "com.mikepenz:aboutlibraries-compose:${versions.aboutLibraries}"
implementation "com.squareup.okhttp3:okhttp:${versions.okhttp}"
implementation "com.squareup.okhttp3:okhttp-brotli:${versions.okhttp}"
implementation "com.squareup.okhttp3:logging-interceptor:${versions.okhttp}"
implementation 'commons-io:commons-io:2.6'
//noinspection GradleDependency - don't update until API level 26 (Android 8) is the minimum API [https://github.com/bitfireAT/davx5/issues/130]
implementation 'commons-io:commons-io:2.8.0'
//noinspection GradleDependency - dnsjava 3+ needs Java 8/Android 7
implementation 'dnsjava:dnsjava:2.1.9'
implementation 'org.apache.commons:commons-collections4:4.4'
implementation 'org.apache.commons:commons-lang3:3.9'
implementation 'net.openid:appauth:0.11.1'
//noinspection GradleDependency
implementation "org.apache.commons:commons-collections4:${versions.commonsCollections}"
//noinspection GradleDependency
implementation "org.apache.commons:commons-lang3:${versions.commonsLang}"
//noinspection GradleDependency
implementation "org.apache.commons:commons-text:${versions.commonsText}"
// for tests
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'junit:junit:4.12'
androidTestImplementation "com.squareup.okhttp3:mockwebserver:${versions.okhttp}"
androidTestImplementation "com.google.dagger:hilt-android-testing:${versions.hilt}"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:${versions.hilt}"
androidTestImplementation "androidx.arch.core:core-testing:2.2.0"
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
androidTestImplementation "androidx.work:work-testing:${versions.workManager}"
androidTestImplementation "com.squareup.okhttp3:mockwebserver:${versions.okhttp}"
androidTestImplementation 'io.mockk:mockk-android:1.13.7'
androidTestImplementation 'junit:junit:4.13.2'
testImplementation 'junit:junit:4.12'
testImplementation "com.squareup.okhttp3:mockwebserver:${versions.okhttp}"
testImplementation 'junit:junit:4.13.2'
}
+47
View File
@@ -0,0 +1,47 @@
# R8 usage for DAVx⁵:
# shrinking yes (only in release builds)
# optimization yes (on by R8 defaults)
# 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
# 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 groovy.**
-dontwarn java.beans.Transient
-dontwarn junit.textui.TestRunner
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl
-dontwarn org.bouncycastle.jsse.**
-dontwarn org.codehaus.groovy.**
-dontwarn org.joda.**
-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.xmlpull.**
-dontwarn sun.net.spi.nameservice.NameService
-dontwarn sun.net.spi.nameservice.NameServiceDescriptor
-50
View File
@@ -1,50 +0,0 @@
# ProGuard usage for DAVx⁵:
# shrinking yes (main reason for using ProGuard)
# optimization yes
# obfuscation no (DAVx⁵ is open-source)
# preverification no
-dontobfuscate
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification
-dontpreverify
# Kotlin
-dontwarn kotlin.**
# https://github.com/material-components/material-components-android/issues/387
-keep class com.google.android.material.tabs.** {*;}
# Apache Commons
-dontwarn javax.script.**
# ez-vcard
-dontwarn ezvcard.io.json.** # JSON serializer (for jCards) not used
-dontwarn freemarker.** # freemarker templating library (for creating hCards) not used
-dontwarn org.jsoup.** # jsoup library (for hCard parsing) not used
-keep class ezvcard.property.** { *; } # keep all vCard properties (created at runtime)
# ical4j: ignore unused dynamic libraries
-dontwarn aQute.**
-dontwarn groovy.** # Groovy-based ContentBuilder not used
-dontwarn javax.cache.** # no JCache support in Android
-dontwarn net.fortuna.ical4j.model.**
-dontwarn org.codehaus.groovy.**
-dontwarn org.apache.log4j.** # ignore warnings from log4j dependency
-keep class net.fortuna.ical4j.** { *; } # keep all model classes (properties/factories, created at runtime)
-keep class org.threeten.bp.** { *; } # keep ThreeTen (for time zone processing)
# okhttp
-dontwarn javax.annotation.**
-dontwarn okio.**
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn org.conscrypt.**
# dnsjava
-dontwarn sun.net.spi.nameservice.** # not available on Android
# DAVx⁵ + libs
-keep class at.bitfire.** { *; } # all DAVx⁵ code is required
@@ -0,0 +1,398 @@
{
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "6fcabe50cbd00a4215dbe536a565dd2a",
"entities": [
{
"tableName": "service",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "principal",
"columnName": "principal",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_service_accountName_type",
"unique": true,
"columnNames": [
"accountName",
"type"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)"
}
],
"foreignKeys": []
},
{
"tableName": "homeset",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "serviceId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "personal",
"columnName": "personal",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "privBind",
"columnName": "privBind",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_homeset_serviceId_url",
"unique": true,
"columnNames": [
"serviceId",
"url"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
}
],
"foreignKeys": [
{
"table": "service",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serviceId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "collection",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `owner` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "serviceId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "homeSetId",
"columnName": "homeSetId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "privWriteContent",
"columnName": "privWriteContent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "privUnbind",
"columnName": "privUnbind",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forceReadOnly",
"columnName": "forceReadOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "owner",
"columnName": "owner",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "timezone",
"columnName": "timezone",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "supportsVEVENT",
"columnName": "supportsVEVENT",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "supportsVTODO",
"columnName": "supportsVTODO",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "supportsVJOURNAL",
"columnName": "supportsVJOURNAL",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sync",
"columnName": "sync",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_collection_serviceId_type",
"unique": false,
"columnNames": [
"serviceId",
"type"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)"
},
{
"name": "index_collection_homeSetId_type",
"unique": false,
"columnNames": [
"homeSetId",
"type"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)"
},
{
"name": "index_collection_url",
"unique": false,
"columnNames": [
"url"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)"
}
],
"foreignKeys": [
{
"table": "service",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serviceId"
],
"referencedColumns": [
"id"
]
},
{
"table": "homeset",
"onDelete": "SET NULL",
"onUpdate": "NO ACTION",
"columns": [
"homeSetId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "syncstats",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectionId",
"columnName": "collectionId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authority",
"columnName": "authority",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSync",
"columnName": "lastSync",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_syncstats_collectionId_authority",
"unique": true,
"columnNames": [
"collectionId",
"authority"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)"
}
],
"foreignKeys": [
{
"table": "collection",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"collectionId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "webdav_mount",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6fcabe50cbd00a4215dbe536a565dd2a')"
]
}
}
@@ -0,0 +1,536 @@
{
"formatVersion": 1,
"database": {
"version": 11,
"identityHash": "223aa7f0fd53730921ca212a663585d8",
"entities": [
{
"tableName": "service",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "principal",
"columnName": "principal",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_service_accountName_type",
"unique": true,
"columnNames": [
"accountName",
"type"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)"
}
],
"foreignKeys": []
},
{
"tableName": "homeset",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "serviceId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "personal",
"columnName": "personal",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "privBind",
"columnName": "privBind",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_homeset_serviceId_url",
"unique": true,
"columnNames": [
"serviceId",
"url"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
}
],
"foreignKeys": [
{
"table": "service",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serviceId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "collection",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `owner` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "serviceId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "homeSetId",
"columnName": "homeSetId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "privWriteContent",
"columnName": "privWriteContent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "privUnbind",
"columnName": "privUnbind",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forceReadOnly",
"columnName": "forceReadOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "owner",
"columnName": "owner",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "timezone",
"columnName": "timezone",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "supportsVEVENT",
"columnName": "supportsVEVENT",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "supportsVTODO",
"columnName": "supportsVTODO",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "supportsVJOURNAL",
"columnName": "supportsVJOURNAL",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sync",
"columnName": "sync",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_collection_serviceId_type",
"unique": false,
"columnNames": [
"serviceId",
"type"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)"
},
{
"name": "index_collection_homeSetId_type",
"unique": false,
"columnNames": [
"homeSetId",
"type"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)"
},
{
"name": "index_collection_url",
"unique": false,
"columnNames": [
"url"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)"
}
],
"foreignKeys": [
{
"table": "service",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serviceId"
],
"referencedColumns": [
"id"
]
},
{
"table": "homeset",
"onDelete": "SET NULL",
"onUpdate": "NO ACTION",
"columns": [
"homeSetId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "syncstats",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectionId",
"columnName": "collectionId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authority",
"columnName": "authority",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSync",
"columnName": "lastSync",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_syncstats_collectionId_authority",
"unique": true,
"columnNames": [
"collectionId",
"authority"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)"
}
],
"foreignKeys": [
{
"table": "collection",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"collectionId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "webdav_document",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mountId",
"columnName": "mountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "parentId",
"columnName": "parentId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isDirectory",
"columnName": "isDirectory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mimeType",
"columnName": "mimeType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "eTag",
"columnName": "eTag",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastModified",
"columnName": "lastModified",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mayBind",
"columnName": "mayBind",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mayUnbind",
"columnName": "mayUnbind",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mayWriteContent",
"columnName": "mayWriteContent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "quotaAvailable",
"columnName": "quotaAvailable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "quotaUsed",
"columnName": "quotaUsed",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_webdav_document_mountId_parentId_name",
"unique": true,
"columnNames": [
"mountId",
"parentId",
"name"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)"
}
],
"foreignKeys": [
{
"table": "webdav_mount",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"mountId"
],
"referencedColumns": [
"id"
]
},
{
"table": "webdav_document",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"parentId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "webdav_mount",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '223aa7f0fd53730921ca212a663585d8')"
]
}
}
@@ -0,0 +1,615 @@
{
"formatVersion": 1,
"database": {
"version": 12,
"identityHash": "67fafceecee2d97cac6a62d46fa2c3e2",
"entities": [
{
"tableName": "service",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "principal",
"columnName": "principal",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_service_accountName_type",
"unique": true,
"columnNames": [
"accountName",
"type"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)"
}
],
"foreignKeys": []
},
{
"tableName": "homeset",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "serviceId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "personal",
"columnName": "personal",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "privBind",
"columnName": "privBind",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_homeset_serviceId_url",
"unique": true,
"columnNames": [
"serviceId",
"url"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
}
],
"foreignKeys": [
{
"table": "service",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serviceId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "collection",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `ownerId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`ownerId`) REFERENCES `principal`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "serviceId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "homeSetId",
"columnName": "homeSetId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "privWriteContent",
"columnName": "privWriteContent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "privUnbind",
"columnName": "privUnbind",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forceReadOnly",
"columnName": "forceReadOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "timezone",
"columnName": "timezone",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "supportsVEVENT",
"columnName": "supportsVEVENT",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "supportsVTODO",
"columnName": "supportsVTODO",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "supportsVJOURNAL",
"columnName": "supportsVJOURNAL",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sync",
"columnName": "sync",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collection_serviceId_type",
"unique": false,
"columnNames": [
"serviceId",
"type"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)"
},
{
"name": "index_collection_homeSetId_type",
"unique": false,
"columnNames": [
"homeSetId",
"type"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)"
},
{
"name": "index_collection_url",
"unique": false,
"columnNames": [
"url"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)"
}
],
"foreignKeys": [
{
"table": "service",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serviceId"
],
"referencedColumns": [
"id"
]
},
{
"table": "homeset",
"onDelete": "SET NULL",
"onUpdate": "NO ACTION",
"columns": [
"homeSetId"
],
"referencedColumns": [
"id"
]
},
{
"table": "principal",
"onDelete": "SET NULL",
"onUpdate": "NO ACTION",
"columns": [
"ownerId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "principal",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `url` TEXT NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "serviceId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_principal_serviceId_url",
"unique": true,
"columnNames": [
"serviceId",
"url"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
}
],
"foreignKeys": [
{
"table": "service",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serviceId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "syncstats",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectionId",
"columnName": "collectionId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authority",
"columnName": "authority",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSync",
"columnName": "lastSync",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_syncstats_collectionId_authority",
"unique": true,
"columnNames": [
"collectionId",
"authority"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)"
}
],
"foreignKeys": [
{
"table": "collection",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"collectionId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "webdav_document",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mountId",
"columnName": "mountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "parentId",
"columnName": "parentId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isDirectory",
"columnName": "isDirectory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mimeType",
"columnName": "mimeType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "eTag",
"columnName": "eTag",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastModified",
"columnName": "lastModified",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mayBind",
"columnName": "mayBind",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mayUnbind",
"columnName": "mayUnbind",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mayWriteContent",
"columnName": "mayWriteContent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "quotaAvailable",
"columnName": "quotaAvailable",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "quotaUsed",
"columnName": "quotaUsed",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_webdav_document_mountId_parentId_name",
"unique": true,
"columnNames": [
"mountId",
"parentId",
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)"
}
],
"foreignKeys": [
{
"table": "webdav_mount",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"mountId"
],
"referencedColumns": [
"id"
]
},
{
"table": "webdav_document",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"parentId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "webdav_mount",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '67fafceecee2d97cac6a62d46fa2c3e2')"
]
}
}
@@ -0,0 +1,298 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "b8699ef3cc4c62e8851df4360fb69e00",
"entities": [
{
"tableName": "service",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "principal",
"columnName": "principal",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_service_accountName_type",
"unique": true,
"columnNames": [
"accountName",
"type"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)"
}
],
"foreignKeys": []
},
{
"tableName": "homeset",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "serviceId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "personal",
"columnName": "personal",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "privBind",
"columnName": "privBind",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_homeset_serviceId_url",
"unique": true,
"columnNames": [
"serviceId",
"url"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
}
],
"foreignKeys": [
{
"table": "service",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serviceId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "collection",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `owner` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "serviceId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "homeSetId",
"columnName": "homeSetId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "privWriteContent",
"columnName": "privWriteContent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "privUnbind",
"columnName": "privUnbind",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forceReadOnly",
"columnName": "forceReadOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "owner",
"columnName": "owner",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "timezone",
"columnName": "timezone",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "supportsVEVENT",
"columnName": "supportsVEVENT",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "supportsVTODO",
"columnName": "supportsVTODO",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "supportsVJOURNAL",
"columnName": "supportsVJOURNAL",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sync",
"columnName": "sync",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_collection_serviceId_type",
"unique": false,
"columnNames": [
"serviceId",
"type"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)"
},
{
"name": "index_collection_homeSetId_type",
"unique": false,
"columnNames": [
"homeSetId",
"type"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)"
}
],
"foreignKeys": [
{
"table": "service",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serviceId"
],
"referencedColumns": [
"id"
]
},
{
"table": "homeset",
"onDelete": "SET NULL",
"onUpdate": "NO ACTION",
"columns": [
"homeSetId"
],
"referencedColumns": [
"id"
]
}
]
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b8699ef3cc4c62e8851df4360fb69e00')"
]
}
}
@@ -0,0 +1,366 @@
{
"formatVersion": 1,
"database": {
"version": 9,
"identityHash": "7e4bfdf7f9fa3529c333cf9485f8cf50",
"entities": [
{
"tableName": "service",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountName",
"columnName": "accountName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "principal",
"columnName": "principal",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_service_accountName_type",
"unique": true,
"columnNames": [
"accountName",
"type"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)"
}
],
"foreignKeys": []
},
{
"tableName": "homeset",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "serviceId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "personal",
"columnName": "personal",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "privBind",
"columnName": "privBind",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_homeset_serviceId_url",
"unique": true,
"columnNames": [
"serviceId",
"url"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
}
],
"foreignKeys": [
{
"table": "service",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serviceId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "collection",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `owner` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "serviceId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "homeSetId",
"columnName": "homeSetId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "privWriteContent",
"columnName": "privWriteContent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "privUnbind",
"columnName": "privUnbind",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forceReadOnly",
"columnName": "forceReadOnly",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "owner",
"columnName": "owner",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "timezone",
"columnName": "timezone",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "supportsVEVENT",
"columnName": "supportsVEVENT",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "supportsVTODO",
"columnName": "supportsVTODO",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "supportsVJOURNAL",
"columnName": "supportsVJOURNAL",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sync",
"columnName": "sync",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_collection_serviceId_type",
"unique": false,
"columnNames": [
"serviceId",
"type"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)"
},
{
"name": "index_collection_homeSetId_type",
"unique": false,
"columnNames": [
"homeSetId",
"type"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)"
},
{
"name": "index_collection_url",
"unique": false,
"columnNames": [
"url"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)"
}
],
"foreignKeys": [
{
"table": "service",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serviceId"
],
"referencedColumns": [
"id"
]
},
{
"table": "homeset",
"onDelete": "SET NULL",
"onUpdate": "NO ACTION",
"columns": [
"homeSetId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "syncstats",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "collectionId",
"columnName": "collectionId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authority",
"columnName": "authority",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSync",
"columnName": "lastSync",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_syncstats_collectionId_authority",
"unique": true,
"columnNames": [
"collectionId",
"authority"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)"
}
],
"foreignKeys": [
{
"table": "collection",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"collectionId"
],
"referencedColumns": [
"id"
]
}
]
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7e4bfdf7f9fa3529c333cf9485f8cf50')"
]
}
}
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="internalOnly">
<!-- account management permissions not required for own accounts since API level 22 -->
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" android:maxSdkVersion="22"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS" android:maxSdkVersion="22"/>
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" android:maxSdkVersion="22"/>
</manifest>
@@ -0,0 +1,17 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.HiltAndroidApp
import dagger.hilt.android.testing.HiltTestApplication
class CustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader, name: String, context: Context) =
super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
@@ -0,0 +1,96 @@
/***************************************************************************************************
* 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.ContentUris
import android.content.ContentValues
import android.os.Build
import android.provider.CalendarContract
import androidx.annotation.RequiresApi
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import at.bitfire.davdroid.log.Logger
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.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
/**
* JUnit ClassRule which initializes the AOSP CalendarProvider.
* Needed for some "flaky" tests which would otherwise only succeed on second run.
*
* Currently tested on development machine (Ryzen) with Android 12 images (with/without Google Play).
* Calendar provider behaves quite randomly, so it may or may not work. If you (the reader
* if this comment) can find out on how to initialize the calendar provider so that the
* tests are reliably run after `adb shell pm clear com.android.providers.calendar`,
* please let us know!
*
* If you run tests manually, just make sure to ignore the first run after the calendar
* provider has been accessed the first time.
*/
class InitCalendarProviderRule private constructor(): TestRule {
companion object {
fun getInstance() = RuleChain
.outerRule(GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR))
.around(InitCalendarProviderRule())
}
override fun apply(base: Statement, description: Description): Statement {
Logger.log.info("Initializing calendar provider before running ${description.displayName}")
return InitCalendarProviderStatement(base)
}
class InitCalendarProviderStatement(val base: Statement): Statement() {
override fun evaluate() {
if (Build.VERSION.SDK_INT < 31)
Logger.log.warning("Calendar provider initialization may or may not work. See InitCalendarProviderRule")
initCalendarProvider()
base.evaluate()
}
private fun initCalendarProvider() {
val account = Account("LocalCalendarTest", CalendarContract.ACCOUNT_TYPE_LOCAL)
val context = InstrumentationRegistry.getInstrumentation().targetContext
val provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
val uri = AndroidCalendar.create(account, provider, ContentValues())
val calendar = AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri))
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()
}
}
}
}
@@ -0,0 +1,31 @@
/***************************************************************************************************
* 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.settings.SettingsManager
import dagger.Module
import dagger.Provides
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import io.mockk.spyk
import javax.inject.Singleton
@Module
@TestInstallIn(
components = [ SingletonComponent::class ],
replaces = [
SettingsManager.SettingsManagerModule::class
]
)
class MockingModule {
@Provides
@Singleton
fun spykSettingsManager(@ApplicationContext context: Context): SettingsManager =
spyk(SettingsManager(context))
}
@@ -0,0 +1,81 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import org.jetbrains.annotations.TestOnly
import org.junit.Assert.assertTrue
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
object TestUtils {
fun assertWithin(expected: Long, actual: Long, tolerance: Long) {
val absDifference = Math.abs(expected - actual)
assertTrue(
"$actual not within ($expected ± $tolerance)",
absDifference <= tolerance
)
}
@TestOnly
fun workScheduledOrRunning(context: Context, workerName: String): Boolean =
workInStates(context, workerName, listOf(
WorkInfo.State.ENQUEUED,
WorkInfo.State.RUNNING
))
@TestOnly
fun workScheduledOrRunningOrSuccessful(context: Context, workerName: String): Boolean =
workInStates(context, workerName, listOf(
WorkInfo.State.ENQUEUED,
WorkInfo.State.RUNNING,
WorkInfo.State.SUCCEEDED
))
@TestOnly
fun workInStates(context: Context, workerName: String, states: List<WorkInfo.State>): Boolean =
WorkManager.getInstance(context).getWorkInfos(WorkQuery.Builder
.fromUniqueWorkNames(listOf(workerName))
.addStates(states)
.build()
).get().isNotEmpty()
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@TestOnly
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(value: T) {
data = value
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
@Suppress("UNCHECKED_CAST")
return data as T
}
}
@@ -0,0 +1,50 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import android.content.Context
import androidx.room.Room
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test
class AppDatabaseTest {
val TEST_DB = "test"
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
@Rule
@JvmField
val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java,
listOf(), // no auto migrations until v8
FrameworkSQLiteOpenHelperFactory()
)
@Test
fun testAllMigrations() {
// DB schema is available since version 8, so create DB with v8
helper.createDatabase(TEST_DB, 8).close()
val db = Room.databaseBuilder(context, AppDatabase::class.java, TEST_DB)
// manual migrations
.addMigrations(*AppDatabase.migrations)
// auto-migrations that need to be specified explicitly
.addAutoMigrationSpec(AppDatabase.AutoMigration11_12(context))
.build()
try {
// open (with version 8) + migrate (to current version) database
db.openHelper.writableDatabase
} finally {
db.close()
}
}
}
@@ -1,34 +1,51 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.model
package at.bitfire.davdroid.db
import android.content.ContentValues
import android.security.NetworkSecurityPolicy
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.property.ResourceType
import at.bitfire.davdroid.HttpClient
import okhttp3.HttpUrl
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.settings.SettingsManager
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.*
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
class CollectionTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Inject
lateinit var settingsManager: SettingsManager
@Before
fun inject() {
hiltRule.inject()
}
private lateinit var httpClient: HttpClient
private val server = MockWebServer()
@Before
fun setUp() {
httpClient = HttpClient.Builder().build()
httpClient = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext).build()
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
}
@After
@@ -129,7 +146,7 @@ class CollectionTest {
}
assertEquals(Collection.TYPE_WEBCAL, info.type)
assertEquals("Sample Subscription", info.displayName)
assertEquals(HttpUrl.get("https://example.com/1.ics"), info.source)
assertEquals("https://example.com/1.ics".toHttpUrl(), info.source)
}
}
@@ -0,0 +1,81 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
class HomesetDaoTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var db: AppDatabase
@Before
fun setUp() {
hiltRule.inject()
}
@After
fun tearDown() {
db.close()
}
@Test
fun testInsertOrUpdate() {
// should insert new row or update (upsert) existing row - without changing its key!
val serviceId = createTestService()
val homeSetDao = db.homeSetDao()
val entry1 = HomeSet(id=0, serviceId=serviceId, personal=true, url="https://example.com/1".toHttpUrl())
val insertId1 = homeSetDao.insertOrUpdateByUrl(entry1)
assertEquals(1L, insertId1)
assertEquals(entry1.apply { id = 1L }, homeSetDao.getById(1L))
val updatedEntry1 = HomeSet(id=0, serviceId=serviceId, personal=true, url="https://example.com/1".toHttpUrl(), displayName="Updated Entry")
val updateId1 = homeSetDao.insertOrUpdateByUrl(updatedEntry1)
assertEquals(1L, updateId1)
assertEquals(updatedEntry1.apply { id = 1L }, homeSetDao.getById(1L))
val entry2 = HomeSet(id=0, serviceId=serviceId, personal=true, url= "https://example.com/2".toHttpUrl())
val insertId2 = homeSetDao.insertOrUpdateByUrl(entry2)
assertEquals(2L, insertId2)
assertEquals(entry2.apply { id = 2L }, homeSetDao.getById(2L))
}
@Test
fun testDelete() {
// should delete row with given primary key (id)
val serviceId = createTestService()
val homesetDao = db.homeSetDao()
val entry1 = HomeSet(id=1, serviceId=serviceId, personal=true, url= "https://example.com/1".toHttpUrl())
val insertId1 = homesetDao.insertOrUpdateByUrl(entry1)
assertEquals(1L, insertId1)
assertEquals(entry1, homesetDao.getById(1L))
homesetDao.delete(entry1)
assertEquals(null, homesetDao.getById(1L))
}
fun createTestService() : Long {
val serviceDao = db.serviceDao()
val service = Service(id=0, accountName="test", type=Service.TYPE_CALDAV, principal = null)
return serviceDao.insertOrReplace(service)
}
}
@@ -0,0 +1,33 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import javax.inject.Singleton
@Module
@TestInstallIn(
components = [ SingletonComponent::class ],
replaces = [
AppDatabase.AppDatabaseModule::class
]
)
class MemoryDbModule {
@Provides
@Singleton
fun inMemoryDatabase(@ApplicationContext context: Context): AppDatabase =
Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
// auto-migrations that need to be specified explicitly
.addAutoMigrationSpec(AppDatabase.AutoMigration11_12(context))
.build()
}
@@ -1,63 +0,0 @@
package at.bitfire.davdroid.model
import androidx.room.Room
import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.HttpUrl
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
class DaoToolsTest {
private lateinit var db: AppDatabase
@Before
fun createDb() {
val context = InstrumentationRegistry.getInstrumentation().context
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
}
@After
fun closeDb() {
db.close()
}
@Test
fun testSyncAll() {
val serviceDao = db.serviceDao()
val service = Service(id=0, accountName="test", type=Service.TYPE_CALDAV, principal = null)
service.id = serviceDao.insertOrReplace(service)
val homeSetDao = db.homeSetDao()
val entry1 = HomeSet(id=1, serviceId=service.id, url=HttpUrl.get("https://example.com/1"))
val entry3 = HomeSet(id=3, serviceId=service.id, url=HttpUrl.get("https://example.com/3"))
val oldItems = listOf(
entry1,
HomeSet(id=2, serviceId=service.id, url=HttpUrl.get("https://example.com/2")),
entry3
)
homeSetDao.insert(oldItems)
val newItems = mutableMapOf<HttpUrl, HomeSet>()
newItems[entry1.url] = entry1
// no id, because identity is given by the url
val updated = HomeSet(id=0, serviceId=service.id,
url=HttpUrl.get("https://example.com/2"), displayName="Updated Entry")
newItems[updated.url] = updated
val created = HomeSet(id=4, serviceId=service.id, url=HttpUrl.get("https://example.com/4"))
newItems[created.url] = created
DaoTools(homeSetDao).syncAll(oldItems, newItems, { it.url })
val afterSync = homeSetDao.getByService(service.id)
assertEquals(afterSync.size, 3)
assertFalse(afterSync.contains(entry3))
assertTrue(afterSync.contains(entry1))
assertTrue(afterSync.contains(updated))
assertTrue(afterSync.contains(created))
}
}
@@ -0,0 +1,35 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.network
import android.os.Build
import androidx.test.filters.SdkSuppress
import org.junit.Assert.assertEquals
import org.junit.Test
import org.xbill.DNS.ARecord
import org.xbill.DNS.Lookup
import org.xbill.DNS.Type
import java.net.Inet4Address
import java.net.InetAddress
class Android10ResolverTest {
val FQDN_DAVX5 = "www.davx5.com"
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
fun testResolveA() {
val www = InetAddress.getAllByName(FQDN_DAVX5).filterIsInstance(Inet4Address::class.java).first()
val srvLookup = Lookup(FQDN_DAVX5, Type.A)
srvLookup.setResolver(Android10Resolver)
val resultGeneric = srvLookup.run()
assertEquals(1, resultGeneric.size)
val result = resultGeneric.first() as ARecord
assertEquals(www, result.address)
}
}
@@ -1,30 +1,38 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid
package at.bitfire.davdroid.network
import android.security.NetworkSecurityPolicy
import androidx.test.platform.app.InstrumentationRegistry
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import okhttp3.Request
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@HiltAndroidTest
class HttpClientTest {
lateinit var server: MockWebServer
lateinit var httpClient: HttpClient
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Before
fun setUp() {
httpClient = HttpClient.Builder().build()
hiltRule.inject()
httpClient = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext).build()
server = MockWebServer()
server.start(30000)
@@ -39,6 +47,7 @@ class HttpClientTest {
@Test
fun testCookies() {
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
val url = server.url("/test")
// set cookie for root path (/) and /test path in first response
@@ -0,0 +1,78 @@
/***************************************************************************************************
* 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 androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.davdroid.R
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@HiltAndroidTest
class LocalAddressBookTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
val context = InstrumentationRegistry.getInstrumentation().targetContext
val mainAccountType = context.getString(R.string.account_type)
val mainAccount = Account("main", mainAccountType)
val addressBookAccountType = context.getString(R.string.account_type_address_book)
val addressBookAccount = Account("sub", addressBookAccountType)
val accountManager = AccountManager.get(context)
@Before
fun setUp() {
hiltRule.inject()
// TODO DOES NOT WORK: the account immediately starts to sync, which creates the sync adapter services.
// The services however can't be created because Hilt is "not ready" (although it has been initialized in the line above).
// assertTrue(AccountUtils.createAccount(context, mainAccount, AccountSettings.initialUserData(null)))
}
@After
fun cleanup() {
accountManager.removeAccount(addressBookAccount, null, null)
accountManager.removeAccount(mainAccount, null, null)
}
// TODO see above
/*@Test
fun testMainAccount_AddressBookAccount_WithMainAccount() {
// create address book account
assertTrue(accountManager.addAccountExplicitly(addressBookAccount, null, Bundle().apply {
putString(LocalAddressBook.USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
putString(LocalAddressBook.USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type)
}))
// check mainAccount()
assertEquals(mainAccount, LocalAddressBook.mainAccount(context, addressBookAccount))
}
@Test(expected = IllegalArgumentException::class)
fun testMainAccount_AddressBookAccount_WithoutMainAccount() {
// create address book account
assertTrue(accountManager.addAccountExplicitly(addressBookAccount, null, Bundle()))
// check mainAccount(); should fail because there's no main account
LocalAddressBook.mainAccount(context, addressBookAccount)
}*/
@Test(expected = IllegalArgumentException::class)
fun testMainAccount_OtherAccount() {
LocalAddressBook.mainAccount(context, Account("Other Account", "com.example"))
}
}
@@ -0,0 +1,145 @@
/***************************************************************************************************
* 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.closeCompat
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
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.*
import org.junit.Assert.assertEquals
import org.junit.rules.TestRule
class LocalCalendarTest {
companion object {
@JvmField
@ClassRule
val initCalendarProviderRule: TestRule = InitCalendarProviderRule.getInstance()
private lateinit var provider: ContentProviderClient
@BeforeClass
@JvmStatic
fun setUpProvider() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
}
@AfterClass
@JvmStatic
fun closeProvider() {
provider.closeCompat()
}
}
private val account = Account("LocalCalendarTest", ACCOUNT_TYPE_LOCAL)
private lateinit var calendar: LocalCalendar
@Before
fun prepare() {
val uri = AndroidCalendar.create(account, provider, ContentValues())
calendar = AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri))
}
@After
fun shutdown() {
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
// Flaky, Needs single or rec init of CalendarProvider (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))
}
}
}
@@ -0,0 +1,472 @@
/***************************************************************************************************
* 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.*
import org.junit.*
import org.junit.Assert.*
import org.junit.rules.TestRule
import java.util.*
class LocalEventTest {
companion object {
@JvmField
@ClassRule
val initCalendarProviderRule: TestRule = InitCalendarProviderRule.getInstance()
private val account = Account("LocalCalendarTest", ACCOUNT_TYPE_LOCAL)
private lateinit var provider: ContentProviderClient
private lateinit var calendar: LocalCalendar
@BeforeClass
@JvmStatic
fun connect() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
}
@AfterClass
@JvmStatic
fun disconnect() {
provider.closeCompat()
}
}
@Before
fun createCalendar() {
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)
val uri = 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))
}
}
}
@@ -0,0 +1,267 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.resource
import android.Manifest
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.provider.ContactsContract
import android.provider.ContactsContract.CommonDataKinds.GroupMembership
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.vcard4android.BatchOperation
import at.bitfire.vcard4android.CachedGroupMembership
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.GroupMethod
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.*
import org.junit.Assert.*
import javax.inject.Inject
@HiltAndroidTest
class LocalGroupTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Inject
lateinit var settingsManager: SettingsManager
@Before
fun inject() {
hiltRule.inject()
}
companion object {
@JvmField
@ClassRule
val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!!
private lateinit var provider: ContentProviderClient
private lateinit var addressBookGroupsAsCategories: LocalTestAddressBook
private lateinit var addressBookGroupsAsVCards: LocalTestAddressBook
@BeforeClass
@JvmStatic
fun connect() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
assertNotNull(provider)
addressBookGroupsAsCategories = LocalTestAddressBook(context, provider, GroupMethod.CATEGORIES)
addressBookGroupsAsVCards = LocalTestAddressBook(context, provider, GroupMethod.GROUP_VCARDS)
}
@AfterClass
@JvmStatic
fun disconnect() {
@Suppress("DEPRECATION")
provider.release()
}
}
private fun newGroup(addressBook: LocalAddressBook = addressBookGroupsAsCategories): LocalGroup =
LocalGroup(addressBook,
Contact().apply {
displayName = "Test Group"
}, null, null, 0
).apply {
add()
}
@Before
fun clearContacts() {
addressBookGroupsAsCategories.clear()
addressBookGroupsAsVCards.clear()
}
@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)
}
}
@@ -0,0 +1,38 @@
/***************************************************************************************************
* 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.Context
import at.bitfire.vcard4android.GroupMethod
class LocalTestAddressBook(
context: Context,
provider: ContentProviderClient,
override val groupMethod: GroupMethod
): LocalAddressBook(context, ACCOUNT, provider) {
companion object {
val ACCOUNT = Account("LocalTestAddressBook", "at.bitfire.davdroid.test")
}
override var mainAccount: Account
get() = throw NotImplementedError()
set(value) = throw NotImplementedError()
override var readOnly: Boolean
get() = false
set(value) = throw NotImplementedError()
fun clear() {
for (contact in queryContacts(null, null))
contact.delete()
for (group in queryGroups(null, null))
group.delete()
}
}
@@ -0,0 +1,63 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.resource.contactrow
import android.Manifest
import android.content.ContentProviderClient
import android.content.ContentValues
import android.provider.ContactsContract
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import at.bitfire.davdroid.resource.LocalContact
import at.bitfire.davdroid.resource.LocalTestAddressBook
import at.bitfire.vcard4android.CachedGroupMembership
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.GroupMethod
import org.junit.*
import org.junit.Assert.assertArrayEquals
class CachedGroupMembershipHandlerTest {
companion object {
@JvmField
@ClassRule
val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!!
private lateinit var provider: ContentProviderClient
private lateinit var addressBook: LocalTestAddressBook
@BeforeClass
@JvmStatic
fun connect() {
val context = InstrumentationRegistry.getInstrumentation().context
provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
Assert.assertNotNull(provider)
addressBook = LocalTestAddressBook(context, provider, GroupMethod.GROUP_VCARDS)
}
@AfterClass
@JvmStatic
fun disconnect() {
@Suppress("DEPRECATION")
provider.release()
}
}
@Test
fun testMembership() {
val contact = Contact()
val localContact = LocalContact(addressBook, contact, null, null, 0)
CachedGroupMembershipHandler(localContact).handle(ContentValues().apply {
put(CachedGroupMembership.GROUP_ID, 123456)
put(CachedGroupMembership.RAW_CONTACT_ID, 789)
}, contact)
assertArrayEquals(arrayOf(123456L), localContact.cachedGroupMemberships.toArray())
}
}
@@ -0,0 +1,75 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.resource.contactrow
import android.Manifest
import android.content.ContentProviderClient
import android.net.Uri
import android.provider.ContactsContract
import android.provider.ContactsContract.CommonDataKinds.GroupMembership
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import at.bitfire.davdroid.resource.LocalTestAddressBook
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.GroupMethod
import org.junit.*
import org.junit.Assert.assertEquals
class GroupMembershipBuilderTest {
companion object {
@JvmField
@ClassRule
val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!!
private lateinit var provider: ContentProviderClient
private lateinit var addressBookGroupsAsCategories: LocalTestAddressBook
private lateinit var addressBookGroupsAsVCards: LocalTestAddressBook
@BeforeClass
@JvmStatic
fun connect() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
Assert.assertNotNull(provider)
addressBookGroupsAsCategories = LocalTestAddressBook(context, provider, GroupMethod.CATEGORIES)
addressBookGroupsAsVCards = LocalTestAddressBook(context, provider, GroupMethod.GROUP_VCARDS)
}
@AfterClass
@JvmStatic
fun disconnect() {
@Suppress("DEPRECATION")
provider.release()
}
}
@Test
fun testCategories_GroupsAsCategories() {
val contact = Contact().apply {
categories += "TEST GROUP"
}
GroupMembershipBuilder(Uri.EMPTY, null, contact, addressBookGroupsAsCategories, false).build().also { result ->
assertEquals(1, result.size)
assertEquals(GroupMembership.CONTENT_ITEM_TYPE, result[0].values[GroupMembership.MIMETYPE])
assertEquals(addressBookGroupsAsCategories.findOrCreateGroup("TEST GROUP"), result[0].values[GroupMembership.GROUP_ROW_ID])
}
}
@Test
fun testCategories_GroupsAsVCards() {
val contact = Contact().apply {
categories += "TEST GROUP"
}
GroupMembershipBuilder(Uri.EMPTY, null, contact, addressBookGroupsAsVCards, false).build().also { result ->
// group membership is constructed during post-processing
assertEquals(0, result.size)
}
}
}
@@ -0,0 +1,79 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.resource.contactrow
import android.Manifest
import android.content.ContentProviderClient
import android.content.ContentValues
import android.provider.ContactsContract
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import at.bitfire.davdroid.resource.LocalContact
import at.bitfire.davdroid.resource.LocalTestAddressBook
import at.bitfire.vcard4android.CachedGroupMembership
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.GroupMethod
import org.junit.*
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertTrue
class GroupMembershipHandlerTest {
@JvmField
@Rule
val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!!
private lateinit var provider: ContentProviderClient
private lateinit var addressBookGroupsAsCategories: LocalTestAddressBook
private var addressBookGroupsAsCategoriesGroup: Long = -1
private lateinit var addressBookGroupsAsVCards: LocalTestAddressBook
@Before
fun connect() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
Assert.assertNotNull(provider)
addressBookGroupsAsCategories = LocalTestAddressBook(context, provider, GroupMethod.CATEGORIES)
addressBookGroupsAsCategoriesGroup = addressBookGroupsAsCategories.findOrCreateGroup("TEST GROUP")
addressBookGroupsAsVCards = LocalTestAddressBook(context, provider, GroupMethod.GROUP_VCARDS)
}
@After
fun disconnect() {
@Suppress("DEPRECATION")
provider.release()
}
@Test
fun testMembership_GroupsAsCategories() {
val contact = Contact()
val localContact = LocalContact(addressBookGroupsAsCategories, contact, null, null, 0)
GroupMembershipHandler(localContact).handle(ContentValues().apply {
put(CachedGroupMembership.GROUP_ID, addressBookGroupsAsCategoriesGroup)
put(CachedGroupMembership.RAW_CONTACT_ID, -1)
}, contact)
assertArrayEquals(arrayOf(addressBookGroupsAsCategoriesGroup), localContact.groupMemberships.toArray())
assertArrayEquals(arrayOf("TEST GROUP"), contact.categories.toArray())
}
@Test
fun testMembership_GroupsAsVCards() {
val contact = Contact()
val localContact = LocalContact(addressBookGroupsAsVCards, contact, null, null, 0)
GroupMembershipHandler(localContact).handle(ContentValues().apply {
put(CachedGroupMembership.GROUP_ID, 12345) // because the group name is not queried and put into CATEGORIES, the group doesn't have to really exist
put(CachedGroupMembership.RAW_CONTACT_ID, -1)
}, contact)
assertArrayEquals(arrayOf(12345L), localContact.groupMemberships.toArray())
assertTrue(contact.categories.isEmpty())
}
}
@@ -0,0 +1,32 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.resource.contactrow
import android.net.Uri
import at.bitfire.vcard4android.Contact
import org.junit.Assert.assertEquals
import org.junit.Test
class UnknownPropertiesBuilderTest {
@Test
fun testUnknownProperties_None() {
UnknownPropertiesBuilder(Uri.EMPTY, null, Contact(), false).build().also { result ->
assertEquals(0, result.size)
}
}
@Test
fun testUnknownProperties_Properties() {
UnknownPropertiesBuilder(Uri.EMPTY, null, Contact().apply {
unknownProperties = "X-TEST:12345"
}, false).build().also { result ->
assertEquals(1, result.size)
assertEquals(UnknownProperties.CONTENT_ITEM_TYPE, result[0].values[UnknownProperties.MIMETYPE])
assertEquals("X-TEST:12345", result[0].values[UnknownProperties.UNKNOWN_PROPERTIES])
}
}
}
@@ -0,0 +1,33 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.resource.contactrow
import android.content.ContentValues
import at.bitfire.vcard4android.Contact
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class UnknownPropertiesHandlerTest {
@Test
fun testUnknownProperties_Empty() {
val contact = Contact()
UnknownPropertiesHandler.handle(ContentValues().apply {
putNull(UnknownProperties.UNKNOWN_PROPERTIES)
}, contact)
assertNull(contact.unknownProperties)
}
@Test
fun testUnknownProperties_Values() {
val contact = Contact()
UnknownPropertiesHandler.handle(ContentValues().apply {
put(UnknownProperties.UNKNOWN_PROPERTIES, "X-TEST:12345")
}, contact)
assertEquals("X-TEST:12345", contact.unknownProperties)
}
}
@@ -1,35 +1,53 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui.setup
package at.bitfire.davdroid.servicedetection
import android.app.Application
import android.security.NetworkSecurityPolicy
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.property.AddressbookHomeSet
import at.bitfire.dav4jvm.property.ResourceType
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.Credentials
import at.bitfire.davdroid.ui.setup.DavResourceFinder.Configuration.ServiceInfo
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.servicedetection.DavResourceFinder.Configuration.ServiceInfo
import at.bitfire.davdroid.settings.SettingsManager
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.After
import org.junit.Assert.*
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.net.URI
import javax.inject.Inject
@HiltAndroidTest
class DavResourceFinderTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Inject
lateinit var settingsManager: SettingsManager
@Before
fun inject() {
hiltRule.inject()
}
companion object {
private const val PATH_NO_DAV = "/nodav"
private const val PATH_CALDAV = "/caldav"
@@ -45,22 +63,21 @@ class DavResourceFinderTest {
lateinit var finder: DavResourceFinder
lateinit var client: HttpClient
lateinit var loginModel: LoginModel
@Before
fun initServerAndClient() {
server.setDispatcher(TestDispatcher())
server.dispatcher = TestDispatcher()
server.start()
val application = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application
loginModel = LoginModel()
loginModel.baseURI = URI.create("/")
loginModel.credentials = Credentials("mock", "12345")
val baseURI = URI.create("/")
val credentials = Credentials("mock", "12345")
finder = DavResourceFinder(InstrumentationRegistry.getInstrumentation().targetContext, loginModel)
client = HttpClient.Builder()
.addAuthentication(null, loginModel.credentials!!)
finder = DavResourceFinder(InstrumentationRegistry.getInstrumentation().targetContext, baseURI, credentials)
client = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext)
.addAuthentication(null, credentials)
.build()
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
}
@After
@@ -76,7 +93,7 @@ class DavResourceFinderTest {
var info = ServiceInfo()
DavResource(client.okHttpClient, server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL))
.propfind(0, AddressbookHomeSet.NAME) { response, _ ->
finder.scanCardDavResponse(response, info)
finder.scanResponse(ResourceType.ADDRESSBOOK, response, info)
}
assertEquals(0, info.collections.size)
assertEquals(1, info.homeSets.size)
@@ -86,7 +103,7 @@ class DavResourceFinderTest {
info = ServiceInfo()
DavResource(client.okHttpClient, server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK))
.propfind(0, ResourceType.NAME) { response, _ ->
finder.scanCardDavResponse(response, info)
finder.scanResponse(ResourceType.ADDRESSBOOK, response, info)
}
assertEquals(1, info.collections.size)
assertEquals(server.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), info.collections.keys.first())
@@ -126,21 +143,31 @@ class DavResourceFinderTest {
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV))
}
@Test
fun testQueryEmailAddress() {
var info = ServiceInfo()
assertArrayEquals(
arrayOf("email1@example.com", "email2@example.com"),
finder.queryEmailAddress(server.url(PATH_CALDAV + SUBPATH_PRINCIPAL)).toTypedArray()
)
assertTrue(finder.queryEmailAddress(server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL)).isEmpty())
}
// mock server
class TestDispatcher: Dispatcher() {
override fun dispatch(rq: RecordedRequest): MockResponse {
if (!checkAuth(rq)) {
override fun dispatch(request: RecordedRequest): MockResponse {
if (!checkAuth(request)) {
val authenticate = MockResponse().setResponseCode(401)
authenticate.setHeader("WWW-Authenticate", "Basic realm=\"test\"")
return authenticate
}
val path = rq.path
val path = request.path!!
if (rq.method.equals("OPTIONS", true)) {
if (request.method.equals("OPTIONS", true)) {
val dav = when {
path.startsWith(PATH_CALDAV) -> "calendar-access"
path.startsWith(PATH_CARDDAV) -> "addressbook"
@@ -151,7 +178,7 @@ class DavResourceFinderTest {
if (dav != null)
response.addHeader("DAV", dav)
return response
} else if (rq.method.equals("PROPFIND", true)) {
} else if (request.method.equals("PROPFIND", true)) {
val props: String?
when (path) {
PATH_CALDAV,
@@ -169,14 +196,21 @@ class DavResourceFinderTest {
" <CARD:addressbook/>" +
"</resourcetype>"
PATH_CALDAV + SUBPATH_PRINCIPAL ->
props = "<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>"
else -> props = null
}
Logger.log.info("Sending props: $props")
return MockResponse()
.setResponseCode(207)
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav' xmlns:CAL='urn:ietf:params:xml:ns:caldav'>" +
"<response>" +
" <href>${rq.path}</href>" +
" <href>${request.path}</href>" +
" <propstat><prop>$props</prop></propstat>" +
"</response>" +
"</multistatus>")
@@ -0,0 +1,771 @@
/***************************************************************************************************
* 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 android.util.Log
import androidx.hilt.work.HiltWorkerFactory
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.WorkManager
import androidx.work.testing.WorkManagerTestInitHelper
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.TestUtils.workScheduledOrRunning
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.db.HomeSet
import at.bitfire.davdroid.db.Principal
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.setup.LoginModel
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.every
import io.mockk.mockk
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.apache.commons.lang3.StringUtils
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.net.URI
import javax.inject.Inject
@HiltAndroidTest
class RefreshCollectionsWorkerTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
@Inject
lateinit var workerFactory: HiltWorkerFactory
@Before
fun setUp() {
hiltRule.inject()
// The test application is an instance of HiltTestApplication, which doesn't initialize notification channels.
// However, we need notification channels for the ongoing work notifications.
NotificationUtils.createChannels(context)
// Initialize WorkManager for instrumentation tests.
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setWorkerFactory(workerFactory)
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
// Test dependencies
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_ADDRESSBOOK_HOMESET = "/addressbooks-homeset"
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"
}
@Inject
lateinit var db: AppDatabase
@Inject
lateinit var settings: SettingsManager
var mockServer = MockWebServer()
lateinit var client: HttpClient
lateinit var loginModel: LoginModel
@Before
fun mockServerSetup() {
// Start mock web server
mockServer.dispatcher = TestDispatcher()
mockServer.start()
loginModel = LoginModel()
loginModel.baseURI = URI.create("/")
loginModel.credentials = Credentials("mock", "12345")
client = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext)
.addAuthentication(null, loginModel.credentials!!)
.build()
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
}
@After
fun cleanUp() {
mockServer.shutdown()
db.close()
}
// Actual tests
@Test
fun testRefreshCollections_enqueuesWorker() {
val service = createTestService(Service.TYPE_CALDAV)!!
val workerName = RefreshCollectionsWorker.refreshCollections(context, service.id)
assertTrue(workScheduledOrRunning(context, workerName))
}
@Test
fun testOnStopped_stopsRefreshThread() {
val service = createTestService(Service.TYPE_CALDAV)!!
val workerName = RefreshCollectionsWorker.refreshCollections(context, service.id)
WorkManager.getInstance(context).cancelUniqueWork(workerName)
assertFalse(workScheduledOrRunning(context, workerName))
// here we should test whether stopping the work really interrupts the refresh thread
}
@Test
fun testQueryHomesets() {
val service = createTestService(Service.TYPE_CARDDAV)!!
val baseUrl = mockServer.url(PATH_CARDDAV + SUBPATH_PRINCIPAL)
// Query home sets
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
.queryHomeSets(baseUrl)
// Check home sets have been saved to database
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET/"), db.homeSetDao().getByService(service.id).first().url)
assertEquals(1, db.homeSetDao().getByService(service.id).size)
}
// 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"))
)
// Refresh
RefreshCollectionsWorker.Refresher(db, service, settings, 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
RefreshCollectionsWorker.Refresher(db, service, settings, 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
RefreshCollectionsWorker.Refresher(db, service, settings, 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
RefreshCollectionsWorker.Refresher(db, service, settings, 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"))
)
// 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)
RefreshCollectionsWorker.Refresher(db, service, settings, 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
RefreshCollectionsWorker.Refresher(db, service, settings, 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
RefreshCollectionsWorker.Refresher(db, service, settings, 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)
RefreshCollectionsWorker.Refresher(db, service, settings, 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
RefreshCollectionsWorker.Refresher(db, service, settings, 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
RefreshCollectionsWorker.Refresher(db, service, settings, 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
RefreshCollectionsWorker.Refresher(db, service, settings, 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)!!
val settings = mockk<SettingsManager>()
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"))
)
val refresher = RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
assertFalse(refresher.shouldPreselect(collection, homesets))
}
@Test
fun shouldPreselect_all() {
val service = createTestService(Service.TYPE_CARDDAV)!!
val settings = mockk<SettingsManager>()
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"))
)
val refresher = RefreshCollectionsWorker.Refresher(db, service, settings, 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/")
val settings = mockk<SettingsManager>()
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"))
)
val refresher = RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
assertFalse(refresher.shouldPreselect(collection, homesets))
}
@Test
fun shouldPreselect_personal_notPersonal() {
val service = createTestService(Service.TYPE_CARDDAV)!!
val settings = mockk<SettingsManager>()
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"))
)
val refresher = RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
assertFalse(refresher.shouldPreselect(collection, homesets))
}
@Test
fun shouldPreselect_personal_isPersonal() {
val service = createTestService(Service.TYPE_CARDDAV)!!
val settings = mockk<SettingsManager>()
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"))
)
val refresher = RefreshCollectionsWorker.Refresher(db, service, settings, 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/")
val settings = mockk<SettingsManager>()
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"))
)
val refresher = RefreshCollectionsWorker.Refresher(db, service, settings, 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)
}
class TestDispatcher: Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
val path = StringUtils.removeEnd(request.path!!, "/")
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}</href>" +
"</CARD:addressbook-home-set>"
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_ADDRESSBOOK,
PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET ->
"<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_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 ->
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.log.info("Queried: $path")
Logger.log.info("Response: $responseBody")
return MockResponse()
.setResponseCode(responseCode)
.setBody(responseBody)
}
return MockResponse().setResponseCode(404)
}
}
}
@@ -0,0 +1,140 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.settings
import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentResolver
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.testing.WorkManagerTestInitHelper
import at.bitfire.davdroid.R
import at.bitfire.davdroid.TestUtils
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.syncadapter.AccountUtils
import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.ical4android.TaskProvider
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
/*@HiltAndroidTest
class AccountSettingsTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Inject
lateinit var settingsManager: SettingsManager
private val context = InstrumentationRegistry.getInstrumentation().targetContext
val account = Account(javaClass.canonicalName, context.getString(R.string.account_type))
val fakeCredentials = Credentials("test", "test")
val authorities = listOf(
context.getString(R.string.address_books_authority),
CalendarContract.AUTHORITY,
TaskProvider.ProviderName.JtxBoard.authority,
TaskProvider.ProviderName.OpenTasks.authority,
TaskProvider.ProviderName.TasksOrg.authority
)
@Before
fun setUp() {
hiltRule.inject()
assertTrue(AccountUtils.createAccount(
context,
account,
AccountSettings.initialUserData(fakeCredentials)
))
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1)
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0)
// The test application is an instance of HiltTestApplication, which doesn't initialize notification channels.
// However, we need notification channels for the ongoing work notifications.
NotificationUtils.createChannels(context)
// Initialize WorkManager for instrumentation tests.
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
@After
@RequiresApi(22)
fun removeAccount() {
AccountManager.get(context).removeAccountExplicitly(account)
}
@Test
fun testSyncIntervals() {
val settings = AccountSettings(context, account)
val presetIntervals =
context.resources.getStringArray(R.array.settings_sync_interval_seconds)
.map { it.toLong() }
.filter { it != AccountSettings.SYNC_INTERVAL_MANUALLY }
for (interval in presetIntervals) {
assertTrue(settings.setSyncInterval(CalendarContract.AUTHORITY, interval))
assertEquals(interval, settings.getSyncInterval(CalendarContract.AUTHORITY))
}
}
@Test
fun testSyncIntervals_Syncable() {
val settings = AccountSettings(context, account)
val interval = 15*60L // 15 min
val result = settings.setSyncInterval(CalendarContract.AUTHORITY, interval)
assertTrue(result)
}
@Test(expected = IllegalArgumentException::class)
fun testSyncIntervals_TooShort() {
val settings = AccountSettings(context, account)
val interval = 60L // 1 min is not supported by Android
settings.setSyncInterval(CalendarContract.AUTHORITY, interval)
}
@Test
fun testSyncIntervals_activatesPeriodicSyncWorker() {
val settings = AccountSettings(context, account)
val interval = 15*60L
for (authority in authorities) {
ContentResolver.setIsSyncable(account, authority, 1)
assertTrue(settings.setSyncInterval(authority, interval))
assertTrue(TestUtils.workScheduledOrRunningOrSuccessful(context, PeriodicSyncWorker.workerName(account, authority)))
assertEquals(interval, settings.getSyncInterval(authority))
}
}
@Test
fun testSyncIntervals_disablesPeriodicSyncWorker() {
val settings = AccountSettings(context, account)
val interval = AccountSettings.SYNC_INTERVAL_MANUALLY // -1
for (authority in authorities) {
ContentResolver.setIsSyncable(account, authority, 1)
assertTrue(settings.setSyncInterval(authority, interval))
assertFalse(TestUtils.workScheduledOrRunningOrSuccessful(context, PeriodicSyncWorker.workerName(account, authority)))
assertEquals(AccountSettings.SYNC_INTERVAL_MANUALLY, settings.getSyncInterval(authority))
}
}
}*/
@@ -1,38 +0,0 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.settings
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test
class DefaultsSettingsProviderTest {
private val provider: SettingsProvider = DefaultsProvider()
@Test
fun testHas() {
assertEquals(Pair(false, true), provider.has("notExisting"))
assertEquals(Pair(true, true), provider.has(Settings.OVERRIDE_PROXY))
}
@Test
fun testGet() {
assertEquals(Pair("localhost", true), provider.getString(Settings.OVERRIDE_PROXY_HOST))
assertEquals(Pair(8118, true), provider.getInt(Settings.OVERRIDE_PROXY_PORT))
}
@Test
fun testPutRemove() {
assertEquals(Pair(false, true), provider.isWritable(Settings.OVERRIDE_PROXY))
assertFalse(provider.putBoolean(Settings.OVERRIDE_PROXY, true))
assertFalse(provider.remove(Settings.OVERRIDE_PROXY))
}
}
@@ -0,0 +1,41 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.settings
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
class SettingsManagerTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Inject lateinit var settingsManager: SettingsManager
@Before
fun inject() {
hiltRule.inject()
}
@Test
fun testContainsKey_NotExisting() {
assertFalse(settingsManager.containsKey("notExisting"))
}
@Test
fun testContainsKey_Existing() {
// provided by DefaultsProvider
assertEquals(Settings.PROXY_TYPE_SYSTEM, settingsManager.getInt(Settings.PROXY_TYPE))
}
}
@@ -1,34 +0,0 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.settings
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class SettingsTest {
lateinit var settings: Settings
@Before
fun initialize() {
settings = Settings.getInstance(InstrumentationRegistry.getInstrumentation().targetContext)
}
@Test
fun testHas() {
assertFalse(settings.has("notExisting"))
// provided by DefaultsProvider
assertTrue(settings.has(Settings.OVERRIDE_PROXY))
}
}
@@ -0,0 +1,13 @@
package at.bitfire.davdroid.ui
import org.junit.Assert.assertEquals
import org.junit.Test
class AppSettingsActivityTest {
@Test
fun testResourceQualifierToLanguageTag() {
assertEquals("en", AppSettingsActivity.resourceQualifierToLanguageTag("en"))
assertEquals("en-GB", AppSettingsActivity.resourceQualifierToLanguageTag("en-GB"))
assertEquals("en-GB", AppSettingsActivity.resourceQualifierToLanguageTag("en-rGB"))
}
}
@@ -0,0 +1,37 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
class DebugInfoActivityTest {
@Test
fun testIntentBuilder_LargeLocalResource() {
val a = 'A'.code.toByte()
val intent = DebugInfoActivity.IntentBuilder(InstrumentationRegistry.getInstrumentation().context)
.withLocalResource(String(ByteArray(1024*1024, { a })))
.build()
val expected = StringBuilder(DebugInfoActivity.IntentBuilder.MAX_ELEMENT_SIZE)
expected.append(String(ByteArray(DebugInfoActivity.IntentBuilder.MAX_ELEMENT_SIZE - 3, { a })))
expected.append("...")
assertEquals(expected.toString(), intent.getStringExtra("localResource"))
}
@Test
fun testIntentBuilder_LargeLogs() {
val a = 'A'.code.toByte()
val intent = DebugInfoActivity.IntentBuilder(InstrumentationRegistry.getInstrumentation().context)
.withLogs(String(ByteArray(1024*1024, { a })))
.build()
val expected = StringBuilder(DebugInfoActivity.IntentBuilder.MAX_ELEMENT_SIZE)
expected.append(String(ByteArray(DebugInfoActivity.IntentBuilder.MAX_ELEMENT_SIZE - 3, { a })))
expected.append("...")
assertEquals(expected.toString(), intent.getStringExtra("logs"))
}
}
@@ -0,0 +1,222 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui.setup
import android.accounts.AccountManager
import android.content.ContentResolver
import android.content.Context
import android.provider.CalendarContract
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.testing.WorkManagerTestInitHelper
import at.bitfire.davdroid.R
import at.bitfire.davdroid.TestUtils.getOrAwaitValue
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.servicedetection.DavResourceFinder
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.GroupMethod
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.*
import org.junit.*
import org.junit.Assert.*
import javax.inject.Inject
// COMMENTED OUT because it doesn't run reliably [see https://github.com/bitfireAT/davx5/pull/320]
/*@HiltAndroidTest
class AccountDetailsFragmentTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule() // required for TestUtils: LiveData.getOrAwaitValue()
@Inject
lateinit var db: AppDatabase
@Inject
lateinit var settingsManager: SettingsManager
private val targetContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
private val fakeCredentials = Credentials("test", "test")
@Before
fun setUp() {
hiltRule.inject()
// The test application is an instance of HiltTestApplication, which doesn't initialize notification channels.
// However, we need notification channels for the ongoing work notifications.
NotificationUtils.createChannels(targetContext)
// Initialize WorkManager for instrumentation tests.
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(targetContext, config)
}
@After
fun tearDown() {
// Remove accounts created by tests
val am = AccountManager.get(targetContext)
val accounts = am.getAccountsByType(targetContext.getString(R.string.account_type))
for (account in accounts) {
am.removeAccountExplicitly(account)
}
}
@Test
fun testModel_CreateAccount_configuresContactsAndCalendars() {
val accountName = "MyAccountName"
val emptyServiceInfo = DavResourceFinder.Configuration.ServiceInfo()
val config = DavResourceFinder.Configuration(emptyServiceInfo, emptyServiceInfo, false, "")
// Create account -> should also set sync interval in settings
val accountCreated = AccountDetailsFragment.Model(targetContext, db, settingsManager)
.createAccount(accountName, fakeCredentials, config, GroupMethod.GROUP_VCARDS)
assertTrue(accountCreated.getOrAwaitValue(5))
// Get the created account
val account = AccountManager.get(targetContext)
.getAccountsByType(targetContext.getString(R.string.account_type))
.first { account -> account.name == accountName }
for (authority in listOf(
targetContext.getString(R.string.address_books_authority),
CalendarContract.AUTHORITY,
)) {
// Check isSyncable was set
assertEquals(1, ContentResolver.getIsSyncable(account, authority))
// Check default sync interval was set for
// [AccountSettings.KEY_SYNC_INTERVAL_ADDRESSBOOKS],
// [AccountSettings.KEY_SYNC_INTERVAL_CALENDARS]
assertEquals(
settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL),
AccountSettings(targetContext, account).getSyncInterval(authority)
)
}
}
@Test
@RequiresApi(28) // for mockkObject
fun testModel_CreateAccount_configuresCalendarsWithTasks() {
for (provider in listOf(
TaskProvider.ProviderName.JtxBoard,
TaskProvider.ProviderName.OpenTasks,
TaskProvider.ProviderName.TasksOrg
)) {
val accountName = "testAccount-$provider"
val emptyServiceInfo = DavResourceFinder.Configuration.ServiceInfo()
val config = DavResourceFinder.Configuration(emptyServiceInfo, emptyServiceInfo, false, "")
// Mock TaskUtils currentProvider method, pretending that one of the task apps is installed :)
mockkObject(TaskUtils)
every { TaskUtils.currentProvider(targetContext) } returns provider
assertEquals(provider, TaskUtils.currentProvider(targetContext))
// Create account -> should also set tasks sync interval in settings
val accountCreated =
AccountDetailsFragment.Model(targetContext, db, settingsManager)
.createAccount(accountName, fakeCredentials, config, GroupMethod.GROUP_VCARDS)
assertTrue(accountCreated.getOrAwaitValue(5))
// Get the created account
val account = AccountManager.get(targetContext)
.getAccountsByType(targetContext.getString(R.string.account_type))
.first { account -> account.name == accountName }
// Calendar: Check isSyncable and default interval are set correctly
assertEquals(1, ContentResolver.getIsSyncable(account, CalendarContract.AUTHORITY))
assertEquals(
settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL),
AccountSettings(targetContext, account).getSyncInterval(CalendarContract.AUTHORITY)
)
// Tasks: Check isSyncable and default sync interval were set
assertEquals(1, ContentResolver.getIsSyncable(account, provider.authority))
assertEquals(
settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL),
AccountSettings(targetContext, account).getSyncInterval(provider.authority)
)
}
}
@Test
@RequiresApi(28)
fun testModel_CreateAccount_configuresCalendarsWithoutTasks() {
try {
val accountName = "testAccount"
val emptyServiceInfo = DavResourceFinder.Configuration.ServiceInfo()
val config = DavResourceFinder.Configuration(emptyServiceInfo, emptyServiceInfo, false, "")
// Mock TaskUtils currentProvider method, pretending that no task app is installed
mockkObject(TaskUtils)
every { TaskUtils.currentProvider(targetContext) } returns null
assertEquals(null, TaskUtils.currentProvider(targetContext))
// Mock static ContentResolver calls
// TODO: Should not be needed, see below
mockkStatic(ContentResolver::class)
every { ContentResolver.setIsSyncable(any(), any(), any()) } returns Unit
every { ContentResolver.getIsSyncable(any(), any()) } returns 1
// Create account will try to start an initial collection refresh, which we don't need, so we mockk it
mockkObject(RefreshCollectionsWorker.Companion)
every { RefreshCollectionsWorker.refreshCollections(any(), any()) } returns ""
// Create account -> should also set tasks sync interval in settings
val accountCreated = AccountDetailsFragment.Model(targetContext, db, settingsManager)
.createAccount(accountName, fakeCredentials, config, GroupMethod.GROUP_VCARDS)
assertTrue(accountCreated.getOrAwaitValue(5))
// Get the created account
val account = AccountManager.get(targetContext)
.getAccountsByType(targetContext.getString(R.string.account_type))
.first { account -> account.name == accountName }
val accountSettings = AccountSettings(targetContext, account)
// Calendar: Check automatic sync is enabled and default interval are set correctly
assertEquals(1, ContentResolver.getIsSyncable(account, CalendarContract.AUTHORITY))
assertEquals(
settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL),
accountSettings.getSyncInterval(CalendarContract.AUTHORITY)
)
// Tasks: Check isSyncable state is unknown (=-1) and sync interval is "unset" (=null)
for (authority in listOf(
TaskProvider.ProviderName.JtxBoard.authority,
TaskProvider.ProviderName.OpenTasks.authority,
TaskProvider.ProviderName.TasksOrg.authority
)) {
// Until below is fixed, just verify the method for enabling sync did not get called
verify(exactly = 0) { ContentResolver.setIsSyncable(account, authority, 1) }
// TODO: Flaky, returns 1, although it should not. It only returns -1 if the test is run
// alone, on a blank emulator and if it's the first test run.
// This seems to have to do with the previous calls to ContentResolver by other tests.
// Finding a a way of resetting the ContentResolver before each test is run should
// solve the issue.
//assertEquals(-1, ContentResolver.getIsSyncable(account, authority))
//assertNull(accountSettings.getSyncInterval(authority)) // Depends on above
}
} catch (e: InterruptedException) {
// The sync adapter framework will start a sync, which can get interrupted. We don't care
// about being interrupted. If it happens the test is not too important.
}
}
}*/
@@ -0,0 +1,76 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui.webdav
import android.security.NetworkSecurityPolicy
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.WebDavMount
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.spyk
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
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 javax.inject.Inject
@HiltAndroidTest
class AddWebdavMountActivityTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Inject
lateinit var db: AppDatabase
@Before
fun setUp() {
hiltRule.inject()
model = spyk(AddWebdavMountActivity.Model(InstrumentationRegistry.getInstrumentation().targetContext, db))
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
}
lateinit var model: AddWebdavMountActivity.Model
val web = MockWebServer()
@Test
fun testHasWebDav_NoDavHeader() {
web.enqueue(MockResponse().setResponseCode(200))
assertFalse(model.hasWebDav(WebDavMount(name = "Test", url = web.url("/")), null))
}
@Test
fun testHasWebDav_DavClass_1() {
web.enqueue(MockResponse()
.setResponseCode(200)
.addHeader("DAV", "1"))
assertTrue(model.hasWebDav(WebDavMount(name = "Test", url = web.url("/")), null))
}
@Test
fun testHasWebDav_DavClass_1and2() {
web.enqueue(MockResponse()
.setResponseCode(200)
.addHeader("DAV", "1,2"))
assertTrue(model.hasWebDav(WebDavMount(name = "Test", url = web.url("/")), null))
}
@Test
fun testHasWebDav_DavClass_2() {
web.enqueue(MockResponse()
.setResponseCode(200)
.addHeader("DAV", "2"))
assertTrue(model.hasWebDav(WebDavMount(name = "Test", url = web.url("/")), null))
}
}
@@ -0,0 +1,26 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.webdav
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.davdroid.db.Credentials
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class CredentialsStoreTest {
private val store = CredentialsStore(InstrumentationRegistry.getInstrumentation().targetContext)
@Test
fun testSetGetDelete() {
store.setCredentials(0, Credentials(userName = "myname", password = "12345"))
assertEquals(Credentials(userName = "myname", password = "12345"), store.getCredentials(0))
store.setCredentials(0, null)
assertNull(store.getCredentials(0))
}
}
@@ -0,0 +1,255 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.webdav
import android.content.Context
import android.security.NetworkSecurityPolicy
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.db.WebDavDocument
import at.bitfire.davdroid.db.WebDavMount
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.setup.LoginModel
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import okhttp3.CookieJar
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.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.net.URI
import javax.inject.Inject
@HiltAndroidTest
class DavDocumentsProviderTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
@Inject lateinit var db: AppDatabase
@Before
fun setUp() {
hiltRule.inject()
}
private var mockServer = MockWebServer()
private lateinit var client: HttpClient
private lateinit var loginModel: LoginModel
companion object {
private const val PATH_WEBDAV_ROOT = "/webdav"
}
@Before
fun mockServerSetup() {
// Start mock web server
mockServer.dispatcher = TestDispatcher()
mockServer.start()
loginModel = LoginModel()
loginModel.baseURI = URI.create("/")
loginModel.credentials = Credentials("mock", "12345")
client = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext)
.addAuthentication(null, loginModel.credentials!!)
.build()
// mock server delivers HTTP without encryption
assertTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
}
@After
fun cleanUp() {
mockServer.shutdown()
db.close()
}
@Test
fun testDoQueryChildren_insert() {
// Create parent and root in database
val id = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", mockServer.url(PATH_WEBDAV_ROOT)))
val webDavMount = db.webDavMountDao().getById(id)
val parent = db.webDavDocumentDao().getOrCreateRoot(webDavMount)
val cookieStore = mutableMapOf<Long, CookieJar>()
// Query
DavDocumentsProvider.DavDocumentsActor(context, db, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority))
.queryChildren(parent)
// Assert new children were inserted into db
assertEquals(3, db.webDavDocumentDao().getChildren(parent.id).size)
assertEquals("Secret_Document.pages", db.webDavDocumentDao().getChildren(parent.id)[0].displayName)
assertEquals("MeowMeow_Cats.docx", db.webDavDocumentDao().getChildren(parent.id)[1].displayName)
assertEquals("Library", db.webDavDocumentDao().getChildren(parent.id)[2].displayName)
}
@Test
fun testDoQueryChildren_update() {
// Create parent and root in database
val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", mockServer.url(PATH_WEBDAV_ROOT)))
val webDavMount = db.webDavMountDao().getById(mountId)
val parent = db.webDavDocumentDao().getOrCreateRoot(webDavMount)
val cookieStore = mutableMapOf<Long, CookieJar>()
assertEquals("Cat food storage", db.webDavDocumentDao().get(parent.id)!!.displayName)
// Create a folder
val folderId = db.webDavDocumentDao().insert(
WebDavDocument(
0,
mountId,
parent.id,
"My_Books",
true,
"My Books",
)
)
assertEquals("My_Books", db.webDavDocumentDao().get(folderId)!!.name)
assertEquals("My Books", db.webDavDocumentDao().get(folderId)!!.displayName)
// Query - should update the parent displayname and folder name
DavDocumentsProvider.DavDocumentsActor(context, db, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority))
.queryChildren(parent)
// Assert parent and children were updated in database
assertEquals("Cats WebDAV", db.webDavDocumentDao().get(parent.id)!!.displayName)
assertEquals("Library", db.webDavDocumentDao().getChildren(parent.id)[2].name)
assertEquals("Library", db.webDavDocumentDao().getChildren(parent.id)[2].displayName)
}
@Test
fun testDoQueryChildren_delete() {
// Create parent and root in database
val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", mockServer.url(PATH_WEBDAV_ROOT)))
val webDavMount = db.webDavMountDao().getById(mountId)
val parent = db.webDavDocumentDao().getOrCreateRoot(webDavMount)
val cookieStore = mutableMapOf<Long, CookieJar>()
// Create a folder
val folderId = db.webDavDocumentDao().insert(
WebDavDocument(0, mountId, parent.id, "deleteme", true, "Should be deleted")
)
assertEquals("deleteme", db.webDavDocumentDao().get(folderId)!!.name)
// Query - discovers serverside deletion
DavDocumentsProvider.DavDocumentsActor(context, db, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority))
.queryChildren(parent)
// Assert folder got deleted
assertEquals(null, db.webDavDocumentDao().get(folderId))
}
@Test
fun testDoQueryChildren_updateTwoParentsSimultaneous() {
// Create root in database
val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", mockServer.url(PATH_WEBDAV_ROOT)))
val webDavMount = db.webDavMountDao().getById(mountId)
val root = db.webDavDocumentDao().getOrCreateRoot(webDavMount)
val cookieStore = mutableMapOf<Long, CookieJar>()
// Create two parents
val parent1Id = db.webDavDocumentDao().insert(WebDavDocument(0, mountId, root.id, "parent1", true))
val parent2Id = db.webDavDocumentDao().insert(WebDavDocument(0, mountId, root.id, "parent2", true))
val parent1 = db.webDavDocumentDao().get(parent1Id)!!
val parent2 = db.webDavDocumentDao().get(parent2Id)!!
assertEquals("parent1", parent1.name)
assertEquals("parent2", parent2.name)
// Query - find children of two nodes simultaneously
DavDocumentsProvider.DavDocumentsActor(context, db, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority))
.queryChildren(parent1)
DavDocumentsProvider.DavDocumentsActor(context, db, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority))
.queryChildren(parent2)
// Assert the two folders names have changed
assertEquals("childOne.txt", db.webDavDocumentDao().getChildren(parent1Id)[0].name)
assertEquals("childTwo.txt", db.webDavDocumentDao().getChildren(parent2Id)[0].name)
}
// mock server
class TestDispatcher: Dispatcher() {
data class Resource(
val name: String,
val props: String
)
override fun dispatch(request: RecordedRequest): MockResponse {
val requestPath = request.path!!.trimEnd('/')
if (request.method.equals("PROPFIND", true)) {
val propsMap = mutableMapOf(
PATH_WEBDAV_ROOT to arrayOf(
Resource("",
"<resourcetype><collection/></resourcetype>" +
"<displayname>Cats WebDAV</displayname>"
),
Resource("Secret_Document.pages",
"<displayname>Secret_Document.pages</displayname>",
),
Resource("MeowMeow_Cats.docx",
"<displayname>MeowMeow_Cats.docx</displayname>"
),
Resource("Library",
"<resourcetype><collection/></resourcetype>" +
"<displayname>Library</displayname>"
)
),
"$PATH_WEBDAV_ROOT/parent1" to arrayOf(
Resource("childOne.txt",
"<displayname>childOne.txt</displayname>"
),
),
"$PATH_WEBDAV_ROOT/parent2" to arrayOf(
Resource("childTwo.txt",
"<displayname>childTwo.txt</displayname>"
)
)
)
val responses = propsMap[requestPath]?.joinToString { resource ->
"<response><href>$requestPath/${resource.name}</href><propstat><prop>" +
resource.props +
"</prop></propstat></response>"
}
val multistatus =
"<multistatus xmlns='DAV:' " +
"xmlns:CARD='urn:ietf:params:xml:ns:carddav' " +
"xmlns:CAL='urn:ietf:params:xml:ns:caldav'>" +
responses +
"</multistatus>"
Logger.log.info("Query path: $requestPath")
Logger.log.info("Response: $multistatus")
return MockResponse()
.setResponseCode(207)
.setBody(multistatus)
}
return MockResponse().setResponseCode(404)
}
}
}
@@ -0,0 +1,78 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.webdav
import at.bitfire.davdroid.webdav.cache.MemoryCache
import org.apache.commons.io.FileUtils
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
class MemoryCacheTest {
companion object {
val SAMPLE_KEY1 = "key1"
val SAMPLE_CONTENT1 = "Sample Content 1".toByteArray()
val SAMPLE_CONTENT2 = "Another Content".toByteArray()
}
lateinit var storage: MemoryCache<String>
@Before
fun createStorage() {
storage = MemoryCache(1*FileUtils.ONE_MB.toInt())
}
@Test
fun testGet() {
// no entry yet, get should return null
assertNull(storage.get(SAMPLE_KEY1))
// add entry
storage.getOrPut(SAMPLE_KEY1) { SAMPLE_CONTENT1 }
assertArrayEquals(SAMPLE_CONTENT1, storage.get(SAMPLE_KEY1))
}
@Test
fun testGetOrPut() {
assertNull(storage.get(SAMPLE_KEY1))
// no entry yet; SAMPLE_CONTENT1 should be generated
var calledGenerateSampleContent1 = false
assertArrayEquals(SAMPLE_CONTENT1, storage.getOrPut(SAMPLE_KEY1) {
calledGenerateSampleContent1 = true
SAMPLE_CONTENT1
})
assertTrue(calledGenerateSampleContent1)
assertNotNull(storage.get(SAMPLE_KEY1))
// now there's a SAMPLE_CONTENT1 entry, it should be returned while SAMPLE_CONTENT2 is not generated
var calledGenerateSampleContent2 = false
assertArrayEquals(SAMPLE_CONTENT1, storage.getOrPut(SAMPLE_KEY1) {
calledGenerateSampleContent2 = true
SAMPLE_CONTENT2
})
assertFalse(calledGenerateSampleContent2)
}
@Test
fun testMaxCacheSize() {
// Cache size is 1 MB. Add 11*100 kB -> the first entry should be gone then
for (i in 0 until 11) {
val key = "key$i"
storage.getOrPut(key) {
ByteArray(100 * FileUtils.ONE_KB.toInt()) { i.toByte() }
}
assertNotNull(storage.get(key))
}
// now key0 should have been evicted and only key1..key11 should be there
assertNull(storage.get("key0"))
for (i in 1 until 11)
assertNotNull(storage.get("key$i"))
}
}
@@ -0,0 +1,159 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.webdav
import at.bitfire.davdroid.webdav.cache.Cache
import at.bitfire.davdroid.webdav.cache.SegmentedCache
import org.apache.commons.io.FileUtils
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Test
class SegmentedCacheTest {
companion object {
const val PAGE_SIZE = 100*FileUtils.ONE_KB.toInt()
const val SAMPLE_KEY1 = "key1"
const val PAGE2_SIZE = 123
}
val noCache = object: Cache<SegmentedCache.SegmentKey<String>> {
override fun get(key: SegmentedCache.SegmentKey<String>) = null
override fun getOrPut(key: SegmentedCache.SegmentKey<String>, generate: () -> ByteArray) = generate()
}
@Test
fun testRead_AcrossPages() {
val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> {
override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int) =
when (key.segment) {
0 -> ByteArray(PAGE_SIZE) { 1 }
1 -> ByteArray(PAGE2_SIZE) { 2 }
else -> throw IndexOutOfBoundsException()
}
}, noCache)
val dst = ByteArray(20)
assertEquals(20, cache.read(SAMPLE_KEY1, (PAGE_SIZE - 10).toLong(), dst.size, dst))
assertArrayEquals(ByteArray(20) { i ->
if (i < 10)
1
else
2
}, dst)
}
@Test
fun testRead_AcrossPagesAndEOF() {
val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> {
override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int) =
when (key.segment) {
0 -> ByteArray(PAGE_SIZE) { 1 }
1 -> ByteArray(PAGE2_SIZE) { 2 }
else -> throw IndexOutOfBoundsException()
}
}, noCache)
val dst = ByteArray(10 + PAGE2_SIZE + 10)
assertEquals(10 + PAGE2_SIZE, cache.read(SAMPLE_KEY1, (PAGE_SIZE - 10).toLong(), dst.size, dst))
assertArrayEquals(ByteArray(10 + PAGE2_SIZE) { i ->
if (i < 10)
1
else
2
}, dst.copyOf(10 + PAGE2_SIZE))
}
@Test
fun testRead_ExactlyPageSize_BufferAlsoPageSize() {
var loadCalled = 0
val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> {
override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int): ByteArray {
loadCalled++
if (key.segment == 0)
return ByteArray(PAGE_SIZE)
else
throw IndexOutOfBoundsException()
}
}, noCache)
val dst = ByteArray(PAGE_SIZE)
assertEquals(PAGE_SIZE, cache.read(SAMPLE_KEY1, 0, dst.size, dst))
assertEquals(1, loadCalled)
}
@Test
fun testRead_ExactlyPageSize_ButLargerBuffer() {
var loadCalled = 0
val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> {
override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int): ByteArray {
loadCalled++
if (key.segment == 0)
return ByteArray(PAGE_SIZE)
else
throw IndexOutOfBoundsException()
}
}, noCache)
val dst = ByteArray(PAGE_SIZE + 10) // 10 bytes more so that the second segment is read
assertEquals(PAGE_SIZE, cache.read(SAMPLE_KEY1, 0, dst.size, dst))
assertEquals(2, loadCalled)
}
@Test
fun testRead_Offset() {
val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> {
override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int): ByteArray {
if (key.segment == 0)
return ByteArray(PAGE_SIZE) { 1 }
else
throw IndexOutOfBoundsException()
}
}, noCache)
val dst = ByteArray(PAGE_SIZE)
assertEquals(PAGE_SIZE - 100, cache.read(SAMPLE_KEY1, 100, dst.size, dst))
assertArrayEquals(ByteArray(PAGE_SIZE) { i ->
if (i < PAGE_SIZE - 100)
1
else
0
}, dst)
}
@Test
fun testRead_OnlyOnePageSmallerThanPageSize_From0() {
val contentSize = 123
val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> {
override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int) =
when (key.segment) {
0 -> ByteArray(contentSize) { it.toByte() }
else -> throw IndexOutOfBoundsException()
}
}, noCache)
// read less than content size
var dst = ByteArray(10) // 10 < contentSize
assertEquals(10, cache.read(SAMPLE_KEY1, 0, dst.size, dst))
assertArrayEquals(ByteArray(10) { it.toByte() }, dst)
// read more than content size
dst = ByteArray(1000) // 1000 > contentSize
assertEquals(contentSize, cache.read(SAMPLE_KEY1, 0, dst.size, dst))
assertArrayEquals(ByteArray(1000) { i ->
if (i < contentSize)
i.toByte()
else
0
}, dst)
}
@Test
fun testRead_ZeroByteFile() {
val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> {
override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int) =
throw IndexOutOfBoundsException()
}, noCache)
val dst = ByteArray(10)
assertEquals(0, cache.read(SAMPLE_KEY1, 10, dst.size, dst))
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2013 2015 Ricki Hirner (bitfire web engineering).
~ Copyright (c) Ricki Hirner (bitfire web engineering).
~ All rights reserved. This program and the accompanying materials
~ are made available under the terms of the GNU Public License v3.0
~ which accompanies this distribution, and is available at
@@ -9,6 +9,6 @@
<resources>
<string name="app_name">DavdroidTest</string>
<string name="app_name">Davx5Test</string>
</resources>
@@ -0,0 +1,3 @@
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="at.bitfire.davdroid.SyncManagerTest"
android:label="Test Account" />
@@ -0,0 +1,44 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.settings.SettingsManager
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
lateinit var settingsManager: SettingsManager
@Before
fun inject() {
hiltRule.inject()
}
@Test
fun testIcloudWithSettings() {
val client = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext)
.build()
client.okHttpClient.newCall(Request.Builder()
.get()
.url("https://icloud.com")
.build())
.execute()
}
}
@@ -0,0 +1,59 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.accounts.AccountManager
import android.os.Bundle
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.davdroid.R
import at.bitfire.davdroid.settings.SettingsManager
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@HiltAndroidTest
class AccountUtilsTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Inject
lateinit var settingsManager: SettingsManager
@Before
fun inject() {
hiltRule.inject()
}
val context by lazy { InstrumentationRegistry.getInstrumentation().targetContext }
val account by lazy { Account("Test Account", context.getString(R.string.account_type)) }
@Test
fun testCreateAccount() {
val userData = Bundle(2)
userData.putString("int", "1")
userData.putString("string", "abc/\"-")
try {
assertTrue(AccountUtils.createAccount(context, account, userData))
// validate user data
val manager = AccountManager.get(context)
assertEquals("1", manager.getUserData(account, "int"))
assertEquals("abc/\"-", manager.getUserData(account, "string"))
} finally {
val futureResult = AccountManager.get(context).removeAccount(account, {}, null)
assertTrue(futureResult.getResult(10, TimeUnit.SECONDS))
}
}
}
@@ -0,0 +1,42 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.syncadapter
import at.bitfire.davdroid.db.SyncState
import at.bitfire.davdroid.resource.LocalCollection
class LocalTestCollection: LocalCollection<LocalTestResource> {
override val tag = "LocalTestCollection"
override val title = "Local Test Collection"
override var lastSyncState: SyncState? = null
val entries = mutableListOf<LocalTestResource>()
override fun findDeleted() = entries.filter { it.deleted }
override fun findDirty() = entries.filter { it.dirty }
override fun findByName(name: String) = entries.filter { it.fileName == name }.firstOrNull()
override fun markNotDirty(flags: Int): Int {
var updated = 0
for (dirty in findDirty()) {
dirty.flags = flags
updated++
}
return updated
}
override fun removeNotDirtyMarked(flags: Int): Int {
val numBefore = entries.size
entries.removeIf { !it.dirty && it.flags == flags }
return numBefore - entries.size
}
override fun forgetETags() {
}
}
@@ -0,0 +1,38 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.syncadapter
import at.bitfire.davdroid.resource.LocalResource
class LocalTestResource: LocalResource<Any> {
override val id: Long? = null
override var fileName: String? = null
override var eTag: String? = null
override var scheduleTag: String? = null
override var flags: Int = 0
var deleted = false
var dirty = false
override fun prepareForUpload() = "generated-file.txt"
override fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String?) {
dirty = false
if (fileName != null)
this.fileName = fileName
this.eTag = eTag
this.scheduleTag = scheduleTag
}
override fun updateFlags(flags: Int) {
this.flags = flags
}
override fun add() = throw NotImplementedError()
override fun update(data: Any) = throw NotImplementedError()
override fun delete() = throw NotImplementedError()
}
@@ -0,0 +1,126 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentResolver
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.testing.TestWorkerBuilder
import androidx.work.testing.WorkManagerTestInitHelper
import androidx.work.workDataOf
import at.bitfire.davdroid.R
import at.bitfire.davdroid.TestUtils
import at.bitfire.davdroid.TestUtils.workScheduledOrRunning
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.ical4android.TaskProvider
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.AfterClass
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.Executors
@HiltAndroidTest
class PeriodicSyncWorkerTest {
companion object {
val context = InstrumentationRegistry.getInstrumentation().targetContext
private val accountManager = AccountManager.get(context)
private val account = Account("Test Account", context.getString(R.string.account_type))
private val fakeCredentials = Credentials("test", "test")
@BeforeClass
@JvmStatic
fun setUp() {
// The test application is an instance of HiltTestApplication, which doesn't initialize notification channels.
// However, we need notification channels for the ongoing work notifications.
NotificationUtils.createChannels(context)
// Initialize WorkManager for instrumentation tests.
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
assertTrue(AccountUtils.createAccount(context, account, AccountSettings.initialUserData(fakeCredentials)))
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1)
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1)
}
@AfterClass
@JvmStatic
fun removeAccount() {
accountManager.removeAccountExplicitly(account)
}
}
private val executor = Executors.newSingleThreadExecutor()
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Before
fun inject() {
hiltRule.inject()
}
@Test
fun enable_enqueuesPeriodicWorker() {
PeriodicSyncWorker.enable(context, account, CalendarContract.AUTHORITY, 60, false)
val workerName = PeriodicSyncWorker.workerName(account, CalendarContract.AUTHORITY)
assertTrue(workScheduledOrRunning(context, workerName))
}
@Test
fun disable_removesPeriodicWorker() {
PeriodicSyncWorker.enable(context, account, CalendarContract.AUTHORITY, 60, false)
PeriodicSyncWorker.disable(context, account, CalendarContract.AUTHORITY)
val workerName = PeriodicSyncWorker.workerName(account, CalendarContract.AUTHORITY)
assertFalse(workScheduledOrRunning(context, workerName))
}
@Test
fun doWork_immediatelyEnqueuesSyncWorkerForGivenAuthority() {
val authorities = listOf(
context.getString(R.string.address_books_authority),
CalendarContract.AUTHORITY,
ContactsContract.AUTHORITY,
TaskProvider.ProviderName.JtxBoard.authority,
TaskProvider.ProviderName.OpenTasks.authority,
TaskProvider.ProviderName.TasksOrg.authority
)
for (authority in authorities) {
val inputData = workDataOf(
PeriodicSyncWorker.ARG_AUTHORITY to authority,
PeriodicSyncWorker.ARG_ACCOUNT_NAME to account.name,
PeriodicSyncWorker.ARG_ACCOUNT_TYPE to account.type
)
// Run PeriodicSyncWorker as TestWorker
TestWorkerBuilder<PeriodicSyncWorker>(context, executor, inputData).build().doWork()
// Check the PeriodicSyncWorker enqueued the right SyncWorker
assertTrue(TestUtils.workScheduledOrRunningOrSuccessful(context,
SyncWorker.workerName(account, authority)
))
}
}
}
@@ -0,0 +1,528 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import android.content.SyncResult
import android.util.Log
import androidx.core.app.NotificationManagerCompat
import androidx.hilt.work.HiltWorkerFactory
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.testing.WorkManagerTestInitHelper
import at.bitfire.dav4jvm.PropStat
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.Response.HrefRelation
import at.bitfire.dav4jvm.property.GetETag
import at.bitfire.davdroid.R
import at.bitfire.davdroid.TestUtils.assertWithin
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.db.SyncState
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.NotificationUtils
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import okhttp3.Protocol
import okhttp3.internal.http.StatusLine
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
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.Rule
import org.junit.Test
import java.time.Instant
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@HiltAndroidTest
class SyncManagerTest {
companion object {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val account = Account("SyncManagerTest", context.getString(R.string.account_type))
@BeforeClass
@JvmStatic
fun createAccount() {
assertTrue(AccountManager.get(context).addAccountExplicitly(account, "test", AccountSettings.initialUserData(Credentials("test", "test"))))
}
@AfterClass
@JvmStatic
fun removeAccount() {
assertTrue(AccountManager.get(context).removeAccount(account, null, null).getResult(10, TimeUnit.SECONDS))
// clear annoying syncError notifications
NotificationManagerCompat.from(context).cancelAll()
}
}
@get:Rule
val hiltRule = HiltAndroidRule(this)
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
@Inject
lateinit var workerFactory: HiltWorkerFactory
val server = MockWebServer()
@Before
fun inject() {
hiltRule.inject()
// The test application is an instance of HiltTestApplication, which doesn't initialize notification channels.
// However, we need notification channels for the ongoing work notifications.
NotificationUtils.createChannels(context)
// Initialize WorkManager for instrumentation tests.
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setWorkerFactory(workerFactory)
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
private fun syncManager(collection: LocalTestCollection, syncResult: SyncResult = SyncResult()) =
TestSyncManager(
context,
account,
arrayOf(),
"TestAuthority",
HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext).build(),
syncResult,
collection,
server
)
@Before
fun startServer() {
server.start()
}
@After
fun stopServer() {
server.close()
}
@Test
fun testGetDelayUntil_defaultOnNull() {
val now = Instant.now()
val delayUntil = SyncManager.getDelayUntil(null).epochSecond
val default = now.plusSeconds(SyncManager.DELAY_UNTIL_DEFAULT).epochSecond
assertWithin(default, delayUntil, 5)
}
@Test
fun testGetDelayUntil_reducesToMax() {
val now = Instant.now()
val delayUntil = SyncManager.getDelayUntil(now.plusSeconds(10*24*60*60)).epochSecond
val max = now.plusSeconds(SyncManager.DELAY_UNTIL_MAX).epochSecond
assertWithin(max, delayUntil, 5)
}
@Test
fun testGetDelayUntil_increasesToMin() {
val delayUntil = SyncManager.getDelayUntil(Instant.EPOCH).epochSecond
val min = Instant.now().plusSeconds(SyncManager.DELAY_UNTIL_MIN).epochSecond
assertWithin(min, delayUntil, 5)
}
private fun queryCapabilitiesResponse(cTag: String? = null): MockResponse {
val body = StringBuilder()
body.append("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
"<multistatus xmlns=\"DAV:\" xmlns:CALDAV=\"http://calendarserver.org/ns/\">\n" +
" <response>\n" +
" <href>/</href>\n" +
" <propstat>\n" +
" <prop>\n")
if (cTag != null)
body.append("<CALDAV:getctag>$cTag</CALDAV:getctag>\n")
body.append(
" </prop>\n" +
" </propstat>\n" +
" </response>\n" +
"</multistatus>")
val response = MockResponse()
.setResponseCode(207)
.setHeader("Content-Type", "text/xml")
.setBody(body.toString())
return response
}
@Test
fun testPerformSync_503RetryAfter_DelaySeconds() {
server.enqueue(MockResponse()
.setResponseCode(503)
.setHeader("Retry-After", "60")) // 60 seconds
val result = SyncResult()
val syncManager = syncManager(LocalTestCollection(), result)
syncManager.performSync()
val expected = Instant.now()
.plusSeconds(60)
.toEpochMilli()
// 5 sec tolerance for test
assertWithin(expected, result.delayUntil*1000, 5000)
}
@Test
fun testPerformSync_FirstSync_Empty() {
val collection = LocalTestCollection() /* no last known ctag */
server.enqueue(queryCapabilitiesResponse())
val syncManager = syncManager(collection)
syncManager.performSync()
assertFalse(syncManager.didGenerateUpload)
assertTrue(syncManager.didListAllRemote)
assertFalse(syncManager.didDownloadRemote)
assertFalse(syncManager.syncResult.hasError())
assertTrue(collection.entries.isEmpty())
}
@Test
fun testPerformSync_UploadNewMember_ETagOnPut() {
val collection = LocalTestCollection().apply {
lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag")
entries += LocalTestResource().apply {
dirty = true
}
}
server.enqueue(queryCapabilitiesResponse("ctag1"))
// PUT -> 204 No Content
server.enqueue(MockResponse()
.setResponseCode(204)
.setHeader("ETag", "etag-from-put"))
// modifications sent, so DAVx5 will query CTag again
server.enqueue(queryCapabilitiesResponse("ctag2"))
val syncManager = syncManager(collection).apply {
listAllRemoteResult = listOf(
Pair(Response(
server.url("/"),
server.url("/generated-file.txt"),
null,
listOf(PropStat(
listOf(
GetETag("\"etag-from-put\"")
),
StatusLine(Protocol.HTTP_1_1, 200, "OK")
)
)), HrefRelation.MEMBER)
)
}
syncManager.performSync()
assertTrue(syncManager.didGenerateUpload)
assertTrue(syncManager.didListAllRemote)
assertFalse(syncManager.didDownloadRemote)
assertFalse(syncManager.syncResult.hasError())
assertEquals(1, collection.entries.size)
assertEquals("etag-from-put", collection.entries.first().eTag)
}
@Test
fun testPerformSync_UploadModifiedMember_ETagOnPut() {
val collection = LocalTestCollection().apply {
lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag")
entries += LocalTestResource().apply {
fileName = "existing-file.txt"
eTag = "old-etag-like-on-server"
dirty = true
}
}
server.enqueue(queryCapabilitiesResponse("ctag1"))
// PUT -> 204 No Content
server.enqueue(MockResponse()
.setResponseCode(204)
.addHeader("ETag", "etag-from-put"))
// modifications sent, so DAVx5 will query CTag again
server.enqueue(queryCapabilitiesResponse("ctag2"))
val syncManager = syncManager(collection).apply {
listAllRemoteResult = listOf(
Pair(Response(
server.url("/"),
server.url("/existing-file.txt"),
null,
listOf(PropStat(
listOf(
GetETag("etag-from-put")
),
StatusLine(Protocol.HTTP_1_1, 200, "OK")
)
)), HrefRelation.MEMBER)
)
assertDownloadRemote = mapOf(Pair(server.url("/existing-file.txt"), "etag-from-put"))
}
syncManager.performSync()
assertTrue(syncManager.didGenerateUpload)
assertTrue(syncManager.didListAllRemote)
assertFalse(syncManager.didDownloadRemote)
assertFalse(syncManager.syncResult.hasError())
assertEquals(1, collection.entries.size)
assertEquals("etag-from-put", collection.entries.first().eTag)
}
@Test
fun testPerformSync_UploadModifiedMember_NoETagOnPut() {
val collection = LocalTestCollection().apply {
lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag")
entries += LocalTestResource().apply {
fileName = "existing-file.txt"
eTag = "old-etag-like-on-server"
dirty = true
}
}
server.enqueue(queryCapabilitiesResponse("ctag1"))
// PUT -> 204 No Content
server.enqueue(MockResponse().setResponseCode(204))
// modifications sent, so DAVx5 will query CTag again
server.enqueue(queryCapabilitiesResponse("ctag2"))
val syncManager = syncManager(collection).apply {
listAllRemoteResult = listOf(
Pair(Response(
server.url("/"),
server.url("/existing-file.txt"),
null,
listOf(PropStat(
listOf(
GetETag("etag-from-propfind")
),
StatusLine(Protocol.HTTP_1_1, 200, "OK")
)
)), HrefRelation.MEMBER)
)
assertDownloadRemote = mapOf(Pair(server.url("/existing-file.txt"), "etag-from-propfind"))
}
syncManager.performSync()
assertTrue(syncManager.didGenerateUpload)
assertTrue(syncManager.didListAllRemote)
assertTrue(syncManager.didDownloadRemote)
assertFalse(syncManager.syncResult.hasError())
assertEquals(1, collection.entries.size)
assertEquals("etag-from-propfind", collection.entries.first().eTag)
}
@Test
fun testPerformSync_UploadModifiedMember_412PreconditionFailed() {
val collection = LocalTestCollection().apply {
lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag")
entries += LocalTestResource().apply {
fileName = "existing-file.txt"
eTag = "etag-that-has-been-changed-on-server-in-the-meanwhile"
dirty = true
}
}
server.enqueue(queryCapabilitiesResponse("ctag1"))
// PUT -> 412 Precondition Failed
server.enqueue(MockResponse()
.setResponseCode(412))
// modifications sent, so DAVx5 will query CTag again
server.enqueue(queryCapabilitiesResponse("ctag1"))
val syncManager = syncManager(collection).apply {
listAllRemoteResult = listOf(
Pair(Response(
server.url("/"),
server.url("/existing-file.txt"),
null,
listOf(PropStat(
listOf(
GetETag("changed-etag-from-server")
),
StatusLine(Protocol.HTTP_1_1, 200, "OK")
)
)), HrefRelation.MEMBER)
)
assertDownloadRemote = mapOf(Pair(server.url("/existing-file.txt"), "changed-etag-from-server"))
}
syncManager.performSync()
assertTrue(syncManager.didGenerateUpload)
assertTrue(syncManager.didListAllRemote)
assertTrue(syncManager.didDownloadRemote)
assertFalse(syncManager.syncResult.hasError())
assertEquals(1, collection.entries.size)
assertEquals("changed-etag-from-server", collection.entries.first().eTag)
}
@Test
fun testPerformSync_NoopOnMemberWithSameETag() {
val collection = LocalTestCollection().apply {
lastSyncState = SyncState(SyncState.Type.CTAG, "ctag1")
entries += LocalTestResource().apply {
fileName = "downloaded-member.txt"
eTag = "MemberETag1"
}
}
server.enqueue(queryCapabilitiesResponse("ctag2"))
val syncManager = syncManager(collection).apply {
listAllRemoteResult = listOf(
Pair(Response(
server.url("/"),
server.url("/downloaded-member.txt"),
null,
listOf(PropStat(
listOf(
GetETag("\"MemberETag1\"")
),
StatusLine(Protocol.HTTP_1_1, 200, "OK")
)
)), HrefRelation.MEMBER)
)
}
syncManager.performSync()
assertFalse(syncManager.didGenerateUpload)
assertTrue(syncManager.didListAllRemote)
assertFalse(syncManager.didDownloadRemote)
assertFalse(syncManager.syncResult.hasError())
assertEquals(1, collection.entries.size)
assertEquals("MemberETag1", collection.entries.first().eTag)
}
@Test
fun testPerformSync_DownloadNewMember() {
val collection = LocalTestCollection().apply {
lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag")
}
server.enqueue(queryCapabilitiesResponse(cTag = "new-ctag"))
val syncManager = syncManager(collection).apply {
listAllRemoteResult = listOf(
Pair(Response(
server.url("/"),
server.url("/new-member.txt"),
null,
listOf(PropStat(
listOf(
GetETag("\"NewMemberETag1\"")
),
StatusLine(Protocol.HTTP_1_1, 200, "OK")
)
)), HrefRelation.MEMBER)
)
assertDownloadRemote = mapOf(Pair(server.url("/new-member.txt"), "NewMemberETag1"))
}
syncManager.performSync()
assertFalse(syncManager.didGenerateUpload)
assertTrue(syncManager.didListAllRemote)
assertTrue(syncManager.didDownloadRemote)
assertFalse(syncManager.syncResult.hasError())
assertEquals(1, collection.entries.size)
assertEquals("NewMemberETag1", collection.entries.first().eTag)
}
@Test
fun testPerformSync_DownloadUpdatedMember() {
val collection = LocalTestCollection().apply {
lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag")
entries += LocalTestResource().apply {
fileName = "downloaded-member.txt"
eTag = "MemberETag1"
}
}
server.enqueue(queryCapabilitiesResponse(cTag = "new-ctag"))
val syncManager = syncManager(collection).apply {
listAllRemoteResult = listOf(
Pair(Response(
server.url("/"),
server.url("/downloaded-member.txt"),
null,
listOf(PropStat(
listOf(
GetETag("\"MemberETag2\"")
),
StatusLine(Protocol.HTTP_1_1, 200, "OK")
)
)), HrefRelation.MEMBER)
)
assertDownloadRemote = mapOf(Pair(server.url("/downloaded-member.txt"), "MemberETag2"))
}
syncManager.performSync()
assertFalse(syncManager.didGenerateUpload)
assertTrue(syncManager.didListAllRemote)
assertTrue(syncManager.didDownloadRemote)
assertFalse(syncManager.syncResult.hasError())
assertEquals(1, collection.entries.size)
assertEquals("MemberETag2", collection.entries.first().eTag)
}
@Test
fun testPerformSync_RemoveVanishedMember() {
val collection = LocalTestCollection().apply {
lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag")
entries += LocalTestResource().apply {
fileName = "downloaded-member.txt"
}
}
server.enqueue(queryCapabilitiesResponse(cTag = "new-ctag"))
val syncManager = syncManager(collection)
syncManager.performSync()
assertFalse(syncManager.didGenerateUpload)
assertTrue(syncManager.didListAllRemote)
assertFalse(syncManager.didDownloadRemote)
assertFalse(syncManager.syncResult.hasError())
assertTrue(collection.entries.isEmpty())
}
@Test
fun testPerformSync_CTagDidntChange() {
val collection = LocalTestCollection().apply {
lastSyncState = SyncState(SyncState.Type.CTAG, "ctag1")
}
server.enqueue(queryCapabilitiesResponse("ctag1"))
val syncManager = syncManager(collection)
syncManager.performSync()
assertFalse(syncManager.didGenerateUpload)
assertFalse(syncManager.didListAllRemote)
assertFalse(syncManager.didDownloadRemote)
assertFalse(syncManager.syncResult.hasError())
assertTrue(collection.entries.isEmpty())
}
}
@@ -0,0 +1,159 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentResolver
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.testing.TestWorkerBuilder
import androidx.work.testing.WorkManagerTestInitHelper
import androidx.work.workDataOf
import at.bitfire.davdroid.R
import at.bitfire.davdroid.TestUtils.workScheduledOrRunningOrSuccessful
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.network.ConnectionUtils
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.NotificationUtils
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.Executors
@HiltAndroidTest
class SyncWorkerTest {
val context = InstrumentationRegistry.getInstrumentation().targetContext
private val accountManager = AccountManager.get(context)
private val account = Account("Test Account", context.getString(R.string.account_type))
private val fakeCredentials = Credentials("test", "test")
private val executor = Executors.newSingleThreadExecutor()
@get:Rule
val hiltRule = HiltAndroidRule(this)
@Before
fun inject() {
hiltRule.inject()
}
@Before
fun setUp() {
assertTrue(AccountUtils.createAccount(context, account, AccountSettings.initialUserData(fakeCredentials)))
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1)
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0)
// The test application is an instance of HiltTestApplication, which doesn't initialize notification channels.
// However, we need notification channels for the ongoing work notifications.
NotificationUtils.createChannels(context)
// Initialize WorkManager for instrumentation tests.
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
@After
fun removeAccount() {
accountManager.removeAccountExplicitly(account)
}
@Test
fun testEnqueue_enqueuesWorker() {
SyncWorker.enqueue(context, account, CalendarContract.AUTHORITY)
val workerName = SyncWorker.workerName(account, CalendarContract.AUTHORITY)
assertTrue(workScheduledOrRunningOrSuccessful(context, workerName))
}
@Test
fun testWifiConditionsMet_withoutWifi() {
val accountSettings = mockk<AccountSettings>()
every { accountSettings.getSyncWifiOnly() } returns false
assertTrue(SyncWorker.wifiConditionsMet(context, accountSettings))
}
@Test
fun testWifiConditionsMet_anyWifi_wifiEnabled() {
val accountSettings = AccountSettings(context, account)
accountSettings.setSyncWiFiOnly(true)
mockkObject(ConnectionUtils)
every { ConnectionUtils.wifiAvailable(any()) } returns true
mockkObject(SyncWorker.Companion)
every { SyncWorker.Companion.correctWifiSsid(any(), any()) } returns true
assertTrue(SyncWorker.wifiConditionsMet(context, accountSettings))
}
@Test
fun testWifiConditionsMet_anyWifi_wifiDisabled() {
val accountSettings = AccountSettings(context, account)
accountSettings.setSyncWiFiOnly(true)
mockkObject(ConnectionUtils)
every { ConnectionUtils.wifiAvailable(any()) } returns false
mockkObject(SyncWorker.Companion)
every { SyncWorker.Companion.correctWifiSsid(any(), any()) } returns true
assertFalse(SyncWorker.wifiConditionsMet(context, accountSettings))
}
@Test
fun testWifiConditionsMet_correctWifiSsid() {
// TODO: Write test
}
@Test
fun testWifiConditionsMet_wrongWifiSsid() {
// TODO: Write test
}
@Test
fun testOnStopped_interruptsSyncThread() {
val authority = CalendarContract.AUTHORITY
val inputData = workDataOf(
SyncWorker.ARG_AUTHORITY to authority,
SyncWorker.ARG_ACCOUNT_NAME to account.name,
SyncWorker.ARG_ACCOUNT_TYPE to account.type
)
// Create SyncWorker as TestWorker
val testSyncWorker = TestWorkerBuilder<SyncWorker>(context, executor, inputData).build()
assertNull(testSyncWorker.syncThread)
// Run SyncWorker and assert sync thread is alive
testSyncWorker.doWork()
assertNotNull(testSyncWorker.syncThread)
assertTrue(testSyncWorker.syncThread!!.isAlive)
assertFalse(testSyncWorker.syncThread!!.isInterrupted) // Sync running
// Stop SyncWorker and assert sync thread was interrupted
testSyncWorker.onStopped()
assertNotNull(testSyncWorker.syncThread)
assertTrue(testSyncWorker.syncThread!!.isAlive)
assertTrue(testSyncWorker.syncThread!!.isInterrupted) // Sync thread interrupted
}
}
@@ -0,0 +1,73 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.Context
import android.content.SyncResult
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.R
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
@HiltAndroidTest
class SyncerTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
val context = InstrumentationRegistry.getInstrumentation().targetContext
/** use our WebDAV provider as a mock provider because it's our own and we don't need any permissions for it */
val mockAuthority = context.getString(R.string.webdav_authority)
val mockProvider = context.contentResolver!!.acquireContentProviderClient(mockAuthority)!!
val account = Account(javaClass.canonicalName, context.getString(R.string.account_type))
@Before
fun setUp() {
hiltRule.inject()
}
@Test
fun testOnPerformSync_runsSyncAndSetsClassLoader() {
val syncer = TestSyncer(context)
syncer.onPerformSync(account, arrayOf(), mockAuthority, mockProvider, SyncResult())
// check whether onPerformSync() actually calls sync()
assertEquals(1, syncer.syncCalled.get())
// check whether contextClassLoader is set
assertEquals(context.classLoader, Thread.currentThread().contextClassLoader)
}
class TestSyncer(context: Context) : Syncer(context) {
val syncCalled = AtomicInteger()
override fun sync(
account: Account,
extras: Array<String>,
authority: String,
httpClient: Lazy<HttpClient>,
provider: ContentProviderClient,
syncResult: SyncResult
) {
Thread.sleep(1000)
syncCalled.incrementAndGet()
}
}
}
@@ -0,0 +1,104 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.content.Context
import android.content.SyncResult
import at.bitfire.dav4jvm.DavCollection
import at.bitfire.dav4jvm.MultiResponseCallback
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.property.GetCTag
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.db.SyncState
import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.util.DavUtils
import okhttp3.HttpUrl
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.mockwebserver.MockWebServer
import org.junit.Assert.assertEquals
class TestSyncManager(
context: Context,
account: Account,
extras: Array<String>,
authority: String,
httpClient: HttpClient,
syncResult: SyncResult,
localCollection: LocalTestCollection,
val mockWebServer: MockWebServer
): SyncManager<LocalTestResource, LocalTestCollection, DavCollection>(context, account, AccountSettings(context, account), httpClient, extras, authority, syncResult, localCollection) {
override fun prepare(): Boolean {
collectionURL = mockWebServer.url("/")
davCollection = DavCollection(httpClient.okHttpClient, collectionURL)
return true
}
var didQueryCapabilities = false
override fun queryCapabilities(): SyncState? {
if (didQueryCapabilities)
throw IllegalStateException("queryCapabilities() must not be called twice")
didQueryCapabilities = true
var cTag: SyncState? = null
davCollection.propfind(0, GetCTag.NAME) { response, rel ->
if (rel == Response.HrefRelation.SELF)
response[GetCTag::class.java]?.cTag?.let {
cTag = SyncState(SyncState.Type.CTAG, it)
}
}
return cTag
}
var didGenerateUpload = false
override fun generateUpload(resource: LocalTestResource): RequestBody {
didGenerateUpload = true
return resource.toString().toRequestBody()
}
override fun syncAlgorithm() = SyncAlgorithm.PROPFIND_REPORT
var listAllRemoteResult = emptyList<Pair<Response, Response.HrefRelation>>()
var didListAllRemote = false
override fun listAllRemote(callback: MultiResponseCallback) {
if (didListAllRemote)
throw IllegalStateException("listAllRemote() must not be called twice")
didListAllRemote = true
for (result in listAllRemoteResult)
callback.onResponse(result.first, result.second)
}
var assertDownloadRemote = emptyMap<HttpUrl, String>()
var didDownloadRemote = false
override fun downloadRemote(bunch: List<HttpUrl>) {
didDownloadRemote = true
assertEquals(assertDownloadRemote.keys.toList(), bunch)
for ((url, eTag) in assertDownloadRemote) {
val fileName = DavUtils.lastSegmentOfUrl(url)
var localEntry = localCollection.entries.filter { it.fileName == fileName }.firstOrNull()
if (localEntry == null) {
val newEntry = LocalTestResource().also {
it.fileName = fileName
}
localCollection.entries += newEntry
localEntry = newEntry
}
localEntry.eTag = eTag
localEntry.flags = LocalResource.FLAG_REMOTELY_PRESENT
}
}
override fun postProcess() {
}
override fun notifyInvalidResourceTitle() =
throw NotImplementedError()
}
+184 -46
View File
@@ -1,16 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="at.bitfire.davdroid"
xmlns:android="http://schemas.android.com/apk/res/android"
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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.FOREGROUND_SERVICE"/>
<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"/>
<!-- account management permissions not required for own accounts since API level 22 -->
@@ -29,28 +31,49 @@
<!-- android.permission-group.LOCATION -->
<!-- getting the WiFi name (for "sync in Wifi only") requires
- coarse location (Android 8.1)
- fine location (Android 10) -->
- 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/AppThemeExt"
tools:ignore="UnusedAttribute">
android:theme="@style/AppTheme"
android:resizeableActivity="true"
tools:ignore="UnusedAttribute"
android:supportsRtl="true">
<service android:name=".DavService"/>
<!-- required for Hilt/WorkManager integration -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
<service android:name=".ForegroundService"/>
<!-- 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" android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".ui.AccountsActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
android:theme="@style/AppTheme.NoActionBar"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@@ -70,28 +93,9 @@
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.setup.LoginActivity"
android:label="@string/login_title"
android:parentActivityName=".ui.AccountsActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
<activity
android:name=".ui.account.AccountActivity"
android:parentActivityName=".ui.AccountsActivity"
android:theme="@style/AppTheme.NoActionBar"/>
<activity android:name=".ui.AccountSettingsActivity"/>
<activity android:name=".ui.CreateAddressBookActivity"
android:label="@string/create_addressbook"/>
<activity android:name=".ui.CreateCalendarActivity"
android:label="@string/create_calendar"/>
<activity
android:name=".ui.DebugInfoActivity"
android:parentActivityName=".ui.AppSettingsActivity"
@@ -99,17 +103,75 @@
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>
<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>
<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:label="@string/login_title"
android:parentActivityName=".ui.AccountsActivity"
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 android:scheme="http" />
<data android:scheme="https" />
</intent-filter>
</activity>
<activity
android:name=".ui.account.AccountActivity"
android:parentActivityName=".ui.AccountsActivity"
android:theme="@style/AppTheme.NoActionBar"
android:exported="true">
</activity>
<activity
android:name=".ui.account.CreateAddressBookActivity"
android:label="@string/create_addressbook"
android:parentActivityName=".ui.account.AccountActivity" />
<activity
android:name=".ui.account.CreateCalendarActivity"
android:label="@string/create_calendar"
android:parentActivityName=".ui.account.AccountActivity" />
<activity
android:name=".ui.account.SettingsActivity"
android:parentActivityName=".ui.account.AccountActivity" />
<activity
android:name=".ui.account.WifiPermissionsActivity"
android:label="@string/wifi_permissions_label"
android:parentActivityName=".ui.account.SettingsActivity" />
<activity
android:name=".ui.webdav.WebdavMountsActivity"
android:label="@string/webdav_mounts_title"
android:parentActivityName=".ui.AccountsActivity"
android:exported="true" />
<activity
android:name=".ui.webdav.AddWebdavMountActivity"
android:label="@string/webdav_add_mount_title"
android:parentActivityName=".ui.webdav.WebdavMountsActivity" />
<!-- account type "DAVx⁵" -->
<service
@@ -118,7 +180,6 @@
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/account_authenticator"/>
@@ -130,28 +191,58 @@
<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=".syncadapter.TasksSyncAdapterService"
android:name=".syncadapter.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_tasks"/>
android:resource="@xml/sync_notes"/>
</service>
<service
android:name=".syncadapter.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=".syncadapter.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=".syncadapter.NullAuthenticatorService"
android:exported="false">
android:name=".syncadapter.AddressBookAuthenticatorService"
android:exported="true"> <!-- 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>
@@ -164,8 +255,7 @@
android:authorities="@string/address_books_authority"
android:exported="false"
android:label="@string/address_books_authority_title"
android:name=".syncadapter.AddressBookProvider"
android:multiprocess="false"/>
android:name=".syncadapter.AddressBookProvider" />
<service
android:name=".syncadapter.AddressBooksSyncAdapterService"
android:exported="true"
@@ -194,6 +284,54 @@
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>
</application>
</manifest>
<!-- 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>
+38
View File
@@ -0,0 +1,38 @@
carddav.a1.net
aol.com
calendar.dingtalk.com/dav/
cloud.disroot.org
dav.edis.at
dav.fruux.com
caldav.gmx.com
caldav.gmx.co.uk
caldav.gmx.de
caldav.gmx.es
caldav.gmx.fr
caldav.gmx.net
carddav.gmx.com
carddav.gmx.co.uk
carddav.gmx.de
carddav.gmx.es
carddav.gmx.fr
carddav.gmx.net
framagenda.org/remote.php/dav/
icloud.com
cloud.liberta.vip
office.luckycloud.de
calendar.mail.ru
dav.mailbox.org
mailfence.com
caldav.mailo.com
carddav.mailo.com
posteo.de:8443
dav.runbox.com
live.teambox.eu
spica.t-online.de
caldav.calendar.yahoo.com
yandex.ru
webmail.your-server.de/rpc.php/
calendar.zoho.com
calendar.zoho.eu
contacts.zoho.com
contacts.zoho.eu
Whitespace-only changes.
+74 -45
View File
@@ -1,62 +1,74 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
/***************************************************************************************************
* 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 android.content.Intent
import android.content.IntentFilter
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Build
import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.content.res.AppCompatResources
import androidx.multidex.MultiDexApplication
import androidx.core.graphics.drawable.toBitmap
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.syncadapter.AccountsUpdatedListener
import at.bitfire.davdroid.syncadapter.SyncUtils
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.UiUtils
import dagger.hilt.android.HiltAndroidApp
import java.util.logging.Level
import javax.inject.Inject
import kotlin.concurrent.thread
import kotlin.system.exitProcess
@Suppress("unused")
class App: MultiDexApplication(), Thread.UncaughtExceptionHandler {
@HiltAndroidApp
class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provider {
companion object {
fun getLauncherBitmap(context: Context): Bitmap? {
val drawableLogo = AppCompatResources.getDrawable(context, R.mipmap.ic_launcher)
return if (drawableLogo is BitmapDrawable)
drawableLogo.bitmap
else
null
}
const val HOMEPAGE_PRIVACY = "privacy"
fun homepageUrl(context: Context) =
Uri.parse(context.getString(R.string.homepage_url)).buildUpon()
.appendQueryParameter("pk_campaign", BuildConfig.APPLICATION_ID)
.appendQueryParameter("pk_kwd", context::class.java.simpleName)
.appendQueryParameter("app-version", BuildConfig.VERSION_NAME)
.build()!!
fun getLauncherBitmap(context: Context) =
AppCompatResources.getDrawable(context, R.mipmap.ic_launcher)?.toBitmap()
/**
* Gets the DAVx5 Web site URL that should be used to open in the user's browser.
* Package ID, version number and calling context name will be appended as arguments.
*
* @param context context name to use
* @param page optional page segment to append (for instance: [HOMEPAGE_PRIVACY]])
*
* @return the Uri for the browser
*/
fun homepageUrl(context: Context, page: String? = null): Uri {
val builder = Uri.parse(context.getString(R.string.homepage_url)).buildUpon()
if (page != null)
builder.appendPath(page)
return builder
.appendQueryParameter("pk_campaign", BuildConfig.APPLICATION_ID)
.appendQueryParameter("pk_kwd", context::class.java.simpleName)
.appendQueryParameter("app-version", BuildConfig.VERSION_NAME)
.build()
}
}
@Inject lateinit var accountsUpdatedListener: AccountsUpdatedListener
@Inject lateinit var storageLowReceiver: StorageLowReceiver
@Inject lateinit var workerFactory: HiltWorkerFactory
override fun onCreate() {
super.onCreate()
Logger.initialize(this)
//if (BuildConfig.FLAVOR == FLAVOR_STANDARD)
Thread.setDefaultUncaughtExceptionHandler(this)
if (BuildConfig.DEBUG)
// debug builds
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectActivityLeaks()
.detectFileUriExposure()
@@ -65,32 +77,49 @@ class App: MultiDexApplication(), Thread.UncaughtExceptionHandler {
.detectLeakedSqlLiteObjects()
.penaltyLog()
.build())
if (Build.VERSION.SDK_INT <= 21)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
else // if (BuildConfig.FLAVOR == FLAVOR_STANDARD)
// handle uncaught exceptions in non-debug standard flavor
Thread.setDefaultUncaughtExceptionHandler(this)
NotificationUtils.createChannels(this)
// set light/dark mode
UiUtils.setTheme(this) // when this is called in the asynchronous thread below, it recreates
// some current activity and causes an IllegalStateException in rare cases
// don't block UI for some background checks
thread {
// watch installed/removed apps
val tasksFilter = IntentFilter()
tasksFilter.addAction(Intent.ACTION_PACKAGE_ADDED)
tasksFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED)
tasksFilter.addDataScheme("package")
registerReceiver(PackageChangedReceiver(), tasksFilter)
// watch for account changes/deletions
accountsUpdatedListener.listen()
// foreground service (possible workaround for devices which prevent DAVx5 from being started)
ForegroundService.startIfActive(this)
// watch storage because low storage means synchronization is stopped
storageLowReceiver.listen()
// watch installed/removed apps
TasksWatcher.watch(this)
// check whether a tasks app is currently installed
PackageChangedReceiver.updateTaskSync(this)
SyncUtils.updateTaskSync(this)
// create/update app shortcuts
UiUtils.updateShortcuts(this)
}
}
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
override fun uncaughtException(t: Thread, e: Throwable) {
Logger.log.log(Level.SEVERE, "Unhandled exception!", e)
val intent = Intent(this, DebugInfoActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.putExtra(DebugInfoActivity.KEY_THROWABLE, e)
val intent = DebugInfoActivity.IntentBuilder(this)
.withCause(e)
.newTask()
.build()
startActivity(intent)
exitProcess(1)
@@ -1,12 +0,0 @@
package at.bitfire.davdroid
import android.content.ContentProviderClient
import android.os.Build
@Suppress("DEPRECATION")
fun ContentProviderClient.closeCompat() {
if (Build.VERSION.SDK_INT >= 24)
close()
else
release()
}
@@ -1,17 +1,14 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid
object Constants {
const val DAVDROID_GREEN_RGBA = 0xFF8bc34a.toInt()
const val DEFAULT_SYNC_INTERVAL = 4 * 3600L // 4 hours
// gplay billing
const val BILLINGCLIENT_CONNECTION_MAX_RETRIES = 4
/**
* Context label for [org.apache.commons.lang3.exception.ContextedException].
@@ -1,374 +0,0 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid
import android.accounts.Account
import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Intent
import android.os.Binder
import android.os.Bundle
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.room.Transaction
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.dav4jvm.property.*
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.*
import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import java.lang.ref.WeakReference
import java.util.*
import java.util.logging.Level
import kotlin.concurrent.thread
class DavService: android.app.Service() {
companion object {
const val ACTION_REFRESH_COLLECTIONS = "refreshCollections"
const val EXTRA_DAV_SERVICE_ID = "davServiceID"
/** Initialize a forced synchronization. Expects intent data
to be an URI of this format:
contents://<authority>/<account.type>/<account name>
**/
const val ACTION_FORCE_SYNC = "forceSync"
val DAV_COLLECTION_PROPERTIES = arrayOf(
ResourceType.NAME,
CurrentUserPrivilegeSet.NAME,
DisplayName.NAME,
AddressbookDescription.NAME, SupportedAddressData.NAME,
CalendarDescription.NAME, CalendarColor.NAME, SupportedCalendarComponentSet.NAME,
Source.NAME
)
}
private val runningRefresh = HashSet<Long>()
private val refreshingStatusListeners = LinkedList<WeakReference<RefreshingStatusListener>>()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.let {
val id = intent.getLongExtra(EXTRA_DAV_SERVICE_ID, -1)
when (intent.action) {
ACTION_REFRESH_COLLECTIONS ->
if (runningRefresh.add(id)) {
refreshingStatusListeners.forEach { listener ->
listener.get()?.onDavRefreshStatusChanged(id, true)
}
thread { refreshCollections(id) }
}
ACTION_FORCE_SYNC -> {
val uri = intent.data!!
val authority = uri.authority!!
val account = Account(
uri.pathSegments[1],
uri.pathSegments[0]
)
forceSync(authority, account)
}
}
}
return START_NOT_STICKY
}
/* BOUND SERVICE PART
for communicating with the activities
*/
interface RefreshingStatusListener {
fun onDavRefreshStatusChanged(id: Long, refreshing: Boolean)
}
private val binder = InfoBinder()
inner class InfoBinder: Binder() {
fun isRefreshing(id: Long) = runningRefresh.contains(id)
fun addRefreshingStatusListener(listener: RefreshingStatusListener, callImmediateIfRunning: Boolean) {
refreshingStatusListeners += WeakReference<RefreshingStatusListener>(listener)
if (callImmediateIfRunning)
runningRefresh.forEach { id -> listener.onDavRefreshStatusChanged(id, true) }
}
fun removeRefreshingStatusListener(listener: RefreshingStatusListener) {
val iter = refreshingStatusListeners.iterator()
while (iter.hasNext()) {
val item = iter.next().get()
if (listener == item)
iter.remove()
}
}
}
override fun onBind(intent: Intent?) = binder
/* ACTION RUNNABLES
which actually do the work
*/
private fun forceSync(authority: String, account: Account) {
Logger.log.info("Forcing $authority synchronization of $account")
val extras = Bundle(2)
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true) // manual sync
extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue)
ContentResolver.requestSync(account, authority, extras)
}
private fun refreshCollections(serviceId: Long) {
val db = AppDatabase.getInstance(this)
val homeSetDao = db.homeSetDao()
val collectionDao = db.collectionDao()
val service = db.serviceDao().get(serviceId) ?: throw IllegalArgumentException("Service not found")
val account = Account(service.accountName, getString(R.string.account_type))
val homeSets = homeSetDao.getByService(serviceId).associateBy { it.url }.toMutableMap()
val collections = collectionDao.getByService(serviceId).associateBy { it.url }.toMutableMap()
/**
* Checks if the given URL defines home sets and adds them to the home set list.
*
* @throws java.io.IOException
* @throws HttpException
* @throws at.bitfire.dav4jvm.exception.DavException
*/
fun queryHomeSets(client: OkHttpClient, url: HttpUrl, recurse: Boolean = true) {
val related = mutableSetOf<HttpUrl>()
fun findRelated(root: HttpUrl, dav: Response) {
// refresh home sets: calendar-proxy-read/write-for
dav[CalendarProxyReadFor::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is a read-only proxy for $href, checking for home sets")
root.resolve(href)?.let { proxyReadFor ->
related += proxyReadFor
}
}
}
dav[CalendarProxyWriteFor::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is a read/write proxy for $href, checking for home sets")
root.resolve(href)?.let { proxyWriteFor ->
related += proxyWriteFor
}
}
}
// refresh home sets: direct group memberships
dav[GroupMembership::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is member of group $href, checking for home sets")
root.resolve(href)?.let { groupMembership ->
related += groupMembership
}
}
}
}
val dav = DavResource(client, url)
when (service.type) {
Service.TYPE_CARDDAV ->
try {
dav.propfind(0, DisplayName.NAME, AddressbookHomeSet.NAME, GroupMembership.NAME) { response, _ ->
response[AddressbookHomeSet::class.java]?.let { homeSet ->
for (href in homeSet.hrefs)
dav.location.resolve(href)?.let {
val foundUrl = UrlUtils.withTrailingSlash(it)
homeSets[foundUrl] = HomeSet(0, service.id, foundUrl)
}
}
if (recurse)
findRelated(dav.location, response)
}
} catch (e: HttpException) {
if (e.code/100 == 4)
Logger.log.log(Level.INFO, "Ignoring Client Error 4xx while looking for addressbook home sets", e)
else
throw e
}
Service.TYPE_CALDAV -> {
try {
dav.propfind(0, DisplayName.NAME, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME) { response, _ ->
response[CalendarHomeSet::class.java]?.let { homeSet ->
for (href in homeSet.hrefs)
dav.location.resolve(href)?.let {
val foundUrl = UrlUtils.withTrailingSlash(it)
homeSets[foundUrl] = HomeSet(0, service.id, foundUrl)
}
}
if (recurse)
findRelated(dav.location, response)
}
} catch (e: HttpException) {
if (e.code/100 == 4)
Logger.log.log(Level.INFO, "Ignoring Client Error 4xx while looking for calendar home sets", e)
else
throw e
}
}
}
for (resource in related)
queryHomeSets(client, resource, false)
}
@Transaction
fun saveHomesets() {
DaoTools(homeSetDao).syncAll(
homeSetDao.getByService(serviceId),
homeSets,
{ it.url })
}
@Transaction
fun saveCollections() {
DaoTools(collectionDao).syncAll(
collectionDao.getByService(serviceId),
collections, { it.url }) { new, old ->
new.forceReadOnly = old.forceReadOnly
new.sync = old.sync
}
}
fun saveResults() {
saveHomesets()
saveCollections()
}
try {
Logger.log.info("Refreshing ${service.type} collections of service #$service")
// cancel previous notification
NotificationManagerCompat.from(this)
.cancel(service.toString(), NotificationUtils.NOTIFY_REFRESH_COLLECTIONS)
// create authenticating OkHttpClient (credentials taken from account settings)
HttpClient.Builder(this, AccountSettings(this, account))
.setForeground(true)
.build().use { client ->
val httpClient = client.okHttpClient
// refresh home set list (from principal)
service.principal?.let { principalUrl ->
Logger.log.fine("Querying principal $principalUrl for home sets")
queryHomeSets(httpClient, principalUrl)
}
// now refresh homesets and their member collections
val itHomeSets = homeSets.iterator()
while (itHomeSets.hasNext()) {
val homeSet = itHomeSets.next()
Logger.log.fine("Listing home set ${homeSet.key}")
try {
DavResource(httpClient, homeSet.key).propfind(1, *DAV_COLLECTION_PROPERTIES) { response, relation ->
if (!response.isSuccess())
return@propfind
if (relation == Response.HrefRelation.SELF) {
// this response is about the homeset itself
homeSet.value.displayName = response[DisplayName::class.java]?.displayName
homeSet.value.privBind = response[CurrentUserPrivilegeSet::class.java]?.mayBind ?: true
} else {
val info = Collection.fromDavResponse(response) ?: return@propfind
info.serviceId = serviceId
info.confirmed = true
Logger.log.log(Level.FINE, "Found collection", info)
if ((service.type == Service.TYPE_CARDDAV && info.type == Collection.TYPE_ADDRESSBOOK) ||
(service.type == Service.TYPE_CALDAV && arrayOf(Collection.TYPE_CALENDAR, Collection.TYPE_WEBCAL).contains(info.type)))
collections[response.href] = info
}
}
} catch(e: HttpException) {
if (e.code in arrayOf(403, 404, 410))
// delete home set only if it was not accessible (40x)
itHomeSets.remove()
}
}
// check/refresh unconfirmed collections
val itCollections = collections.entries.iterator()
while (itCollections.hasNext()) {
val (url, info) = itCollections.next()
if (!info.confirmed)
try {
DavResource(httpClient, url).propfind(0, *DAV_COLLECTION_PROPERTIES) { response, _ ->
if (!response.isSuccess())
return@propfind
val collection = Collection.fromDavResponse(response) ?: return@propfind
collection.confirmed = true
// remove unusable collections
if ((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))
itCollections.remove()
}
} catch(e: HttpException) {
if (e.code in arrayOf(403, 404, 410))
// delete collection only if it was not accessible (40x)
itCollections.remove()
else
throw e
}
}
}
saveResults()
} catch(e: InvalidAccountException) {
Logger.log.log(Level.SEVERE, "Invalid account", e)
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't refresh collection list", e)
val debugIntent = Intent(this, DebugInfoActivity::class.java)
debugIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e)
debugIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account)
val notify = NotificationUtils.newBuilder(this, NotificationUtils.CHANNEL_GENERAL)
.setSmallIcon(R.drawable.ic_sync_problem_notify)
.setContentTitle(getString(R.string.dav_service_refresh_failed))
.setContentText(getString(R.string.dav_service_refresh_couldnt_refresh))
.setContentIntent(PendingIntent.getActivity(this, 0, debugIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setSubText(account.name)
.setCategory(NotificationCompat.CATEGORY_ERROR)
.build()
NotificationManagerCompat.from(this)
.notify(serviceId.toString(), NotificationUtils.NOTIFY_REFRESH_COLLECTIONS, notify)
} finally {
runningRefresh.remove(serviceId)
refreshingStatusListeners.mapNotNull { it.get() }.forEach {
it.onDavRefreshStatusChanged(serviceId, false)
}
}
}
}
@@ -1,107 +0,0 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid
import android.accounts.Account
import android.annotation.TargetApi
import android.content.ContentResolver
import android.content.Context
import android.net.ConnectivityManager
import android.os.Build
import android.os.Bundle
import android.provider.CalendarContract
import at.bitfire.davdroid.log.Logger
import at.bitfire.ical4android.TaskProvider
import okhttp3.HttpUrl
import org.xbill.DNS.*
import java.util.*
/**
* Some WebDAV and related network utility methods
*/
object DavUtils {
fun ARGBtoCalDAVColor(colorWithAlpha: Int): String {
val alpha = (colorWithAlpha shr 24) and 0xFF
val color = colorWithAlpha and 0xFFFFFF
return String.format("#%06X%02X", color, alpha)
}
fun lastSegmentOfUrl(url: HttpUrl): String {
// the list returned by HttpUrl.pathSegments() is unmodifiable, so we have to create a copy
val segments = LinkedList<String>(url.pathSegments())
segments.reverse()
return segments.firstOrNull { it.isNotEmpty() } ?: "/"
}
fun prepareLookup(context: Context, lookup: Lookup) {
@TargetApi(Build.VERSION_CODES.O)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
/* Since Android 8, the system properties net.dns1, net.dns2, ... are not available anymore.
The current version of dnsjava relies on these properties to find the default name servers,
so we have to add the servers explicitly (fortunately, there's an Android API to
get the active DNS servers). */
val connectivity = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeLink = connectivity.getLinkProperties(connectivity.activeNetwork)
if (activeLink != null) {
// get DNS servers of active network link and set them for dnsjava so that it can send SRV queries
val simpleResolvers = activeLink.dnsServers.map {
Logger.log.fine("Using DNS server ${it.hostAddress}")
val resolver = SimpleResolver()
resolver.setAddress(it)
resolver
}
val resolver = ExtendedResolver(simpleResolvers.toTypedArray())
lookup.setResolver(resolver)
} else
Logger.log.severe("Couldn't determine DNS servers, dnsjava queries (SRV/TXT records) won't work")
}
}
fun selectSRVRecord(records: Array<Record>?): SRVRecord? {
val srvRecords = records?.filterIsInstance(SRVRecord::class.java)
srvRecords?.let {
if (it.size > 1)
Logger.log.warning("Multiple SRV records not supported yet; using first one")
return it.firstOrNull()
}
return null
}
fun pathsFromTXTRecords(records: Array<Record>?): List<String> {
val paths = LinkedList<String>()
records?.filterIsInstance(TXTRecord::class.java)?.forEach { txt ->
@Suppress("UNCHECKED_CAST")
for (segment in txt.strings as List<String>)
if (segment.startsWith("path=")) {
paths.add(segment.substring(5))
break
}
}
return paths
}
fun requestSync(context: Context, account: Account) {
val authorities = arrayOf(
context.getString(R.string.address_books_authority),
CalendarContract.AUTHORITY,
TaskProvider.ProviderName.OpenTasks.authority
)
for (authority in authorities) {
val extras = Bundle(2)
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true) // manual sync
extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue)
ContentResolver.requestSync(account, authority, extras)
}
}
}
@@ -0,0 +1,119 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.AppSettingsActivity
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
class ForegroundService : Service() {
companion object {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface ForegroundServiceEntryPoint {
fun settingsManager(): SettingsManager
}
/**
* Starts/stops a foreground service, according to the app setting [Settings.FOREGROUND_SERVICE]
* if [Settings.BATTERY_OPTIMIZATION] is enabled - meaning DAVx5 is whitelisted from optimization.
*/
const val ACTION_FOREGROUND = "foreground"
/**
* Whether the app is currently exempted from battery optimization.
* @return true if battery optimization is not applied to the current app; false if battery optimization is applied
*/
fun batteryOptimizationWhitelisted(context: Context) =
if (Build.VERSION.SDK_INT >= 23) { // battery optimization exists since Android 6 (SDK level 23)
val powerManager = context.getSystemService(PowerManager::class.java)
powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)
} else
true
/**
* Whether the foreground service is enabled (checked) in the app settings.
* @return true: foreground service enabled; false: foreground service not enabled
*/
fun foregroundServiceActivated(context: Context): Boolean {
val settingsManager = EntryPointAccessors.fromApplication(context, ForegroundServiceEntryPoint::class.java).settingsManager()
return settingsManager.getBooleanOrNull(Settings.FOREGROUND_SERVICE) == true
}
/**
* Starts the foreground service when enabled in the app settings and applicable.
*/
fun startIfActive(context: Context) {
if (foregroundServiceActivated(context)) {
if (batteryOptimizationWhitelisted(context)) {
val serviceIntent = Intent(ACTION_FOREGROUND, null, context, ForegroundService::class.java)
if (Build.VERSION.SDK_INT >= 26)
context.startForegroundService(serviceIntent)
else
context.startService(serviceIntent)
} else
notifyBatteryOptimization(context)
}
}
private fun notifyBatteryOptimization(context: Context) {
val settingsIntent = Intent(context, AppSettingsActivity::class.java).apply {
putExtra(AppSettingsActivity.EXTRA_SCROLL_TO, Settings.BATTERY_OPTIMIZATION)
}
val pendingSettingsIntent = PendingIntent.getActivity(context, 0, settingsIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val builder =
NotificationCompat.Builder(context, NotificationUtils.CHANNEL_DEBUG)
.setSmallIcon(R.drawable.ic_warning_notify)
.setContentTitle(context.getString(R.string.battery_optimization_notify_title))
.setContentText(context.getString(R.string.battery_optimization_notify_text))
.setContentIntent(pendingSettingsIntent)
.setCategory(NotificationCompat.CATEGORY_ERROR)
val nm = NotificationManagerCompat.from(context)
nm.notifyIfPossible(NotificationUtils.NOTIFY_BATTERY_OPTIMIZATION, builder.build())
}
}
override fun onBind(intent: Intent?): Nothing? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (foregroundServiceActivated(this)) {
val settingsIntent = Intent(this, AppSettingsActivity::class.java).apply {
putExtra(AppSettingsActivity.EXTRA_SCROLL_TO, Settings.FOREGROUND_SERVICE)
}
val builder = NotificationCompat.Builder(this, NotificationUtils.CHANNEL_STATUS)
.setSmallIcon(R.drawable.ic_foreground_notify)
.setContentTitle(getString(R.string.foreground_service_notify_title))
.setContentText(getString(R.string.foreground_service_notify_text))
.setStyle(NotificationCompat.BigTextStyle())
.setContentIntent(PendingIntent.getActivity(this, 0, settingsIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.setCategory(NotificationCompat.CATEGORY_STATUS)
startForeground(NotificationUtils.NOTIFY_FOREGROUND, builder.build())
return START_STICKY
} else {
stopForeground(true)
return START_NOT_STICKY
}
}
}
@@ -1,252 +0,0 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid
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.Constants
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.Credentials
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.Settings
import okhttp3.*
import okhttp3.internal.tls.OkHostnameVerifier
import okhttp3.logging.HttpLoggingInterceptor
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.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.logging.Level
import javax.net.ssl.*
class HttpClient private constructor(
val okHttpClient: OkHttpClient,
private val certManager: CustomCertManager?
): AutoCloseable {
companion object {
/** max. size of disk cache (10 MB) */
const val DISK_CACHE_MAX_SIZE: Long = 10*1024*1024
/** [OkHttpClient] singleton to build all clients from */
val sharedClient: OkHttpClient = OkHttpClient.Builder()
// set timeouts
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
// don't allow redirects by default, because it would break PROPFIND handling
.followRedirects(false)
// add User-Agent to every request
.addNetworkInterceptor(UserAgentInterceptor)
.build()
}
override fun close() {
okHttpClient.cache()?.close()
certManager?.close()
}
class Builder(
val context: Context? = null,
accountSettings: AccountSettings? = null,
val logger: java.util.logging.Logger = Logger.log
) {
private var certManager: CustomCertManager? = null
private var certificateAlias: String? = null
private var cache: Cache? = null
private val orig = sharedClient.newBuilder()
init {
// add cookie store for non-persistent cookies (some services like Horde use cookies for session tracking)
orig.cookieJar(MemoryCookieStore())
// add network logging, if requested
if (logger.isLoggable(Level.FINEST)) {
val loggingInterceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger {
message -> logger.finest(message)
})
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
orig.addInterceptor(loggingInterceptor)
}
context?.let {
val settings = Settings.getInstance(context)
// custom proxy support
try {
if (settings.getBoolean(Settings.OVERRIDE_PROXY) == true) {
val address = InetSocketAddress(
settings.getString(Settings.OVERRIDE_PROXY_HOST)
?: Settings.OVERRIDE_PROXY_HOST_DEFAULT,
settings.getInt(Settings.OVERRIDE_PROXY_PORT)
?: Settings.OVERRIDE_PROXY_PORT_DEFAULT
)
val proxy = Proxy(Proxy.Type.HTTP, address)
orig.proxy(proxy)
Logger.log.log(Level.INFO, "Using proxy", proxy)
}
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Can't set proxy, ignoring", e)
}
//if (BuildConfig.customCerts)
customCertManager(CustomCertManager(context, true,
!(settings.getBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES)
?: Settings.DISTRUST_SYSTEM_CERTIFICATES_DEFAULT)))
}
// use account settings for authentication
accountSettings?.let {
addAuthentication(null, it.credentials())
}
}
constructor(context: Context, host: String?, credentials: Credentials): this(context) {
addAuthentication(host, credentials)
}
fun withDiskCache(): Builder {
val context = context ?: throw IllegalArgumentException("Context is required to find the cache directory")
for (dir in arrayOf(context.externalCacheDir, context.cacheDir).filterNotNull()) {
if (dir.exists() && dir.canWrite()) {
val cacheDir = File(dir, "HttpClient")
cacheDir.mkdir()
Logger.log.fine("Using disk cache: $cacheDir")
orig.cache(Cache(cacheDir, DISK_CACHE_MAX_SIZE))
break
}
}
return this
}
fun followRedirects(follow: Boolean): Builder {
orig.followRedirects(follow)
return this
}
fun customCertManager(manager: CustomCertManager) {
certManager = manager
}
fun setForeground(foreground: Boolean): Builder {
certManager?.appInForeground = foreground
return this
}
fun addAuthentication(host: String?, credentials: Credentials): Builder {
when (credentials.type) {
Credentials.Type.UsernamePassword -> {
val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), credentials.userName!!, credentials.password!!)
orig .addNetworkInterceptor(authHandler)
.authenticator(authHandler)
}
Credentials.Type.ClientCertificate -> {
certificateAlias = credentials.certificateAlias
}
}
return this
}
fun build(): HttpClient {
val trustManager = certManager ?: {
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
factory.init(null as KeyStore?)
factory.trustManagers.first() as X509TrustManager
}()
val hostnameVerifier = certManager?.hostnameVerifier(OkHostnameVerifier.INSTANCE)
?: OkHostnameVerifier.INSTANCE
var keyManager: KeyManager? = null
certificateAlias?.let { alias ->
try {
val context = requireNotNull(context)
// 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 Android KeyStore (performs key operations without revealing secret data to DAVx5)
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
// 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))
} catch (e: Exception) {
logger.log(Level.SEVERE, "Couldn't set up provider certificate authentication", e)
}
}
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 HttpClient(orig.build(), certManager)
}
}
private object UserAgentInterceptor: Interceptor {
// use Locale.US because numbers may be encoded as non-ASCII characters in other locales
private val userAgentDateFormat = SimpleDateFormat("yyyy/MM/dd", Locale.US)
private val userAgentDate = userAgentDateFormat.format(Date(BuildConfig.buildTime))
private val userAgent = "${BuildConfig.userAgent}/${BuildConfig.VERSION_NAME} ($userAgentDate; dav4jvm; " +
"okhttp/${Constants.okhttpVersion}) Android/${Build.VERSION.RELEASE}"
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)
}
}
}
@@ -1,10 +1,6 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid
@@ -1,58 +1,29 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid
import android.accounts.Account
import android.content.BroadcastReceiver
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.annotation.WorkerThread
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.model.Service
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.ical4android.TaskProvider.ProviderName.OpenTasks
import kotlin.concurrent.thread
import android.content.IntentFilter
class PackageChangedReceiver: BroadcastReceiver() {
abstract class PackageChangedReceiver(
val context: Context
): BroadcastReceiver(), AutoCloseable {
companion object {
@WorkerThread
fun updateTaskSync(context: Context) {
val tasksInstalled = LocalTaskList.tasksProviderAvailable(context)
Logger.log.info("Tasks provider available = $tasksInstalled")
// check all accounts and (de)activate OpenTasks if a CalDAV service is defined
val db = AppDatabase.getInstance(context)
db.serviceDao().getByType(Service.TYPE_CALDAV).forEach { service ->
val account = Account(service.accountName, context.getString(R.string.account_type))
if (tasksInstalled) {
if (ContentResolver.getIsSyncable(account, OpenTasks.authority) <= 0) {
ContentResolver.setIsSyncable(account, OpenTasks.authority, 1)
ContentResolver.addPeriodicSync(account, OpenTasks.authority, Bundle(), Constants.DEFAULT_SYNC_INTERVAL)
}
} else
ContentResolver.setIsSyncable(account, OpenTasks.authority, 0)
}
init {
val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
addAction(Intent.ACTION_PACKAGE_CHANGED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}
context.registerReceiver(this, filter)
}
override fun onReceive(context: Context, intent: Intent) {
thread {
updateTaskSync(context)
}
override fun close() {
context.unregisterReceiver(this)
}
}
}
@@ -0,0 +1,90 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.provider.Settings
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.MutableLiveData
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
class StorageLowReceiver private constructor(
val context: Context
): BroadcastReceiver(), AutoCloseable {
@Module
@InstallIn(SingletonComponent::class)
object StorageLowReceiverModule {
@Provides
@Singleton
fun storageLowReceiver(@ApplicationContext context: Context) = StorageLowReceiver(context)
}
val storageLow = MutableLiveData<Boolean>(false)
fun listen() {
Logger.log.fine("Listening for device storage low/OK broadcasts")
val filter = IntentFilter().apply {
addAction(Intent.ACTION_DEVICE_STORAGE_LOW)
addAction(Intent.ACTION_DEVICE_STORAGE_OK)
}
context.registerReceiver(this, filter)
}
override fun close() {
context.unregisterReceiver(this)
}
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_DEVICE_STORAGE_LOW -> onStorageLow()
Intent.ACTION_DEVICE_STORAGE_OK -> onStorageOk()
}
}
fun onStorageLow() {
Logger.log.warning("Low storage, sync will not be started by Android!")
storageLow.postValue(true)
val notify = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_ERRORS)
.setSmallIcon(R.drawable.ic_storage_notify)
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setContentTitle(context.getString(R.string.storage_low_notify_title))
.setContentText(context.getString(R.string.storage_low_notify_text))
val settingsIntent = Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS)
if (settingsIntent.resolveActivity(context.packageManager) != null)
notify.setContentIntent(PendingIntent.getActivity(context, 0, settingsIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE))
val nm = NotificationManagerCompat.from(context)
nm.notifyIfPossible(NotificationUtils.NOTIFY_LOW_STORAGE, notify.build())
}
fun onStorageOk() {
Logger.log.info("Storage OK again")
storageLow.postValue(false)
val nm = NotificationManagerCompat.from(context)
nm.cancel(NotificationUtils.NOTIFY_LOW_STORAGE)
}
}
@@ -0,0 +1,31 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid
import android.content.Context
import android.content.Intent
import at.bitfire.davdroid.syncadapter.SyncUtils.updateTaskSync
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class TasksWatcher protected constructor(
context: Context
): PackageChangedReceiver(context) {
companion object {
fun watch(context: Context) = TasksWatcher(context)
}
override fun onReceive(context: Context, intent: Intent) {
CoroutineScope(Dispatchers.Default).launch {
updateTaskSync(context)
}
}
}
@@ -0,0 +1,83 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid
import org.apache.commons.lang3.StringUtils
import java.util.*
class TextTable(
vararg val headers: String
) {
companion object {
fun indent(str: String, pos: Int): String =
" ".repeat(pos) +
str.split('\n').joinToString("\n" + " ".repeat(pos))
}
private val lines = mutableListOf<Array<String>>()
fun addLine(vararg values: Any?) {
if (values.size != headers.size)
throw IllegalArgumentException("Table line must have ${headers.size} column(s)")
lines += values.map {
it?.toString() ?: ""
}.toTypedArray()
}
override fun toString(): String {
val sb = StringBuilder()
val headerWidths = headers.map { it.length }
val colWidths = Array<Int>(headers.size) { colIdx ->
Collections.max(listOf(headerWidths[colIdx]) + lines.map { it[colIdx] }.map { it.length })
}
// first line
sb.append("\n")
for (colIdx in headers.indices)
sb .append(StringUtils.repeat('─', colWidths[colIdx] + 2))
.append(if (colIdx == headers.size - 1) '┐' else '┬')
sb.append('\n')
// header
sb.append('│')
for (colIdx in headers.indices)
sb .append(' ')
.append(StringUtils.rightPad(headers[colIdx], colWidths[colIdx] + 1))
.append('│')
sb.append('\n')
// separator between header and body
sb.append('├')
for (colIdx in headers.indices) {
sb .append(StringUtils.repeat('─', colWidths[colIdx] + 2))
.append(if (colIdx == headers.size - 1) '┤' else '┼')
}
sb.append('\n')
// body
for (line in lines) {
for (colIdx in headers.indices)
sb .append("")
.append(StringUtils.rightPad(line[colIdx], colWidths[colIdx] + 1))
sb.append("\n")
}
// last line
sb.append("")
for (colIdx in headers.indices) {
sb .append("".repeat(colWidths[colIdx] + 2))
.append(if (colIdx == headers.size - 1) '┘' else '┴')
}
sb.append("\n\n")
return sb.toString()
}
}
@@ -0,0 +1,298 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import android.accounts.AccountManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.database.sqlite.SQLiteQueryBuilder
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.database.getStringOrNull
import androidx.room.*
import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import at.bitfire.davdroid.R
import at.bitfire.davdroid.TextTable
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
import at.bitfire.davdroid.ui.AccountsActivity
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import java.io.Writer
import javax.inject.Singleton
@Suppress("ClassName")
@Database(entities = [
Service::class,
HomeSet::class,
Collection::class,
Principal::class,
SyncStats::class,
WebDavDocument::class,
WebDavMount::class
], exportSchema = true, version = 12, autoMigrations = [
AutoMigration(from = 9, to = 10),
AutoMigration(from = 10, to = 11),
AutoMigration(from = 11, to = 12, spec = AppDatabase.AutoMigration11_12::class)
])
@TypeConverters(Converters::class)
abstract class AppDatabase: RoomDatabase() {
@Module
@InstallIn(SingletonComponent::class)
object AppDatabaseModule {
@Provides
@Singleton
fun appDatabase(@ApplicationContext context: Context): AppDatabase =
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "services.db")
.addMigrations(*migrations)
.addAutoMigrationSpec(AutoMigration11_12(context))
.fallbackToDestructiveMigration() // as a last fallback, recreate database instead of crashing
.addCallback(object: Callback() {
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
val nm = NotificationManagerCompat.from(context)
val launcherIntent = Intent(context, AccountsActivity::class.java)
val notify = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_GENERAL)
.setSmallIcon(R.drawable.ic_warning_notify)
.setContentTitle(context.getString(R.string.database_destructive_migration_title))
.setContentText(context.getString(R.string.database_destructive_migration_text))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setContentIntent(PendingIntent.getActivity(context, 0, launcherIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.setAutoCancel(true)
.build()
nm.notifyIfPossible(NotificationUtils.NOTIFY_DATABASE_CORRUPTED, notify)
// remove all accounts because they're unfortunately useless without database
val am = AccountManager.get(context)
for (account in am.getAccountsByType(context.getString(R.string.account_type)))
am.removeAccount(account, null, null)
}
})
.build()
}
// auto migrations
@ProvidedAutoMigrationSpec
@DeleteColumn(tableName = "collection", columnName = "owner")
class AutoMigration11_12(val context: Context): AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
Logger.log.info("Database update to v12, refreshing services to get display names of owners")
db.query("SELECT id FROM service", arrayOf()).use { cursor ->
while (cursor.moveToNext()) {
val serviceId = cursor.getLong(0)
RefreshCollectionsWorker.refreshCollections(context, serviceId)
}
}
}
}
companion object {
// manual migrations
val migrations: Array<Migration> = arrayOf(
object : Migration(8, 9) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE syncstats (" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
"collectionId INTEGER NOT NULL REFERENCES collection(id) ON DELETE CASCADE," +
"authority TEXT NOT NULL," +
"lastSync INTEGER NOT NULL)")
db.execSQL("CREATE UNIQUE INDEX index_syncstats_collectionId_authority ON syncstats(collectionId, authority)")
db.execSQL("CREATE INDEX index_collection_url ON collection(url)")
}
},
object : Migration(7, 8) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE homeset ADD COLUMN personal INTEGER NOT NULL DEFAULT 1")
db.execSQL("ALTER TABLE collection ADD COLUMN homeSetId INTEGER DEFAULT NULL REFERENCES homeset(id) ON DELETE SET NULL")
db.execSQL("ALTER TABLE collection ADD COLUMN owner TEXT DEFAULT NULL")
db.execSQL("CREATE INDEX index_collection_homeSetId_type ON collection(homeSetId, type)")
}
},
object : Migration(6, 7) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE homeset ADD COLUMN privBind INTEGER NOT NULL DEFAULT 1")
db.execSQL("ALTER TABLE homeset ADD COLUMN displayName TEXT DEFAULT NULL")
}
},
object : Migration(5, 6) {
override fun migrate(db: SupportSQLiteDatabase) {
val sql = arrayOf(
// migrate "services" to "service": rename columns, make id NOT NULL
"CREATE TABLE service(" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
"accountName TEXT NOT NULL," +
"type TEXT NOT NULL," +
"principal TEXT DEFAULT NULL" +
")",
"CREATE UNIQUE INDEX index_service_accountName_type ON service(accountName, type)",
"INSERT INTO service(id, accountName, type, principal) SELECT _id, accountName, service, principal FROM services",
"DROP TABLE services",
// migrate "homesets" to "homeset": rename columns, make id NOT NULL
"CREATE TABLE homeset(" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
"serviceId INTEGER NOT NULL," +
"url TEXT NOT NULL," +
"FOREIGN KEY (serviceId) REFERENCES service(id) ON DELETE CASCADE" +
")",
"CREATE UNIQUE INDEX index_homeset_serviceId_url ON homeset(serviceId, url)",
"INSERT INTO homeset(id, serviceId, url) SELECT _id, serviceID, url FROM homesets",
"DROP TABLE homesets",
// migrate "collections" to "collection": rename columns, make id NOT NULL
"CREATE TABLE collection(" +
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
"serviceId INTEGER NOT NULL," +
"type TEXT NOT NULL," +
"url TEXT NOT NULL," +
"privWriteContent INTEGER NOT NULL DEFAULT 1," +
"privUnbind INTEGER NOT NULL DEFAULT 1," +
"forceReadOnly INTEGER NOT NULL DEFAULT 0," +
"displayName TEXT DEFAULT NULL," +
"description TEXT DEFAULT NULL," +
"color INTEGER DEFAULT NULL," +
"timezone TEXT DEFAULT NULL," +
"supportsVEVENT INTEGER DEFAULT NULL," +
"supportsVTODO INTEGER DEFAULT NULL," +
"supportsVJOURNAL INTEGER DEFAULT NULL," +
"source TEXT DEFAULT NULL," +
"sync INTEGER NOT NULL DEFAULT 0," +
"FOREIGN KEY (serviceId) REFERENCES service(id) ON DELETE CASCADE" +
")",
"CREATE INDEX index_collection_serviceId_type ON collection(serviceId,type)",
"INSERT INTO collection(id, serviceId, type, url, privWriteContent, privUnbind, forceReadOnly, displayName, description, color, timezone, supportsVEVENT, supportsVTODO, source, sync) " +
"SELECT _id, serviceID, type, url, privWriteContent, privUnbind, forceReadOnly, displayName, description, color, timezone, supportsVEVENT, supportsVTODO, source, sync FROM collections",
"DROP TABLE collections"
)
sql.forEach { db.execSQL(it) }
}
},
object : Migration(4, 5) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE collections ADD COLUMN privWriteContent INTEGER DEFAULT 0 NOT NULL")
db.execSQL("UPDATE collections SET privWriteContent=NOT readOnly")
db.execSQL("ALTER TABLE collections ADD COLUMN privUnbind INTEGER DEFAULT 0 NOT NULL")
db.execSQL("UPDATE collections SET privUnbind=NOT readOnly")
// there's no DROP COLUMN in SQLite, so just keep the "readOnly" column
}
},
object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE collections ADD COLUMN forceReadOnly INTEGER DEFAULT 0 NOT NULL")
}
},
object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
// We don't have access to the context in a Room migration now, so
// we will just drop those settings from old DAVx5 versions.
Logger.log.warning("Dropping settings distrustSystemCerts and overrideProxy*")
/*val edit = PreferenceManager.getDefaultSharedPreferences(context).edit()
try {
db.query("settings", arrayOf("setting", "value"), null, null, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
when (cursor.getString(0)) {
"distrustSystemCerts" -> edit.putBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, cursor.getInt(1) != 0)
"overrideProxy" -> edit.putBoolean(App.OVERRIDE_PROXY, cursor.getInt(1) != 0)
"overrideProxyHost" -> edit.putString(App.OVERRIDE_PROXY_HOST, cursor.getString(1))
"overrideProxyPort" -> edit.putInt(App.OVERRIDE_PROXY_PORT, cursor.getInt(1))
StartupDialogFragment.HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED ->
edit.putBoolean(StartupDialogFragment.HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED, cursor.getInt(1) != 0)
StartupDialogFragment.HINT_OPENTASKS_NOT_INSTALLED ->
edit.putBoolean(StartupDialogFragment.HINT_OPENTASKS_NOT_INSTALLED, cursor.getInt(1) != 0)
}
}
}
db.execSQL("DROP TABLE settings")
} finally {
edit.apply()
}*/
}
},
object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE collections ADD COLUMN type TEXT NOT NULL DEFAULT ''")
db.execSQL("ALTER TABLE collections ADD COLUMN source TEXT DEFAULT NULL")
db.execSQL("UPDATE collections SET type=(" +
"SELECT CASE service WHEN ? THEN ? ELSE ? END " +
"FROM services WHERE _id=collections.serviceID" +
")",
arrayOf("caldav", "CALENDAR", "ADDRESS_BOOK"))
}
}
)
}
// DAOs
abstract fun serviceDao(): ServiceDao
abstract fun homeSetDao(): HomeSetDao
abstract fun collectionDao(): CollectionDao
abstract fun principalDao(): PrincipalDao
abstract fun syncStatsDao(): SyncStatsDao
abstract fun webDavDocumentDao(): WebDavDocumentDao
abstract fun webDavMountDao(): WebDavMountDao
// helpers
fun dump(writer: Writer, ignoreTables: Array<String>) {
val db = openHelper.readableDatabase
db.beginTransactionNonExclusive()
// iterate through all tables
db.query(SQLiteQueryBuilder.buildQueryString(false, "sqlite_master", arrayOf("name"), "type='table'", null, null, null, null)).use { cursorTables ->
while (cursorTables.moveToNext()) {
val tableName = cursorTables.getString(0)
if (ignoreTables.contains(tableName)) {
writer.append("$tableName: ")
db.query("SELECT COUNT(*) FROM $tableName").use { cursor ->
if (cursor.moveToNext())
writer.append("${cursor.getInt(0)} row(s), data not listed here\n\n")
}
} else {
writer.append("$tableName\n")
db.query("SELECT * FROM $tableName").use { cursor ->
val table = TextTable(*cursor.columnNames)
val cols = cursor.columnCount
// print rows
while (cursor.moveToNext()) {
val values = Array(cols) { idx -> cursor.getStringOrNull(idx) }
table.addLine(*values)
}
writer.append(table.toString())
}
}
}
db.endTransaction()
}
}
}
@@ -1,27 +1,59 @@
package at.bitfire.davdroid.model
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import androidx.room.*
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.dav4jvm.property.*
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.util.DavUtils
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.apache.commons.lang3.StringUtils
@Entity(tableName = "collection",
foreignKeys = [
ForeignKey(entity = Service::class, parentColumns = arrayOf("id"), childColumns = arrayOf("serviceId"), onDelete = ForeignKey.CASCADE)
ForeignKey(entity = Service::class, parentColumns = arrayOf("id"), childColumns = arrayOf("serviceId"), onDelete = ForeignKey.CASCADE),
ForeignKey(entity = HomeSet::class, parentColumns = arrayOf("id"), childColumns = arrayOf("homeSetId"), onDelete = ForeignKey.SET_NULL),
ForeignKey(entity = Principal::class, parentColumns = arrayOf("id"), childColumns = arrayOf("ownerId"), onDelete = ForeignKey.SET_NULL)
],
indices = [
Index("serviceId","type")
Index("serviceId","type"),
Index("homeSetId","type"),
Index("url")
]
)
data class Collection(
@PrimaryKey(autoGenerate = true)
override var id: Long = 0,
var id: Long = 0,
/**
* Service, which this collection belongs to. Services are unique, so a [Collection] is uniquely
* identifiable via its [serviceId] and [url].
*/
var serviceId: Long = 0,
/**
* A home set this collection belongs to. Multiple homesets are not supported.
* If *null* the collection is considered homeless.
*/
var homeSetId: Long? = null,
/**
* Principal who is owner of this collection.
*/
var ownerId: Long? = null,
/**
* Type of service. CalDAV or CardDAV
*/
var type: String,
/**
* Address where this collection lives - with trailing slash
*/
var url: HttpUrl,
var privWriteContent: Boolean = true,
@@ -52,7 +84,7 @@ data class Collection(
/** whether this collection has been selected for synchronization */
var sync: Boolean = false
): IdEntity() {
) {
companion object {
@@ -83,11 +115,7 @@ data class Collection(
privUnbind = privilegeSet.mayUnbind
}
var displayName: String? = null
dav[DisplayName::class.java]?.let {
if (!it.displayName.isNullOrEmpty())
displayName = it.displayName
}
val displayName = StringUtils.trimToNull(dav[DisplayName::class.java]?.displayName)
var description: String? = null
var color: Int? = null
@@ -115,12 +143,14 @@ data class Collection(
supportsVJOURNAL = it.supportsJournal
}
} else { // Type.WEBCAL
dav[Source::class.java]?.let { source = it.hrefs.firstOrNull()?.let { rawHref ->
val href = rawHref
.replace("^webcal://".toRegex(), "http://")
.replace("^webcals://".toRegex(), "https://")
HttpUrl.parse(href)
} }
dav[Source::class.java]?.let {
source = it.hrefs.firstOrNull()?.let { rawHref ->
val href = rawHref
.replace("^webcal://".toRegex(), "http://")
.replace("^webcals://".toRegex(), "https://")
href.toHttpUrlOrNull()
}
}
supportsVEVENT = true
}
}
@@ -144,12 +174,6 @@ data class Collection(
}
// non-persistent properties
@Ignore
var confirmed: Boolean = false
// calculated properties
fun title() = displayName ?: DavUtils.lastSegmentOfUrl(url)
fun readOnly() = forceReadOnly || !privWriteContent
@@ -0,0 +1,106 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import androidx.lifecycle.LiveData
import androidx.paging.PagingSource
import androidx.room.*
@Dao
interface CollectionDao {
@Query("SELECT DISTINCT color FROM collection WHERE serviceId=:id")
fun colorsByServiceLive(id: Long): LiveData<List<Int>>
@Query("SELECT * FROM collection WHERE id=:id")
fun get(id: Long): Collection?
@Query("SELECT * FROM collection WHERE id=:id")
fun getLive(id: Long): LiveData<Collection>
@Query("SELECT * FROM collection WHERE serviceId=:serviceId")
fun getByService(serviceId: Long): List<Collection>
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND homeSetId IS :homeSetId")
fun getByServiceAndHomeset(serviceId: Long, homeSetId: Long?): List<Collection>
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type=:type ORDER BY displayName, url")
fun getByServiceAndType(serviceId: Long, type: String): List<Collection>
/**
* Returns collections which
* - support VEVENT and/or VTODO (= supported calendar collections), or
* - have supportsVEVENT = supportsVTODO = null (= address books)
*/
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type=:type " +
"AND (supportsVTODO OR supportsVEVENT OR supportsVJOURNAL OR (supportsVEVENT IS NULL AND supportsVTODO IS NULL AND supportsVJOURNAL IS NULL)) ORDER BY displayName, URL")
fun pageByServiceAndType(serviceId: Long, type: String): PagingSource<Int, Collection>
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND sync")
fun getByServiceAndSync(serviceId: Long): List<Collection>
@Query("SELECT collection.* FROM collection, homeset WHERE collection.serviceId=:serviceId AND type=:type AND homeSetId=homeset.id AND homeset.personal ORDER BY collection.displayName, collection.url")
fun pagePersonalByServiceAndType(serviceId: Long, type: String): PagingSource<Int, Collection>
@Deprecated("Use getByServiceAndUrl instead")
@Query("SELECT * FROM collection WHERE url=:url")
fun getByUrl(url: String): Collection?
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND url=:url")
fun getByServiceAndUrl(serviceId: Long, url: String): Collection?
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type='${Collection.TYPE_CALENDAR}' AND supportsVEVENT AND sync ORDER BY displayName, url")
fun getSyncCalendars(serviceId: Long): List<Collection>
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type='${Collection.TYPE_CALENDAR}' AND (supportsVTODO OR supportsVJOURNAL) AND sync ORDER BY displayName, url")
fun getSyncJtxCollections(serviceId: Long): List<Collection>
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type='${Collection.TYPE_CALENDAR}' AND supportsVTODO AND sync ORDER BY displayName, url")
fun getSyncTaskLists(serviceId: Long): List<Collection>
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(collection: Collection): Long
@Update
fun update(collection: Collection)
/**
* 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!
*
* @param collection Collection to be inserted or updated
* @return ID of the row, that has been inserted or updated. -1 If the insert fails due to other reasons.
*/
@Transaction
fun insertOrUpdateByUrl(collection: Collection): Long = getByServiceAndUrl(
collection.serviceId,
collection.url.toString()
)?.let { localCollection ->
update(collection.copy(id = localCollection.id))
localCollection.id
} ?: insert(collection)
/**
* Inserts or updates the collection. On update it will not update flag values ([Collection.sync],
* [Collection.forceReadOnly]), but use the values of the already existing collection.
*
* @param newCollection Collection to be inserted or updated
*/
fun insertOrUpdateByUrlAndRememberFlags(newCollection: Collection) {
// remember locally set flags
getByServiceAndUrl(newCollection.serviceId, newCollection.url.toString())?.let { oldCollection ->
newCollection.sync = oldCollection.sync
newCollection.forceReadOnly = oldCollection.forceReadOnly
}
// commit to database
insertOrUpdateByUrl(newCollection)
}
@Delete
fun delete(collection: Collection)
}
@@ -0,0 +1,31 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import androidx.room.TypeConverter
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
class Converters {
@TypeConverter
fun httpUrlToString(url: HttpUrl?) =
url?.toString()
@TypeConverter
fun mediaTypeToString(mediaType: MediaType?) =
mediaType?.toString()
@TypeConverter
fun stringToHttpUrl(url: String?): HttpUrl? =
url?.toHttpUrlOrNull()
@TypeConverter
fun stringToMediaType(mimeType: String?): MediaType? =
mimeType?.toMediaTypeOrNull()
}
@@ -0,0 +1,35 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import net.openid.appauth.AuthState
data class Credentials(
val userName: String? = null,
val password: String? = null,
val certificateAlias: String? = null,
val authState: AuthState? = null
) {
override fun toString(): String {
val s = mutableListOf<String>()
if (userName != null)
s += "userName=$userName"
if (password != null)
s += "password=*****"
if (certificateAlias != null)
s += "certificateAlias=$certificateAlias"
if (authState != null)
s += "authState=${authState.jsonSerializeString()}"
return "Credentials(" + s.joinToString(", ") + ")"
}
}
@@ -1,4 +1,8 @@
package at.bitfire.davdroid.model
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import androidx.room.Entity
import androidx.room.ForeignKey
@@ -8,7 +12,7 @@ import okhttp3.HttpUrl
@Entity(tableName = "homeset",
foreignKeys = [
ForeignKey(entity = Service::class, parentColumns = arrayOf("id"), childColumns = arrayOf("serviceId"), onDelete = ForeignKey.CASCADE)
ForeignKey(entity = Service::class, parentColumns = ["id"], childColumns = ["serviceId"], onDelete = ForeignKey.CASCADE)
],
indices = [
// index by service; no duplicate URLs per service
@@ -17,12 +21,18 @@ import okhttp3.HttpUrl
)
data class HomeSet(
@PrimaryKey(autoGenerate = true)
override var id: Long,
var id: Long,
var serviceId: Long,
/**
* Whether this homeset belongs to the [Service.principal] given by [serviceId].
*/
var personal: Boolean,
var url: HttpUrl,
var privBind: Boolean = true,
var displayName: String? = null
): IdEntity()
)
@@ -0,0 +1,51 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
interface HomeSetDao {
@Query("SELECT * FROM homeset WHERE id=:homesetId")
fun getById(homesetId: Long): HomeSet
@Query("SELECT * FROM homeset WHERE serviceId=:serviceId AND url=:url")
fun getByUrl(serviceId: Long, url: String): HomeSet?
@Query("SELECT * FROM homeset WHERE serviceId=:serviceId")
fun getByService(serviceId: Long): List<HomeSet>
@Query("SELECT * FROM homeset WHERE serviceId=:serviceId AND privBind")
fun getBindableByService(serviceId: Long): List<HomeSet>
@Query("SELECT COUNT(*) FROM homeset WHERE serviceId=:serviceId AND privBind")
fun hasBindableByServiceLive(serviceId: Long): LiveData<Boolean>
@Insert
fun insert(homeSet: HomeSet): Long
@Update
fun update(homeset: HomeSet)
/**
* 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 =
getByUrl(homeset.serviceId, homeset.url.toString())?.let { existingHomeset ->
update(homeset.copy(id = existingHomeset.id))
existingHomeset.id
} ?: insert(homeset)
@Delete
fun delete(homeset: HomeSet)
}
@@ -0,0 +1,69 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.dav4jvm.property.DisplayName
import at.bitfire.dav4jvm.property.ResourceType
import okhttp3.HttpUrl
import org.apache.commons.lang3.StringUtils
@Entity(tableName = "principal",
foreignKeys = [
ForeignKey(entity = Service::class, parentColumns = arrayOf("id"), childColumns = arrayOf("serviceId"), onDelete = ForeignKey.CASCADE)
],
indices = [
// index by service, urls are unique
Index("serviceId", "url", unique = true)
]
)
data class Principal(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
var serviceId: Long,
/** URL of the principal, always without trailing slash */
var url: HttpUrl,
var displayName: String? = null
) {
companion object {
/**
* Generates a principal entity from a WebDAV response.
* @param dav WebDAV response (make sure that you have queried `DAV:resource-type` and `DAV:display-name`)
* @return generated principal data object (with `id`=0), `null` if the response doesn't represent a principal
*/
fun fromDavResponse(serviceId: Long, dav: Response): Principal? {
// Check if response is a principal
val resourceType = dav[ResourceType::class.java] ?: return null
if (!resourceType.types.contains(ResourceType.PRINCIPAL))
return null
// Try getting the display name of the principal
val displayName: String? = StringUtils.trimToNull(
dav[DisplayName::class.java]?.displayName
)
// Create and return principal - even without it's display name
return Principal(
serviceId = serviceId,
url = UrlUtils.omitTrailingSlash(dav.href),
displayName = displayName
)
}
fun fromServiceAndUrl(service: Service, url: HttpUrl) = Principal(
serviceId = service.id,
url = UrlUtils.omitTrailingSlash(url)
)
}
}
@@ -0,0 +1,55 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import androidx.lifecycle.LiveData
import androidx.room.*
import okhttp3.HttpUrl
@Dao
interface PrincipalDao {
@Query("SELECT * FROM principal WHERE id=:id")
fun get(id: Long): Principal
@Query("SELECT * FROM principal WHERE id=:id")
fun getLive(id: Long): LiveData<Principal>
@Query("SELECT * FROM principal WHERE serviceId=:serviceId")
fun getByService(serviceId: Long): List<Principal>
@Query("SELECT * FROM principal WHERE serviceId=:serviceId AND url=:url")
fun getByUrl(serviceId: Long, url: HttpUrl): Principal?
/**
* Gets all principals who do not own any collections
*/
@Query("SELECT * FROM principal WHERE principal.id NOT IN (SELECT ownerId FROM collection WHERE ownerId IS NOT NULL)")
fun getAllWithoutCollections(): List<Principal>
@Insert
fun insert(principal: Principal): Long
@Update
fun update(principal: Principal)
@Delete
fun delete(principal: Principal)
/**
* Inserts, updates or just gets existing principal if its display name has not
* changed (will not update/overwrite with null values).
*
* @param principal Principal to be inserted or updated
* @return ID of the newly inserted or already existing principal
*/
fun insertOrUpdate(serviceId: Long, principal: Principal): Long =
getByUrl(serviceId, principal.url)?.let { oldPrincipal ->
if (principal.displayName != oldPrincipal.displayName)
update(principal.copy(id = oldPrincipal.id))
return oldPrincipal.id
} ?: insert(principal)
}
@@ -1,10 +1,19 @@
package at.bitfire.davdroid.model
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import okhttp3.HttpUrl
/**
* A service entity.
*
* Services represent accounts and are unique. They are of type CardDAV or CalDAV and may have an associated principal.
*/
@Entity(tableName = "service",
indices = [
// only one service per type and account
@@ -1,5 +1,11 @@
package at.bitfire.davdroid.model
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import androidx.lifecycle.LiveData
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
@@ -12,14 +18,14 @@ interface ServiceDao {
fun getByAccountAndType(accountName: String, type: String): Service?
@Query("SELECT id FROM service WHERE accountName=:accountName AND type=:type")
fun getIdByAccountAndType(accountName: String, type: String): Long?
fun getIdByAccountAndType(accountName: String, type: String): LiveData<Long>
@Query("SELECT type, id FROM service WHERE accountName=:accountName")
fun getServiceTypeAndIdsByAccount(accountName: String): LiveData<List<ServiceTypeAndId>>
@Query("SELECT * FROM service WHERE id=:id")
fun get(id: Long): Service?
@Query("SELECT * FROM service WHERE type=:type")
fun getByType(type: String): List<Service>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(service: Service): Long
@@ -32,4 +38,9 @@ interface ServiceDao {
@Query("UPDATE service SET accountName=:newName WHERE accountName=:oldName")
fun renameAccount(oldName: String, newName: String)
}
}
data class ServiceTypeAndId(
@ColumnInfo(name = "type") val type: String,
@ColumnInfo(name = "id") val id: Long
)
@@ -1,12 +1,8 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.model
package at.bitfire.davdroid.db
import at.bitfire.dav4jvm.property.SyncToken
import org.json.JSONException
@@ -0,0 +1,28 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = "syncstats",
foreignKeys = [
ForeignKey(childColumns = arrayOf("collectionId"), entity = Collection::class, parentColumns = arrayOf("id"), onDelete = ForeignKey.CASCADE)
],
indices = [
Index("collectionId", "authority", unique = true),
]
)
data class SyncStats(
@PrimaryKey(autoGenerate = true)
val id: Long,
val collectionId: Long,
val authority: String,
var lastSync: Long
)
@@ -0,0 +1,21 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface SyncStatsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(syncStats: SyncStats)
@Query("SELECT * FROM syncstats WHERE collectionId=:id")
fun getLiveByCollectionId(id: Long): LiveData<List<SyncStats>>
}
@@ -0,0 +1,119 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import android.annotation.SuppressLint
import android.os.Bundle
import android.provider.DocumentsContract.Document
import android.webkit.MimeTypeMap
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import at.bitfire.davdroid.util.DavUtils.MEDIA_TYPE_OCTET_STREAM
import okhttp3.HttpUrl
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import java.io.FileNotFoundException
@Entity(
tableName = "webdav_document",
foreignKeys = [
ForeignKey(entity = WebDavMount::class, parentColumns = ["id"], childColumns = ["mountId"], onDelete = ForeignKey.CASCADE),
ForeignKey(entity = WebDavDocument::class, parentColumns = ["id"], childColumns = ["parentId"], onDelete = ForeignKey.CASCADE)
],
indices = [
Index("mountId", "parentId", "name", unique = true)
]
)
data class WebDavDocument(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
/** refers to the [WebDavMount] the document belongs to */
val mountId: Long,
/** refers to parent document (*null* when this document is a root document) */
var parentId: Long?,
/** file name (without any slashes) */
var name: String,
var isDirectory: Boolean = false,
var displayName: String? = null,
var mimeType: MediaType? = null,
var eTag: String? = null,
var lastModified: Long? = null,
var size: Long? = null,
var mayBind: Boolean? = null,
var mayUnbind: Boolean? = null,
var mayWriteContent: Boolean? = null,
var quotaAvailable: Long? = null,
var quotaUsed: Long? = null
) {
@SuppressLint("InlinedApi")
fun toBundle(parent: WebDavDocument?): Bundle {
if (parent?.isDirectory == false)
throw IllegalArgumentException("Parent must be a directory")
val bundle = Bundle()
bundle.putString(Document.COLUMN_DOCUMENT_ID, id.toString())
bundle.putString(Document.COLUMN_DISPLAY_NAME, name)
displayName?.let { bundle.putString(Document.COLUMN_SUMMARY, it) }
size?.let { bundle.putLong(Document.COLUMN_SIZE, it) }
lastModified?.let { bundle.putLong(Document.COLUMN_LAST_MODIFIED, it) }
// see RFC 3744 appendix B for required privileges for the various operations
var flags = Document.FLAG_SUPPORTS_COPY
if (isDirectory) {
bundle.putString(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR)
if (mayBind != false)
flags += Document.FLAG_DIR_SUPPORTS_CREATE
} else {
val reportedMimeType = mimeType ?:
MimeTypeMap.getSingleton().getMimeTypeFromExtension(
MimeTypeMap.getFileExtensionFromUrl(name)
)?.toMediaTypeOrNull() ?:
MEDIA_TYPE_OCTET_STREAM
bundle.putString(Document.COLUMN_MIME_TYPE, reportedMimeType.toString())
if (mimeType?.type == "image")
flags += Document.FLAG_SUPPORTS_THUMBNAIL
if (mayWriteContent != false)
flags += Document.FLAG_SUPPORTS_WRITE
}
if (parent?.mayUnbind != false)
flags += Document.FLAG_SUPPORTS_DELETE or
Document.FLAG_SUPPORTS_MOVE or
Document.FLAG_SUPPORTS_RENAME
bundle.putInt(Document.COLUMN_FLAGS, flags)
return bundle
}
fun toHttpUrl(db: AppDatabase): HttpUrl {
val mount = db.webDavMountDao().getById(mountId)
val segments = mutableListOf(name)
var parentIter = parentId
while (parentIter != null) {
val parent = db.webDavDocumentDao().get(parentIter) ?: throw FileNotFoundException()
segments += parent.name
parentIter = parent.parentId
}
val builder = mount.url.newBuilder()
for (segment in segments.reversed())
builder.addPathSegment(segment)
return builder.build()
}
}
@@ -0,0 +1,78 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
interface WebDavDocumentDao {
@Query("SELECT * FROM webdav_document WHERE id=:id")
fun get(id: Long): WebDavDocument?
@Query("SELECT * FROM webdav_document WHERE mountId=:mountId AND (parentId=:parentId OR (parentId IS NULL AND :parentId IS NULL)) AND name=:name")
fun getByParentAndName(mountId: Long, parentId: Long?, name: String): WebDavDocument?
@Query("SELECT * FROM webdav_document WHERE parentId=:parentId")
fun getChildren(parentId: Long): List<WebDavDocument>
@Query("SELECT * FROM webdav_document WHERE parentId IS NULL")
fun getRootsLive(): LiveData<List<WebDavDocument>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(document: WebDavDocument): Long
@Query("DELETE FROM webdav_document WHERE parentId=:parentId")
fun removeChildren(parentId: Long)
@Insert
fun insert(document: WebDavDocument): Long
@Update
fun update(document: WebDavDocument)
@Delete
fun delete(document: WebDavDocument)
// complex operations
/**
* 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 insertOrUpdate(document: WebDavDocument): Long {
val parentId = document.parentId
?: return insert(document)
val existingDocument = getByParentAndName(document.mountId, parentId, document.name)
?: return insert(document)
update(document.copy(id = existingDocument.id))
return existingDocument.id
}
@Transaction
fun getOrCreateRoot(mount: WebDavMount): WebDavDocument {
getByParentAndName(mount.id, null, "")?.let { existing ->
return existing
}
val newDoc = WebDavDocument(
mountId = mount.id,
parentId = null,
name = "",
isDirectory = true,
displayName = mount.name
)
val id = insertOrReplace(newDoc)
newDoc.id = id
return newDoc
}
}
@@ -0,0 +1,24 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import androidx.room.Entity
import androidx.room.PrimaryKey
import okhttp3.HttpUrl
@Entity(tableName = "webdav_mount")
data class WebDavMount(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
/** display name of the WebDAV mount */
var name: String,
/** URL of the WebDAV service, including trailing slash */
var url: HttpUrl
// credentials are stored using CredentialsStore
)
@@ -0,0 +1,31 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.db
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
@Dao
interface WebDavMountDao {
@Delete
fun delete(mount: WebDavMount)
@Query("SELECT * FROM webdav_mount ORDER BY name, url")
fun getAll(): List<WebDavMount>
@Query("SELECT * FROM webdav_mount ORDER BY name, url")
fun getAllLive(): LiveData<List<WebDavMount>>
@Query("SELECT * FROM webdav_mount WHERE id=:id")
fun getById(id: Long): WebDavMount
@Insert
fun insert(mount: WebDavMount): Long
}
Loaded 100 of 577 files, more files were not shown because too many files have changed in this diff. Show more