Compare commits

..

955 Commits

Author SHA1 Message Date
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
Ricki Hirner
46df7a63c7 Version bump to 2.6.1-beta4 2019-11-16 16:21:57 +01:00
Ricki Hirner
b4ebfa4fe5 SyncManager: generate all UID/file names before uploading any resources
- provider queries: use boolean syntax ("WHERE dirty" instead of "WHERE dirty!=0" etc.)
2019-11-16 16:21:39 +01:00
Ricki Hirner
9979dc95c2 Fetch translations from Transifex 2019-11-15 23:48:45 +01:00
Ricki Hirner
ebf1273c37 Version bump to 2.6.1-beta3 2019-11-15 23:47:59 +01:00
Ricki Hirner
3bfe33e37a Correctly handle EMAIL reminders 2019-11-15 23:47:41 +01:00
Ricki Hirner
6d273f8b0d Account settings migration: download all tasks again to parse relations etc. 2019-11-13 17:17:23 +01:00
Ricki Hirner
6922a68b77 Version bump to 2.6.1-beta2 2019-11-13 14:12:15 +01:00
Ricki Hirner
af3a588632 Tasks: directly use Relation rows, no need for DelayedRelation anymore 2019-11-13 14:11:58 +01:00
Ricki Hirner
285dfe9307 Fetch translations from Transifex 2019-11-13 00:04:00 +01:00
Ricki Hirner
bc80e74a83 Version bump to 2.6.1-beta1 2019-11-13 00:03:17 +01:00
Ricki Hirner
6fe5fafed7 Tasks: support RELATED-TO (subtasks) 2019-11-13 00:02:55 +01:00
Ricki Hirner
058354b9a4 Update dependencies 2019-11-12 14:02:56 +01:00
Ricki Hirner
16e1d5041f Dokka: add links to source code and libraries 2019-11-12 14:02:56 +01:00
Ricki Hirner
4bca002892 ical4android: save/restore unknown properties of tasks; disable allowBackup because it won't work anyway 2019-11-11 00:10:30 +01:00
Ricki Hirner
fba759583d Login with URL: assume https:// URI scheme if none given 2019-11-10 10:36:12 +01:00
Ricki Hirner
3ed16ae5b2 Add CONTRIBUTING 2019-11-09 22:06:47 +01:00
Ricki Hirner
888ffc8d90 Require privileged container for tests 2019-11-09 19:52:18 +01:00
Ricki Hirner
5ff90d8e20 Update dependencies 2019-11-09 19:52:04 +01:00
Ricki Hirner
edab897732 Use headless emulator from new repo for testing 2019-11-08 00:57:01 +01:00
Ricki Hirner
06637b4b47 Update dependencies 2019-11-08 00:56:56 +01:00
Ricki Hirner
2ff839837a Fetch translations from Transifex 2019-10-25 11:09:35 +02:00
Ricki Hirner
e80243b133 Version bump to 2.6 2019-10-25 11:08:55 +02:00
Ricki Hirner
481eccc4d6 Login Flow: optimization 2019-10-24 11:21:35 +02:00
Ricki Hirner
2e641a9e9b Merge branch 'fixLoginFlow' into 'master-ose'
Nextcloud login flow: correct concatenate url with davPath

See merge request bitfireAT/davx5-ose!22
2019-10-24 08:48:21 +00:00
Ricki Hirner
fe8b4b1d24 Merge branch 'fixUsername' into 'master-ose'
username was retrieved from intent, but not used

See merge request bitfireAT/davx5-ose!23
2019-10-24 08:44:13 +00:00
tobiasKaminsky
2fc9d2862e username was retrieved from intent, but not used 2019-10-22 09:48:01 +02:00
tobiasKaminsky
23a9be403b correct concatenate url with davPath.
Previously a subfolder was omitted:
serverUrl: http://localhost/nc
davPath: /remote.php/dav
-->
wrong: http://localhost/remote.php/dav
correct: http://localhost/nc/remote.php/dav
2019-10-22 09:37:09 +02:00
Ricki Hirner
cd9518c619 Version bump to 2.6-beta3 2019-10-09 11:04:28 +02:00
Ricki Hirner
29c0a9b586 Account list fragment network detection: compatibility with Android <6 2019-10-08 19:07:57 +02:00
Ricki Hirner
aac25b3bdc Connectivity check: check for VALIDATED INTERNET capability 2019-10-07 17:17:35 +02:00
Ricki Hirner
31b47a8554 Fetch translations from Transifex 2019-10-07 11:45:34 +02:00
Ricki Hirner
f8eb7a6d56 cert4android: fetch translations from Transifex 2019-10-07 11:43:53 +02:00
Ricki Hirner
f41786f9f1 Version bump to 2.6-beta1 2019-10-07 11:43:52 +02:00
Ricki Hirner
80f31bbc03 Implement Nextcloud Login Flow 2019-10-07 11:37:13 +02:00
Ricki Hirner
4c229c81a1 Account list fragment: Show "Network unavailable" message if there is no Internet connection (only API 21+) 2019-10-07 11:35:05 +02:00
Ricki Hirner
0ac6dc8538 Set target SDK to level 29 (Android 10)
- ask for ACCESS_FINE_LOCATION/ACCESS_BACKGROUND_LOCATION when sync is restricted to specific WiFi SSIDs
- use Android 5+ way to determine active network connections, if possible
2019-09-30 17:28:11 +02:00
Ricki Hirner
1a52794bd1 Keep "force read only" flag when refreshing collections 2019-09-30 15:36:52 +02:00
Ricki Hirner
5f9f5e2732 Get dav4jvm from jitpack instead of using a submodule 2019-09-30 13:45:34 +02:00
Ricki Hirner
9c50903fd6 Fetch translations from Transifex 2019-09-21 11:48:08 +02:00
Ricki Hirner
aa4b3657e5 Version bump to 2.5.5 2019-09-21 11:46:33 +02:00
Ricki Hirner
ed23b4834c Fetch translations from Transifex (thanks for Bulgarian and Silesian!) 2019-09-20 00:10:18 +02:00
Ricki Hirner
da81482bad lint 2019-09-19 14:27:27 +02:00
Ricki Hirner
f453d06929 Minor method signature updates for SDK level 29 2019-09-19 13:58:28 +02:00
Ricki Hirner
0d07fe9020 Version bump to 2.5.5-beta1, compile against SDK level 29 2019-09-19 13:43:30 +02:00
Ricki Hirner
ddd3ffb2c2 Remove conscrypt version (set by cert4android) 2019-09-19 13:43:03 +02:00
Ricki Hirner
b4b87bc4f5 Update cert4android 2019-09-18 23:40:24 +02:00
Ricki Hirner
e74a919812 Update dependencies; new findPreference syntax 2019-09-18 23:40:24 +02:00
Ricki Hirner
ef8931c9c4 Tasks: support CATEGORIES 2019-09-18 23:40:24 +02:00
Ricki Hirner
13e9d7c7b4 Enable Autofill for login screen 2019-08-25 16:35:43 +02:00
Ricki Hirner
4c4cbf6f2e Version bump to 2.5.4.1 2019-08-25 15:32:05 +02:00
Ricki Hirner
55a51c0b87 Fetch translations from Transifex 2019-08-25 15:31:01 +02:00
Ricki Hirner
e7833e403f Update gradle plugin 2019-08-25 15:29:27 +02:00
Ricki Hirner
ee36b10fd4 Add ProGuard line to avoid AppCompat crash 2019-08-13 18:59:57 +02:00
Ricki Hirner
79c4e24816 Version bump to 2.5.4 2019-08-06 12:45:47 +02:00
Ricki Hirner
4a8bf47aa3 Update material components to 1.1.0-alpha09, allegedly fixes Meizu crash 2019-08-05 22:57:02 +02:00
Ricki Hirner
d32a142ef0 Fetch translations from Transifex 2019-07-20 22:12:18 +02:00
Ricki Hirner
1a8171a55e Version bump to 2.5.3 2019-07-20 22:07:37 +02:00
Ricki Hirner
d5a03d7837 Update dependencies 2019-07-19 14:32:47 +02:00
Ricki Hirner
bcc8e02d77 Create collections: enable sync of created collections by default 2019-07-19 14:30:55 +02:00
Ricki Hirner
75124d99bc Home set enumeration: use correct displayName, also honor bind privilege; version bump to 2.5.3-beta1 2019-07-19 14:30:55 +02:00
Ricki Hirner
a081ef210b Re-enable HTTP/2 again (was disabled for all connections and not only when using client certificates by mistake) 2019-07-19 14:30:55 +02:00
Ricki Hirner
1882dee0c2 Process home-set display names 2019-07-16 12:52:41 +02:00
Ricki Hirner
fb7012e7c6 Update gradle plugin 2019-07-12 22:48:10 +02:00
Ricki Hirner
bbb32d501f Fetch translations from Transifex 2019-07-10 14:57:29 +02:00
Ricki Hirner
338375789a Fetch translations from Transifex 2019-07-08 21:46:08 +02:00
Ricki Hirner
123c5906bd Update ical4j; version bump to 2.5.2 2019-07-08 16:45:54 +02:00
devvv
eeac458d93 Update README.md 2019-06-24 17:18:14 +00:00
Ricki Hirner
471722496e ical4android: fix threading problem 2019-06-23 15:53:53 +02:00
Ricki Hirner
1ecd16c229 Address books accounts: set initial user data twice for older Android versions 2019-06-20 12:26:19 +02:00
Ricki Hirner
1804c4af96 Debug info: also show UnknownHostException; update Room dependency 2019-06-19 01:06:27 +02:00
Ricki Hirner
5974ffc7f6 Fetch translations from Transifex 2019-06-09 11:18:31 +02:00
Ricki Hirner
5073a56fd6 Version bump to 2.5.1 2019-06-09 11:18:31 +02:00
Ricki Hirner
1e76a92d02 Version bump to 2.5.1-beta2 2019-06-07 21:26:56 +02:00
Ricki Hirner
5709fd38d9 Webcal subscriptions: handle webcal(s):// URLs and treat them as http(s):// 2019-06-07 21:26:31 +02:00
Ricki Hirner
4bf9ba5f2d Update dependencies 2019-06-07 21:26:31 +02:00
Ricki Hirner
f5e317a08a Fetch translations from Transifex 2019-05-29 18:12:07 +02:00
Ricki Hirner
8936ad7810 Update dokka, Kotlin 2019-05-29 18:10:14 +02:00
Ricki Hirner
1d20dbe4e8 Version bump to 2.5.1-beta1 2019-05-29 18:05:58 +02:00
Ricki Hirner
b247e70d2a Use android:hint in TextInputLayout instead of TextInputEditText (should fix Meizu crashes) 2019-05-29 18:05:31 +02:00
Ricki Hirner
477ee085fc Show unhandled exceptions with DebugInfoActivity 2019-05-25 11:14:45 +02:00
Ricki Hirner
8f62c185ec Don't use HTTP/2 with client certificates 2019-05-22 23:58:00 +02:00
Ricki Hirner
f6144dc0ab Update gradle plugin, library dependencies 2019-05-17 12:04:03 +02:00
Ricki Hirner
a958ed48f9 AccountActivity: set color for swipe refresh 2019-05-15 11:52:21 +02:00
Ricki Hirner
3458f786f5 Fetch translations from Transifex; add Finnish 2019-05-15 11:43:56 +02:00
Ricki Hirner
db037f1dfb Bump version code to 288 2019-05-15 11:39:43 +02:00
Ricki Hirner
c4ee6037ec Don't show permantenly pending sync for CalDAV if OpenTasks is not installed 2019-05-14 21:12:56 +02:00
Ricki Hirner
82310b2c0d Fetch translations from Transifex 2019-05-13 20:20:09 +02:00
Ricki Hirner
292b72b62f Version bump to 2.5; minor lint fixes 2019-05-13 20:13:08 +02:00
Ricki Hirner
4cdf26d674 cert4android: update translations 2019-05-10 16:47:07 +02:00
Ricki Hirner
c5c68ad7c9 Fetch translations from Transifex; new translation: Slovak (thanks brango67!) 2019-05-10 16:46:51 +02:00
Ricki Hirner
c734e1a1f2 Debug info: set informational text for shared logs 2019-05-09 21:21:56 +02:00
Ricki Hirner
459f9da486 Fetch translations from Transifex 2019-05-09 12:22:12 +02:00
Ricki Hirner
1fbd532237 Version bump to 2.5-beta2 2019-05-09 12:17:21 +02:00
Ricki Hirner
a67a56e0c5 Fix NPE in WebcalFragment 2019-05-09 12:16:57 +02:00
Ricki Hirner
787c5e480a Update icon resolutions, names and action bar icon color 2019-05-09 12:16:48 +02:00
Ricki Hirner
38ecd7a4c4 Update libraries 2019-05-09 12:16:41 +02:00
Ricki Hirner
17cf4a6289 Version bump to 2.5-beta1 2019-05-06 21:42:11 +02:00
Ricki Hirner
ac8558ce6a Progress bars for AccountActivity (which now also show pending syncs) 2019-05-06 21:41:42 +02:00
Ricki Hirner
4db40ac223 Fetch translations from Transifex 2019-05-06 19:35:54 +02:00
Ricki Hirner
38ab785382 Update ical4j: now allows UTC properties to have non-UTC values for compatibility 2019-05-06 19:23:30 +02:00
Ricki Hirner
0c5bb19be9 Minor fixes 2019-05-06 19:20:22 +02:00
Ricki Hirner
310e5a1720 More AccountActivity; fix bug in CreateCalendarActivity & co 2019-05-06 19:20:15 +02:00
Ricki Hirner
9df0db1ec3 Correctly handle permissions in WebcalFragment 2019-05-06 19:20:08 +02:00
Ricki Hirner
f6f3ebb48b DavService: don't delete all homesets/collections on refresh, but update only changed rows 2019-05-06 19:20:01 +02:00
Ricki Hirner
ffe59915d9 Rename AccountActivity2 to AccountActivity 2019-05-06 19:19:54 +02:00
Ricki Hirner
94a8c4c218 Make AccountActivity2 fully working again 2019-05-06 19:19:46 +02:00
Ricki Hirner
efa168b941 Use AppBarLayout for AccountActivity 2019-05-06 19:19:38 +02:00
Ricki Hirner
b8abc60752 Linuxtage Action 2019-05-06 19:19:01 +02:00
Ricki Hirner
77d6fc7243 Fix Android 4.4 compatibility; version code bump 2019-04-19 14:15:49 +02:00
Ricki Hirner
f50f86c1ad vCard: use "groupX" instead of "davdroidX" for property grouping 2019-04-19 12:57:55 +02:00
Ricki Hirner
3c07bc3abc Use Conscrypt; show progress bar when loading account activity 2019-04-19 02:33:01 +02:00
Ricki Hirner
400b7babbc Improve database migrations 2019-04-18 13:35:29 +02:00
Ricki Hirner
5ff86d809f Fix crash in account setup 2019-04-18 13:35:29 +02:00
Ricki Hirner
9af246a01a Version bump to 2.4.1-beta1 2019-04-18 13:25:39 +02:00
Ricki Hirner
d7cefa84f4 Update Kotlin, gradle, gradle plugin 2019-04-17 22:53:19 +02:00
Ricki Hirner
d7b73a35e1 Tests 2019-04-17 12:24:33 +02:00
Ricki Hirner
f20d921559 Handle permissions with LiveData; manifest: add app settings/debug info activity 2019-04-16 21:36:14 +02:00
Ricki Hirner
cdec545568 Use live paged data for AccountActivity 2019-04-16 21:36:14 +02:00
Ricki Hirner
6af50dbc43 Use Room for database 2019-04-16 21:36:14 +02:00
Ricki Hirner
7a44f61887 Minor cert4android/ical4android update 2019-04-03 00:00:53 +02:00
Ricki Hirner
4ec98acdd1 Fetch translations from Transifex 2019-04-02 23:48:55 +02:00
Ricki Hirner
e0a000cbd8 Hotfix: AccountActivity: avoid endless loop of asking for permissions; version bump to 2.4.0.1 2019-04-02 23:48:14 +02:00
Ricki Hirner
7c8f74ab14 Version bump to 2.4 2019-03-29 15:07:42 +01:00
Ricki Hirner
03cbe4f665 Update read-only state or raw contacts/data rows according to the address book 2019-03-27 12:34:44 +01:00
Ricki Hirner
a76829c3dd Version bump to 2.4-beta4 2019-03-27 12:15:20 +01:00
Ricki Hirner
d83a4df524 Account activity: update "force read only" responsively 2019-03-27 12:13:21 +01:00
Ricki Hirner
afdfb735bd Fetch translations from Transifex 2019-03-26 22:02:48 +01:00
Ricki Hirner
414f771156 Remove unused sync status notification channel 2019-03-26 22:00:03 +01:00
Ricki Hirner
c690dfd148 Support read-only flag in raw contacts/data rows for read-only address books 2019-03-26 21:11:56 +01:00
Ricki Hirner
50775f26ce Change secondary text color to darker gray 2019-03-26 18:19:06 +01:00
Ricki Hirner
40a1748461 Use .closeCompat for ContentProviderClient instances 2019-03-26 18:19:06 +01:00
Ricki Hirner
63e360fb3a AccountActivity: remove "Select collections to synchronize" 2019-03-26 13:01:33 +01:00
Ricki Hirner
d7216f5ffa Update libraries; bump version to 2.4-beta3 2019-03-26 12:14:53 +01:00
Ricki Hirner
a749a9fd34 Rename "debug model" (artifact from search/replace) to "debug info" again 2019-03-26 12:14:25 +01:00
Ricki Hirner
6951397945 AccountActivity: use ViewModel, RecyclerView 2019-03-25 11:37:44 +01:00
Ricki Hirner
6153b3aafe AccountAdapter not-null assertion 2019-03-24 17:17:37 +01:00
Ricki Hirner
325dd41820 Version bump to 2.4-beta2 2019-03-24 00:05:59 +01:00
Ricki Hirner
be94ee19d8 Enable gradle incubation 2019-03-23 23:27:17 +01:00
Ricki Hirner
d03ef78db9 Update tests 2019-03-23 23:24:37 +01:00
Ricki Hirner
e7e580132f Enable Gitlab CI again 2019-03-23 23:24:37 +01:00
Ricki Hirner
a7c62d11ca Update libs, About logo 2019-03-23 22:54:38 +01:00
Ricki Hirner
d978e6a17d Fetch translations from Transifex 2019-03-23 21:19:20 +01:00
Ricki Hirner
87457bf6d4 Small fixes 2019-03-23 21:19:20 +01:00
Ricki Hirner
bff9c87c1f Create collections: use ViewModel 2019-03-23 20:53:49 +01:00
Ricki Hirner
d27261f18a Handle exceptions when acquiring task provider 2019-03-23 20:52:09 +01:00
Ricki Hirner
affce28652 Don't rely on Manifest-declared implicit broadcast to detect when a tasks app is being installed/removed (Android 8 compatibility) 2019-03-23 20:50:28 +01:00
Ricki Hirner
b319fef88a Version bump to 2.4-beta1 2019-03-23 20:37:18 +01:00
Ricki Hirner
6369ae1d36 About: use vector icon 2019-03-23 20:37:18 +01:00
Ricki Hirner
8fe093d975 Create calendar: new UI, use ViewModel 2019-03-23 20:37:18 +01:00
Ricki Hirner
e99acb2921 Delete collection: use ViewModel 2019-03-23 20:37:18 +01:00
Ricki Hirner
88e0c16f83 AboutActivity: use ViewModel 2019-03-23 20:37:18 +01:00
Ricki Hirner
b348f54f95 Update beta feedback email address, libraries 2019-03-23 20:37:18 +01:00
Ricki Hirner
b75f8d140a Login: couple user name and email address 2019-03-23 20:37:18 +01:00
Ricki Hirner
206c2e8aa9 Account setup: fix crash 2019-03-23 20:37:18 +01:00
Ricki Hirner
e8f14c3745 Enable Multidex for >64k methods on Android 4.4 2019-03-23 20:37:18 +01:00
Ricki Hirner
c2e6514a3c AccountListFragment: use ViewModel 2019-03-23 20:37:18 +01:00
Ricki Hirner
676f74a528 HttpClient: close cache, provide app:html binding 2019-03-16 13:05:43 +01:00
Ricki Hirner
22b9624c0a Use ViewModel and data binding for login process 2019-03-16 13:02:10 +01:00
Ricki Hirner
f22dd3e013 Debug info: use ViewModel 2019-03-16 13:01:38 +01:00
Ricki Hirner
913b395e56 Update libraries; enable Java 8 compatibility 2019-03-16 13:01:26 +01:00
Ricki Hirner
f9974dbb7e Move CustomTlsSocketFactory to cert4android so it can be used in other apps like ICSx⁵; update ical4android 2019-02-20 18:29:43 +01:00
Ricki Hirner
49fae85852 Notify on invalid events, too 2019-02-19 21:24:09 +01:00
Ricki Hirner
3addab5bc0 Version bump to 2.3 2019-02-19 21:24:09 +01:00
Ricki Hirner
1bfacff3df Merge branch 'remove-stray-settings' into 'master-ose'
Remove stray Settings.kt file

See merge request bitfireAT/davx5-ose!18
2019-02-14 10:13:41 +00:00
Michael Biebl
643c4f04c9 Remove stray Settings.kt file
It was most likely added by accident in commit
7d4689969a
2019-02-14 01:40:54 +01:00
Ricki Hirner
517b60aa21 Metadata: allow translations over Transifex 2019-02-12 16:46:55 +01:00
Ricki Hirner
98eeaf171e Fetch translations from Transifex 2019-02-09 14:20:41 +01:00
Ricki Hirner
9320de54c3 Metadata: add feature graphic 2019-02-08 17:50:41 +01:00
Ricki Hirner
24730d1925 Notify on invalid iCalendar/vCard objects
* add notification channel: sync warnings
* notify on invalid iCalendar/vCard objects
* add app setting: Notification settings (only when notification channels are available)
2019-02-08 17:42:51 +01:00
Ricki Hirner
94fd665b6d Update Kotlin, gradle, dav4jvm 2019-02-07 13:21:25 +01:00
Ricki Hirner
7d9d9ea57c Translations: fix positional argument (thanks @mbiebl) 2019-02-06 17:28:33 +01:00
Ricki Hirner
bc1b1ca40f Update README, use absolute submodules URLs 2019-02-06 17:01:24 +01:00
Ricki Hirner
4ff8a5eb4e README update 2019-02-06 16:46:00 +01:00
Ricki Hirner
517a859dbf Fetch translations from Transifex 2019-02-06 16:45:38 +01:00
Ricki Hirner
1b6689133a Add Greek translation (thanks Efstathios Iosifidis) 2019-02-06 16:44:40 +01:00
Ricki Hirner
8a18e1e738 CalenderSyncManager/TaskSyncManager: log created/updated data; update
libs
2019-02-06 01:25:41 +01:00
Ricki Hirner
f241de264d Update ical4j; show new davx5 logger in scripts/adb-log.sh 2019-01-30 17:45:57 +01:00
Ricki Hirner
2cf73f0f07 Use AppCompatResources.getDrawable instead of context.getDrawable 2019-01-29 23:01:01 +01:00
Ricki Hirner
15008c1a53 Use HtmlCompat.fromHtml instead of deprected Html.fromHtml 2019-01-29 22:55:00 +01:00
Ricki Hirner
c90e491739 Update gradle, Kotlin 2019-01-29 20:02:08 +01:00
Ricki Hirner
1b5ea52138 Settings provider: more logging, scripts update, lib updates 2019-01-29 19:18:38 +01:00
Ricki Hirner
48478c421f Finish renaming dav4android → dav4jvm (thanks @mbiebl) 2019-01-27 12:15:52 +01:00
Ricki Hirner
f4823f9524 Version bump to 2.2.3.1 2019-01-18 16:16:01 +01:00
Ricki Hirner
2abe164e08 Don't use shouldShowRequestPermissionRationale in debug info (crashes on Motorola Android 6 when permission name is unknown) 2019-01-18 16:14:47 +01:00
Ricki Hirner
fa5856604d update homepage links; version bump to 2.2.3 2019-01-18 11:33:14 +01:00
Ricki Hirner
c88508ea66 Add Galician translation (thanks Xosé M. Lamas) 2019-01-18 09:58:55 +01:00
Ricki Hirner
e1c5fb2bfd Add Galician translation (thanks) 2019-01-18 09:58:32 +01:00
Ricki Hirner
9536d43047 Debug info: show location permission; show "Dont ask again" for permissions; show CalDAV "use event colors" option 2019-01-18 02:08:58 +01:00
Ricki Hirner
273c23ee44 Change some DAVdroid leftovers to DAVx⁵; remove Lombok config (thanks @mbiebl) 2019-01-18 01:43:33 +01:00
Ricki Hirner
a34b3b1fee AccountSettings version 9: disable OpenTasks isSyncable for non-CalDAV accounts 2019-01-18 01:33:30 +01:00
Ricki Hirner
6157d6e767 Disable OpenTasks sync on account creation without CalDAV service; Account UI: finish activity from main thread on account deletion 2019-01-18 01:33:24 +01:00
Ricki Hirner
013729ed73 Account settings: fix sync interval setting once more. Maybe it now works like this? 2019-01-17 12:27:51 +01:00
Ricki Hirner
2287b802ce Update README
* fix Twitter link (thanks Felix Eckhofer)
* update dav4jvm URL
2019-01-16 17:22:28 +01:00
Ricki Hirner
0166008ab6 Fetch translations from Transifex 2019-01-13 21:46:46 +01:00
Ricki Hirner
739bf3e53d Version bump to 2.2.3-beta1 2019-01-13 21:46:20 +01:00
Ricki Hirner
e36102721f Restrict sync to WiFi SSIDs: explain why Location permissions are required 2019-01-12 16:01:51 +01:00
Ricki Hirner
5ef9693ac8 Add some phone screenshots 2019-01-08 19:27:29 +01:00
Ricki Hirner
19320dda17 Version bump to 2.2.2 2019-01-08 19:19:43 +01:00
Ricki Hirner
322f9a0da0 Fetch translations from Transifex 2019-01-08 19:17:31 +01:00
Ricki Hirner
ba20d74b64 User-Agent and iCalendar/vCard product IDs: useBuild config; change to dav4jvm 2019-01-08 19:14:03 +01:00
Ricki Hirner
6a512e1f0f Fix crash on single-core devices and ProGuard rules 2019-01-08 01:21:37 +01:00
Ricki Hirner
4300954101 Fix CI in libs; include dav4jvm as standalone project 2019-01-06 20:01:29 +01:00
Ricki Hirner
2fe19e9342 Rename dav4android to dav4jvm; update gradle 2019-01-06 18:17:19 +01:00
Ricki Hirner
e764d31d18 Login: Show error message when account name is already taken 2019-01-05 11:55:32 +01:00
Ricki Hirner
8d0d920033 Version bump to 2.2.1 2019-01-04 22:30:41 +01:00
Ricki Hirner
e2fcbbea0b Fetch translations from Transifex 2019-01-04 22:30:41 +01:00
Ricki Hirner
1cefa88b98 Notifications: fix crashes (for Android 4.4 and when wrong CardDAV password occurs) 2019-01-04 22:13:47 +01:00
Ricki Hirner
be4a650deb Remove unnecessary permissions; ical4android update
* remove external storage permissions because logs are now written to the app data directory
* ical4android: use ical4j 2.2.3 with built-in RFC 7986 properties
2019-01-04 14:26:04 +01:00
Ricki Hirner
ab47b2b222 Debug info: fix onShare
* bump version code to 261
2019-01-04 01:04:40 +01:00
Ricki Hirner
7f301640da Account settings: reload in UI thread on Android callback
* version bump to 260
2019-01-03 19:20:52 +01:00
Ricki Hirner
83656ef497 Bump version code to 259 2019-01-03 18:55:09 +01:00
Ricki Hirner
77c54a2150 Fetch translations from Transifex 2019-01-03 18:54:52 +01:00
Ricki Hirner
16e3526a66 Retain sync intervals and isSyncable when renaming an account 2019-01-03 18:14:38 +01:00
Ricki Hirner
3ae5653da4 Install Webcal app: rename ICSdroid to ICSx⁵ 2019-01-03 17:13:40 +01:00
Ricki Hirner
c02b033f8c Version bump to 2.2 2019-01-03 16:53:24 +01:00
Ricki Hirner
4afe4837e7 Update strings and ical4j 2019-01-03 16:53:01 +01:00
Ricki Hirner
56813ad4d6 Logging: Use FileProvider instead of external file 2019-01-02 23:56:24 +01:00
Ricki Hirner
036fc405f4 Make sure the account is seen by OpenTasks 2019-01-02 22:07:25 +01:00
Ricki Hirner
95173dcd7d Ask for contacts/calendar permissions as soon there is a known CalDAV/CardDAV service 2019-01-02 22:07:25 +01:00
Ricki Hirner
be754d6ede Add F-Droid metadata 2018-12-31 11:26:34 +01:00
Ricki Hirner
20097c0b69 Version bump to 2.1 2018-12-30 20:34:36 +01:00
Ricki Hirner
0d1c265e97 Show Color Picker in README 2018-12-30 18:57:25 +01:00
Ricki Hirner
7ce739c271 Further DAVdroid -> DAVx5 replacements 2018-12-30 16:52:06 +01:00
Ricki Hirner
241957f2dd New launcher icons 2018-12-30 15:37:41 +01:00
Ricki Hirner
3dea33e100 Fix string references 2018-12-30 14:16:39 +01:00
Ricki Hirner
f8f60f4b52 Update CI script 2018-12-30 13:41:05 +01:00
Ricki Hirner
0d75cf0cdc Update strings 2018-12-30 12:37:54 +01:00
Ricki Hirner
556843df4f Remove "Accounts may be gone after rebooting" startup dialog 2018-12-30 12:08:45 +01:00
Ricki Hirner
8b12cb7a29 Fetch translations from Transifex 2018-12-30 11:59:27 +01:00
Ricki Hirner
07c5cdc754 Rename DAVdroid to DAVx⁵ 2018-12-30 11:16:23 +01:00
Ricki Hirner
1d33e4c311 Replace AmbilWarna by ColorPicker 2018-12-26 13:58:41 +01:00
Ricki Hirner
7d4689969a Refactor Settings provider
* don't use a separate :sync process anymore, so that settings management doesn't need IPC
* remove Settings service and IPC, use singleton with application Context instead
* adapt default number of sync worker threads
* library updates
2018-12-26 13:30:00 +01:00
Ricki Hirner
b863d355f6 Fetch translations from Transifex 2018-12-22 11:52:27 +01:00
Ricki Hirner
bcd468f2e5 DAV service detection: cancel previous notification (MR #15 thanks @mbiebl) 2018-12-22 11:52:27 +01:00
Ricki Hirner
21fdf2cebc Version bump to 2.0.7, fix small NPE 2018-12-22 11:52:27 +01:00
Ricki Hirner
ea071bbd1a Debug info: show details about DAVdroid, contact/calendar providers and contact/calendar/task apps 2018-12-21 18:03:36 +01:00
Ricki Hirner
bc5f1e935e Ignore HTTP 403 when the resource upload fails because of missing permissions (server will win) 2018-12-21 17:02:33 +01:00
Ricki Hirner
72749addcd Settings: add some icons; rearrange account settings 2018-12-08 23:25:19 +01:00
Ricki Hirner
6abbd019ad Version bump to 2.0.6 2018-12-05 12:56:51 +01:00
Ricki Hirner
67b1685d01 Version bump to 2.0.6-beta2 2018-12-03 11:19:01 +01:00
Ricki Hirner
4bbb2b8419 Ignore non-successful multiget responses 2018-12-01 22:01:37 +01:00
Ricki Hirner
8bdf03bfc5 Minor changes (lint/remove warnings) 2018-11-30 13:46:52 +01:00
Ricki Hirner
8b43677d01 Don't do emulator checks (because we can only use shared runners at the moment) 2018-11-30 11:40:24 +01:00
Ricki Hirner
e454fa398a Update ProGuard rules 2018-11-29 23:37:55 +01:00
Ricki Hirner
f5b1194599 Version bump to 2.0.6-beta1 2018-11-29 23:26:13 +01:00
Ricki Hirner
e549a25812 Update to okhttp 3.12.0 2018-11-29 23:25:43 +01:00
Ricki Hirner
fb90955b2a Switch to AndroidX 2018-11-29 23:03:38 +01:00
Ricki Hirner
a7f0161983 Merge branch 'dont-hardcode-account_type' into 'master-ose'
Don't hard-code accountType and use @string/account_type instead

See merge request bitfireAT/davdroid!12
2018-11-04 17:28:41 +00:00
Ricki Hirner
45efb88b1f Fetch translations from Transifex 2018-11-04 18:16:58 +01:00
Ricki Hirner
2c022d46fe Version bump to 2.0.5 2018-11-04 18:16:11 +01:00
Ricki Hirner
50c4ee94e2 Handle unresolvable ACTION_VIEW intents; update gradle, Kotlin 2018-11-04 18:15:07 +01:00
Michael Biebl
d06d24a13e Don't hard-code accountType and use @string/account_type instead 2018-10-18 19:57:05 +02:00
Ricki Hirner
2c5cdf813a Allow clear-text traffic (sync with http://) explicitly for Android 9 2018-10-07 18:53:39 +02:00
Ricki Hirner
0250baeeb3 Lint cleanup for Android P 2018-10-05 13:41:03 +02:00
Ricki Hirner
05d1970754 Version bump to 2.1-beta1 2018-10-05 13:21:04 +02:00
Ricki Hirner
a97be98b11 Mark as compatible with Android P (SDK level 28) 2018-10-05 13:21:04 +02:00
Ricki Hirner
1625f092fd Update Kotlin, gradle, build tools 2018-10-05 13:21:04 +02:00
Ricki Hirner
c0ff8f61f9 Resource detection: separate CalDAV/CardDAV detection; handle SocketTimeoutException better; version bump to 2.0.4 2018-09-08 21:49:21 +02:00
Ricki Hirner
dbfb74a16f Fetch translations from Transifex 2018-08-28 13:40:14 +02:00
Ricki Hirner
39b277bc6d Version bump to 2.0.3 2018-08-28 13:38:03 +02:00
Ricki Hirner
d09c70f52b More fine-grained WebDAV permissions
* service DB: split readOnly into privWriteContent and privUnbind
* collections: use privWriteContent (DAV:write-content privilege) for read-only detection
* AccountActivity: allow collection deletion only when privUnbind (DAV:unbind privilege) is set
2018-08-26 20:05:48 +02:00
Ricki Hirner
2f0ee8e230 Make debug info text selectable 2018-08-26 20:05:40 +02:00
Ricki Hirner
c320094b3b Service detection: ignore HTTP 4xx errors when looking for homesets 2018-08-25 15:59:05 +02:00
Ricki Hirner
5b12015307 Don't offer collection deletion for read-only collections 2018-08-25 15:59:05 +02:00
Ricki Hirner
e7d2c23989 Update Kotlin, dokka, build tools, FAQ URL, libs 2018-08-22 13:17:53 +02:00
Ricki Hirner
80971c52b5 Version bump to 2.0.2 2018-08-15 13:09:46 +02:00
Ricki Hirner
23abf7c1f0 DAVdroid address book sync: only sync address book sub-accounts of current account 2018-08-15 13:08:37 +02:00
Ricki Hirner
613b88ad5e Fix crash in resource detection 2018-08-15 12:43:22 +02:00
Ricki Hirner
b09d6c13b5 Fetch translations from Transifex 2018-08-10 21:25:43 +02:00
Ricki Hirner
e30ae04534 Version bump to 2.0.1 2018-08-10 21:25:01 +02:00
Ricki Hirner
0ed54bb671 Don't keep Acticity context when creating an account 2018-08-09 12:56:23 +02:00
Ricki Hirner
889ebe160e AccountActivity: use AsyncTask instead of own Executor 2018-08-07 19:20:36 +02:00
Ricki Hirner
602e14d2bf Really cancel resource detection when dialog is cancelled 2018-08-06 12:33:29 +02:00
Ricki Hirner
7ed275712e Use AsyncTask and progress indicator for creating an account 2018-08-06 12:33:25 +02:00
Ricki Hirner
3dc3e75721 Fetch translations from Transifex; always replace "..." by "…" 2018-08-05 10:55:15 +02:00
Ricki Hirner
a63e717ca7 OSE AboutActivity: take GPL from file and use loader 2018-08-05 10:47:58 +02:00
Ricki Hirner
c0044ae901 Don't use gradle variables in version name (F-Droid workaround) 2018-08-03 14:30:27 +02:00
Ricki Hirner
edabdbadc9 Collection properties: replace "Copy URL" button by selectable TextView 2018-08-03 14:30:04 +02:00
Ricki Hirner
47543e448c Version bump to 2.0 2018-07-30 10:16:19 +02:00
Ricki Hirner
5ff88c5b2b Fetch translations from Transifex 2018-07-28 14:33:53 +02:00
Ricki Hirner
eadfa39760 Version bump to 2.0-rc1 2018-07-28 14:33:05 +02:00
Ricki Hirner
2342bc92ca Vector drawable: fix Android 4.4 compatibility 2018-07-28 14:33:00 +02:00
Ricki Hirner
f0d150d491 Fetch translations from Transifex 2018-07-27 16:52:39 +02:00
Ricki Hirner
949a1916b5 Fix/improve error handling; bump version to 1.12-beta4 2018-07-27 16:49:57 +02:00
Ricki Hirner
efa4b7dc66 Version bump to 1.12-beta3 2018-07-22 11:24:14 +02:00
Ricki Hirner
971be44ffe Fetch translations from Transifex 2018-07-22 11:17:32 +02:00
Ricki Hirner
88efd5ab04 Use Apache Commons ContextedException instead of own class 2018-07-22 11:16:35 +02:00
Ricki Hirner
b44306eaf2 Rewrite startup strings 2018-07-21 11:08:12 +02:00
Ricki Hirner
abb0850889 Version bump to 1.12-beta2 2018-07-17 13:34:11 +02:00
Ricki Hirner
7760bddca7 Collection info: allow copying URL to clipboard 2018-07-17 13:24:57 +02:00
Ricki Hirner
60527e83fe Collection info fragment 2018-07-17 13:24:52 +02:00
Ricki Hirner
54cc9b39e1 Account activity: show "Select collections to synchronize" hint when no collections are selected 2018-07-16 20:05:40 +02:00
Ricki Hirner
be9d404e6c Update to okhttp 3.11 2018-07-16 14:01:38 +02:00
Ricki Hirner
fd40b5a4e3 Update build tools; enable parallel sync of address books authority 2018-07-15 17:10:57 +02:00
Ricki Hirner
abb142d118 Fetch translations from Transifex 2018-07-13 15:21:23 +02:00
Ricki Hirner
7a7a940089 New About activity
* new About activity using AboutLibraries library
* include DAVdroid version and other non-personal information in URL when DAVdroid homepage is opened
  (so that we know what DAVdroid versions are used out there and maybe can provide version-specific help)
2018-07-13 15:21:19 +02:00
Ricki Hirner
f0e1e03095 New sync logic with XML streaming
* refactored dav4android to use XML streaming
* refactored collection detection
* refactored sync logic
2018-07-12 00:05:53 +02:00
Ricki Hirner
2358996940 Enable strict mode for debugging; use thread for DB changes in AccountActivity 2018-06-27 12:55:07 +02:00
Ricki Hirner
09b42fc940 Change detection error message 2018-06-24 21:33:59 +02:00
Ricki Hirner
9c796f8226 Rename address book accounts correctly when renaming accounts; drop unparsable fields from vCards 2018-06-20 12:39:35 +02:00
Ricki Hirner
9a635875a1 Version bump to 1.11.5 2018-06-17 16:37:50 +02:00
Ricki Hirner
9d598cead7 move lambda expressions out of parentheses; use CREATOR-named companion objects for Parcelable 2018-06-17 16:37:11 +02:00
Ricki Hirner
2915a1f2b6 Fix lateinit null value in resource detection; update Kotlin and gradle 2018-06-16 15:08:06 +02:00
Ricki Hirner
5316fb42a5 Trigger a full calendar sync when past event time limit is changed in account settings 2018-06-14 09:21:42 +02:00
Ricki Hirner
1c3a5ceb09 Don't show contacts permission notification when no address book is selected for synchronization 2018-06-13 17:23:55 +02:00
Ricki Hirner
c34ca0f8e6 Version bump to 1.11.4.1 2018-06-12 11:15:40 +02:00
Ricki Hirner
b9f629f6ce Collection sync: don't reset "present remotely" flag after enumerating resources 2018-06-12 00:43:12 +02:00
Ricki Hirner
c6e37fcc58 Version bump to 1.11.4 2018-06-08 11:52:16 +02:00
Ricki Hirner
cb8c1f45ad Fetch translations from Transifex 2018-06-08 11:52:16 +02:00
Ricki Hirner
f7e1b97a66 Update gradle plugin 2018-06-08 11:52:16 +02:00
Ricki Hirner
90a70f39e8 Fix crash when collection is deleted 2018-05-30 10:33:26 +02:00
Ricki Hirner
9226d867c3 Update copyright 2018-05-28 12:04:56 +02:00
Ricki Hirner
216579680d Add maxOccurs to contacts.xml group membership (allows editing of contacts again) 2018-05-28 11:56:47 +02:00
Ricki Hirner
fe8b50c76d Fetch translations from Transifex 2018-05-28 10:47:33 +02:00
Ricki Hirner
9ab7b3b105 Add group memberships to contacts.xml so that they can be edited with some Contacts apps 2018-05-28 10:47:04 +02:00
Ricki Hirner
b90ec5b64c Version bump to 1.11.4-beta2 2018-05-27 16:28:41 +02:00
Ricki Hirner
b197ac7da2 Show HTTP request/response in debug info 2018-05-27 16:28:02 +02:00
Ricki Hirner
9aa58d170f Use US locale for date in User-Agent 2018-05-27 15:34:30 +02:00
Ricki Hirner
553b782339 Fix lateinit problem 2018-05-26 12:42:02 +02:00
Ricki Hirner
8b6376e01d Use HttpUrl whenever possible
* HttpUrl is the preferred class because we use URLs mainly for okhttp
* don't use URI or String for URLs, if possible
* HttpUrl is not Serializable, so use Parcelable for data classes with HttpUrl

X
2018-05-26 12:29:42 +02:00
Ricki Hirner
5da229ed88 Use weak reference for sync adapter thread lock 2018-05-26 12:29:38 +02:00
Ricki Hirner
a0bb681f48 Don't show InterruptedIOException; tests 2018-05-25 10:57:03 +02:00
Ricki Hirner
2e3987e6af Version bump to 1.11.4-beta1 2018-05-24 14:14:27 +02:00
Ricki Hirner
f72588a691 Update ical4android 2018-05-24 14:14:27 +02:00
Ricki Hirner
56fc2121f8 Integrate adaptive icon 2018-05-24 14:14:27 +02:00
Lokesh Krishna
c863d05f3f Adaptive icon 2018-05-24 14:14:27 +02:00
Ricki Hirner
f0ff12ebb9 dav4android update (immutable responses) 2018-05-24 14:14:27 +02:00
Ricki Hirner
87cb43b0e6 Fetch translations from Transifex 2018-05-24 14:14:27 +02:00
Ricki Hirner
2e073897b8 Collection sync: handle 403 with valid-sync-token precondition 2018-05-24 14:14:27 +02:00
Ricki Hirner
bdc032d1b9 Update build.gradle 2018-05-12 11:27:19 +02:00
Ricki Hirner
d07fcdcddc ical4android: reduce size of sent VTIMEZONEs 2018-05-11 23:18:54 +02:00
Ricki Hirner
5f2f151e14 Version bump to 1.11.3 2018-05-10 12:57:00 +02:00
Ricki Hirner
33ddb06272 Enable Collection Synchronization for CalDAV when past time event limit is disabled 2018-05-10 12:55:32 +02:00
Ricki Hirner
ef9f13d1d6 Version bump to 1.11.2 2018-05-04 11:01:06 +02:00
Ricki Hirner
6b9520287b Collection sync: mark skipped entries as locally present 2018-05-01 16:34:38 +02:00
Ricki Hirner
3944ab0222 Version bump to 1.11.2-beta1 2018-05-01 10:19:57 +02:00
Ricki Hirner
103459464c Improve collection sync for contacts 2018-04-30 12:42:15 +02:00
Ricki Hirner
be2d10278d Collection sync: don't download already available resources 2018-04-30 12:41:27 +02:00
Ricki Hirner
f4b864b3d0 Support collection sync (RFC 6578) for contacts 2018-04-30 12:41:21 +02:00
Ricki Hirner
a576701ee3 Code cleanup (lint) 2018-04-28 21:23:37 +02:00
Ricki Hirner
40bca54c09 Fix memory leak in vcard4android 2018-04-27 00:54:01 +02:00
Ricki Hirner
88efbd7a00 Add message to local storage error notification 2018-04-26 12:29:16 +02:00
Ricki Hirner
292eb3bb52 Fetch translations from Transifex 2018-04-26 10:53:53 +02:00
Ricki Hirner
bfddf92a3b Don't throw exception when content provider doesn't return all results; ignore RemoteException on getting contacts sync state 2018-04-26 10:53:27 +02:00
Ricki Hirner
872bfa2f40 Version bump to 1.11.1 2018-04-26 10:52:30 +02:00
Ricki Hirner
73d9dff1b7 Show and use ical4j and okhttp version numbers when possible 2018-04-25 01:04:13 +02:00
Ricki Hirner
fd84ff37e5 Log remote resource info for tasks, too 2018-04-19 08:30:23 +02:00
Ricki Hirner
d280ce9d63 Treat InterruptedIOException like IOException 2018-04-13 15:24:03 +02:00
Ricki Hirner
67c7be900a Fetch translations from Transifex 2018-04-13 10:24:33 +02:00
Ricki Hirner
c9cd6de476 Version bump to 1.11 2018-04-13 10:19:29 +02:00
Ricki Hirner
924ef0243d Fetch translations from Transifex 2018-03-29 14:41:22 +02:00
Ricki Hirner
62d9668081 Version bump to 1.11-rc1 2018-03-29 14:41:00 +02:00
Ricki Hirner
ab6fa7ecbf Update gradle, kotlin, build tools 2018-03-29 14:07:52 +02:00
Ricki Hirner
74b0e5cd31 Fetch translations from Transifex 2018-03-26 09:37:19 +02:00
Ricki Hirner
e2960ab572 Ask for LOCATION_COARSE permission for WiFi name detection on Android 8.1+ 2018-03-26 09:36:14 +02:00
Ricki Hirner
50bd119871 Don't re-schedule non-existent master events of deleted exceptions 2018-03-24 23:14:03 +01:00
Ricki Hirner
c1f3165f5d Improve OpenTasks install message 2018-03-20 12:26:13 +01:00
Ricki Hirner
915cd6ec44 Restrict more method names by interfaces 2018-03-16 13:16:17 +01:00
Ricki Hirner
73e936aeb5 Fix sync bugs 2018-03-15 19:37:04 +01:00
Ricki Hirner
190fc020ae Minor fixes 2018-03-15 18:14:28 +01:00
Ricki Hirner
c40a6bca2f OSE fixes 2018-03-15 17:37:19 +01:00
Ricki Hirner
3129fd909f Use Google repo 2018-03-15 13:11:00 +01:00
Ricki Hirner
31c77a1d57 Themeing, minor refactoring 2018-03-15 13:05:57 +01:00
Ricki Hirner
380562bd28 Add "app auto-start permission" startup dialog for specific vendors 2018-03-15 13:04:38 +01:00
Ricki Hirner
80b1cb55c7 Tests, minor refactoring 2018-03-15 13:01:41 +01:00
Ricki Hirner
59dd66383e Sync error notifications: retry action, lower importance of IOEXceptions 2018-03-15 13:01:19 +01:00
Ricki Hirner
44af04e310 Account activity: remove SnackBar message when a read-only address book is selected 2018-03-15 13:00:11 +01:00
Ricki Hirner
adf45cb569 Show startup fragments only once per AccountActivity lifecycle 2018-03-15 12:59:58 +01:00
Ricki Hirner
ab88020c15 Sync logic fix, theming, ProGuard 2018-03-15 12:58:56 +01:00
Ricki Hirner
e27c6fde08 Update to support library 27.1.0 and use it wherever possible
* Fragment transactions can now be done in onLoadFinished().
2018-03-15 12:57:51 +01:00
Ricki Hirner
3135af78af Tests 2018-03-15 12:44:43 +01:00
Ricki Hirner
9200ec89e1 Further improve notifications 2018-03-15 12:44:23 +01:00
Ricki Hirner
b37a2ad0e0 Remove Project Lombok from About
* not used anymore because of Kotlin
* thanks to Project Lombok!
2018-03-15 12:43:44 +01:00
Ricki Hirner
1df42350cc Remove PermissionsActivity
* asking for permissions is already (and better) done by AccountActivity
2018-03-15 12:43:32 +01:00
Ricki Hirner
3dbd5e3d18 Synchronization error messages / notifications 2018-03-15 12:41:53 +01:00
Ricki Hirner
7fdcb3710b Rewrite sync algorithm, prepare for WebDAV collection sync 2018-03-15 12:40:32 +01:00
Ricki Hirner
2f9f4f1d7b Optimize notification icons
* don't show DAVdroid icon unless there's a strong relationship to DAVdroid itself
2018-03-15 12:39:13 +01:00
Ricki Hirner
9fc3921b32 Account activity: show progress bar at beginning 2018-01-30 14:58:50 +01:00
Ricki Hirner
4f4a22a14e ical4android update 2018-01-27 22:57:34 +01:00
Ricki Hirner
16c44d8ad3 OpenTasks is installed by ical4android tests 2018-01-25 15:11:24 +01:00
Ricki Hirner
6cd2aa783e Prefer AutoCloseable over Closeable 2018-01-25 15:09:57 +01:00
Ricki Hirner
c2e161dac6 lib updates 2018-01-25 15:04:43 +01:00
Ricki Hirner
5a686a9a4c Version bump to 1.10.1.1 2018-01-20 17:11:58 +01:00
Ricki Hirner
7ba98d1c71 Hotfix: don't clear event colors 2018-01-20 17:08:29 +01:00
Ricki Hirner
fee5ab5064 Version bump to 1.10.1 2018-01-20 15:26:15 +01:00
Ricki Hirner
6e1b42e6a4 Fetch translations from Transifex 2018-01-20 15:23:32 +01:00
Ricki Hirner
c1ce953e3e ical4android: work around missing account separation when updating events
* fixes removed eventColor_index fields when there is at least one DAVdroid
  account with enabled calendar colors and at least one account with disabled
  calendar colors
2018-01-20 15:01:05 +01:00
Ricki Hirner
3a6db13df8 Update Kotlin 2018-01-18 16:17:29 +01:00
Ricki Hirner
d1d771b16a Version bump to 1.10 2018-01-18 16:11:54 +01:00
Ricki Hirner
f986d9807e Fetch translations from Transifex 2018-01-18 16:11:14 +01:00
Ricki Hirner
e3f33a5603 Test for client certificates 2018-01-18 16:10:26 +01:00
Ricki Hirner
702888e9bd Login activity: add padding; sync: re-throw Interrupted(IO)Exception 2018-01-15 21:33:00 +01:00
Ricki Hirner
6b41849cae Fetch translations from Transifex 2018-01-15 20:52:13 +01:00
Ricki Hirner
9dfb23b1d2 Remove unnecessary strings 2018-01-15 20:49:47 +01:00
Ricki Hirner
3468473f63 Improve sync error notifications
* refactor checking for cancelled sync
* notify on SSLHandshakeException (except when a certificate was rejected by cert4android)
* show exception cause in debug info
2018-01-15 20:43:07 +01:00
Ricki Hirner
bc63559a24 Login activity
* use TextInputLayout for input fields
* use support library instead of custom EditPassword widget
* improve client certificate UI
2018-01-15 14:00:03 +01:00
Ricki Hirner
3689df1385 Login with client certificates
* setup UI: login with URL and client certificate
* account settings UI: show either username/password or client certificate alias
* AccountSettings: serve credentials in generalized Credentials objects
* HttpClient: use Credentials (instead of username/password) for authentication
* HttpClient: always use CustomTlsSocketFactory
* CustomTlsSocketFactory: support client certificates
2018-01-13 22:56:13 +01:00
Ricki Hirner
dd3b326f66 Version bump to 1.9.10 2018-01-03 12:14:12 +01:00
Ricki Hirner
e2ca9eb7d7 dav4android update 2018-01-02 20:26:56 +01:00
Ricki Hirner
b001be1626 Fetch translations from Transifex 2018-01-02 19:41:53 +01:00
Ricki Hirner
e09714af32 Fix DB upgrade logic 2018-01-02 19:29:42 +01:00
Ricki Hirner
a4a816296b Refactoring 2018-01-01 15:13:57 +01:00
Ricki Hirner
65db706e06 Do contact provider settings only at address book creation (fixes changed contact visibility at sync) 2017-12-31 13:38:49 +01:00
Ricki Hirner
abd0eb533e Version bump to 1.9.9 2017-12-27 13:06:58 +01:00
Ricki Hirner
323cd48a30 Fetch translations from Transifex 2017-12-26 13:08:09 +01:00
Ricki Hirner
07b59fb6be Update for OpenTasks support
* ical4android update
* use uid field for tasks
* require OpenTasks 1.1.8.2
2017-12-26 13:08:04 +01:00
Ricki Hirner
3e2e17bdb2 Fix crash 2017-12-18 09:25:09 +01:00
Ricki Hirner
73c5c9ef64 Fix string resource 2017-12-16 22:09:21 +01:00
Ricki Hirner
12261a23ea Version bump to 1.9.8.1 2017-12-16 22:02:55 +01:00
Ricki Hirner
e19b045494 Fetch translations from Transifex 2017-12-16 22:01:35 +01:00
Ricki Hirner
076962e5ed Specify DNS server for dnsjava explicitly 2017-12-16 21:55:29 +01:00
Ricki Hirner
04306ed919 Version bump to 1.9.8 2017-12-16 18:56:16 +01:00
Ricki Hirner
93abef95f7 Fetch translations from Transifex 2017-12-16 18:54:16 +01:00
Ricki Hirner
54cc3bec9f Refactor Loaders, HttpClient
* improve Loader implementation
* use one shared HttpClient singleton
2017-12-16 18:47:48 +01:00
Ricki Hirner
d4324bcf8e Navigation drawer: add link to manual 2017-12-11 22:58:16 +01:00
Ricki Hirner
eff4438e9f Version bump to 1.9.7 2017-12-11 20:02:30 +01:00
Ricki Hirner
1f7298f947 Don't rely on LOGIN_ACCOUNTS_CHANGED_ACTION 2017-12-11 18:17:28 +01:00
Ricki Hirner
c823eb6efd Fetch translations from Transifex 2017-12-03 15:22:47 +01:00
Ricki Hirner
7e97017c41 Version bump to 1.9.6 2017-12-03 15:18:45 +01:00
Ricki Hirner
5e3b98a676 Target Android 8.1 (Oreo) 2017-11-30 12:58:08 +01:00
Ricki Hirner
8d60ee8d12 Define notification channels 2017-11-30 12:58:07 +01:00
Ricki Hirner
45af16afa6 Version bump to 1.9.5 2017-11-28 12:47:13 +01:00
Ricki Hirner
1b5bb875e0 Cert4android update 2017-11-28 12:46:59 +01:00
Ricki Hirner
09f946d5f4 Process colors for Webcal calendars, too 2017-11-27 17:54:49 +01:00
Ricki Hirner
0342242937 Provide title and color in Webcal intent; ical4android update 2017-11-27 17:25:57 +01:00
Ricki Hirner
46463bc2bc Fetch translations from Transifex 2017-11-25 18:48:51 +01:00
Ricki Hirner
3b13c0ef3c Version bump to 1.9.4 2017-11-25 18:48:14 +01:00
Ricki Hirner
a409273d8d Ask for permissions when account is opened (the permission notification is not always reliable) 2017-11-25 18:47:53 +01:00
Ricki Hirner
270607084e Upgrade libraries 2017-11-25 16:47:50 +01:00
Ricki Hirner
4be57313d7 New collection setting: "force read-only"
* collections in AccountActivity: replace long click by action overflow
2017-11-13 18:11:28 +01:00
Ricki Hirner
4e4cf7648b Version bump to 1.9.3 2017-11-12 17:34:41 +01:00
Ricki Hirner
5d084166f4 Fetch translations from Transifex 2017-11-11 21:25:39 +01:00
Ricki Hirner
173d744c2e Dokka KDoc 2017-11-11 21:18:31 +01:00
Ricki Hirner
d0e0b5d9e6 Raise API level, compatibility, make sync work again
* raise API level to 19 (required by ical4j)
* make HttpClient use Settings to get sync working again
* don't use vector graphics for notification icons (crashes on Android 4.x)
* various fixes and improvements
* library and build tools updates
2017-11-11 20:51:04 +01:00
Ricki Hirner
b2475d2b50 Refactoring
* refactor Settings and HttpClient
* library updates
2017-11-11 20:49:50 +01:00
Ricki Hirner
d0341ef1d0 cert4android: fetch translations 2017-11-04 12:00:19 +01:00
Ricki Hirner
34a55e68b0 Version bump to 1.9.2 2017-11-04 00:46:19 +01:00
Ricki Hirner
9a0cd64f68 SDK/build tools 27 2017-11-04 00:46:19 +01:00
Ricki Hirner
591a4ad423 another 5 minutes wasted life time 2017-10-31 14:01:42 +01:00
Ricki Hirner
7b479d270a Version bump 2017-10-31 13:41:29 +01:00
Ricki Hirner
58c44f6213 Fetch translations from Transifex 2017-10-31 13:38:38 +01:00
Ricki Hirner
b81a0c1eb6 Change tasks icon, add some view descriptions 2017-10-31 13:36:50 +01:00
Ricki Hirner
37a28df7d8 Handle exceptions in settings providers 2017-10-31 13:22:52 +01:00
Ricki Hirner
19796e6669 Fetch translations from Transifex 2017-10-31 09:19:50 +01:00
Ricki Hirner
87719f322b ical4android minor bug fix 2017-10-31 09:19:12 +01:00
Ricki Hirner
d40c2f5c56 lint 2017-10-29 14:57:40 +01:00
Ricki Hirner
48962d1bfa Use vector drawable support library for Android <5 2017-10-29 14:57:38 +01:00
Ricki Hirner
42ba48253f Update build tools, gradle, support library 2017-10-28 00:46:09 +02:00
Ricki Hirner
d55a1aa8b5 Version bump to 1.9.1 2017-10-25 20:43:00 +02:00
Ricki Hirner
dbc4ef9618 Fetch translations from Transifex 2017-10-25 20:37:40 +02:00
Ricki Hirner
3bf9be1443 Prevent multiple syncs to be run at the same time for the same account and authority 2017-10-25 18:40:02 +02:00
Ricki Hirner
e2b5ce7b10 Better handling of contact group method change
* ask for confirmation when group method is changed in account settings activity
* reset address books/force reload when contact group method has changed since last sync
2017-10-25 18:38:50 +02:00
Ricki Hirner
3d537b4472 Settings: add force reload 2017-10-25 15:22:32 +02:00
Ricki Hirner
64c3ca96ce Change homepage to davdroid.com 2017-10-23 14:23:41 +02:00
Ricki Hirner
71f73d66f2 Resource detection: use related replies to gather collection info, too 2017-10-20 13:56:38 +02:00
Ricki Hirner
0e4bb317f5 Version bump to 1.9-ose 2017-10-15 18:14:50 +02:00
Ricki Hirner
583280cf55 Change CI 2017-10-15 17:53:55 +02:00
Ricki Hirner
018f06fd53 Fetch translations from Transifex 2017-10-15 17:43:06 +02:00
Ricki Hirner
bf63605d50 Calendars: allow email reminders for events, too 2017-10-15 17:34:30 +02:00
Ricki Hirner
9643cb7661 StartupDialogFragment: Use activity instead of context 2017-10-13 21:37:00 +02:00
Ricki Hirner
46669dd0e9 README updates 2017-10-13 21:21:50 +02:00
Ricki Hirner
299f50f6d9 Fetch translations from Transifex 2017-10-13 20:43:58 +02:00
Ricki Hirner
af7f5e60bf Move to davdroid-ose repo 2017-10-13 14:55:38 +02:00
Ricki Hirner
d869ecbc0c Minor bug fixes, move beta feedback to navigation drawer 2017-10-13 13:41:14 +02:00
Ricki Hirner
d8536765e1 Managed DAVdroid, settings framework, theming, launcher icons, lib update 2017-10-13 12:54:43 +02:00
Ricki Hirner
9036f3e130 Assign UID/file name for empty contact even when creating in read-only address book (so that it can be deleted later) 2017-09-25 14:41:41 +02:00
Ricki Hirner
ab284c80df cert4android: fetch translations from Transifex 2017-09-25 12:08:37 +02:00
Ricki Hirner
89b324b299 Fetch translations from Transifex 2017-09-25 12:07:10 +02:00
Ricki Hirner
a5f86b13bd Version bump to 1.8.1 2017-09-25 12:03:46 +02:00
Ricki Hirner
3cdfc70692 Optimize read-only contact notifications 2017-09-25 12:03:04 +02:00
Ricki Hirner
29eb1fda9f Fetch translations from Transifex 2017-09-23 21:23:15 +02:00
Ricki Hirner
3c00520a09 Notify on read-only address books
* notify when a read-only address book is selected for synchronization
* notify when local changes have been discarded during sync
2017-09-23 21:12:09 +02:00
Ricki Hirner
d20aa562ad Initial implementation of read-only address books 2017-09-23 21:12:09 +02:00
Ricki Hirner
550612a282 Fetch translations from Transifex 2017-09-20 22:15:09 +02:00
Ricki Hirner
ef496f3986 Version bump to 1.8 2017-09-20 22:14:27 +02:00
Ricki Hirner
f4e7fd957d Reminders: use seconds, too 2017-09-20 22:13:45 +02:00
Ricki Hirner
4f6051a03b Fetch translations from Transifex 2017-09-15 13:29:02 +02:00
Ricki Hirner
670dc5ae9b Abbreviate too long log messages to reduce OOM errors 2017-09-15 13:26:03 +02:00
Ricki Hirner
7d6a052e74 Move strings out of global App context 2017-09-15 13:26:01 +02:00
Ricki Hirner
a7f59ded3f Change options for sync intervals
* remove 5 and 10 minutes because Android now enforces a minimum interval of 15 min
  (which makes sense in terms of battery optimization)
* add 30 min
2017-09-14 18:18:32 +02:00
Ricki Hirner
67646e4ce0 ical4j update 2017-09-14 17:29:58 +02:00
Ricki Hirner
d8346fa24a Fetch translations from Transifex 2017-09-13 17:20:51 +02:00
Ricki Hirner
54a41f02a4 Tests 2017-09-13 17:17:18 +02:00
Ricki Hirner
62d989d40a Webcal support in UI 2017-09-13 17:11:12 +02:00
Ricki Hirner
533abe80a1 database support for subscribed calendars (Webcal feeds)
* resource detection: detect/check subscribed calendars
* database: distinguish between CalDAV calendar and subscribed calendar
2017-09-13 17:09:01 +02:00
Ricki Hirner
4c8fa249cd Version bump to 1.7.3 2017-09-10 20:19:01 +02:00
Ricki Hirner
0551140f90 Fetch translations from Transifex 2017-09-10 20:17:10 +02:00
Ricki Hirner
a0bfe7a784 Make SyncManager Closeable 2017-09-10 20:16:08 +02:00
Ricki Hirner
45d6bf1eae Don't allow calendars which don't support VEVENT and/or VTODO to be selected for sync 2017-09-10 14:04:08 +02:00
Ricki Hirner
856f0bdf53 Upgrade to okhttp/3.9.0 2017-09-10 01:31:19 +02:00
Ricki Hirner
27831a0d4d cert4android: don't keep CustomCertManager in memory all the time
* remove static reference to CustomCertManager (and thus a Context)
* HttpClient now wraps OkHttpClient and is Closeable
2017-09-09 22:34:45 +02:00
Ricki Hirner
39fa8a1a14 Update cert4android 2017-09-09 22:25:42 +02:00
Ricki Hirner
13ca1a7ef9 Fetch cert4android translations from Transifex 2017-09-01 15:13:30 +02:00
Ricki Hirner
71804a59bb Fetch translations from Transifex 2017-09-01 15:12:16 +02:00
Ricki Hirner
37b3ee879d Version bump to 1.7.2 2017-09-01 15:11:46 +02:00
Ricki Hirner
57cb80768d donation popup; ProGuard
* don't show donation popup too often
* ProGuard fixes
2017-08-31 01:05:38 +02:00
Ricki Hirner
b6d378d346 Fetch cert4android translations from Transifex 2017-08-30 21:04:49 +02:00
Ricki Hirner
9ff53fbb1e Fetch translations from Transifex 2017-08-30 21:01:55 +02:00
Ricki Hirner
eac976379a Make event color support optional (opt-in) 2017-08-30 20:22:04 +02:00
Ricki Hirner
d444d95f12 Update to Android support library 26.0.1 2017-08-30 12:32:16 +02:00
Ricki Hirner
af61ac38c8 Optimize logging 2017-08-29 18:07:45 +02:00
Ricki Hirner
2e9f1d2d66 Fetch translations from Transifex 2017-08-22 15:16:54 +02:00
Ricki Hirner
2262685465 Version bump to 1.7.1 2017-08-22 15:16:04 +02:00
Ricki Hirner
4df37759d7 Add donation startup fragment again 2017-08-22 14:58:11 +02:00
Ricki Hirner
fd9e626593 ProGuard: keep ThreeTen; update build tools and Kotlin version 2017-08-22 14:44:38 +02:00
Ricki Hirner
9f890a8c8d Fetch translations from Transifex 2017-08-19 12:16:52 +02:00
Ricki Hirner
1e0a146dc4 Version bump to 1.7 2017-08-19 12:16:26 +02:00
Ricki Hirner
8836bf89a8 Fix migration 2017-08-15 16:11:38 +02:00
Ricki Hirner
dcbbc9a545 Fetch translations from Transifex 2017-08-15 15:52:23 +02:00
Ricki Hirner
319845f5b1 Update Kotlin, vcard4android 2017-08-15 15:51:55 +02:00
Ricki Hirner
dd7f1399f3 Handle conflict when renaming accounts 2017-08-10 17:39:39 +02:00
Ricki Hirner
8de6ebbe8f WiFi restriction: allow multiple SSIDs (comma-separated) 2017-08-08 13:28:04 +02:00
Ricki Hirner
82c5d424a7 Add support for event colors 2017-08-07 22:50:19 +02:00
Ricki Hirner
bdb15e0845 Scroll to external log settings when tapping notification 2017-08-05 15:50:50 +02:00
Ricki Hirner
14b5e66192 Version bump to 1.6.5 (160) 2017-08-05 13:30:02 +02:00
Ricki Hirner
636dd1b86a Fetch lib translations from Transifex 2017-08-05 13:15:10 +02:00
Ricki Hirner
152624a984 Fetch translations from Transifex 2017-08-05 13:13:25 +02:00
Ricki Hirner
3b9e95d4e3 Minor optimizations 2017-08-05 13:12:47 +02:00
Ricki Hirner
e64a5f408c Fix DavResourceFinder 2017-08-03 23:51:39 +02:00
Ricki Hirner
09e53e898d Fix device rotate crash bug, cert4android race condition 2017-08-03 20:47:28 +02:00
Ricki Hirner
d0f879820b Fix custom certificate socket factory 2017-08-02 17:58:57 +02:00
Ricki Hirner
02f9843d47 Remove TextUtils dependency; tests 2017-08-02 17:31:51 +02:00
Ricki Hirner
1c303e43eb Tests 2017-08-02 16:35:28 +02:00
Ricki Hirner
cfc7f96f83 Rewrite last UI classes to Kotlin; allow cancellation of resource detection 2017-08-02 16:28:59 +02:00
Ricki Hirner
47c1e0b8ea Rewrite most UI classes to Kotlin 2017-08-01 15:00:06 +02:00
Ricki Hirner
c5958de17b Fix addAccount authenticator crash 2017-07-31 17:48:18 +02:00
Ricki Hirner
b890c73d04 Rewrite some UI classes to Kotlin 2017-07-31 17:38:28 +02:00
Ricki Hirner
13b749974b Rewrite to Kotlin
* rewrite some UI classes
* move logic from App to CustomCertificates and Logger singletons
2017-07-31 15:55:01 +02:00
Ricki Hirner
f65ac7275e Allow to remove all WiFi SSID restrictions when "sync only on WiFi" is active 2017-07-30 14:55:49 +02:00
Ricki Hirner
0210ba0ad9 cert4android: fetch translations from Transifex 2017-07-27 16:51:08 +02:00
Ricki Hirner
d5a9a65209 Fetch translations from Transifex 2017-07-27 16:49:22 +02:00
Ricki Hirner
b16c7679b8 Version bump to 1.6.4 2017-07-27 16:48:06 +02:00
Ricki Hirner
c4ecc28d66 cert4android, vcard4android updates 2017-07-27 16:47:52 +02:00
Ricki Hirner
b638c2584b Improve debug info 2017-07-27 16:47:52 +02:00
Ricki Hirner
4be6a25eb6 Some minor fixes 2017-07-20 17:26:50 +02:00
Ricki Hirner
797bb78d20 Rewrite LoginCredentials to Kotlin 2017-07-19 22:05:58 +02:00
Ricki Hirner
ee696d233e Fix: SSLSocket doesn't implement Closeable in Android 4.1 2017-07-19 19:49:49 +02:00
Ricki Hirner
a1d2b2ab59 Update copyright 2017-07-19 19:32:46 +02:00
Ricki Hirner
4cb60ca78c Rewrite syncadapter package to Kotlin 2017-07-19 19:11:02 +02:00
Ricki Hirner
e2c47bbe92 Rewrite all remaining classes except syncadapter and ui package 2017-07-16 15:30:58 +02:00
Ricki Hirner
53e333ee22 Rewrite DavService to Kotlin 2017-07-16 01:07:58 +02:00
Ricki Hirner
a3181d35b4 Rewrite resource package to Kotlin 2017-07-14 20:58:29 +02:00
Ricki Hirner
4948471068 Rewrite some classes to Kotlin 2017-07-10 21:09:15 +02:00
Ricki Hirner
ed1acb47c7 Rewrite model package to Kotlin 2017-07-10 01:31:45 +02:00
Ricki Hirner
c73d47b1ec Rewrite log package to Kotlin 2017-07-08 14:31:23 +02:00
Ricki Hirner
b306933f95 Use x86 emulator 2017-07-07 00:14:05 +02:00
Ricki Hirner
dba55d2f2a Fetch translations from Transifex 2017-07-06 12:10:08 +02:00
Ricki Hirner
d20f0986d9 Version bump to 1.6.3 2017-07-06 12:07:27 +02:00
Ricki Hirner
1bc6880dc0 More logging (ical4j) + ProGuard 2017-07-06 12:06:54 +02:00
Ricki Hirner
8d0e4c6a46 Some fixes
* don't acquire contacts provider client for address book management (would require contacts permission check)
* rename task lists: handle all kinds of Exception
* lib updates
2017-07-04 15:01:06 +02:00
Ricki Hirner
1da8543e24 Minor bug/NPE fixes 2017-07-04 15:01:06 +02:00
Ricki Hirner
29792d915a Fetch translations from Transifex 2017-07-03 18:54:12 +02:00
Ricki Hirner
9654a23fd1 Require SDK level 16
In Android < 4.1 (< level 16), android.database.Cursor doesn't implement Closeable,
although it provides close(). Since we now use the Kotlin .use() idiom (which requires
a Closeable) instead of calling close() manually or via Lombok, we need Closeable
Cursors and it's not worth to work around this because Android 4.0.4 is now very old.
2017-07-03 18:46:56 +02:00
Ricki Hirner
4f8031a3a4 Improve preview release startup fragment 2017-07-02 14:42:01 +02:00
Ricki Hirner
eb6fa1ab7f Contacts: use hashCode() hack only for Android 7; problem has been fixed by Android 8 2017-07-02 14:08:04 +02:00
Ricki Hirner
29bf7ad204 vcard4android: Kotlin 2017-06-25 16:06:15 +02:00
Ricki Hirner
18e2a07415 Update gradle plugin 2017-06-21 13:00:08 +02:00
Ricki Hirner
49d1011b4a SDK/build tools level 26 2017-06-21 00:03:44 +02:00
Ricki Hirner
9b6c9b6c03 Version bump to 1.6.2 2017-06-20 15:18:16 +02:00
Ricki Hirner
953ffa6afe Update libs; update Commons Lang to 3.6 2017-06-20 15:17:30 +02:00
Ricki Hirner
e0a0f92c1e Lib updates, okhttp/3.8.1 2017-06-20 15:17:30 +02:00
Ricki Hirner
ce493865e7 ical4android: Kotlin 2017-06-20 15:17:30 +02:00
Ricki Hirner
77c9126be4 Version bump to 1.6.1.1 (151) 2017-06-16 18:57:06 +02:00
Ricki Hirner
9bb56d4f58 Lib updates 2017-06-16 17:54:41 +02:00
Ricki Hirner
908a3be678 Fetch translations from Transifex 2017-06-09 16:33:38 +02:00
Ricki Hirner
75dbbbf4ed Version bump to 1.6.1 2017-06-09 16:33:06 +02:00
Ricki Hirner
2599a15515 dav4android: Kotlin, cert4android: handle InterruptedException 2017-06-09 00:41:34 +02:00
Ricki Hirner
15fe102049 Birthdays without years, again 2017-06-05 14:35:54 +02:00
Ricki Hirner
6f55e51fef Fetch translations from Transifex 2017-06-03 19:27:49 +02:00
Ricki Hirner
1bdb004daf Version bump to 1.6 2017-06-03 19:27:06 +02:00
Ricki Hirner
30e83f45a5 Improve resource detection
* scan unrequested responses for useful auto-detection information
* cert4android: Kotlin
2017-06-03 19:26:43 +02:00
Ricki Hirner
7c8a8ee9fc Support for birthdays without year 2017-06-02 00:38:23 +02:00
Ricki Hirner
8747deeea7 Remove "ical4android" from iCal PRODID (same format as for VCard) 2017-05-14 13:36:49 +02:00
Ricki Hirner
63d2a7d0ef okhttp 3.8.0 ProGuard settings 2017-05-14 13:01:13 +02:00
Ricki Hirner
d89e0a392f Fetch translations from Transifex 2017-05-14 12:18:50 +02:00
Ricki Hirner
bb6ce6a355 Version bump to 1.5.2 2017-05-14 12:18:02 +02:00
Ricki Hirner
f59fd7205d Remove "vcard4android" from VCard PRODID to avoid folding for better compatibility 2017-05-14 12:11:58 +02:00
Ricki Hirner
0dd1fa47b8 Upgrade to okhttp/3.8.0 2017-05-14 11:33:42 +02:00
Ricki Hirner
a067ff03b3 Use UUIDs for newly generated event/task UIDs (RFC 7986 5.3 UID Property) 2017-04-27 13:36:10 +02:00
Ricki Hirner
a652a230c6 Fetch translations from Transifex 2017-04-25 14:11:47 +02:00
Ricki Hirner
c2a616fe52 Version bump to 1.5.1-ose 2017-04-25 14:11:22 +02:00
Ricki Hirner
1ac573d403 Use untranslated User-Agent string
* refactoring: don't use global string variables
2017-04-24 23:00:11 +02:00
Ricki Hirner
80a8ce6f8c Check sync conditions for contacts sync, too 2017-04-23 18:10:01 +02:00
Ricki Hirner
33e45d3ae9 Remove unnecessary InvalidAccountException 2017-04-22 21:37:19 +02:00
Ricki Hirner
f701a63e27 Allow null values for IS_ORGANIZER 2017-04-18 23:54:11 +02:00
Ricki Hirner
5594d2b284 README updates 2017-04-16 21:58:30 +02:00
Ricki Hirner
dcd99f7bca Version bump to 1.5.0.3 2017-04-16 15:51:51 +02:00
Ricki Hirner
df086e7ab5 Fetch translations from Transifex 2017-04-16 15:49:02 +02:00
Ricki Hirner
5b31d06dcf Upgrade libraries 2017-04-16 14:31:50 +02:00
Ricki Hirner
a39564a179 Unify strings
* HTTP client: use app name as User-Agent
* use string resources for homepage URLs
2017-04-11 16:35:29 +02:00
Ricki Hirner
18d8162074 gradle, dav4android updates
* update Android gradle plugin to 2.3.1
* dav4android update: follow redirects on DELETE
2017-04-08 20:26:38 +02:00
Ricki Hirner
27a4c47c35 Version bump to 1.5.0.2 2017-04-02 19:23:38 +02:00
Ricki Hirner
1bed502e67 Fix some inconsistencies 2017-04-02 19:19:38 +02:00
Ricki Hirner
d2dd27f99a Open DAVdroid main activity when add a "DAVdroid Address book" account is added over Settings 2017-03-30 21:23:29 +02:00
Ricki Hirner
2d83ed6be4 Account settings: restart loader after sync interval update
* debug info: add signature
2017-03-29 12:39:59 +02:00
Ricki Hirner
de608312a2 Hotfix: don't crash on empty address book displayName 2017-03-27 11:43:14 +02:00
Ricki Hirner
0b1bd35517 Version bump to 1.5 2017-03-26 19:30:16 +02:00
Ricki Hirner
44fb3713a4 Fetch translations from Transifex 2017-03-26 19:30:13 +02:00
Ricki Hirner
ce2e7e24b1 Remove all references to Constants.ACCOUNT_TYPE (now a to string resource) 2017-03-26 19:13:51 +02:00
Ricki Hirner
fa574aaa8d Improve address book details in debug info 2017-03-26 19:07:26 +02:00
Ricki Hirner
82aa7012d3 Add more debug information
* power saving status
* permissions
* address book accounts
2017-03-25 20:13:07 +01:00
Ricki Hirner
0789134948 Enable SSL_RSA_WITH_3DES_EDE_CBC_SHA for all Android versions
* refactor cipher selection
2017-03-25 20:13:07 +01:00
Ricki Hirner
d4bd96d846 Version bump to 1.5-beta1 2017-03-19 20:44:07 +01:00
Ricki Hirner
038b106b37 Multiple address books, segment 2
* migration from old address book to new address book accounts (5 -> 6)
* support renaming of address book accounts
* change address book accounts properly when the main account is renamed
2017-03-19 20:36:06 +01:00
Ricki Hirner
5a7aeb3eba Support for multiple address books per account
* new account type: "DAVdroid address book", which is assigned to a DAVdroid main account
* stub content provider "Address books", which enumerates all DAVdroid address book accounts and runs contacts sync for them
2017-03-19 20:28:13 +01:00
Ricki Hirner
a33e70162c Unify action bar icon colors 2017-03-12 13:47:52 +01:00
Ricki Hirner
3245b47635 Fix maven 2017-03-03 13:25:51 +01:00
Ricki Hirner
7b965769c9 Update translations from Transifex 2017-03-03 12:52:51 +01:00
Ricki Hirner
91f62614ab Update gradle 2017-03-03 12:51:12 +01:00
Ricki Hirner
c875b4b3ae Version bump to 1.4.1 2017-03-02 23:44:43 +01:00
Ricki Hirner
286ceeea3f Fetch translations from Transifex 2017-03-02 23:44:43 +01:00
Ricki Hirner
8b026d7e16 AccountsActivity: show message when global sync is disabled 2017-03-02 23:44:39 +01:00
Ricki Hirner
2f97830f28 Gradle update to 3.4 2017-02-27 17:39:22 +01:00
Ricki Hirner
75827d89c5 Fetch translations from Transifex 2017-02-17 13:55:08 +01:00
Ricki Hirner
1899c86494 Version bump to 1.4.0.3 2017-02-17 13:54:09 +01:00
Ricki Hirner
4a7b6cf546 Don't use uid2445 column on Android <4.2; alarm ACTION: compare only value (ignore parameters) 2017-02-17 09:55:50 +01:00
Ricki Hirner
0b83c1ad2f Version bump to 1.4.0.2 2017-02-12 19:01:43 +01:00
Ricki Hirner
a9380a8e1b Improve Android 7 workaround behavior in combination with CATEGORIES/VCard4 contact groups 2017-02-12 18:59:41 +01:00
Ricki Hirner
b756a5f54c Retain Events.UID_2445 when preparing events for upload
* move file name/UID generation from SyncManager to LocalContact, LocalEvent, LocalTask
* rename updateFileNameAndUID() to prepareForUpload()
* use random UUID for contacts, UidGenerator with Android device ID for events/tasks
* LocalEvent.prepareForUpload(): use existing UID_2445 if available
2017-02-10 18:06:28 +01:00
Ricki Hirner
762c379c54 Android 7 workaround: update hash after group membership operations 2017-02-10 18:06:28 +01:00
Ricki Hirner
8f8421e65e Version bump to 1.4.0.1 2017-02-06 11:59:06 +01:00
Ricki Hirner
0e67da5718 Android 7 workaround bugfix
* use local version of contact before calculating hash code
* don't stop upload sync if there are deleted contacts
2017-02-06 11:57:51 +01:00
Ricki Hirner
cf8b1e97bd Version bump to 1.4.0 2017-02-05 17:21:09 +01:00
Ricki Hirner
61885b41d9 Use contact hash codes only on Android 7+ (workaround)
vcard4android: don't hash CATEGORIES, more verbose logging
2017-02-05 17:20:49 +01:00
Ricki Hirner
f74cecdc1f Implement checksum to check whether DIRTY contacts have "really" changed
* contact data hash code = hash code of data fields and group memberships
* Before every contact sync, all dirty contacts are checked whether they're
  "really dirty" (= data hash code has changed). If they're not, the DIRTY
  flag is reset. Works around Android 7 behavior of setting contacts to DIRTY
  even if onky meta data has been updated (for instance, lastContacted after
  a call or SMS),
* When an "upload" sync is initiated by notifyChange and there are no
  "really dirty" contacts, the sync is ignored.
* contact upload: clearDirty() saves hash code, too
* contact download: create()/update() saves hash code, too
* debugging: sync flags (extras) are now logged
2017-02-01 19:19:17 +01:00
Ricki Hirner
d8ffd83e1e AccountSettingsActivity: use loader
* use Loader for AccountSettingsActivity sync intervals (fixes Android 7 display "issues")
* SyncManager: allow prepare() to skip synchronization
2017-02-01 01:20:44 +01:00
Ricki Hirner
666233136d Upgrade to okhttp 3.6.0 2017-01-30 22:27:54 +01:00
Ricki Hirner
eae5631d8a Version bump to 1.3.8 2017-01-29 20:11:15 +01:00
Ricki Hirner
dc614bf733 Fetch translations from Transifex (fixes crash in Spanish version) 2017-01-29 19:18:57 +01:00
Ricki Hirner
22c9603b3c Add information about current local and remote resource to debug info 2017-01-29 19:17:53 +01:00
Ricki Hirner
073f9230ba Upgrade to gradle 3.3 2017-01-29 19:17:53 +01:00
Ricki Hirner
3948e83f83 Use isAlwaysSyncable for contacts/calendars again because of buggy Android firmwares 2017-01-09 21:28:28 +01:00
Ricki Hirner
12664652e5 Version bump to 1.3.7 2017-01-08 23:35:41 +01:00
Ricki Hirner
d1162abf4c Change authentication restriction to domains instead of host names 2017-01-08 19:09:34 +01:00
Ricki Hirner
a93fa72d5a Delete local contacts when no CardDAV collection is selected 2017-01-06 15:53:23 +01:00
Ricki Hirner
3c8c3fe9b8 Version bump to 1.3.6 2017-01-01 12:43:47 +01:00
Ricki Hirner
a46f2d118c Fetch translations from Transifex 2017-01-01 12:26:02 +01:00
Ricki Hirner
dc0221dabb Update to ez-vcard 0.10.1
* fix REV and PREF problems
2017-01-01 01:13:03 +01:00
Ricki Hirner
a5e80d2268 Fix permissions notification
* ical4android: remove ORGANIZER from all VEVENT components if there are not attendees
2016-12-31 14:16:18 +01:00
Ricki Hirner
dbd200cb6c AccountSettings version 5: enable/disable OpenTasks by availability (Android 7.1.1 fix)
* better handling of setIsSyncable
2016-12-30 14:29:56 +01:00
Ricki Hirner
aacff632b7 Don't show warning on AccountSettings version updates 2016-12-30 14:28:28 +01:00
Ricki Hirner
e4861b0a68 Update to SDK level 25 2016-12-30 02:58:54 +01:00
Ricki Hirner
8d1b6da197 Change handling of tasks sync when OpenTasks is not installed
* AccountDetailsFragment: at account creation, enable task sync only when OpenTasks is installed
* PackageChangedReceiver: when packages are (un)installed, check for OpenTasks availability and (de)activate task sync for all accounts accordingly
* LocalTaskList: don't cache OpenTasks availability
* sync_*.xml: don't activate sync by default
2016-12-28 22:23:13 +01:00
Ricki Hirner
bf21c1338e Fetch translations from Transifex 2016-12-23 15:54:20 +01:00
Ricki Hirner
9452d6667a Version bump to 1.3.5 2016-12-23 15:51:56 +01:00
Ricki Hirner
6967773690 Address book selection changed: update URL as soon as possible 2016-12-19 18:58:35 +01:00
Ricki Hirner
73b95379fe Update gradle to 3.2.1; ical4android/vcard4android updates 2016-12-18 22:02:44 +01:00
Ricki Hirner
9f89173638 Update okhttp to 3.5.0 2016-12-02 15:01:52 +01:00
Ricki Hirner
77a0201327 Log group assignments more verbosely 2016-11-25 21:40:40 +01:00
Ricki Hirner
21c3f81a8b Rename account: don't crash when content providers are not accessible 2016-11-17 19:59:23 +01:00
Ricki Hirner
035dc4f0b2 Version bump to 1.3.4.1 2016-11-14 18:48:26 +01:00
Ricki Hirner
48fec240cf Fetch translations from Transifex 2016-11-14 18:41:48 +01:00
Ricki Hirner
82a6faa8de Avoid some crashes
* check whether ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATION can be resolved before launching it
* cert4android: don't crash when service can't be bound
2016-11-14 18:39:25 +01:00
Ricki Hirner
a4bd6d6a07 Allow renaming of accounts
* allow renaming of accounts
* always open AccountActivity, even if there are no services (so that users can delete the account from within DAVdroid)
2016-11-14 01:14:46 +01:00
Ricki Hirner
b91743239a Fetch translations from Transifex 2016-11-13 20:34:11 +01:00
Ricki Hirner
611dff15bb Update build tools to 25.0.0, fix WiFiManager leak 2016-11-13 20:22:10 +01:00
Ricki Hirner
cdd56d88fb Fetch translations from Transifex
ical4android: fix for events without dtend/duration
2016-11-06 17:36:59 +01:00
Ricki Hirner
198c53698d Fetch translations from Transifex 2016-11-04 12:02:08 +01:00
Ricki Hirner
3924b72a40 Version bump to 1.3.4
* library updates
2016-11-04 12:01:24 +01:00
Ricki Hirner
6520a4dff8 Add app-wide HTTP proxy setting 2016-10-30 22:21:54 +01:00
Ricki Hirner
9e62c44bef Debug info: send inline on Android <4.1 and when creating an attachment doesn't work 2016-10-22 02:05:58 +02:00
Ricki Hirner
9df01a7477 Version bump to 1.3.3.1 2016-10-21 19:52:55 +02:00
Ricki Hirner
c62eb6cd0e Fetch translations from Transifex 2016-10-21 19:50:09 +02:00
Ricki Hirner
8465df483e Library updates
* dav4android: disable compression for GET requests because it may change the ETag
* better logging for ical4j messages
* tests
2016-10-21 11:08:50 +02:00
Ricki Hirner
2366356922 ProGuard update; signing config 2016-10-18 12:36:41 +02:00
Ricki Hirner
5655aad59a Use string resource for logging file provider authority; vcard4android update 2016-10-17 23:57:53 +02:00
Ricki Hirner
65ddab6ed5 Share debug info: always use attachment
* share debug info: always use attachment (before: send inline if it was small enough)
* use FileProvider for debug info attachment (for Android 7 compatibility)
* dav4android, ical4android fixes
2016-10-17 17:47:27 +02:00
Ricki Hirner
d914addbb2 Add useless ProGuard rule 2016-10-15 16:51:35 +02:00
Ricki Hirner
b9a4489f3c Fetch translations from Transifex 2016-10-14 21:19:39 +02:00
Ricki Hirner
9d3b9c963a Version bump to 1.3.3 2016-10-14 21:00:29 +02:00
Ricki Hirner
72d981f361 dav4android: always use UTF-8 for Basic/Digest auth credentials 2016-10-13 15:23:11 +02:00
Ricki Hirner
6eeb484c56 ical4android: ignore invalid DUE < DTSTART for tasks 2016-10-12 17:03:46 +02:00
Ricki Hirner
cd7332b67c Remove VCard RFC6868 setting (always enabled now; setting not needed for Posteo compatibility anymore) 2016-10-12 16:45:26 +02:00
Ricki Hirner
1a8ff69750 Gitlab CI: install OpenTasks before tests 2016-10-12 13:04:21 +02:00
Ricki Hirner
991221f0ee vcard4android: ez-vcard 0.10.0 2016-10-11 23:36:30 +02:00
Ricki Hirner
ef4267e173 Test adaptions 2016-10-11 00:28:22 +02:00
Ricki Hirner
ba029e3da4 Switch to JUnit4 2016-10-10 21:03:18 +02:00
Ricki Hirner
4270807e2a Add Gitlab CI 2016-10-10 20:18:37 +02:00
Ricki Hirner
8456d078f1 Improve tests 2016-10-07 14:39:21 +02:00
Ricki Hirner
aa693cebb9 Fix NPE in "is refreshing progress bar" 2016-10-07 14:39:18 +02:00
Ricki Hirner
219a7ed38f Version bump to 1.3.2.2 2016-10-05 11:14:41 +02:00
Ricki Hirner
2aa90b4f9c Enable verbose logging of allow loggers (for instance, okhttp) / dav4android update 2016-10-04 23:42:03 +02:00
Ricki Hirner
26f9352411 Android 4.0/4.1 fixes
* require API level 15 for TransactionTooLargeException
* use SQLite WAL only on API level 16+
* various database access, provider access and UI fixes
2016-10-04 16:23:23 +02:00
Ricki Hirner
279c51c4a6 Version bump to 1.3.2 2016-10-03 20:57:14 +02:00
Ricki Hirner
98f4dd4c87 Fetch translations from Transifex 2016-10-03 20:43:01 +02:00
Ricki Hirner
5e1cb21d55 Avoid "no transaction" exception 2016-10-03 20:11:56 +02:00
Ricki Hirner
aa6e43cc94 Minimal layout change 2016-10-03 12:13:59 +02:00
Ricki Hirner
992e83fdb1 Show progress bar when synchronization is active 2016-09-26 23:07:35 +02:00
Ricki Hirner
cb73c1ebf7 Increase SEQUENCE only when we're ORGANIZER 2016-09-26 23:07:35 +02:00
Ricki Hirner
09d8f63515 Query/use CalDAV email address as account name, if available 2016-09-26 23:07:35 +02:00
Ricki Hirner
4f4cf25027 Always increase SEQUENCE 2016-09-26 23:07:35 +02:00
Ricki Hirner
de4e81977a lint: don't keep references to Context in static fields 2016-09-26 23:07:35 +02:00
Ricki Hirner
10bbfcabcb Version bump to 1.3.1
* some cert4android tests
2016-09-18 17:39:07 +02:00
Ricki Hirner
b5d295e57d Import strings from Transifex 2016-09-18 16:50:27 +02:00
Ricki Hirner
49555c34d8 Always use PROPFIND instead of REPORT addressbook-query 2016-09-18 16:43:11 +02:00
Ricki Hirner
9b6903572a README changes 2016-09-02 12:22:45 +02:00
Ricki Hirner
f16d64d636 Fetch translations from Transifex 2016-09-02 12:13:22 +02:00
Ricki Hirner
bceb5b643a lint optimizations
* permissions: declare AUTHENTICATE_ACCOUNTS, GET_ACCOUNTS and MANAGE_ACCOUNTS only until SDK level 22
* minor optimizations and bug fixes
2016-09-02 12:02:42 +02:00
Ricki Hirner
7e783feecf Version bump to 1.3
* vcard4android: fix bug concerning generated formatted postal addresses
2016-09-02 00:55:44 +02:00
Ricki Hirner
f9040f65fe New launcher logo 2016-09-02 00:55:39 +02:00
Ricki Hirner
b73a825ac9 Use cert4android instead of MemorizingTrustManager
* use cert4android instead of MemorizingTrustManager
* new app setting: distrust system certificates
* add network security config to manifest so that user-installed CAs will be accepted in Android 7 again
* update gradle
2016-09-02 00:38:02 +02:00
Ricki Hirner
67104b96fd Accept intent extras for LoginActivity 2016-08-13 23:14:33 +02:00
Ricki Hirner
e2c8fc9662 Fetch translations from Transifex 2016-08-06 00:13:16 +02:00
Ricki Hirner
40e46b721f Fix OpenTasks regression bug
* version bump to 1.2.3
* enable OpenTasks sync on Android <6 again
2016-08-05 23:32:03 +02:00
Ricki Hirner
30e6f44e44 Improve HTTP authentication
* use preemptive Basic auth automatically for HTTPS connections
* cache auth parameters (Basic/Digest)
2016-08-05 23:20:19 +02:00
Ricki Hirner
fd06c00fca Fetch translations from Transifex 2016-08-02 19:30:20 +02:00
Ricki Hirner
307c234f4a Request ignoring battery optimization
* startup dialog: request to ignore battery optimizations
* remove F-Droid donation startup dialog (only useful for davdroid-ose)
* version bump to 1.2.2
2016-08-02 19:30:15 +02:00
Ricki Hirner
d6dda6fbeb Avoid sync error when OpenTasks is not installed 2016-08-01 21:54:56 +02:00
Ricki Hirner
b4cb8234f1 Clean up launcher icon
* clean up launcher icon
* update dependencies
2016-08-01 21:15:55 +02:00
Ricki Hirner
a5fb9fd214 Allow large transactions
* version bump to 1.2.1-ose
* upgrade to okhttp 3.4.1
* ical4android/vcard4android: split oversized transactions
2016-07-27 14:33:06 +02:00
Ricki Hirner
3bbbba15b3 Remove gplay flavour to keep DAVdroid-OSE repo clean
* Remove gplay flavour to keep DAVdroid-OSE repo clean
* update Android gradle plugin to 2.1.2
2016-07-11 13:45:27 +02:00
370 changed files with 17170 additions and 9830 deletions

31
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,31 @@
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
.gitmodules vendored
View File

@@ -1,12 +1,9 @@
[submodule "dav4android"]
path = dav4android
url = ../dav4android.git
[submodule "ical4android"]
path = ical4android
url = ../ical4android.git
url = https://gitlab.com/bitfireAT/ical4android.git
[submodule "vcard4android"]
path = vcard4android
url = ../vcard4android.git
url = https://gitlab.com/bitfireAT/vcard4android.git
[submodule "cert4android"]
path = cert4android
url = ../cert4android.git
url = https://gitlab.com/bitfireAT/cert4android.git

47
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,47 @@
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:
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).
Questions, discussion
=====================
We're happy to see questions, discussions etc. in the
[DAVx⁵ development forum](https://www.davx5.com/forums)!
Licensing
=========
All code has to be licensed under the GPL.
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).
Please find more about this in the Contributor's License Agreement (CLA)
we'll send to you if you want to contribute.

37
README.md Normal file
View File

@@ -0,0 +1,37 @@
![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⁵.
DAVx⁵ is licensed under the [GPLv3 License](LICENSE).
News and updates: [@davx5app](https://twitter.com/davx5app) on Twitter
Help, discussion, feature requests, bug reports and "issues": [DAVx⁵ forums](https://www.davx5.com/forums)
**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/
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
USED THIRD-PARTY LIBRARIES
==========================
Those libraries 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)
* [okhttp](https://square.github.io/okhttp) [Apache License, Version 2.0](https://square.github.io/okhttp/#license)

View File

@@ -1,9 +0,0 @@
flavor directory
gplay main + davdroid + gplay
icloud main + icloud
managed main + managed
soldupe main + soldupe
standard main + davdroid

View File

@@ -7,161 +7,143 @@
*/
apply plugin: 'com.android.application'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'org.jetbrains.dokka-android'
ext {
baseVersionName = '2.0'
}
apply plugin: 'kotlin-kapt'
apply plugin: 'org.jetbrains.dokka'
android {
compileSdkVersion 27
buildToolsVersion '28.0.1'
compileSdkVersion 29
buildToolsVersion '29.0.2'
defaultConfig {
applicationId "at.bitfire.davdroid"
resValue "string", "packageID", applicationId
versionCode 241
versionCode 302000002
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
minSdkVersion 19 // Android 4.4
targetSdkVersion 27 // Android 8.1
minSdkVersion 21 // Android 5.0
targetSdkVersion 29 // Android 10.0
buildConfigField "boolean", "customCerts", "false"
buildConfigField "boolean", "customCertsUI", "true"
buildConfigField "String", "userAgent", "\"DAVx5\""
// when using this, make sure that notification icons are real bitmaps
vectorDrawables.useSupportLibrary = true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
flavorDimensions "type"
compileOptions {
// enable because ical4android requires desugaring
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures.dataBinding = true
flavorDimensions "distribution"
productFlavors {
standard {
dimension "type"
versionName baseVersionName
buildConfigField "boolean", "customCerts", "true"
}
managed {
dimension "type"
versionName "$baseVersionName-mgd"
applicationId "com.davdroid.managed"
resValue "string", "packageID", applicationId
buildConfigField "boolean", "customCerts", "true"
buildConfigField "boolean", "customCertsUI", "false"
minSdkVersion 21
}
gplay {
dimension "type"
versionName "$baseVersionName-gplay"
buildConfigField "boolean", "customCerts", "true"
}
icloud {
dimension "type"
versionName "$baseVersionName-icloud"
applicationId "at.bitfire.cloudsync"
resValue "string", "packageID", applicationId
}
soldupe {
dimension "type"
versionName "$baseVersionName-soldupe"
applicationId "com.soldupe.cloudsync"
resValue "string", "packageID", applicationId
minSdkVersion 21
}
}
sourceSets {
standard.java.srcDirs = [ "src/davdroid/java" ]
standard.res.srcDirs = [ "src/davdroid/res" ]
gplay.java.srcDirs = [ "src/gplay/java", "src/davdroid/java" ]
gplay.res.srcDirs = [ "src/gplay/res", "src/davdroid/res" ]
androidTest.java.srcDirs = [ "src/androidTest/java", "src/espressoTest/java" ]
}
signingConfigs {
bitfire {
storeFile file("${System.env.HOME}/Entwicklung/GooglePlay/bitfire.jks")
storePassword '***REMOVED***'
keyAlias 'bitfire'
keyPassword '***REMOVED***'
}
soldupe {
storeFile file("${System.env.HOME}/Entwicklung/GooglePlay/soldupe.jks")
storePassword 'hei8eePh'
keyAlias 'soldupe'
keyPassword 'ocaip6oZ'
versionName "3.2-beta3-ose"
}
}
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'
signingConfig signingConfigs.bitfire
productFlavors.soldupe.signingConfig signingConfigs.soldupe
shrinkResources true
}
}
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 "OnClick" // doesn't recognize Kotlin onClick methods
disable 'RtlEnabled'
disable 'RtlHardcoded'
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', 'RtlHardcoded' // RTL not supported yet
disable 'Typos'
}
packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/LICENSE'
}
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
dokka.configuration {
sourceLink {
url = "https://gitlab.com/bitfireAT/davx5-ose/tree/master-ose/"
lineSuffix = "#L"
}
jdkVersion = 8
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")
}
}
}
dependencies {
implementation project(':cert4android')
implementation project(':dav4android')
implementation project(':ical4android')
implementation project(':vcard4android')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:preference-v14:27.1.1'
implementation 'androidx.appcompat:appcompat:1.2.0-rc01'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7'
implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.fragment:fragment-ktx:1.2.5'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.google.android:flexbox:1.1.0'
implementation 'com.google.android.material:material:1.2.0-beta01'
implementation 'com.github.yukuku:ambilwarna:2.0.1'
implementation 'com.mikepenz:aboutlibraries:6.0.9'
def room_version = '2.2.5'
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
implementation 'com.jaredrummler:colorpicker:1.1.0'
implementation "com.github.AppIntro:AppIntro:${versions.appIntro}"
implementation "com.gitlab.bitfireAT:dav4jvm:${versions.dav4jvm}"
implementation "com.mikepenz:aboutlibraries:${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'
implementation 'dnsjava:dnsjava:2.1.8'
implementation 'org.apache.commons:commons-lang3:3.7'
implementation 'org.apache.commons:commons-collections4:4.1'
//noinspection GradleDependency - dnsjava 3+ needs Java 8/Android 7
implementation 'dnsjava:dnsjava:2.1.9'
implementation 'org.apache.commons:commons-collections4:4.4'
//noinspection GradleDependency - commons-lang 3.10+ needs Java 8/Android 7
implementation 'org.apache.commons:commons-lang3:3.9'
implementation 'org.apache.commons:commons-text:1.8'
// for tests
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'junit:junit:4.12'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.11.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'junit:junit:4.13'
androidTestImplementation "com.squareup.okhttp3:mockwebserver:${versions.okhttp}"
testImplementation 'junit:junit:4.12'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.11.0'
testImplementation 'junit:junit:4.13'
testImplementation "com.squareup.okhttp3:mockwebserver:${versions.okhttp}"
}

View File

@@ -0,0 +1,28 @@
# 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
# okhttp
-keepclassmembers class okhttp3.internal.Util.** { *; }
# 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.** { *; }
# 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);
}

View File

@@ -1,44 +0,0 @@
# ProGuard usage for DAVdroid:
# shrinking yes (main reason for using ProGuard)
# optimization yes
# obfuscation no (DAVdroid is open-source)
# preverification no
-dontobfuscate
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification
-dontpreverify
# Kotlin
-dontwarn kotlin.**
# 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 okio.**
-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault
-dontwarn org.conscrypt.**
# dnsjava
-dontwarn sun.net.spi.nameservice.** # not available on Android
# DAVdroid + libs
-keep class at.bitfire.** { *; } # all DAVdroid code is required

View File

@@ -1,128 +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.support.test.InstrumentationRegistry.getInstrumentation
import at.bitfire.cert4android.CustomCertManager
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.mockwebserver.MockWebServer
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.ArrayUtils.contains
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import java.net.Socket
import java.security.KeyFactory
import java.security.KeyStore
import java.security.Principal
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.PKCS8EncodedKeySpec
import javax.net.ssl.SSLSocket
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509ExtendedKeyManager
import javax.net.ssl.X509TrustManager
class CustomTlsSocketFactoryTest {
private lateinit var certMgr: CustomCertManager
private lateinit var factory: CustomTlsSocketFactory
private val server = MockWebServer()
@Before
fun startServer() {
certMgr = CustomCertManager(getInstrumentation().context, false, true)
factory = CustomTlsSocketFactory(null, certMgr)
server.start()
}
@After
fun stopServer() {
server.shutdown()
certMgr.close()
}
@Test
fun testSendClientCertificate() {
var public: X509Certificate? = null
javaClass.classLoader.getResourceAsStream("sample.crt").use {
public = CertificateFactory.getInstance("X509").generateCertificate(it) as? X509Certificate
}
assertNotNull(public)
val keyFactory = KeyFactory.getInstance("RSA")
val private = keyFactory.generatePrivate(PKCS8EncodedKeySpec(readResource("sample.key")))
assertNotNull(private)
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val alias = "sample"
keyStore.setKeyEntry(alias, private, null, arrayOf(public))
assertTrue(keyStore.containsAlias(alias))
val trustManagerFactory = TrustManagerFactory.getInstance("X509")
trustManagerFactory.init(null as KeyStore?)
val trustManager = trustManagerFactory.trustManagers.first() as X509TrustManager
val factory = CustomTlsSocketFactory(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?) =
arrayOf(public).takeIf { forAlias == alias }
override fun getPrivateKey(forAlias: String?) =
private.takeIf { forAlias == alias }
}, trustManager)
/* known client cert test URLs (thanks!):
* - https://prod.idrix.eu/secure/
* - https://server.cryptomix.com/secure/
*/
val client = OkHttpClient.Builder()
.sslSocketFactory(factory, trustManager)
.build()
client.newCall(Request.Builder()
.get()
.url("https://prod.idrix.eu/secure/")
.build()).execute().use { response ->
assertTrue(response.isSuccessful)
assertTrue(response.body()!!.string().contains("CN=User Cert,O=Internet Widgits Pty Ltd,ST=Some-State,C=CA"))
}
}
@Test
fun testUpgradeTLS() {
val s = factory.createSocket(server.hostName, server.port)
assertTrue(s is SSLSocket)
val ssl = s as SSLSocket
assertFalse(contains(ssl.enabledProtocols, "SSLv3"))
assertTrue(contains(ssl.enabledProtocols, "TLSv1"))
assertTrue(contains(ssl.enabledProtocols, "TLSv1.1"))
assertTrue(contains(ssl.enabledProtocols, "TLSv1.2"))
}
private fun readResource(name: String): ByteArray {
this.javaClass.classLoader.getResourceAsStream(name).use {
return IOUtils.toByteArray(it)
}
}
}

View File

@@ -8,13 +8,11 @@
package at.bitfire.davdroid.model
import android.content.ContentValues
import android.support.test.filters.SmallTest
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.property.ResourceType
import androidx.test.filters.SmallTest
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.property.ResourceType
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.model.ServiceDB.Collections
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
@@ -22,7 +20,7 @@ import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
class CollectionInfoTest {
class CollectionTest {
private lateinit var httpClient: HttpClient
private val server = MockWebServer()
@@ -40,7 +38,7 @@ class CollectionInfoTest {
@Test
@SmallTest
fun testFromDavResource() {
fun testFromDavResponseAddressBook() {
// r/w address book
server.enqueue(MockResponse()
.setResponseCode(207)
@@ -55,16 +53,24 @@ class CollectionInfoTest {
"</response>" +
"</multistatus>"))
var info: CollectionInfo? = null
lateinit var info: Collection
DavResource(httpClient.okHttpClient, server.url("/"))
.propfind(0, ResourceType.NAME) { response, _ ->
info = CollectionInfo(response)
info = Collection.fromDavResponse(response) ?: throw IllegalArgumentException()
}
assertEquals(CollectionInfo.Type.ADDRESS_BOOK, info?.type)
assertFalse(info!!.readOnly)
assertEquals("My Contacts", info?.displayName)
assertEquals("My Contacts Description", info?.description)
assertEquals(Collection.TYPE_ADDRESSBOOK, info.type)
assertTrue(info.privWriteContent)
assertTrue(info.privUnbind)
assertNull(info.supportsVEVENT)
assertNull(info.supportsVTODO)
assertNull(info.supportsVJOURNAL)
assertEquals("My Contacts", info.displayName)
assertEquals("My Contacts Description", info.description)
}
@Test
@SmallTest
fun testFromDavResponseCalendar() {
// read-only calendar, no display name
server.enqueue(MockResponse()
.setResponseCode(207)
@@ -81,50 +87,48 @@ class CollectionInfoTest {
"</response>" +
"</multistatus>"))
info = null
lateinit var info: Collection
DavResource(httpClient.okHttpClient, server.url("/"))
.propfind(0, ResourceType.NAME) { response, _ ->
info = CollectionInfo(response)
}
assertEquals(CollectionInfo.Type.CALENDAR, info?.type)
assertTrue(info!!.readOnly)
assertNull(info?.displayName)
assertEquals("My Calendar", info?.description)
assertEquals(0xFFFF0000.toInt(), info?.color)
assertEquals("tzdata", info?.timeZone)
assertTrue(info!!.supportsVEVENT)
assertTrue(info!!.supportsVTODO)
info = Collection.fromDavResponse(response) ?: throw IllegalArgumentException()
}
assertEquals(Collection.TYPE_CALENDAR, info.type)
assertFalse(info.privWriteContent)
assertFalse(info.privUnbind)
assertNull(info.displayName)
assertEquals("My Calendar", info.description)
assertEquals(0xFFFF0000.toInt(), info.color)
assertEquals("tzdata", info.timezone)
assertTrue(info.supportsVEVENT!!)
assertTrue(info.supportsVTODO!!)
assertTrue(info.supportsVJOURNAL!!)
}
@Test
fun testFromDB() {
val values = ContentValues()
values.put(Collections.ID, 1)
values.put(Collections.SERVICE_ID, 1)
values.put(Collections.TYPE, CollectionInfo.Type.CALENDAR.name)
values.put(Collections.URL, "http://example.com")
values.put(Collections.READ_ONLY, 1)
values.put(Collections.DISPLAY_NAME, "display name")
values.put(Collections.DESCRIPTION, "description")
values.put(Collections.COLOR, 0xFFFF0000)
values.put(Collections.TIME_ZONE, "tzdata")
values.put(Collections.SUPPORTS_VEVENT, 1)
values.put(Collections.SUPPORTS_VTODO, 1)
values.put(Collections.SYNC, 1)
@SmallTest
fun testFromDavResponseWebcal() {
// Webcal subscription
server.enqueue(MockResponse()
.setResponseCode(207)
.setBody("<multistatus xmlns='DAV:' xmlns:CS='http://calendarserver.org/ns/'>" +
"<response>" +
" <href>/webcal1</href>" +
" <propstat><prop>" +
" <displayname>Sample Subscription</displayname>" +
" <resourcetype><collection/><CS:subscribed/></resourcetype>" +
" <CS:source><href>webcals://example.com/1.ics</href></CS:source>" +
" </prop></propstat>" +
"</response>" +
"</multistatus>"))
val info = CollectionInfo(values)
assertEquals(CollectionInfo.Type.CALENDAR, info.type)
assertEquals(1.toLong(), info.id)
assertEquals(1.toLong(), info.serviceID)
assertEquals(HttpUrl.parse("http://example.com/"), info.url)
assertTrue(info.readOnly)
assertEquals("display name", info.displayName)
assertEquals("description", info.description)
assertEquals(0xFFFF0000.toInt(), info.color)
assertEquals("tzdata", info.timeZone)
assertTrue(info.supportsVEVENT)
assertTrue(info.supportsVTODO)
assertTrue(info.selected)
lateinit var info: Collection
DavResource(httpClient.okHttpClient, server.url("/"))
.propfind(0, ResourceType.NAME) { response, _ ->
info = Collection.fromDavResponse(response) ?: throw IllegalArgumentException()
}
assertEquals(Collection.TYPE_WEBCAL, info.type)
assertEquals("Sample Subscription", info.displayName)
assertEquals("https://example.com/1.ics".toHttpUrl(), info.source)
}
}

View File

@@ -0,0 +1,64 @@
package at.bitfire.davdroid.model
import androidx.room.Room
import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
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= "https://example.com/1".toHttpUrl())
val entry3 = HomeSet(id=3, serviceId=service.id, url= "https://example.com/3".toHttpUrl())
val oldItems = listOf(
entry1,
HomeSet(id=2, serviceId=service.id, url= "https://example.com/2".toHttpUrl()),
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= "https://example.com/2".toHttpUrl(), displayName="Updated Entry")
newItems[updated.url] = updated
val created = HomeSet(id=4, serviceId=service.id, url= "https://example.com/4".toHttpUrl())
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))
}
}

View File

@@ -1,39 +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 at.bitfire.davdroid.App
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import org.junit.Test
class DefaultsProviderTest {
private val provider: Provider = DefaultsProvider()
@Test
fun testHas() {
assertEquals(Pair(false, true), provider.has("notExisting"))
assertEquals(Pair(true, true), provider.has(App.OVERRIDE_PROXY))
}
@Test
fun testGet() {
assertEquals(Pair("localhost", true), provider.getString(App.OVERRIDE_PROXY_HOST))
assertEquals(Pair(8118, true), provider.getInt(App.OVERRIDE_PROXY_PORT))
}
@Test
fun testPutRemove() {
assertEquals(Pair(false, true), provider.isWritable(App.OVERRIDE_PROXY))
assertFalse(provider.putBoolean(App.OVERRIDE_PROXY, true))
assertFalse(provider.remove(App.OVERRIDE_PROXY))
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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 SettingsManagerTest {
lateinit var settingsManager: SettingsManager
@Before
fun initialize() {
settingsManager = SettingsManager.getInstance(InstrumentationRegistry.getInstrumentation().targetContext)
}
@Test
fun testContainsKey() {
assertFalse(settingsManager.containsKey("notExisting"))
// provided by DefaultsProvider
assertTrue(settingsManager.containsKey(Settings.OVERRIDE_PROXY))
}
}

View File

@@ -1,44 +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 android.support.test.InstrumentationRegistry
import android.support.test.InstrumentationRegistry.getTargetContext
import at.bitfire.davdroid.App
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.After
import org.junit.Before
import org.junit.Test
class SettingsTest {
lateinit var settings: Settings.Stub
@Before
fun init() {
InstrumentationRegistry.getContext().isRestricted
settings = Settings.getInstance(getTargetContext())!!
}
@After
fun shutdown() {
settings.close()
}
@Test
fun testHas() {
assertFalse(settings.has("notExisting"))
// provided by DefaultsProvider
assertTrue(settings.has(App.OVERRIDE_PROXY))
}
}

View File

@@ -0,0 +1,31 @@
package at.bitfire.davdroid.syncadapter
import android.os.Bundle
import androidx.test.filters.SmallTest
import at.bitfire.davdroid.syncadapter.SyncAdapterService.SyncAdapter
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class SyncAdapterServiceTest {
@Test
@SmallTest
fun testPriorityCollections() {
val extras = Bundle()
assertTrue(SyncAdapter.priorityCollections(extras).isEmpty())
extras.putString(SyncAdapterService.SYNC_EXTRAS_PRIORITY_COLLECTIONS, "")
assertTrue(SyncAdapter.priorityCollections(extras).isEmpty())
extras.putString(SyncAdapterService.SYNC_EXTRAS_PRIORITY_COLLECTIONS, "123")
assertArrayEquals(longArrayOf(123), SyncAdapter.priorityCollections(extras).toLongArray())
extras.putString(SyncAdapterService.SYNC_EXTRAS_PRIORITY_COLLECTIONS, ",x,")
assertTrue(SyncAdapter.priorityCollections(extras).isEmpty())
extras.putString(SyncAdapterService.SYNC_EXTRAS_PRIORITY_COLLECTIONS, "1,2,3")
assertArrayEquals(longArrayOf(1,2,3), SyncAdapter.priorityCollections(extras).toLongArray())
}
}

View File

@@ -8,11 +8,11 @@
package at.bitfire.davdroid.ui.setup
import android.support.test.InstrumentationRegistry.getTargetContext
import android.support.test.filters.SmallTest
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.property.AddressbookHomeSet
import at.bitfire.dav4android.property.ResourceType
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.log.Logger
import at.bitfire.davdroid.model.Credentials
@@ -44,18 +44,20 @@ class DavResourceFinderTest {
lateinit var finder: DavResourceFinder
lateinit var client: HttpClient
lateinit var loginInfo: LoginInfo
lateinit var loginModel: LoginModel
@Before
fun initServerAndClient() {
server.setDispatcher(TestDispatcher())
server.dispatcher = TestDispatcher()
server.start()
loginInfo = LoginInfo(URI.create("/"), Credentials("mock", "12345"))
finder = DavResourceFinder(getTargetContext(), loginInfo)
loginModel = LoginModel()
loginModel.baseURI = URI.create("/")
loginModel.credentials = Credentials("mock", "12345")
finder = DavResourceFinder(InstrumentationRegistry.getInstrumentation().targetContext, loginModel)
client = HttpClient.Builder()
.addAuthentication(null, loginInfo.credentials)
.addAuthentication(null, loginModel.credentials!!)
.build()
}
@@ -122,21 +124,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"
@@ -147,7 +159,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,
@@ -165,14 +177,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>")

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,24 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEEzCCAfsCAQEwDQYJKoZIhvcNAQEFBQAwRjELMAkGA1UEBhMCQ0ExEzARBgNV
BAgMClNvbWUtU3RhdGUxEDAOBgNVBAoMB0NBIENlcnQxEDAOBgNVBAMMB0NBIENl
cnQwHhcNMTgwMTEzMjAyOTI5WhcNMTkwMTEzMjAyOTI5WjBZMQswCQYDVQQGEwJD
QTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
cyBQdHkgTHRkMRIwEAYDVQQDDAlVc2VyIENlcnQwggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQDqOyHAeG4psE/f6i/eTfwbhn6j7WaFXxZiSOWwpQZmzRrx
MrfkABJCk0X7KNgCaJcmBkG9G1Ri4HfKrxvJFswMXknlq+0ulGBk7oDnZM+pihuX
3D9VCWMMkCqYhLCGADj2zB2mkX4LpcMRi6XoOetKURE/vcIy7rSLAtJM6ZRdftfh
2ZxnautS1Tyujh9Au3NI/+Of80tT/nA+oBJQeT1fB/ga1OQlZP5kjSaA7IPiIbTz
QBO+r898MvqK/lwsvOYnWAp7TY03z+vPfCs0zjijZEl9Wrl0hW6o5db5kU1v5bcr
p87hxFJsGD2HIr2y6kvYfL2hn+h9iANyYdRnUgapAgMBAAEwDQYJKoZIhvcNAQEF
BQADggIBAHANsiJITedXPyp89lVMEmGY3zKtOqgQ3tqjvjlNt2sdPnj7wmZbmrNd
sa90S/UwOn8PzEFOVxYy1BPlljlEjtjmc4OHMcm4P4Zv36uawHilmK8V+zT59gCK
ftB5FP2TLFUFi2X9o8J06d0xJRE77uewN155NV4RmPuP4b/tMmeixoQppHqLqEr5
lgEUnt3Mh1ctmeFQFJR6lJ01hlB0gdpVHIhzrVLTO3uo8ePLJTmxP6tyKl/HXj9F
mpVsKb1kriKwbkGczfw99OUZeUVbTwQOR07r0SrG71B7IuDvxIORnhQc1OUjt7ob
wjdaZauAHxpGBRu+hw9Yqaxchk9Gldy1nEjGyyVCD0FU5taXbl8PhBWEDc4U9tI+
xVNmPpsSuCsbz3Mjd1YIVRGL99vLrKsQcj+TNM+jJKKRKes3ihl+l/0FwG6UuO7L
EvjlUg5hOtYi1D7xuYyMjroGBGh7swYMt6w4eCDbcjzcCkaCi0H2pScM/rLBpDjS
LIoGCvZ1LBdi933/iOj1/8dxGZwY6fEgcyiD2n0xAgYIniLWjEZXOMdIK5FNTNga
Tswanvp+6Noa4oIu/hl/LXvPMsouaWfSEbRe0Dshi3GpLj3YtEHoN9DHB8bn7jy5
34By81GT41m5kq3hWP//x9kSHYSADpbovCbKbElU1qSt6vTVR4nq
-----END CERTIFICATE-----

View File

Binary file not shown.

View File

@@ -1,68 +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.ui
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.view.Menu
import android.view.MenuItem
import at.bitfire.davdroid.App
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.settings.ISettings
class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
companion object {
private const val BETA_FEEDBACK_URI = "mailto:support@davdroid.com?subject=${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} feedback (${BuildConfig.VERSION_CODE})"
}
override fun onSettingsChanged(settings: ISettings?, menu: Menu) {
if (BuildConfig.VERSION_NAME.contains("-beta") || BuildConfig.VERSION_NAME.contains("-rc"))
menu.findItem(R.id.nav_beta_feedback).isVisible = true
}
override fun onNavigationItemSelected(activity: Activity, item: MenuItem): Boolean {
when (item.itemId) {
R.id.nav_about ->
activity.startActivity(Intent(activity, AboutActivity::class.java))
R.id.nav_app_settings ->
activity.startActivity(Intent(activity, AppSettingsActivity::class.java))
R.id.nav_beta_feedback -> {
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse(BETA_FEEDBACK_URI))
if (activity.packageManager.resolveActivity(intent, 0) != null)
activity.startActivity(intent)
}
R.id.nav_twitter ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://twitter.com/davdroidapp")))
R.id.nav_website ->
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)))
R.id.nav_manual ->
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)
.buildUpon().appendEncodedPath("manual/").build()))
R.id.nav_faq ->
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)
.buildUpon().appendEncodedPath("faq/").build()))
R.id.nav_forums ->
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)
.buildUpon().appendEncodedPath("forums/").build()))
R.id.nav_donate ->
if (BuildConfig.FLAVOR != App.FLAVOR_GOOGLE_PLAY)
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)
.buildUpon().appendEncodedPath("donate/").build()))
else ->
return false
}
return true
}
}

View File

@@ -1,211 +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.ui.setup
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.security.KeyChain
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import at.bitfire.dav4android.Constants
import at.bitfire.davdroid.R
import kotlinx.android.synthetic.standard.login_credentials_fragment.view.*
import java.net.IDN
import java.net.URI
import java.net.URISyntaxException
import java.util.logging.Level
class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChangeListener {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val v = inflater.inflate(R.layout.login_credentials_fragment, container, false)
if (savedInstanceState == null) {
// first call
activity?.intent?.let {
// we've got initial login data
val url = it.getStringExtra(LoginActivity.EXTRA_URL)
val username = it.getStringExtra(LoginActivity.EXTRA_USERNAME)
val password = it.getStringExtra(LoginActivity.EXTRA_PASSWORD)
if (url != null) {
v.login_type_urlpwd.isChecked = true
v.urlpwd_base_url.setText(url)
v.urlpwd_user_name.setText(username)
v.urlpwd_password.setText(password)
} else {
v.login_type_email.isChecked = true
v.email_address.setText(username)
v.email_password.setText(password)
}
}
}
v.urlcert_select_cert.setOnClickListener {
KeyChain.choosePrivateKeyAlias(activity, { alias ->
Handler(Looper.getMainLooper()).post {
v.urlcert_cert_alias.text = alias
v.urlcert_cert_alias.error = null
}
}, null, null, null, -1, view!!.urlcert_cert_alias.text.toString())
}
v.login.setOnClickListener {
validateLoginData()?.let { info ->
DetectConfigurationFragment.newInstance(info).show(fragmentManager, null)
}
}
// initialize to Login by email
onCheckedChanged(v)
v.login_type_email.setOnCheckedChangeListener(this)
v.login_type_urlpwd.setOnCheckedChangeListener(this)
v.login_type_urlcert.setOnCheckedChangeListener(this)
return v
}
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
onCheckedChanged(view!!)
}
private fun onCheckedChanged(v: View) {
v.login_type_email_details.visibility = if (v.login_type_email.isChecked) View.VISIBLE else View.GONE
v.login_type_urlpwd_details.visibility = if (v.login_type_urlpwd.isChecked) View.VISIBLE else View.GONE
v.login_type_urlcert_details.visibility = if (v.login_type_urlcert.isChecked) View.VISIBLE else View.GONE
}
private fun validateLoginData(): LoginInfo? {
val view = requireNotNull(view)
when {
// Login with email address
view.login_type_email.isChecked -> {
var uri: URI? = null
var valid = true
val email = view.email_address.text.toString()
if (!email.matches(Regex(".+@.+"))) {
view.email_address.error = getString(R.string.login_email_address_error)
valid = false
} else
try {
uri = URI("mailto", email, null)
} catch (e: URISyntaxException) {
view.email_address.error = e.localizedMessage
valid = false
}
val password = view.email_password.text.toString()
if (password.isEmpty()) {
view.email_password.error = getString(R.string.login_password_required)
valid = false
}
return if (valid && uri != null)
LoginInfo(uri, email, password)
else
null
}
// Login with URL and user name
view.login_type_urlpwd.isChecked -> {
var valid = true
val baseUrl = Uri.parse(view.urlpwd_base_url.text.toString())
val uri = validateBaseUrl(baseUrl, false) { message ->
view.urlpwd_base_url.error = message
valid = false
}
val userName = view.urlpwd_user_name.text.toString()
if (userName.isBlank()) {
view.urlpwd_user_name.error = getString(R.string.login_user_name_required)
valid = false
}
val password = view.urlpwd_password.text.toString()
if (password.isEmpty()) {
view.urlpwd_password.error = getString(R.string.login_password_required)
valid = false
}
return if (valid && uri != null)
LoginInfo(uri, userName, password)
else
null
}
// Login with URL and client certificate
view.login_type_urlcert.isChecked -> {
var valid = true
val baseUrl = Uri.parse(view.urlcert_base_url.text.toString())
val uri = validateBaseUrl(baseUrl, true) { message ->
view.urlcert_base_url.error = message
valid = false
}
val alias = view.urlcert_cert_alias.text.toString()
if (alias.isEmpty()) {
view.urlcert_cert_alias.error = ""
valid = false
}
if (valid && uri != null)
return LoginInfo(uri, certificateAlias = alias)
}
}
return null
}
private fun validateBaseUrl(baseUrl: Uri, httpsRequired: Boolean, reportError: (String) -> Unit): URI? {
var uri: URI? = null
val scheme = baseUrl.scheme
if ((!httpsRequired && scheme.equals("http", true)) || scheme.equals("https", true)) {
var host = baseUrl.host
if (host.isNullOrBlank())
reportError(getString(R.string.login_url_host_name_required))
else
try {
host = IDN.toASCII(host)
} catch (e: IllegalArgumentException) {
Constants.log.log(Level.WARNING, "Host name not conforming to RFC 3490", e)
}
val path = baseUrl.encodedPath
val port = baseUrl.port
try {
uri = URI(baseUrl.scheme, null, host, port, path, null, null)
} catch (e: URISyntaxException) {
reportError(e.localizedMessage)
}
} else
reportError(getString(if (httpsRequired)
R.string.login_url_must_be_https
else
R.string.login_url_must_be_http_or_https))
return uri
}
class Factory: ILoginCredentialsFragment {
override fun getFragment() = DefaultLoginCredentialsFragment()
}
}

View File

@@ -1,201 +0,0 @@
<?xml version="1.0" encoding="utf-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
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- We don't want the keyboard up when the user arrives in this initial screen -->
<View android:layout_height="0dp"
android:layout_width="0dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:contentDescription="@null"
android:importantForAccessibility="no" tools:ignore="UnusedAttribute">
<requestFocus/>
</View>
<ScrollView android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="@dimen/activity_margin">
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:animateLayoutChanges="true">
<RadioButton
android:id="@+id/login_type_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/login_type_email"
android:paddingLeft="14dp" tools:ignore="RtlSymmetry"
style="@style/login_type_headline"/>
<LinearLayout
android:id="@+id/login_type_email_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:orientation="vertical">
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TextInputEditText
android:id="@+id/email_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_email_address"
android:inputType="textEmailAddress"/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="true">
<android.support.design.widget.TextInputEditText
android:id="@+id/email_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_password"
android:fontFamily="monospace"
android:inputType="textPassword"/>
</android.support.design.widget.TextInputLayout>
</LinearLayout>
<RadioButton
android:id="@+id/login_type_urlpwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_type_url"
android:layout_marginTop="16dp"
android:paddingLeft="14dp" tools:ignore="RtlSymmetry"
style="@style/login_type_headline"/>
<LinearLayout
android:id="@+id/login_type_urlpwd_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:orientation="vertical">
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TextInputEditText
android:id="@+id/urlpwd_base_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_base_url"
android:inputType="textUri"/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TextInputEditText
android:id="@+id/urlpwd_user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_user_name"
android:inputType="textEmailAddress"/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="true">
<android.support.design.widget.TextInputEditText
android:id="@+id/urlpwd_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:inputType="textPassword"
android:hint="@string/login_password"/>
</android.support.design.widget.TextInputLayout>
</LinearLayout>
<RadioButton
android:id="@+id/login_type_urlcert"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_type_url_certificate"
android:layout_marginTop="16dp"
android:paddingLeft="14dp" tools:ignore="RtlSymmetry"
style="@style/login_type_headline"/>
<LinearLayout
android:id="@+id/login_type_urlcert_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:orientation="vertical">
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TextInputEditText
android:id="@+id/urlcert_base_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_base_url"
android:inputType="textUri"/>
</android.support.design.widget.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/urlcert_cert_alias"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:paddingLeft="3dp"
android:paddingRight="3dp"
style="@style/Base.TextAppearance.AppCompat.Body1"
android:textSize="16sp"/>
<Button
android:id="@+id/urlcert_select_cert"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:text="@string/login_select_certificate"/>
</LinearLayout>
</LinearLayout>
</RadioGroup>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/stepper_nav_bar">
<Space
android:layout_width="0dp"
android:layout_weight="1"
style="@style/stepper_nav_button"/>
<Button
android:id="@+id/login"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/login_login"
style="@style/stepper_nav_button"/>
</LinearLayout>
</LinearLayout>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -1,179 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="app_name">DAVdroid</string>
<string name="help">Pomoc</string>
<string name="manage_accounts">Spravovat účty</string>
<string name="please_wait">Chvíli strpení ...</string>
<string name="send">Odeslat</string>
<!--startup dialogs-->
<string name="startup_battery_optimization_disable">Vypnout pro DAVdroid</string>
<string name="startup_dont_show_again">Již nezobrazovat</string>
<string name="startup_donate">Open Source informace</string>
<string name="startup_donate_message">Jsme velice rádi že používáte DAVdroid, software s otevřeným zdrojovým kódem (GPLv3). Vývoj této aplikace je náročný a trval již několik tisíc hodin, velice nás potěší přispějete-li na jeho vývoj.</string>
<string name="startup_donate_now">Zobrazit stránku pro obdarování</string>
<string name="startup_donate_later">Možná později</string>
<string name="startup_google_play_accounts_removed">Informace o chybě DRM Obchodu Play</string>
<string name="startup_google_play_accounts_removed_message">Za určitých podmínek může dojít po restartu nebo aktualizaci aplikace DAVdroid k vymazání účtů kvůli chybě DRM Obchodu Play. Pokud jste postiženi touto chybou (ale pouze v tomto případě), nainstalujte prosím z Obchodu Play aplikaci \"DAVdroid JB Workaround\".</string>
<string name="startup_opentasks_not_installed">OpenTasks není nainstalován</string>
<string name="startup_opentasks_reinstall_davdroid">Po instalaci OpenTasks musíte PŘEINSTALOVAT DAVdroid a přidat znovu své účty (Android chyba).</string>
<string name="startup_opentasks_not_installed_install">Nainstalovat OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_info_no_warranty">Tento program je distribuován BEZ JAKÉKOLIV ZÁRUKY. Je to volně dostupný software a lze jej za určitých podmínek dále distribuovat.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid logování do souboru</string>
<string name="logging_to_external_storage">Logování do externího úložiště: %s</string>
<string name="logging_couldnt_create_file">Nelze vytvořit externí soubor logu: %s</string>
<string name="logging_no_external_storage">Externí úložiště nenalezeno</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">Otevřít panel navigace</string>
<string name="navigation_drawer_close">Zavřít panel navigace</string>
<string name="navigation_drawer_subtitle">CalDAV/CardDAV adapter synchronizace</string>
<string name="navigation_drawer_about">O aplikaci / Licence</string>
<string name="navigation_drawer_settings">Nastavení</string>
<string name="navigation_drawer_news_updates">Novinky &amp; aktualizace</string>
<string name="navigation_drawer_external_links">Externí odkazy</string>
<string name="navigation_drawer_website">Webová stránka</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_donate">Obdarovat</string>
<string name="account_list_empty">Vítejte v aplikaci DAVdroid!\n\nNyní můžete přidat CalDAV/CardDAV účet.</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Vyhledání služby selhalo</string>
<string name="dav_service_refresh_couldnt_refresh">Nelze obnovit seznam sbírky</string>
<!--AppSettingsActivity-->
<string name="app_settings">Nastavení</string>
<string name="app_settings_user_interface">Uživatelské prostředí</string>
<string name="app_settings_reset_hints">Resetovat nápovědu</string>
<string name="app_settings_reset_hints_summary">Znovu povolí vypnuté texty nápovědy</string>
<string name="app_settings_reset_hints_success">Budou zobrazovány všechny texty nápovědy</string>
<string name="app_settings_connection">Připojení</string>
<string name="app_settings_override_proxy">Přepsat proxy nastavení</string>
<string name="app_settings_override_proxy_on">Použít vlastní proxy nastavení</string>
<string name="app_settings_override_proxy_off">Použít výchozí systémová proxy nastavení</string>
<string name="app_settings_override_proxy_host">HTTP proxy hostname</string>
<string name="app_settings_override_proxy_port">HTTP proxy port</string>
<string name="app_settings_security">Zabezpečení</string>
<string name="app_settings_distrust_system_certs">Nedůvěřovat systémovým certifikátům</string>
<string name="app_settings_distrust_system_certs_on">Systémovým a uživatelem přidaným CA nebude důvěřováno</string>
<string name="app_settings_distrust_system_certs_off">Systémovým a uživatelem přidaným CA bude důvěřováno (doporučeno)</string>
<string name="app_settings_reset_certificates">Resetovat (ne)důvěryhodné certifikáty</string>
<string name="app_settings_reset_certificates_summary">Resetovat důvěryhodnost všech vlastních certifikátů</string>
<string name="app_settings_reset_certificates_success">Všechny vlastní certifikáty byly resetovány</string>
<string name="app_settings_debug">Ladění</string>
<string name="app_settings_log_to_external_storage">Logovat do externího souboru</string>
<string name="app_settings_log_to_external_storage_on">Logování do externího úložiště (pokud dostupné)</string>
<string name="app_settings_log_to_external_storage_off">Logování do externího souboru je vypnuto</string>
<string name="app_settings_show_debug_info">Zobrazit ladící informace</string>
<string name="app_settings_show_debug_info_details">Zobrazit/sdílet software a detaily konfigurace</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Synchronizovat nyní</string>
<string name="account_synchronizing_now">Probíhá synchronizace</string>
<string name="account_settings">Nastavení účtu</string>
<string name="account_rename">Přejmenovat účet</string>
<string name="account_rename_new_name">Neuložená místní data mohou být vynechána. Po přejmenování je vyžadována nová synchronizace. Nové jméno účtu:</string>
<string name="account_rename_rename">Přejmenovat</string>
<string name="account_delete">Smazat účet</string>
<string name="account_delete_confirmation_title">Opravdu smazat účet?</string>
<string name="account_delete_confirmation_text">Všechny místní kopie adresáře, kalendářů a úkolů budou smazány.</string>
<string name="account_refresh_address_book_list">Obnovit seznam adresářů</string>
<string name="account_create_new_address_book">Vytvořit nový adresář</string>
<string name="account_refresh_calendar_list">Obnovit seznam kalendářů</string>
<string name="account_create_new_calendar">Vytvořit nový kalendář</string>
<!--AddAccountActivity-->
<string name="login_title">Přidat účet</string>
<string name="login_type_email">Přihlášení s emailovou adresou</string>
<string name="login_email_address">Emailová adresa</string>
<string name="login_email_address_error">Vyžadován platný email</string>
<string name="login_password">Heslo</string>
<string name="login_password_required">Vyžadováno heslo</string>
<string name="login_type_url">Přihlášení s URL a uživatelským jménem</string>
<string name="login_url_must_be_http_or_https">URL musí začínat na http(s)://</string>
<string name="login_url_host_name_required">Vyžadováno hostname</string>
<string name="login_user_name">Uživatelské jméno</string>
<string name="login_user_name_required">Vyžadováno uživatelské jméno</string>
<string name="login_base_url">Základní URL</string>
<string name="login_login">Login</string>
<string name="login_back">Zpět</string>
<string name="login_create_account">Vytvořit účet</string>
<string name="login_account_name">Jméno účtu</string>
<string name="login_account_name_info">Pro jméno účtu použijte svou emailovou adresu, protože Android bude brát jméno účtu jako údaj pro ORGANIZÁTORA vytvořených událostí. Nelze mít dva účty stejného jména.</string>
<string name="login_account_contact_group_method">Metoda seskupování kontaktů:</string>
<string name="login_account_name_required">Vyžadováno jméno účtu</string>
<string name="login_account_not_created">Účet nelze vytvořit</string>
<string name="login_configuration_detection">Vyhledání konfigurace</string>
<string name="login_querying_server">Chvíli strpení, probíhá dotazování serveru...</string>
<string name="login_no_caldav_carddav">Nelze nalézt službu CalDAV nebo CardDAV.</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Nastavení: %s</string>
<string name="settings_authentication">Ověření</string>
<string name="settings_username">Uživatelské jméno</string>
<string name="settings_enter_username">Zadat uživatelské jméno</string>
<string name="settings_password">Heslo</string>
<string name="settings_password_summary">Aktualizovat heslo dle svého serveru.</string>
<string name="settings_enter_password">Vložit své heslo:</string>
<string name="settings_sync">Synchronizace</string>
<string name="settings_sync_interval_contacts">Interval synchronizace kontaktů</string>
<string name="settings_sync_summary_manually">Pouze manuálně</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Každých %d minut a ihned při lokálních změnách</string>
<string name="settings_sync_interval_calendars">Interval synchronizace kalendáře</string>
<string name="settings_sync_interval_tasks">Interval synchronizace úkolů</string>
<string name="settings_sync_wifi_only">Synchronizovat pouze přes WiFi</string>
<string name="settings_sync_wifi_only_on">Synchronizace omezena na WiFi připojení</string>
<string name="settings_sync_wifi_only_off">Druh připojení není brán v potaz</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Metoda seskupování kontaktů</string>
<string-array name="settings_contact_group_method_values">
<item>GROUP_VCARDS</item>
<item>CATEGORIES</item>
</string-array>
<string-array name="settings_contact_group_method_entries">
<item>Skupiny jsou oddělené soubory VCard</item>
<item>Skupiny jsou kategorie na kontakt</item>
</string-array>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Časový limit pro staré události</string>
<string name="settings_sync_time_range_past_none">Synchronizovat všechny události</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Ignorovat události starší než 1 den</item>
<item quantity="few">Ignorovat události starší než %d dny</item>
<item quantity="many">Ignorovat události starší než %d dnů</item>
<item quantity="other">Ignorovat události starší než %d dnů</item>
</plurals>
<string name="settings_sync_time_range_past_message">Události z minulosti starší než vyznačený počet dnů budou ignorovány (lze zadat 0). Ponechte prázdné pro synchronizaci všech událostí.</string>
<string name="settings_manage_calendar_colors">Spravovat barvy kalendářů</string>
<string name="settings_manage_calendar_colors_on">Barvy kalendářů spravuje DAVdroid</string>
<string name="settings_manage_calendar_colors_off">Barvy kalendářů nespravuje DAVdroid</string>
<!--collection management-->
<string name="create_addressbook">Vytvořit adresář</string>
<string name="create_addressbook_display_name_hint">Můj adresář</string>
<string name="create_calendar">Vytvořit CalDAV sbírku</string>
<string name="create_calendar_display_name_hint">Můj kalendář</string>
<string name="create_calendar_time_zone">Časová zóna:</string>
<string name="create_calendar_type">Typ sbírky:</string>
<string name="create_calendar_type_only_events">Kalendář (pouze události)</string>
<string name="create_calendar_type_only_tasks">Seznam úkolů (pouze úkoly)</string>
<string name="create_calendar_type_events_and_tasks">Kombinovaná (události a úkoly)</string>
<string name="create_collection_color">Nastavit barvu sbírky</string>
<string name="create_collection_creating">Vytváření sbírky</string>
<string name="create_collection_display_name">Zobrazit jméno (nadpis) této sbírky:</string>
<string name="create_collection_display_name_required">Nadpis je vyžadován</string>
<string name="create_collection_description">Popis (volitelný):</string>
<string name="create_collection_home_set">Domácí sbírka:</string>
<string name="create_collection_create">Vytvořit</string>
<string name="delete_collection">Smazat sbírku</string>
<string name="delete_collection_confirm_title">Jste si jisti?</string>
<string name="delete_collection_confirm_warning">Tato sbírka (%s) a všechna její data budou odstraněna ze serveru.</string>
<string name="delete_collection_deleting_collection">Mazání sbírky</string>
<!--ExceptionInfoFragment-->
<string name="exception">Došlo k chybě.</string>
<string name="exception_httpexception">Došlo k HTTP chybě.</string>
<string name="exception_ioexception">Došlo k I/O chybě.</string>
<string name="exception_show_details">Zobrazit detaily</string>
<!--sync adapters and DebugInfoActivity-->
<string name="debug_info_title">Ladící informace</string>
<string name="sync_error_permissions">DAVdroid oprávnění</string>
<string name="sync_error_permissions_text">Vyžadována dodatečná oprávnění</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: Zabezpečení připojení</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid nalezl neznámý certifikát. Chcete mu důvěřovat?</string>
</resources>

View File

@@ -1,206 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">Carnet d\'adresses DAVdroid</string>
<string name="address_books_authority_title">Carnets d\'adresses</string>
<string name="help">Aide</string>
<string name="manage_accounts">Gestion des comptes</string>
<string name="please_wait">patientez ...</string>
<string name="send">Envoyer</string>
<!--startup dialogs-->
<string name="startup_battery_optimization_disable">Désactiver pour DAVdroid</string>
<string name="startup_dont_show_again">Ne plus afficher</string>
<string name="startup_donate">Open-Source Information</string>
<string name="startup_donate_message">Nous sommes heureux que vous utilisez DAVdroid, qui est un logiciel open-source (GPLv3). Parce que développer DAVdroid est un travail difficile et nous a pris de nombreuses heures, s\'il vous plaît envisager de faire un don.</string>
<string name="startup_donate_now">Faire un don</string>
<string name="startup_donate_later">Plus tard</string>
<string name="startup_google_play_accounts_removed">Erreur information Play Store DRM</string>
<string name="startup_google_play_accounts_removed_message">Dans certaines conditions, Play Store DRM peut provoquer la disparition de tous les comptes DAVdroid après un redémarrage ou après la mise à niveau de DAVdroid. Si vous êtes concerné par ce problème (et seulement alors), s\'il vous plaît installer \"DAVdroid JB Solution\" du Play Store.</string>
<string name="startup_opentasks_not_installed">L\'application OpenTasks n\'est pas installée</string>
<string name="startup_opentasks_reinstall_davdroid">Après l\'installation OpenTasks, vous devez RE-INSTALLER DAVdroid et ajoutez vos comptes à nouveau (bug Android).</string>
<string name="startup_opentasks_not_installed_install">Installer OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_info_no_warranty">Ce programme est fourni sans AUCUNE GARANTIE. C\'est un logiciel libre, et vous êtes en droit de le redistribuer sous certaines conditions.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid fichier de journalisation</string>
<string name="logging_to_external_storage">Se connecter au stockage externe: %s</string>
<string name="logging_couldnt_create_file">Impossible de créer le fichier journal externe: %s</string>
<string name="logging_no_external_storage">Stockage externe introuvable</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">Ouvrir le tiroir de navigation</string>
<string name="navigation_drawer_close">Fermer le tiroir de navigation</string>
<string name="navigation_drawer_subtitle">Adaptateur de synchronisation CalDAV/CardDAV</string>
<string name="navigation_drawer_about">A propos / Licence</string>
<string name="navigation_drawer_settings">Paramètres</string>
<string name="navigation_drawer_news_updates">Actualités &amp; mises à jour</string>
<string name="navigation_drawer_external_links">Liens externes</string>
<string name="navigation_drawer_website">Site Web</string>
<string name="navigation_drawer_faq">Foire aux questions</string>
<string name="navigation_drawer_forums">Aide/Forum</string>
<string name="navigation_drawer_donate">Faire un don</string>
<string name="account_list_empty">Bienvenue sur DAVdroid!\n\nVous pouvez maintenant ajouter un compte CalDAV ou CardDAV.</string>
<string name="accounts_global_sync_disabled">La synchronisation automatique globale est désactivée</string>
<string name="accounts_global_sync_enable">Activer</string>
<!--DavService-->
<string name="dav_service_refresh_failed">La détection du service a échoué</string>
<string name="dav_service_refresh_couldnt_refresh">Impossible d\'actualiser la liste de collection</string>
<!--AppSettingsActivity-->
<string name="app_settings">Paramètres</string>
<string name="app_settings_user_interface">Interface utilisateur</string>
<string name="app_settings_reset_hints">Réinitialiser les astuces</string>
<string name="app_settings_reset_hints_summary">Réactiver les astuces qui ont été vues précédemment</string>
<string name="app_settings_reset_hints_success">Toutes les astuces seront affichés à nouveau</string>
<string name="app_settings_connection">Connexion</string>
<string name="app_settings_override_proxy">Ignorer les paramètres proxy</string>
<string name="app_settings_override_proxy_on">Utiliser des paramètres proxy personnalisés</string>
<string name="app_settings_override_proxy_off">Utiliser les paramètres proxy du système</string>
<string name="app_settings_override_proxy_host">Nom de l\'hôte du proxy HTTP</string>
<string name="app_settings_override_proxy_port">Port du proxy HTTP</string>
<string name="app_settings_security">Sécurité</string>
<string name="app_settings_distrust_system_certs">Révoquer les certificats du système</string>
<string name="app_settings_distrust_system_certs_on">Les certificats du système et ceux ajoutés par l\'utilisateur ne seront pas dignes de confiance</string>
<string name="app_settings_distrust_system_certs_off">Les certificats du système et ceux ajoutés par l\'utilisateur seront dignes de confiance (recommandé)</string>
<string name="app_settings_reset_certificates">Réinitialiser les certificats de (non)confiance</string>
<string name="app_settings_reset_certificates_summary">Réinitialiser la confiance de tous les certificats personnalisés</string>
<string name="app_settings_reset_certificates_success">Tous les certificats personnalisés ont été effacés</string>
<string name="app_settings_debug">Débogage</string>
<string name="app_settings_log_to_external_storage">Journaliser dans un fichier externe</string>
<string name="app_settings_log_to_external_storage_on">Journaliser sur le stockage externe (si disponible)</string>
<string name="app_settings_log_to_external_storage_off">Le fichier externe n\'est pas disponible.</string>
<string name="app_settings_show_debug_info">Afficher les infos de débogage</string>
<string name="app_settings_show_debug_info_details">Voir/partager l\'application et les détails de configuration</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Synchroniser maintenant</string>
<string name="account_synchronizing_now">Synchronisation en cours</string>
<string name="account_settings">Paramètres du compte</string>
<string name="account_rename">Renommer le compte</string>
<string name="account_rename_new_name">Les données locales non enregistrées pourraient être perdues. Une re-synchronisation est nécessaire après avoir renommé le compte. Nouveau nom du compte : </string>
<string name="account_rename_rename">Renommer</string>
<string name="account_delete">Supprimer le compte</string>
<string name="account_delete_confirmation_title">Voulez-vous vraiment supprimer le compte?</string>
<string name="account_delete_confirmation_text">Toutes les copies locales des carnets d\'adresses, des calendriers et des listes de tâches seront supprimées.</string>
<string name="account_carddav">CardDAV (les carnets d\'adresse) </string>
<string name="account_caldav">CalDAV (les agendas) </string>
<string name="account_webcal">WebCal (les anciens agenda)</string>
<string name="account_calendar">calendrier</string>
<string name="account_task_list">liste de tâche</string>
<string name="account_refresh_address_book_list">Actualiser le carnet d\'adresses</string>
<string name="account_create_new_address_book">Créer un nouveau carnet d\'adresses</string>
<string name="account_refresh_calendar_list">Actualiser le calendrier</string>
<string name="account_create_new_calendar">Créer un nouveau calendrier</string>
<string name="account_no_webcal_handler_found">Aucune application compatible WebCal</string>
<string name="account_install_icsdroid">Installer ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_title">Ajouter un compte</string>
<string name="login_type_email">Connexion avec une adresse email</string>
<string name="login_email_address">Adresse mail</string>
<string name="login_email_address_error">Une adresse e-mail valide est requise</string>
<string name="login_password">Mot de passe</string>
<string name="login_password_required">Mot de passe requis</string>
<string name="login_type_url">Connexion avec une URL et un nom d\'utilisateur</string>
<string name="login_url_must_be_http_or_https">L\'URL doit commencer par http(s)://</string>
<string name="login_url_host_name_required">Nom d\'hôte requis</string>
<string name="login_user_name">Nom d\'utilisateur</string>
<string name="login_user_name_required">Nom d\'utilisateur requis</string>
<string name="login_base_url">URL de base</string>
<string name="login_login">Se connecter</string>
<string name="login_back">Retour</string>
<string name="login_create_account">Créer un compte</string>
<string name="login_account_name">Nom du compte</string>
<string name="login_account_name_info">Utilisez votre adresse e-mail comme nom de compte car Android utilisera ce nom en tant que champ ORGANISATEUR pour les événements que vous créerez. Vous ne pouvez pas avoir deux comptes avec le même nom.</string>
<string name="login_account_contact_group_method">Méthode pour les contacts de type groupe :</string>
<string name="login_account_name_required">Nom du compte requis</string>
<string name="login_account_not_created">Le compte n\'a pas pu être créé</string>
<string name="login_configuration_detection">Détection de la configuration</string>
<string name="login_querying_server">Veuillez patienter, nous interrogeons le serveur ...</string>
<string name="login_no_caldav_carddav">Aucun accès possible au service CalDAV ou CardDAV.</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Paramètres: %s</string>
<string name="settings_authentication">Authentification</string>
<string name="settings_username">Nom d\'utilisateur</string>
<string name="settings_enter_username">Saisissez votre nom d\'utilisateur :</string>
<string name="settings_password">Mot de passe</string>
<string name="settings_password_summary">Mettre à jour le mot de passe </string>
<string name="settings_enter_password">Saisissez votre mot de passe :</string>
<string name="settings_sync">Synchronisation</string>
<string name="settings_sync_interval_contacts">Intervalle de synchronisation des carnets d\'adresses</string>
<string name="settings_sync_summary_manually">Manuellement</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Toutes les %d minutes et immédiatement après un changement local</string>
<string name="settings_sync_interval_calendars">Intervalle de synchronisation des agendas</string>
<string name="settings_sync_interval_tasks">Intervalle de synchronisation des tâches</string>
<string-array name="settings_sync_interval_names">
<item>Manuellement</item>
<item>Tous les quarts d\'heure</item>
<item>Toutes les demi-heures</item>
<item>Toutes les heures</item>
<item>Toutes les deux heures</item>
<item>Toutes les quatre heures</item>
<item>Une fois par jour</item>
</string-array>
<string name="settings_sync_wifi_only">Synchronisation en Wifi seulement</string>
<string name="settings_sync_wifi_only_on">La synchronisation est limitée aux connexions WiFi</string>
<string name="settings_sync_wifi_only_off">Le type de connexion n\'est pas pris en charge</string>
<string name="settings_sync_wifi_only_ssids">Restriction WiFi SSID</string>
<string name="settings_sync_wifi_only_ssids_on">Synchronisation possible seulement en %s</string>
<string name="settings_sync_wifi_only_ssids_off">Toutes les connexions WiFi seront utilisées</string>
<string name="settings_sync_wifi_only_ssids_message">Liste des points d\'accès WiFi (SSID) autorisés, séparés par des virgules. (Laissez vide pour tous)</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Méthode pour les contacts de type groupe</string>
<string-array name="settings_contact_group_method_values">
<item>GROUP_VCARDS</item>
<item>CATEGORIES</item>
</string-array>
<string-array name="settings_contact_group_method_entries">
<item>Les groupes sont des VCards indépendantes</item>
<item>Les groupes sont des catégories pour chacun des contacts</item>
</string-array>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Limite des événements passés</string>
<string name="settings_sync_time_range_past_none">Tous les événements seront synchronisés</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Les événements de plus dun jour passé seront ignorés</item>
<item quantity="other">Les événements de plus de %d jours passés seront ignorés</item>
</plurals>
<string name="settings_sync_time_range_past_message">Les événements antérieurs à ce nombre de jours seront ignorés (peut être 0). Laissez vide pour synchroniser tous les événements.</string>
<string name="settings_manage_calendar_colors">Choisir couleur du calendrier</string>
<string name="settings_manage_calendar_colors_on">Les couleurs de calendrier sont gérées par DAVdroid</string>
<string name="settings_manage_calendar_colors_off">Les couleurs de calendrier ne sont pas gérées par DAVdroid</string>
<string name="settings_event_colors">Couleur associée aux événements</string>
<string name="settings_event_colors_on">Synchroniser la couleur associée aux événements</string>
<string name="settings_event_colors_off">Ne pas synchroniser la couleur associée aux événements</string>
<string name="settings_event_colors_off_confirm">Modifier la couleur associée aux événements peut affecter les valeurs déjà synchronisées.</string>
<!--collection management-->
<string name="create_addressbook">Créer un carnet d\'adresses</string>
<string name="create_addressbook_display_name_hint">Mon carnet d\'adresses</string>
<string name="create_calendar">Créer une collection CalDAV</string>
<string name="create_calendar_display_name_hint">Mon calendrier</string>
<string name="create_calendar_time_zone">Fuseau horaire:</string>
<string name="create_calendar_type">Type de collection:</string>
<string name="create_calendar_type_only_events">Calendrier (événements seulement)</string>
<string name="create_calendar_type_only_tasks">Liste de tâches (tâches seulement)</string>
<string name="create_calendar_type_events_and_tasks">Fusionner (événements et tâches)</string>
<string name="create_collection_color">Choisir une couleur pour la collection</string>
<string name="create_collection_creating">Création collection</string>
<string name="create_collection_display_name">Le nom affiché (titre) pour cette collection:</string>
<string name="create_collection_display_name_required">Titre requis</string>
<string name="create_collection_description">Description (facultatif)</string>
<string name="create_collection_home_set">Accueil:</string>
<string name="create_collection_create">Créer</string>
<string name="delete_collection">Supprimer la collection</string>
<string name="delete_collection_confirm_title">Êtes-vous sur?</string>
<string name="delete_collection_confirm_warning">Cette collection (%s) et toutes ses données seront supprimées du serveur.</string>
<string name="delete_collection_deleting_collection">Suppression de la collection</string>
<!--ExceptionInfoFragment-->
<string name="exception">Une erreur est survenue.</string>
<string name="exception_httpexception">Une erreur HTTP est survenue.</string>
<string name="exception_ioexception">Une erreur I/O est survenue.</string>
<string name="exception_show_details">Voir détails</string>
<!--sync adapters and DebugInfoActivity-->
<string name="debug_info_title">Infos de débogage</string>
<string name="sync_error_permissions">Autorisations DAVdroid</string>
<string name="sync_error_permissions_text">Autorisations supplémentaires demandées</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid : Sécurité de la connexion</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid a rencontré un certificat inconnu. Voulez-vous lui faire confiance?</string>
</resources>

View File

@@ -1,195 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">DAVdroid Adresboek</string>
<string name="address_books_authority_title">Adresboeken</string>
<string name="help">Help</string>
<string name="manage_accounts">Beheer accounts</string>
<string name="please_wait">Een moment geduld...</string>
<string name="send">Verzenden</string>
<!--startup dialogs-->
<string name="startup_battery_optimization_disable">DAVdroid afsluiten</string>
<string name="startup_dont_show_again">Niet opnieuw weergeven</string>
<string name="startup_donate">Open-Source informatie</string>
<string name="startup_donate_message">We zijn blij dat je DAVdroid gebruikt, wat open-source software (GPLv3) is. Omdat de ontwikkeling van DAVdroid hard werk is en duizenden uren in beslag neemt. overweeg alstublieft een donatie.</string>
<string name="startup_donate_now">Toon donatie pagina</string>
<string name="startup_donate_later">Misschien later</string>
<string name="startup_google_play_accounts_removed">Play Store DRM fout-informatie</string>
<string name="startup_google_play_accounts_removed_message">Onder bepaalde omstandigheden, kan Play Store DRM ervoor zorgen dat accounts kwijt zijn na een herstart of na een DAVdroid update. Als dit probleem zich bij je voordoet (en alleen dan), Installeer dan \"DAVdroid JB Workaround\" vanuit de Play Store</string>
<string name="startup_opentasks_not_installed">OpenTasks niet geinstalleerd</string>
<string name="startup_opentasks_reinstall_davdroid">Na installatie van OpenTasks dient u DAVdroid opnieuw te installeren en de accounts toe te voegen (Android bug).</string>
<string name="startup_opentasks_not_installed_install">OpenTasks installeren</string>
<!--AboutActivity-->
<string name="about_license_info_no_warranty">Dit programma kom met ABSOLUUT GEEN GARANTIE. Het is gratis software, en je bent welkom dit te herdistribueren onder bepaalde voorwaarden.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVDroid bestand loggen</string>
<string name="logging_to_external_storage">Loggen naar externe opslag: %s</string>
<string name="logging_couldnt_create_file">Kon extern log bestand niet verwijderen: %s</string>
<string name="logging_no_external_storage">Externe opslag niet gevonden</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">Open navigatie drawer</string>
<string name="navigation_drawer_close">Sluit navigatie drawer</string>
<string name="navigation_drawer_subtitle">CalDAV/CardDav Sync adapter</string>
<string name="navigation_drawer_about">Over / Licentie</string>
<string name="navigation_drawer_beta_feedback">Beta terugkoppeling</string>
<string name="navigation_drawer_settings">Instellingen</string>
<string name="navigation_drawer_news_updates">Nieuws &amp; updates</string>
<string name="navigation_drawer_external_links">Externe links</string>
<string name="navigation_drawer_website">Website</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_forums">Help / Forums</string>
<string name="navigation_drawer_donate">Doneren</string>
<string name="account_list_empty">Welkom bij DAVdroid!\n\nJe kunt nu een CalDAV/CardDAv account toevoegen.</string>
<string name="accounts_global_sync_disabled">Systeembrede automatische synchronisatie is uitgeschakeld</string>
<string name="accounts_global_sync_enable">Inschakelen</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Service herkenning is mislukt</string>
<string name="dav_service_refresh_couldnt_refresh">Kon de collectie lijst niet vernieuwen</string>
<!--AppSettingsActivity-->
<string name="app_settings">Instellingen</string>
<string name="app_settings_user_interface">Gebruikers interface</string>
<string name="app_settings_reset_hints">Hints resetten </string>
<string name="app_settings_reset_hints_summary">Hints die al gezien zijn opnieuw weergeven</string>
<string name="app_settings_reset_hints_success">Alle hints worden opnieuw weergegeven</string>
<string name="app_settings_connection">Verbinding</string>
<string name="app_settings_override_proxy">Proxy instellingen overschrijven</string>
<string name="app_settings_override_proxy_on">Eigen proxy instellingen gebruiken</string>
<string name="app_settings_override_proxy_off">Systeem proxy instellingen gebruiken</string>
<string name="app_settings_override_proxy_host">HTTP proxy beheerder naam</string>
<string name="app_settings_override_proxy_port">HTTP proxy poort</string>
<string name="app_settings_security">Beveiliging</string>
<string name="app_settings_distrust_system_certs">Systeem certificaten niet vertrouwen</string>
<string name="app_settings_distrust_system_certs_on">Systeem en CAs van toegevoegde gebruiker wordt niet vertrouwd</string>
<string name="app_settings_distrust_system_certs_off">Systeem en CAs van toegevoegde gebruiker wordt vertrouwd (aanbevolen)</string>
<string name="app_settings_reset_certificates">Resetten (niet) vertrouwde certificaten</string>
<string name="app_settings_reset_certificates_summary">Resetten alle bewerkte certificaten</string>
<string name="app_settings_reset_certificates_success">Alle bewerkte certificaten zijn vrijgemaakt</string>
<string name="app_settings_debug">Debuggen</string>
<string name="app_settings_log_to_external_storage">Log naar extern bestand</string>
<string name="app_settings_log_to_external_storage_on">Loggen naar externe opslag (wanneer beschikbaar)</string>
<string name="app_settings_log_to_external_storage_off">Extern bestands loggen uitgeschakeld</string>
<string name="app_settings_show_debug_info">Debug info tonen</string>
<string name="app_settings_show_debug_info_details">Bekijk/deel software configuratie details</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Synchroniseer nu</string>
<string name="account_synchronizing_now">Aan het synchronizeren...</string>
<string name="account_settings">Account instellingen</string>
<string name="account_rename">Account hernoemen</string>
<string name="account_rename_new_name">Niet opgeslagen lokale informatie mag verloren gaan. Synchronisatie is noodzakelijk na hernoemen. Nieuw account naam:</string>
<string name="account_rename_rename">Hernoemen</string>
<string name="account_delete">Account verwijderen</string>
<string name="account_delete_confirmation_title">Account echt verwijderen?</string>
<string name="account_delete_confirmation_text">Alle lokale kopieën van adresboeken, agenda\'s en taken worden verwijderd.</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_refresh_address_book_list">Adresboeken vernieuwen</string>
<string name="account_create_new_address_book">Maak een nieuw adresboek</string>
<string name="account_refresh_calendar_list">Agenda\'s vernieuwen</string>
<string name="account_create_new_calendar">Maak een nieuwe agenda</string>
<string name="account_no_webcal_handler_found">Geen mogelijke Webcal app gevonden</string>
<string name="account_install_icsdroid">ICSdroid installeren</string>
<!--AddAccountActivity-->
<string name="login_title">Account toevoegen</string>
<string name="login_type_email">Inloggen met e-mailadres</string>
<string name="login_email_address">Email adres</string>
<string name="login_email_address_error">Geldig email adres vereist</string>
<string name="login_password">Wachtwoord</string>
<string name="login_password_required">Wachtwoord vereist</string>
<string name="login_type_url">Inloggen met URL en gebruikersnaam</string>
<string name="login_url_must_be_http_or_https">URL moet met http(s):// beginnen</string>
<string name="login_url_host_name_required">Hostnaam vereist</string>
<string name="login_user_name">Gebruikersnaam</string>
<string name="login_user_name_required">Gebruikersnaam vereist</string>
<string name="login_base_url">Basis URL</string>
<string name="login_login">Login</string>
<string name="login_back">Terug</string>
<string name="login_create_account">Maak een account</string>
<string name="login_account_name">Accountnaam</string>
<string name="login_account_name_info">Gebruik je email adres als account naam want Android zal je account naam gebruiken als ORGANIZER veld voor gemaakte afspraken. Je kunt geen 2 accounts met dezelfde naam hebben,</string>
<string name="login_account_contact_group_method">Contact groep methode:</string>
<string name="login_account_name_required">Accountnaam vereist</string>
<string name="login_account_not_created">Account kon niet gemaakt worden.</string>
<string name="login_configuration_detection">Configuratie detectie</string>
<string name="login_querying_server">Even geduld, verzoek naar server...</string>
<string name="login_no_caldav_carddav">Kon geen CalDAV of CardDAV service vinden.</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Instellingen: %s</string>
<string name="settings_authentication">Authenticatie</string>
<string name="settings_username">Gebruikersnaam</string>
<string name="settings_enter_username">Gebruikersnaam invoeren:</string>
<string name="settings_password">Wachtwoord</string>
<string name="settings_password_summary">Gebruik het zelfde wachtwoord als op de server.</string>
<string name="settings_enter_password">Wachtwoord invoeren:</string>
<string name="settings_sync">Synchronisatie</string>
<string name="settings_sync_interval_contacts">Contacten verversen</string>
<string name="settings_sync_summary_manually">Alleen handmatig</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Elke %d minuten + meteen na wijziging</string>
<string name="settings_sync_interval_calendars">Agenda\'s verversen</string>
<string name="settings_sync_interval_tasks">Taak sync. tussentijd</string>
<string name="settings_sync_wifi_only">Sync alleen tijdens WiFi</string>
<string name="settings_sync_wifi_only_on">Synchronisatie is voorbehouden tijdens WiFi verbindingen</string>
<string name="settings_sync_wifi_only_off">Verbinding type is niet overwogen</string>
<string name="settings_sync_wifi_only_ssids">WiFi SSID beperking</string>
<string name="settings_sync_wifi_only_ssids_on">Zal alleen synchroniseren over %s</string>
<string name="settings_sync_wifi_only_ssids_off">Alle WiFI verbindingen zullen worden gebruikt</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Contact groep methode</string>
<string-array name="settings_contact_group_method_values">
<item>GROUP_VCARDS</item>
<item>CATEGORIES</item>
</string-array>
<string-array name="settings_contact_group_method_entries">
<item>Groepen zijn apparte VCards</item>
<item>Groepen zijn per-contact categories</item>
</string-array>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Tijdslimiet verleden afspraken</string>
<string name="settings_sync_time_range_past_none">Alle afspraken worden gesynchronizeerd</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Afspraken ouder dan een dag worden genegeerd</item>
<item quantity="other">Afspraken ouder dan %d dagen worden genegeerd</item>
</plurals>
<string name="settings_sync_time_range_past_message">Afspraken ouder dan dit aantal dagen worden genegeerd (mag 0 zijn). Laat leeg om alle afspraken te synchronizeren.</string>
<string name="settings_manage_calendar_colors">Agenda kleuren beheren</string>
<string name="settings_manage_calendar_colors_on">Agenda kleuren worden door DAVdroid beheerd.</string>
<string name="settings_manage_calendar_colors_off">Agenda kleuren worden niet door DAVdroid ingesteld</string>
<string name="settings_event_colors">Evenement kleur ondersteuning</string>
<string name="settings_event_colors_on">Evenement kleuren synchroniseren</string>
<string name="settings_event_colors_off">Evenement kleuren niet synchroniseren</string>
<!--collection management-->
<string name="create_addressbook">Maak adresboek</string>
<string name="create_addressbook_display_name_hint">Mijn adresboek</string>
<string name="create_calendar">Maak CalDAV collectie</string>
<string name="create_calendar_display_name_hint">Mijn agenda</string>
<string name="create_calendar_time_zone">Tijdzone:</string>
<string name="create_calendar_type">Collectie type:</string>
<string name="create_calendar_type_only_events">Agenda (alleen afspraken)</string>
<string name="create_calendar_type_only_tasks">Takenlijst (alleen taken)</string>
<string name="create_calendar_type_events_and_tasks">Gecombineerd (afspraken en taken)</string>
<string name="create_collection_color">Stel een collectie kleur in</string>
<string name="create_collection_creating">Collectie aan het maken</string>
<string name="create_collection_display_name">Weergave naam (titel) van deze collectie:</string>
<string name="create_collection_display_name_required">Titel is vereist</string>
<string name="create_collection_description">Beschrijving (optioneel):</string>
<string name="create_collection_home_set">Begin map:</string>
<string name="create_collection_create">Maak</string>
<string name="delete_collection">Verwijder collectie</string>
<string name="delete_collection_confirm_title">Weet je het zeker?</string>
<string name="delete_collection_confirm_warning">Deze collectie (%s) en alle data zal verwijderd worden van de server.</string>
<string name="delete_collection_deleting_collection">Collectie aan het verwijderen</string>
<!--ExceptionInfoFragment-->
<string name="exception">Er is een fout opgetreden.</string>
<string name="exception_httpexception">Er is een HTTP fout opgetreden.</string>
<string name="exception_ioexception">Er is een I/O fout opgetreden.</string>
<string name="exception_show_details">Toon details</string>
<!--sync adapters and DebugInfoActivity-->
<string name="debug_info_title">Debug informatie</string>
<string name="sync_contacts_read_only_address_book">Alleen-lezen adresboek</string>
<string name="sync_error_permissions">DAVdroid rechten</string>
<string name="sync_error_permissions_text">Aanvullende rechten vereist</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: Verbinding beveiliging</string>
<string name="trust_certificate_unknown_certificate_found">Davdroid is benaderd door een onbekend certificaat. Vertrouwd u dit?</string>
</resources>

View File

@@ -1,225 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">Książka adresowa DAVdroid</string>
<string name="address_books_authority_title">Książka adresowa</string>
<string name="help">Pomoc</string>
<string name="manage_accounts">Zadządzaj kontami</string>
<string name="please_wait">Proszę czekać</string>
<string name="send">Wyślij</string>
<string name="notification_channel_debugging">Debugowanie</string>
<!--startup dialogs-->
<string name="startup_battery_optimization_disable">Wyłącz dla DAVdroid</string>
<string name="startup_dont_show_again">Nie pokazuj ponownie</string>
<string name="startup_donate">Informacje Open-Source</string>
<string name="startup_donate_message">Jesteśmy szczęśliwi, że używasz DAVdroid, który jest oprogramowaniem open-source (GPLv3). Ponieważ rozwijanie DAVdroid jest ciężką pracą i zajęło nam tysiące godzin pracy, prosimy o rozważenie darowizny.</string>
<string name="startup_donate_now">Pokaż stronę darowizny</string>
<string name="startup_donate_later">Może później</string>
<string name="startup_google_play_accounts_removed">Informacje o błędzie DRM Sklepu Play</string>
<string name="startup_google_play_accounts_removed_message">Pod pewnymi warunkami, DRM Sklepu Play może powodować, że wszystkie konta DAVdroid mogą zostać usunięte po uruchomieniu lub po uaktualnieniu DAVdroid. Jeśli jesteś dotknięty tym problemem (i tylko wtedy) należy zainstalować \"DAVdroid JB Obejście\" ze Sklepu Play.</string>
<string name="startup_opentasks_not_installed">OpenTasks nie jest zainstalowany</string>
<string name="startup_opentasks_reinstall_davdroid">Po zainstalowaniu OpenTasks konieczne jest PRZEINSTALOWANIE DAVdroid i ponowne dodanie twoich kont (błąd Androida).</string>
<string name="startup_opentasks_not_installed_install">Zainstaluj OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_info_no_warranty">Ten program jest ABSOLUTNIE BEZ GWARANCJI. To jest wolne oprogramowanie i mile widziane jest dalsze rozpowszechnianie go pod pewnymi warunkami.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">Plik logów DAVdroid</string>
<string name="logging_to_external_storage">Logowanie do zewnątrznej pamięci: %s</string>
<string name="logging_couldnt_create_file">Nie można stworzyć zewnętrznego pliku logów: %s</string>
<string name="logging_no_external_storage">Zewnętrzna pamięci nie została naleziona</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">Otwórz menu nawigacji</string>
<string name="navigation_drawer_close">Zamknij menu nawigacji</string>
<string name="navigation_drawer_subtitle">Adapter synchronizacji CalDAV/CardDAV</string>
<string name="navigation_drawer_about">O DAVdroid / Licencja</string>
<string name="navigation_drawer_beta_feedback">Przekaż opinię</string>
<string name="navigation_drawer_settings">Ustawienia</string>
<string name="navigation_drawer_news_updates">Nowości &amp; aktualizacje</string>
<string name="navigation_drawer_external_links">Zewnętrzne odnośniki</string>
<string name="navigation_drawer_website">Strona WWW</string>
<string name="navigation_drawer_manual">Ręcznie</string>
<string name="navigation_drawer_faq">Pytania i odpowiedzi</string>
<string name="navigation_drawer_forums">Pomoc / Forum</string>
<string name="navigation_drawer_donate">Dotacja</string>
<string name="account_list_empty">Witamy w DAVdroid!\n\nMożesz teraz dodać konto CalDAV/CardDAV.</string>
<string name="accounts_global_sync_disabled">Automatyczna synchronizacja dla całego systemu jest wyłączona</string>
<string name="accounts_global_sync_enable">Włącz</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Wykrycie serwisu nie powiodło się</string>
<string name="dav_service_refresh_couldnt_refresh">Nie można odświeżyć listy kolekcji</string>
<!--AppSettingsActivity-->
<string name="app_settings">Ustawienia</string>
<string name="app_settings_user_interface">Interfejs użytkownika</string>
<string name="app_settings_reset_hints">Zresetuj podpowiedzi</string>
<string name="app_settings_reset_hints_summary">Ponownie włącz wskazówki, które zostały usunięte wcześniej</string>
<string name="app_settings_reset_hints_success">Wszystkie wskazówki pojawią się ponownie</string>
<string name="app_settings_connection">Łączność</string>
<string name="app_settings_override_proxy">Nadpisz ustawienia proxy</string>
<string name="app_settings_override_proxy_on">Użyj niestandardowych ustawień proxy </string>
<string name="app_settings_override_proxy_off">Użyj systemowych ustawień proxy</string>
<string name="app_settings_override_proxy_host">Nazwa hosta proxy HTTP</string>
<string name="app_settings_override_proxy_port">Port proxy HTTP</string>
<string name="app_settings_security">Bezpieczeństwo</string>
<string name="app_settings_distrust_system_certs">Usuń certyfikaty systemowe</string>
<string name="app_settings_distrust_system_certs_on">CA systemowe i użytkownika nie zostaną dodane</string>
<string name="app_settings_distrust_system_certs_off">CA systemowe i użytkownika zostaną dodane (zalecane)</string>
<string name="app_settings_reset_certificates">Zresetuj (nie)zaufane certyfikaty</string>
<string name="app_settings_reset_certificates_summary">Zresetuj wszystkie niestandardowe certyfikaty.</string>
<string name="app_settings_reset_certificates_success">Wszystkie niestandardowe certyfikaty zostały wyczyszczone</string>
<string name="app_settings_debug">Debugowanie</string>
<string name="app_settings_log_to_external_storage">Loguj do zewnętrznego pliku</string>
<string name="app_settings_log_to_external_storage_on">Logowanie do zewnętrznej pamięci (jeśli jest dostępna)</string>
<string name="app_settings_log_to_external_storage_off">Logowanie do zewnętrznego pliku jest wyłączone</string>
<string name="app_settings_show_debug_info">Pokaż informacje do debugowania</string>
<string name="app_settings_show_debug_info_details">Przejrzyj lub udostępnij informacje o programie i jego konfiguracji</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Synchronizuj teraz</string>
<string name="account_synchronizing_now">Synchronizcja w toku</string>
<string name="account_settings">Ustawienia konta</string>
<string name="account_rename">Zmień nazwę konta</string>
<string name="account_rename_new_name">Niezapisane dane lokalne mogą zostać usunięte. Ponowna synchronizacja jest wymagana po zmianie nazwy. Nowa nazwa konta:</string>
<string name="account_rename_rename">Zmień nazwę</string>
<string name="account_delete">Usuń konto</string>
<string name="account_delete_confirmation_title">Naprawdę chcesz usunąć konto?</string>
<string name="account_delete_confirmation_text">Wszystkie lokalne kopie książek adresowych, kalendarzy i list zadań zostaną usunięte.</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_synchronize_this_collection">Synchronizuj kolekcję</string>
<string name="account_read_only">tylko do odczytu</string>
<string name="account_calendar">kalendarz</string>
<string name="account_task_list">lista zadań</string>
<string name="account_refresh_address_book_list">Odśwież listę książek adresowych</string>
<string name="account_create_new_address_book">Stwórz nową książkę adresową</string>
<string name="account_refresh_calendar_list">Odśwież listę kalendarzy</string>
<string name="account_create_new_calendar">Stwórz nowy kalendarz</string>
<string name="account_no_webcal_handler_found">Nie znaleziono aplikacji obsługującej Webcal</string>
<string name="account_install_icsdroid">Zainstaluj ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_title">Dodaj konto</string>
<string name="login_type_email">Logowanie za pomocą adresu e-mail</string>
<string name="login_email_address">Adres e-mail</string>
<string name="login_email_address_error">Wymagany poprawny adres e-mail</string>
<string name="login_password">Hasło</string>
<string name="login_password_required">Wymagane hasło</string>
<string name="login_type_url">Logowanie za pomocą adresu URL i nazwy użytkownika</string>
<string name="login_url_must_be_http_or_https">URL musi zaczynać się od http(s)://</string>
<string name="login_url_must_be_https">Adres URL musi zaczynać się od https://</string>
<string name="login_url_host_name_required">Wymagana nazwa hosta</string>
<string name="login_user_name">Nazwa użytkownika</string>
<string name="login_user_name_required">Wymagana nazwa użytkownika</string>
<string name="login_base_url">Podstawowy URL</string>
<string name="login_type_url_certificate">Logowanie za pomocą adresu URL i certyfikatu klienta</string>
<string name="login_select_certificate">Wybierz certyfikat</string>
<string name="login_login">Zaloguj</string>
<string name="login_back">Wróć</string>
<string name="login_create_account">Stwórz konto</string>
<string name="login_account_name">Nazwa konta</string>
<string name="login_account_name_info">Użyj swojego adresu e-mail jako nazwy konta, ponieważ Android będzie używał nazwy konta jako pola ORGANIZATOR dla wydarzeń, które stworzysz. Nie możesz posiadać dwóch kont o takiej samej nazwie.</string>
<string name="login_account_contact_group_method">Metoda grupowania kontaktów:</string>
<string name="login_account_name_required">Wymagana nazwa konta</string>
<string name="login_account_not_created">Konto nie mogło zostać stworzone</string>
<string name="login_configuration_detection">Wykrywanie konfiguracji</string>
<string name="login_querying_server">Proszę czekać, odpytywanie serwera...</string>
<string name="login_no_caldav_carddav">Nie można znaleźć usługi CalDAV lub CardDAV.</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Ustawienia: %s</string>
<string name="settings_authentication">Uwierzytelnianie</string>
<string name="settings_username">Nazwa użytkownika</string>
<string name="settings_enter_username">Wpisz nazwę użytkownika:</string>
<string name="settings_password">Hasło</string>
<string name="settings_password_summary">Zaktualizuj hasło zgodnie z serwerem.</string>
<string name="settings_enter_password">Wpisz hasło:</string>
<string name="settings_certificate_alias">Alias certyfikatu klienta</string>
<string name="settings_sync">Synchronizacja</string>
<string name="settings_sync_interval_contacts">Częstotliwość synchronizacji kontaktów</string>
<string name="settings_sync_summary_manually">Tylko ręcznie</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Co %d minut oraz natychmiast przy zmianach lokalnych</string>
<string name="settings_sync_interval_calendars">Częstotliwość synchronizacji kalendarzy</string>
<string name="settings_sync_interval_tasks">Częstotliwość synchronizacji list zadań</string>
<string-array name="settings_sync_interval_names">
<item>Tylko ręcznie</item>
<item>Co 15 minut</item>
<item>Co 30 minut</item>
<item>Co godzinę</item>
<item>Co 2 godziny</item>
<item>Co 4 godziny</item>
<item>Raz dziennie</item>
</string-array>
<string name="settings_sync_wifi_only">Synchronizuj tylko przez WiFi</string>
<string name="settings_sync_wifi_only_on">Synchronizacja jest ograniczona do połączeń WiFi</string>
<string name="settings_sync_wifi_only_off">Rodzaj połączenia nie jest brany pod uwagę</string>
<string name="settings_sync_wifi_only_ssids">Ograniczenia WiFi SSID</string>
<string name="settings_sync_wifi_only_ssids_on">Będzie synchronizować tylko w %s</string>
<string name="settings_sync_wifi_only_ssids_off">Wszystkie połączenia WiFi będą używane</string>
<string name="settings_sync_wifi_only_ssids_message">Nazwy oddzielone przecinkami (SSID) dozwolonych sieci WiFi (pozostaw puste dla wszystkich)</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Metoda grupowania kontaktów</string>
<string-array name="settings_contact_group_method_values">
<item>GROUP_VCARDS</item>
<item>CATEGORIES</item>
</string-array>
<string name="settings_contact_group_method_change">Zmień metodę grupową</string>
<string name="settings_contact_group_method_change_reload_contacts">Wymaga to ponownego pobrania wszystkich kontaktów. Niezapisane zmiany z tego telefonu zostaną usunięte.</string>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Limit czasowy przeszłych wydarzeń</string>
<string name="settings_sync_time_range_past_none">Wszystkie wydarzenia zostaną zsynchronizowane</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Wydarzenia starsze niż jeden dzień zostaną zignorowane.</item>
<item quantity="few">Wydarzenia starsze niż %d dni zostaną zignorowane.</item>
<item quantity="many">Wydarzenia starsze niż %d dni zostaną zignorowane.</item>
<item quantity="other">Wydarzenia starsze niż %d dni zostaną zignorowane.</item>
</plurals>
<string name="settings_sync_time_range_past_message">Wydarzenia, które są starsze niż podana liczba dni zostaną zignorowane (może być 0). Zostaw puste, aby synchronizować wszystkie wydarzenia.</string>
<string name="settings_manage_calendar_colors">Zarządzaj kolorami kalendarza</string>
<string name="settings_manage_calendar_colors_on">Kolory kalendarza są zarządzane przez DAVdroid</string>
<string name="settings_manage_calendar_colors_off">Kolory kalendarze nie są ustawiane przez DAVdroid</string>
<string name="settings_event_colors">Obsługa kolorów wydarzeń</string>
<string name="settings_event_colors_on">Synchronizuj kolory zdarzeń</string>
<string name="settings_event_colors_off">Nie synchronizuj kolorów zdarzeń</string>
<string name="settings_event_colors_off_confirm">Wyłączenie kolorów zdarzeń może usunąć już zsynchronizowane kolory zdarzeń.</string>
<!--collection management-->
<string name="create_addressbook">Stwórz książkę adresową</string>
<string name="create_addressbook_display_name_hint">Moja książka adresowa</string>
<string name="create_calendar">Stwórz kolekcję CalDAV</string>
<string name="create_calendar_display_name_hint">Mój kalendarz</string>
<string name="create_calendar_time_zone">Strefa czasowa:</string>
<string name="create_calendar_type">Typ kolekcji:</string>
<string name="create_calendar_type_only_events">Kalendarz (tylko wydarzenia)</string>
<string name="create_calendar_type_only_tasks">Lista zadań (tylko zadań)</string>
<string name="create_calendar_type_events_and_tasks">Połączone (wydarzenia i zadania)</string>
<string name="create_collection_color">Ustaw kolor kolekcji</string>
<string name="create_collection_creating">Tworzenie kolekcji</string>
<string name="create_collection_display_name">Nazwa wyświetlana (tytuł) kolekcji:</string>
<string name="create_collection_display_name_required">Tytuł jest wymagany</string>
<string name="create_collection_description">Opis (opcjonalnie)</string>
<string name="create_collection_home_set">Ustaw początek:</string>
<string name="create_collection_create">Stwórz</string>
<string name="delete_collection">Usuń kolekcję</string>
<string name="delete_collection_confirm_title">Czy jesteś pewien?</string>
<string name="delete_collection_confirm_warning">Kolekcja (%s) i jej wszystkie dane zostaną usunięte z serwera.</string>
<string name="delete_collection_deleting_collection">Usuwanie kolekcji</string>
<string name="collection_force_read_only">Wymuś tylko do odczytu</string>
<!--ExceptionInfoFragment-->
<string name="exception">Wystąpił błąd.</string>
<string name="exception_httpexception">Wystąpił błąd HTTP.</string>
<string name="exception_ioexception">Wystąpił błąd I/O.</string>
<string name="exception_show_details">Pokaż szczegóły</string>
<!--sync adapters and DebugInfoActivity-->
<string name="debug_info_title">Informacje debugowe</string>
<string name="sync_contacts_read_only_address_book">Książka adresowa tylko do odczytu</string>
<plurals name="sync_contacts_local_contact_changes_discarded">
<item quantity="one">Lokalny kontakt zostanie odrzucony</item>
<item quantity="few">%d lokalne kontakty zostaną odrzucone</item>
<item quantity="many">%d lokalne kontakty zostaną odrzucone</item>
<item quantity="other">%d lokalne kontakty zostaną odrzucone</item>
</plurals>
<string name="sync_error_permissions">Uprawnienia DAVdroid</string>
<string name="sync_error_permissions_text">Wymagane dodatkowe uprawnienia</string>
<string name="sync_error_opentasks_too_old">OpenTask jest niekompatybilny</string>
<string name="sync_error_opentasks_required_version">Wymagana wersja: %1$s (obecna %2$s)</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: Bezpieczeństwo połączenia</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid napotkał nieznany certyfikat. Czy chcesz go dodać?</string>
</resources>

View File

@@ -1,17 +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
-->
<resources>
<style name="AppThemeExt" parent="AppTheme">
<item name="android:windowActivityTransitions">true</item>
<item name="android:windowEnterTransition">@android:transition/slide_right</item>
<item name="android:windowExitTransition">@android:transition/slide_left</item>
</style>
</resources>

View File

@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="utf-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
-->
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_about"
android:icon="@drawable/ic_info_dark"
android:title="@string/navigation_drawer_about"/>
<item
android:id="@+id/nav_beta_feedback"
android:icon="@drawable/ic_forum_dark"
android:title="@string/navigation_drawer_beta_feedback"
android:visible="false"/>
<item
android:id="@+id/nav_app_settings"
android:icon="@drawable/ic_settings_dark"
android:title="@string/navigation_drawer_settings"/>
<item android:title="@string/navigation_drawer_news_updates">
<menu>
<item
android:id="@+id/nav_twitter"
android:icon="@drawable/twitter"
android:title="\@davdroidapp"
tools:ignore="HardcodedText"/>
</menu>
</item>
<item android:title="@string/navigation_drawer_external_links">
<menu>
<item
android:id="@+id/nav_website"
android:icon="@drawable/ic_home_dark"
android:title="@string/navigation_drawer_website"/>
<item
android:id="@+id/nav_manual"
android:icon="@drawable/ic_info_dark"
android:title="@string/navigation_drawer_manual"/>
<item
android:id="@+id/nav_faq"
android:icon="@drawable/ic_help_dark"
android:title="@string/navigation_drawer_faq"/>
<item
android:id="@+id/nav_forums"
android:icon="@drawable/ic_forum_dark"
android:title="@string/navigation_drawer_forums"/>
<item
android:id="@+id/nav_donate"
android:icon="@drawable/ic_attach_money_dark"
android:title="@string/navigation_drawer_donate"
android:visible="false"/>
</menu>
</item>
</menu>

View File

@@ -1,11 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2013 2015 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
-->
<manifest package="at.bitfire.davdroid"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
@@ -25,13 +18,6 @@
<uses-permission android:name="android.permission.GET_ACCOUNTS" android:maxSdkVersion="22"/>
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" android:maxSdkVersion="22"/>
<!--
for writing external log files; permission only required for SDK <= 18 because since then,
writing to app-private directory doesn't require extra permissions
-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="18"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="18"/>
<!-- other permissions -->
<!-- android.permission-group.CONTACTS -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
@@ -41,24 +27,27 @@
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
<!-- android.permission-group.LOCATION -->
<!-- required since Android 8.1 to get the WiFi name (for "sync in Wifi only" feature) -->
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- getting the WiFi name (for "sync in Wifi only") requires
- coarse location (Android 8.1)
- fine location (Android 10) -->
<uses-permission-sdk-23 android:name="android.permission.ACCESS_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 -->
<application
android:name=".App"
android:allowBackup="true"
android:fullBackupContent="false"
android:allowBackup="false"
android:networkSecurityConfig="@xml/network_security_config"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppThemeExt"
android:theme="@style/AppTheme"
tools:ignore="UnusedAttribute">
<service android:name=".DavService"/>
<service android:name=".settings.Settings"/>
<activity android:name=".ui.intro.IntroActivity" android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".ui.AccountsActivity"
android:label="@string/app_name"
@@ -78,7 +67,13 @@
<activity
android:name=".ui.AppSettingsActivity"
android:label="@string/app_settings"
android:parentActivityName=".ui.AccountsActivity"/>
android:parentActivityName=".ui.AccountsActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ui.setup.LoginActivity"
@@ -90,10 +85,10 @@
</activity>
<activity
android:name=".ui.AccountActivity"
android:parentActivityName=".ui.AccountsActivity">
</activity>
<activity android:name=".ui.AccountSettingsActivity"/>
android:name=".ui.account.AccountActivity"
android:parentActivityName=".ui.AccountsActivity"
android:theme="@style/AppTheme.NoActionBar"/>
<activity android:name=".ui.account.SettingsActivity"/>
<activity android:name=".ui.CreateAddressBookActivity"
android:label="@string/create_addressbook"/>
<activity android:name=".ui.CreateCalendarActivity"
@@ -104,18 +99,27 @@
android:parentActivityName=".ui.AppSettingsActivity"
android:exported="true"
android:label="@string/debug_info_title">
<intent-filter>
<action android:name="android.intent.action.BUG_REPORT"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="@string/authority_log_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/log_paths" />
android:resource="@xml/debug_paths" />
</provider>
<!-- account type "DAVdroid" -->
<activity
android:name=".ui.PermissionsActivity"
android:label="@string/app_settings_security_app_permissions"
android:parentActivityName=".ui.AppSettingsActivity" />
<!-- account type "DAVx⁵" -->
<service
android:name=".syncadapter.AccountAuthenticatorService"
android:exported="false">
@@ -130,7 +134,6 @@
<service
android:name=".syncadapter.CalendarsSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
@@ -143,7 +146,6 @@
<service
android:name=".syncadapter.TasksSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
@@ -154,7 +156,7 @@
android:resource="@xml/sync_tasks"/>
</service>
<!-- account type "DAVdroid Address book" -->
<!-- account type "DAVx⁵ Address book" -->
<service
android:name=".syncadapter.NullAuthenticatorService"
android:exported="false">
@@ -175,7 +177,6 @@
<service
android:name=".syncadapter.AddressBooksSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
@@ -188,7 +189,6 @@
<service
android:name=".syncadapter.ContactsSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>

View File

@@ -1,28 +0,0 @@
package at.bitfire.davdroid.settings;
import at.bitfire.davdroid.settings.ISettingsObserver;
interface ISettings {
void forceReload();
boolean has(String key);
boolean getBoolean(String key, boolean defaultValue);
int getInt(String key, int defaultValue);
long getLong(String key, long defaultValue);
String getString(String key, String defaultValue);
boolean isWritable(String key);
boolean putBoolean(String key, boolean value);
boolean putInt(String key, int value);
boolean putLong(String key, long value);
boolean putString(String key, String value);
boolean remove(String key);
void registerObserver(ISettingsObserver observer);
void unregisterObserver(ISettingsObserver observer);
}

View File

@@ -1,7 +0,0 @@
package at.bitfire.davdroid.settings;
interface ISettingsObserver {
void onSettingsChanged();
}

View File

@@ -1,78 +1,10 @@
<?xml version="1.0" encoding="utf-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
-->
<resources>
<string name="define_ambilwarna" translatable="false" />
<string name="library_ambilwarna_author" translatable="false">Randy Sugianto</string>
<string name="library_ambilwarna_authorWebsite">https://github.com/yukuku</string>
<string name="library_ambilwarna_libraryName" translatable="false">AmbilWarna</string>
<string name="library_ambilwarna_libraryDescription">This is a small library for your application to enable the users to select an arbitrary color.</string>
<string name="library_ambilwarna_libraryWebsite" translatable="false">https://github.com/yukuku/ambilwarna</string>
<string name="library_ambilwarna_isOpenSource" translatable="false">true</string>
<string name="library_ambilwarna_licenseId">apache_2_0</string>
<string name="define_commons" translatable="false" />
<string name="library_commons_author" translatable="false">Apache Software Foundation</string>
<string name="library_commons_authorWebsite">https://www.apache.org/</string>
<string name="library_commons_libraryName" translatable="false">Apache Commons</string>
<string name="library_commons_libraryDescription">Apache Commons is an Apache project focused on all aspects of reusable Java components.</string>
<string name="library_commons_libraryWebsite" translatable="false">https://commons.apache.org/components.html</string>
<string name="library_commons_isOpenSource" translatable="false">true</string>
<string name="library_commons_licenseId">apache_2_0</string>
<string name="define_dnsjava" translatable="false" />
<string name="library_dnsjava_author" translatable="false">Brian Wellington</string>
<string name="library_dnsjava_authorWebsite">http://www.xbill.org/~bwelling/</string>
<string name="library_dnsjava_libraryName" translatable="false">dnsjava</string>
<string name="library_dnsjava_libraryDescription">dnsjava is an implementation of DNS in Java.</string>
<string name="library_dnsjava_libraryWebsite" translatable="false">http://dnsjava.org/</string>
<string name="library_dnsjava_isOpenSource" translatable="false">true</string>
<string name="library_dnsjava_licenseId">bsd_3</string>
<string name="define_ezvcard" translatable="false" />
<string name="library_ezvcard_author" translatable="false">Michael Angstadt</string>
<string name="library_ezvcard_authorWebsite" translatable="false">http://mikeangstadt.name/</string>
<string name="library_ezvcard_libraryName" translatable="false">ez-vcard</string>
<string name="library_ezvcard_libraryDescription">ez-vcard is a vCard library written in Java.</string>
<string name="library_ezvcard_libraryWebsite" translatable="false">https://github.com/mangstadt/ez-vcard</string>
<string name="library_ezvcard_isOpenSource" translatable="false">true</string>
<string name="library_ezvcard_licenseId">bsd_2</string>
<string name="define_ical4j" translatable="false" />
<string name="library_ical4j_author" translatable="false">Ben Fortuna</string>
<string name="library_ical4j_authorWebsite">http://basepatterns.org/</string>
<string name="library_ical4j_libraryName" translatable="false">ical4j</string>
<string name="library_ical4j_libraryDescription">iCal4j is a Java API that provides support for the iCalendar specification as defined in RFC2445.</string>
<string name="library_ical4j_libraryWebsite" translatable="false">https://ical4j.github.io/</string>
<string name="library_ical4j_isOpenSource" translatable="false">true</string>
<string name="library_ical4j_licenseId">bsd_3</string>
<string name="define_okhttp" translatable="false" />
<string name="library_okhttp_author" translatable="false">Square, Inc.</string>
<string name="library_okhttp_authorWebsite">https://squareup.com/</string>
<string name="library_okhttp_libraryName" translatable="false">okhttp</string>
<string name="library_okhttp_libraryDescription">An HTTP+HTTP/2 client for Android and Java applications that\s efficient by default.</string>
<string name="library_okhttp_libraryWebsite" translatable="false">http://square.github.io/okhttp/</string>
<string name="library_okhttp_isOpenSource" translatable="false">true</string>
<string name="library_okhttp_licenseId">apache_2_0</string>
<!-- license texts -->
<string name="gpl_v3"><![CDATA[
<h3 style="text-align: center;">GNU GENERAL PUBLIC LICENSE</h3>
<h3 style="text-align: center;">GNU GENERAL PUBLIC LICENSE</h3>
<p style="text-align: center;">Version 3, 29 June 2007</p>
<p>Copyright &copy; 2007 Free Software Foundation, Inc.
&lt;<a href="http://fsf.org/">http://fsf.org/</a>&gt;</p><p>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.</p>
&lt;<a href="http://fsf.org/">http://fsf.org/</a>&gt;</p><p>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.</p>
<h3><a name="preamble"></a>Preamble</h3>
@@ -694,6 +626,3 @@ Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.</p>
<p>END OF TERMS AND CONDITIONS</p>
]]></string>
</resources>

View File

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

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,29 @@
package at.bitfire.davdroid
import android.content.Context
abstract class AndroidSingleton<T> {
var creatingSingleton = false
var singleton: T? = null
@Synchronized
fun getInstance(context: Context): T {
singleton?.let {
return it
}
if (creatingSingleton)
throw IllegalStateException("AndroidSingleton::getInstance() must not be called while createInstance()")
creatingSingleton = true
val newSingleton = createInstance(context.applicationContext)
singleton = newSingleton
creatingSingleton = false
return newSingleton
}
abstract fun createInstance(context: Context): T
}

View File

@@ -11,56 +11,26 @@ 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 android.support.v7.app.AppCompatDelegate
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.graphics.drawable.toBitmap
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import kotlin.concurrent.thread
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.logging.Level
import kotlin.system.exitProcess
class App: Application() {
@Suppress("unused")
class App: Application(), Thread.UncaughtExceptionHandler {
companion object {
const val FLAVOR_GOOGLE_PLAY = "gplay"
const val FLAVOR_ICLOUD = "icloud"
const val FLAVOR_MANAGED = "managed"
const val FLAVOR_SOLDUPE = "soldupe"
const val FLAVOR_STANDARD = "standard"
const val ORGANIZATION = "organization"
const val ORGANIZATION_LOGO_URL = "logo_url"
const val SUPPORT_HOMEPAGE = "support_homepage_url"
const val SUPPORT_PHONE = "support_phone_number"
const val SUPPORT_EMAIL = "support_email_address"
const val MAX_ACCOUNTS = "max_accounts"
const val DISTRUST_SYSTEM_CERTIFICATES = "distrust_system_certs"
const val OVERRIDE_PROXY = "override_proxy"
const val OVERRIDE_PROXY_HOST = "override_proxy_host"
const val OVERRIDE_PROXY_PORT = "override_proxy_port"
const val OVERRIDE_PROXY_HOST_DEFAULT = "localhost"
const val OVERRIDE_PROXY_PORT_DEFAULT = 8118
fun getLauncherBitmap(context: Context): Bitmap? {
val drawableLogo = if (android.os.Build.VERSION.SDK_INT >= 21)
context.getDrawable(R.mipmap.ic_launcher)
else
@Suppress("deprecation")
context.resources.getDrawable(R.mipmap.ic_launcher)
return if (drawableLogo is BitmapDrawable)
drawableLogo.bitmap
else
null
}
fun getLauncherBitmap(context: Context) =
AppCompatResources.getDrawable(context, R.mipmap.ic_launcher)?.toBitmap()
fun homepageUrl(context: Context) =
Uri.parse(context.getString(R.string.homepage_url)).buildUpon()
@@ -76,7 +46,8 @@ class App: Application() {
super.onCreate()
Logger.initialize(this)
if (BuildConfig.DEBUG) {
if (BuildConfig.DEBUG)
// debug builds
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectActivityLeaks()
.detectFileUriExposure()
@@ -85,31 +56,31 @@ class App: Application() {
.detectLeakedSqlLiteObjects()
.penaltyLog()
.build())
// main thread
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.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)
// don't block UI for some background checks
thread {
CoroutineScope(Dispatchers.Default).launch {
// 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)
OpenTasksWatcher(this@App)
// check whether a tasks app is currently installed
PackageChangedReceiver.updateTaskSync(this)
OpenTasksWatcher.updateTaskSync(this@App)
}
}
}
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)
startActivity(intent)
exitProcess(1)
}
}

View File

@@ -0,0 +1,12 @@
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()
}

View File

@@ -1,165 +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.os.Build
import at.bitfire.davdroid.log.Logger
import java.io.IOException
import java.net.InetAddress
import java.net.Socket
import java.security.GeneralSecurityException
import java.util.*
import javax.net.ssl.*
/**
* Custom TLS socket factory with support for
* - enabling/disabling algorithms depending on the Android version,
* - client certificate authentication
*/
class CustomTlsSocketFactory(
keyManager: KeyManager?,
trustManager: X509TrustManager
): SSLSocketFactory() {
private var delegate: SSLSocketFactory
companion object {
// Android 5.0+ (API level 21) provides reasonable default settings
// but it still allows SSLv3
// https://developer.android.com/reference/javax/net/ssl/SSLSocket.html
var protocols: Array<String>? = null
var cipherSuites: Array<String>? = null
init {
if (Build.VERSION.SDK_INT >= 23) {
// Since Android 6.0 (API level 23),
// - TLSv1.1 and TLSv1.2 is enabled by default
// - SSLv3 is disabled by default
// - all modern ciphers are activated by default
protocols = null
cipherSuites = null
Logger.log.fine("Using device default TLS protocols/ciphers")
} else {
(SSLSocketFactory.getDefault().createSocket() as? SSLSocket)?.use { socket ->
try {
/* set reasonable protocol versions */
// - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0)
// - remove all SSL versions (especially SSLv3) because they're insecure now
val _protocols = LinkedList<String>()
for (protocol in socket.supportedProtocols.filterNot { it.contains("SSL", true) })
_protocols += protocol
Logger.log.info("Enabling (only) these TLS protocols: ${_protocols.joinToString(", ")}")
protocols = _protocols.toTypedArray()
/* set up reasonable cipher suites */
val knownCiphers = arrayOf(
// TLS 1.2
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
// maximum interoperability
"TLS_RSA_WITH_3DES_EDE_CBC_SHA",
"SSL_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_RSA_WITH_AES_128_CBC_SHA",
// additionally
"TLS_RSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"
)
val availableCiphers = socket.supportedCipherSuites
Logger.log.info("Available cipher suites: ${availableCiphers.joinToString(", ")}")
/* For maximum security, preferredCiphers should *replace* enabled ciphers (thus
* disabling ciphers which are enabled by default, but have become unsecure), but for
* the security level of DAVdroid and maximum compatibility, disabling of insecure
* ciphers should be a server-side task */
// for the final set of enabled ciphers, take the ciphers enabled by default, ...
val _cipherSuites = LinkedList<String>()
_cipherSuites.addAll(socket.enabledCipherSuites)
Logger.log.fine("Cipher suites enabled by default: ${_cipherSuites.joinToString(", ")}")
// ... add explicitly allowed ciphers ...
_cipherSuites.addAll(knownCiphers)
// ... and keep only those which are actually available
_cipherSuites.retainAll(availableCiphers)
Logger.log.info("Enabling (only) these TLS ciphers: " + _cipherSuites.joinToString(", "))
cipherSuites = _cipherSuites.toTypedArray()
} catch (e: IOException) {
Logger.log.severe("Couldn't determine default TLS settings")
}
}
}
}
}
init {
try {
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(
if (keyManager != null) arrayOf(keyManager) else null,
arrayOf(trustManager),
null)
delegate = sslContext.socketFactory
} catch (e: GeneralSecurityException) {
throw IllegalStateException() // system has no TLS
}
}
override fun getDefaultCipherSuites(): Array<String>? = cipherSuites ?: delegate.defaultCipherSuites
override fun getSupportedCipherSuites(): Array<String>? = cipherSuites ?: delegate.supportedCipherSuites
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
val ssl = delegate.createSocket(s, host, port, autoClose)
if (ssl is SSLSocket)
upgradeTLS(ssl)
return ssl
}
override fun createSocket(host: String, port: Int): Socket {
val ssl = delegate.createSocket(host, port)
if (ssl is SSLSocket)
upgradeTLS(ssl)
return ssl
}
override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket {
val ssl = delegate.createSocket(host, port, localHost, localPort)
if (ssl is SSLSocket)
upgradeTLS(ssl)
return ssl
}
override fun createSocket(host: InetAddress, port: Int): Socket {
val ssl = delegate.createSocket(host, port)
if (ssl is SSLSocket)
upgradeTLS(ssl)
return ssl
}
override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket {
val ssl = delegate.createSocket(address, port, localAddress, localPort)
if (ssl is SSLSocket)
upgradeTLS(ssl)
return ssl
}
private fun upgradeTLS(ssl: SSLSocket) {
protocols?.let { ssl.enabledProtocols = it }
cipherSuites?.let { ssl.enabledCipherSuites = it }
}
}

View File

@@ -10,38 +10,34 @@ package at.bitfire.davdroid
import android.accounts.Account
import android.app.PendingIntent
import android.app.Service
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Intent
import android.database.DatabaseUtils
import android.database.sqlite.SQLiteDatabase
import android.os.Binder
import android.os.Bundle
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.UrlUtils
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.exception.HttpException
import at.bitfire.dav4android.property.*
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.CollectionInfo
import at.bitfire.davdroid.model.ServiceDB.*
import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.settings.Settings
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import java.io.IOException
import java.lang.ref.WeakReference
import java.util.*
import java.util.logging.Level
import kotlin.concurrent.thread
class DavService: Service() {
class DavService: android.app.Service() {
companion object {
const val ACTION_REFRESH_COLLECTIONS = "refreshCollections"
@@ -52,6 +48,16 @@ class DavService: Service() {
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>()
@@ -65,15 +71,20 @@ class DavService: Service() {
when (intent.action) {
ACTION_REFRESH_COLLECTIONS ->
if (runningRefresh.add(id)) {
thread { refreshCollections(id) }
refreshingStatusListeners.forEach { it.get()?.onDavRefreshStatusChanged(id, true) }
refreshingStatusListeners.forEach { listener ->
listener.get()?.onDavRefreshStatusChanged(id, true)
}
CoroutineScope(Dispatchers.IO).launch {
refreshCollections(id)
}
}
ACTION_FORCE_SYNC -> {
val authority = intent.data.authority
val uri = intent.data!!
val authority = uri.authority!!
val account = Account(
intent.data.pathSegments[1],
intent.data.pathSegments[0]
uri.pathSegments[1],
uri.pathSegments[0]
)
forceSync(authority, account)
}
@@ -97,9 +108,9 @@ class DavService: Service() {
inner class InfoBinder: Binder() {
fun isRefreshing(id: Long) = runningRefresh.contains(id)
fun addRefreshingStatusListener(listener: RefreshingStatusListener, callImmediate: Boolean) {
fun addRefreshingStatusListener(listener: RefreshingStatusListener, callImmediateIfRunning: Boolean) {
refreshingStatusListeners += WeakReference<RefreshingStatusListener>(listener)
if (callImmediate)
if (callImmediateIfRunning)
runningRefresh.forEach { id -> listener.onDavRefreshStatusChanged(id, true) }
}
@@ -129,257 +140,237 @@ class DavService: Service() {
ContentResolver.requestSync(account, authority, extras)
}
private fun refreshCollections(service: Long) {
OpenHelper(this@DavService).use { dbHelper ->
val db = dbHelper.writableDatabase
private fun refreshCollections(serviceId: Long) {
val db = AppDatabase.getInstance(this)
val homeSetDao = db.homeSetDao()
val collectionDao = db.collectionDao()
val serviceType by lazy {
db.query(Services._TABLE, arrayOf(Services.SERVICE), "${Services.ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
return@lazy cursor.getString(0)
} ?: throw IllegalArgumentException("Service not found")
}
val service = db.serviceDao().get(serviceId) ?: throw IllegalArgumentException("Service not found")
val account = Account(service.accountName, getString(R.string.account_type))
val account by lazy {
db.query(Services._TABLE, arrayOf(Services.ACCOUNT_NAME), "${Services.ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
return@lazy Account(cursor.getString(0), getString(R.string.account_type))
}
throw IllegalArgumentException("Account not found")
}
val homeSets = homeSetDao.getByService(serviceId).associateBy { it.url }.toMutableMap()
val collections = collectionDao.getByService(serviceId).associateBy { it.url }.toMutableMap()
val homeSets by lazy {
val homeSets = mutableSetOf<HttpUrl>()
db.query(HomeSets._TABLE, arrayOf(HomeSets.URL), "${HomeSets.SERVICE_ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
while (cursor.moveToNext())
HttpUrl.parse(cursor.getString(0))?.let { homeSets += it }
}
homeSets
}
/**
* 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>()
val collections by lazy {
val collections = mutableMapOf<HttpUrl, CollectionInfo>()
db.query(Collections._TABLE, null, "${Collections.SERVICE_ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
val values = ContentValues()
DatabaseUtils.cursorRowToContentValues(cursor, values)
values.getAsString(Collections.URL)?.let { url ->
HttpUrl.parse(url)?.let { collections.put(it, CollectionInfo(values)) }
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
}
}
}
collections
}
fun readPrincipal(): HttpUrl? {
db.query(Services._TABLE, arrayOf(Services.PRINCIPAL), "${Services.ID}=?", arrayOf(service.toString()), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
cursor.getString(0)?.let { return HttpUrl.parse(it) }
}
return null
}
/**
* Checks if the given URL defines home sets and adds them to the home set list.
*/
@Throws(IOException::class, HttpException::class, DavException::class)
fun queryHomeSets(client: OkHttpClient, url: HttpUrl, recurse: Boolean = true) {
var related = setOf<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 {
related += it
}
}
}
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 {
related += it
}
}
}
// 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 {
related += it
}
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
}
}
}
val dav = DavResource(client, url)
when (serviceType) {
Services.SERVICE_CARDDAV ->
dav.propfind(0, AddressbookHomeSet.NAME, GroupMembership.NAME) { response, _ ->
response[AddressbookHomeSet::class.java]?.let {
for (href in it.hrefs)
dav.location.resolve(href)?.let { homeSets += UrlUtils.withTrailingSlash(it) }
}
if (recurse)
findRelated(dav.location, response)
}
Services.SERVICE_CALDAV -> {
dav.propfind(0, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME) { response, _ ->
response[CalendarHomeSet::class.java]?.let {
for (href in it.hrefs)
dav.location.resolve(href)?.let { homeSets.add(UrlUtils.withTrailingSlash(it)) }
}
if (recurse)
findRelated(dav.location, response)
// 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
}
}
}
for (resource in related)
queryHomeSets(client, resource, false)
}
fun saveHomeSets() {
db.delete(HomeSets._TABLE, "${HomeSets.SERVICE_ID}=?", arrayOf(service.toString()))
for (homeSet in homeSets) {
val values = ContentValues(2)
values.put(HomeSets.SERVICE_ID, service)
values.put(HomeSets.URL, homeSet.toString())
db.insertOrThrow(HomeSets._TABLE, null, values)
}
}
fun saveCollections() {
db.delete(Collections._TABLE, "${HomeSets.SERVICE_ID}=?", arrayOf(service.toString()))
for ((_,collection) in collections) {
val values = collection.toDB()
Logger.log.log(Level.FINE, "Saving collection", values)
values.put(Collections.SERVICE_ID, service)
db.insertWithOnConflict(Collections._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE)
}
}
try {
Logger.log.info("Refreshing $serviceType collections of service #$service")
Settings.getInstance(this)?.use { settings ->
// create authenticating OkHttpClient (credentials taken from account settings)
HttpClient.Builder(this, settings, AccountSettings(this, settings, account))
.setForeground(true)
.build().use { client ->
val httpClient = client.okHttpClient
// refresh home set list (from principal)
readPrincipal()?.let { principalUrl ->
Logger.log.fine("Querying principal $principalUrl for home sets")
queryHomeSets(httpClient, principalUrl)
}
// remember selected collections
val selectedCollections = HashSet<HttpUrl>()
collections.values
.filter { it.selected }
.forEach { (url, _) -> selectedCollections.add(url) }
// now refresh collections (taken from home sets)
val itHomeSets = homeSets.iterator()
while (itHomeSets.hasNext()) {
val homeSetUrl = itHomeSets.next()
Logger.log.fine("Listing home set $homeSetUrl")
try {
DavResource(httpClient, homeSetUrl).propfind(1, *CollectionInfo.DAV_PROPERTIES) { response, _ ->
if (!response.isSuccess())
return@propfind
val info = CollectionInfo(response)
info.confirmed = true
Logger.log.log(Level.FINE, "Found collection", info)
if ((serviceType == Services.SERVICE_CARDDAV && info.type == CollectionInfo.Type.ADDRESS_BOOK) ||
(serviceType == Services.SERVICE_CALDAV && arrayOf(CollectionInfo.Type.CALENDAR, CollectionInfo.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, *CollectionInfo.DAV_PROPERTIES) { response, _ ->
if (!response.isSuccess())
return@propfind
val info = CollectionInfo(response)
info.confirmed = true
// remove unusable collections
if ((serviceType == Services.SERVICE_CARDDAV && info.type != CollectionInfo.Type.ADDRESS_BOOK) ||
(serviceType == Services.SERVICE_CALDAV && !arrayOf(CollectionInfo.Type.CALENDAR, CollectionInfo.Type.WEBCAL).contains(info.type)) ||
(info.type == CollectionInfo.Type.WEBCAL && info.source == null))
itCollections.remove()
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)
}
} 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
}
}
if (recurse)
findRelated(dav.location, response)
}
// restore selections
for (url in selectedCollections)
collections[url]?.let { it.selected = true }
} 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)
}
db.beginTransactionNonExclusive()
try {
saveHomeSets()
saveCollections()
db.setTransactionSuccessful()
} finally {
db.endTransaction()
// 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
}
// in any case, check whether the response is about a useable collection
val info = Collection.fromDavResponse(response) ?: return@propfind
info.serviceId = serviceId
info.confirmed = true
Logger.log.log(Level.FINE, "Found collection", info)
// remember usable collections
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()
}
}
} 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)
// 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 debugIntent = Intent(this, DebugInfoActivity::class.java)
debugIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e)
debugIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account)
val collection = Collection.fromDavResponse(response) ?: return@propfind
collection.confirmed = true
val notify = NotificationUtils.newBuilder(this)
.setSmallIcon(R.drawable.ic_sync_error_notification)
.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(service.toString(), NotificationUtils.NOTIFY_REFRESH_COLLECTIONS, notify)
} finally {
runningRefresh.remove(service)
refreshingStatusListeners.forEach { it.get()?.onDavRefreshStatusChanged(service, false) }
// 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)
}
}

View File

@@ -8,11 +8,17 @@
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 androidx.core.content.getSystemService
import at.bitfire.davdroid.log.Logger
import at.bitfire.ical4android.TaskProvider
import okhttp3.HttpUrl
import org.xbill.DNS.*
import java.util.*
@@ -22,15 +28,17 @@ import java.util.*
*/
object DavUtils {
@Suppress("FunctionName")
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())
val segments = LinkedList<String>(url.pathSegments)
segments.reverse()
return segments.firstOrNull { it.isNotEmpty() } ?: "/"
@@ -43,27 +51,65 @@ object DavUtils {
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 connectivity = context.getSystemService<ConnectivityManager>()!!
val activeLink = connectivity.getLinkProperties(connectivity.activeNetwork)
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)
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()
fun selectSRVRecord(records: Array<out Record>): SRVRecord? {
val srvRecords = records.filterIsInstance(SRVRecord::class.java)
if (srvRecords.size <= 1)
return srvRecords.firstOrNull()
/* RFC 2782
Priority
The priority of this target host. A client MUST attempt to
contact the target host with the lowest-numbered priority it can
reach; target hosts with the same priority SHOULD be tried in an
order defined by the weight field. [...]
Weight
A server selection mechanism. The weight field specifies a
relative weight for entries with the same priority. [...]
To select a target to be contacted next, arrange all SRV RRs
(that have not been ordered yet) in any order, except that all
those with weight 0 are placed at the beginning of the list.
Compute the sum of the weights of those RRs, and with each RR
associate the running sum in the selected order. Then choose a
uniform random number between 0 and the sum computed
(inclusive), and select the RR whose running sum value is the
first in the selected order which is greater than or equal to
the random number selected. The target host specified in the
selected SRV RR is the next one to be contacted by the client.
*/
val minPriority = srvRecords.map { it.priority }.min()
val useableRecords = srvRecords.filter { it.priority == minPriority }.sortedBy { it.weight != 0 }
val map = TreeMap<Int, SRVRecord>()
var runningWeight = 0
for (record in useableRecords) {
val weight = record.weight
runningWeight += weight
map[runningWeight] = record
}
return null
val selector = (0..runningWeight).random()
return map.ceilingEntry(selector)!!.value
}
fun pathsFromTXTRecords(records: Array<Record>?): List<String> {
@@ -79,4 +125,19 @@ object DavUtils {
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)
}
}
}

View File

@@ -12,16 +12,15 @@ import android.content.Context
import android.os.Build
import android.security.KeyChain
import at.bitfire.cert4android.CustomCertManager
import at.bitfire.dav4android.BasicDigestAuthHandler
import at.bitfire.dav4android.Constants
import at.bitfire.dav4android.UrlUtils
import at.bitfire.dav4jvm.BasicDigestAuthHandler
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.Credentials
import at.bitfire.davdroid.settings.ISettings
import okhttp3.Cache
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import okhttp3.*
import okhttp3.brotli.BrotliInterceptor
import okhttp3.internal.tls.OkHostnameVerifier
import okhttp3.logging.HttpLoggingInterceptor
import java.io.File
@@ -34,10 +33,7 @@ import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.logging.Level
import javax.net.ssl.KeyManager
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509ExtendedKeyManager
import javax.net.ssl.X509TrustManager
import javax.net.ssl.*
class HttpClient private constructor(
val okHttpClient: OkHttpClient,
@@ -45,34 +41,48 @@ class HttpClient private constructor(
): 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.Builder()
val sharedClient: OkHttpClient = OkHttpClient.Builder()
// set timeouts
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
// keep TLS 1.0 and 1.1 for now; remove when major browsers have dropped it (probably 2020)
.connectionSpecs(listOf(
ConnectionSpec.CLEARTEXT,
ConnectionSpec.COMPATIBLE_TLS
))
// don't allow redirects by default, because it would break PROPFIND handling
.followRedirects(false)
// offer Brotli and gzip compression
.addInterceptor(BrotliInterceptor)
// add User-Agent to every request
.addNetworkInterceptor(UserAgentInterceptor)
.build()!!
.build()
}
override fun close() {
okHttpClient.cache?.close()
certManager?.close()
}
class Builder(
val context: Context? = null,
val settings: ISettings? = 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()
@@ -82,39 +92,41 @@ class HttpClient private constructor(
// add network logging, if requested
if (logger.isLoggable(Level.FINEST)) {
val loggingInterceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger {
message -> logger.finest(message)
val loggingInterceptor = HttpLoggingInterceptor(object: HttpLoggingInterceptor.Logger {
override fun log(message: String) {
logger.finest(message)
}
})
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
orig.addInterceptor(loggingInterceptor)
}
settings?.let {
if (context != null) {
val settings = SettingsManager.getInstance(context)
// custom proxy support
try {
if (settings.getBoolean(App.OVERRIDE_PROXY, false)) {
if (settings.getBoolean(Settings.OVERRIDE_PROXY) == true) {
val address = InetSocketAddress(
settings.getString(App.OVERRIDE_PROXY_HOST, App.OVERRIDE_PROXY_HOST_DEFAULT),
settings.getInt(App.OVERRIDE_PROXY_PORT, App.OVERRIDE_PROXY_PORT_DEFAULT)
settings.getString(Settings.OVERRIDE_PROXY_HOST),
settings.getInt(Settings.OVERRIDE_PROXY_PORT)
)
val proxy = Proxy(Proxy.Type.HTTP, address)
orig.proxy(proxy)
Logger.log.log(Level.INFO, "Using proxy", proxy)
}
} catch(e: Exception) {
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Can't set proxy, ignoring", e)
}
context?.let {
if (BuildConfig.customCerts)
customCertManager(CustomCertManager(context, BuildConfig.customCertsUI, !settings.getBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, false)))
customCertManager(CustomCertManager(context, true /*BuildConfig.customCertsUI*/,
!(settings.getBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES))))
}
// use account settings for authentication
accountSettings?.let {
addAuthentication(null, it.credentials())
}
}
// use account settings for authentication
accountSettings?.let {
addAuthentication(null, it.credentials())
}
}
@@ -122,14 +134,13 @@ class HttpClient private constructor(
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)) {
fun withDiskCache(context: Context): Builder {
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, 10*1024*1024))
orig.cache(Cache(cacheDir, DISK_CACHE_MAX_SIZE))
break
}
}
@@ -144,7 +155,6 @@ class HttpClient private constructor(
fun customCertManager(manager: CustomCertManager) {
certManager = manager
}
fun setForeground(foreground: Boolean): Builder {
certManager?.appInForeground = foreground
return this
@@ -171,18 +181,20 @@ class HttpClient private constructor(
factory.trustManagers.first() as X509TrustManager
}()
val hostnameVerifier = certManager?.hostnameVerifier(OkHostnameVerifier.INSTANCE)
?: OkHostnameVerifier.INSTANCE
val hostnameVerifier = certManager?.hostnameVerifier(OkHostnameVerifier)
?: OkHostnameVerifier
var keyManager: KeyManager? = null
try {
certificateAlias?.let { alias ->
// get client certificate and private key
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 client certificate $alias for authentication (chain length: ${certs.size})")
logger.fine("Using provider certificate $alias for authentication (chain length: ${certs.size})")
// create Android KeyStore (performs key operations without revealing secret data to DAVdroid)
// create Android KeyStore (performs key operations without revealing secret data to DAVx5)
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
@@ -203,12 +215,21 @@ class HttpClient private constructor(
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)
}
} catch (e: Exception) {
logger.log(Level.SEVERE, "Couldn't set up client certificate authentication", e)
}
orig.sslSocketFactory(CustomTlsSocketFactory(keyManager, trustManager), trustManager)
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)
@@ -218,17 +239,11 @@ class HttpClient private constructor(
private object UserAgentInterceptor: Interceptor {
private val productName = when(BuildConfig.FLAVOR) {
App.FLAVOR_ICLOUD -> "MultiSync for Cloud"
App.FLAVOR_SOLDUPE -> "Soldupe Sync"
else -> "DAVdroid"
}
// 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 = "$productName/${BuildConfig.VERSION_NAME} ($userAgentDate; dav4android; okhttp/${Constants.okHttpVersion}) Android/${Build.VERSION.RELEASE}"
private val userAgent = "${BuildConfig.userAgent}/${BuildConfig.VERSION_NAME} ($userAgentDate; dav4jvm; " +
"okhttp/${OkHttp.VERSION}) Android/${Build.VERSION.RELEASE}"
override fun intercept(chain: Interceptor.Chain): Response {
val locale = Locale.getDefault()

View File

@@ -32,7 +32,7 @@ class MemoryCookieStore: CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
synchronized(storage) {
for (cookie in cookies)
storage.put(cookie.name(), cookie.domain(), cookie.path(), cookie)
storage.put(cookie.name, cookie.domain, cookie.path, cookie)
}
}
@@ -46,7 +46,7 @@ class MemoryCookieStore: CookieJar {
val cookie = iter.value
// remove expired cookies
if (cookie.expiresAt() <= System.currentTimeMillis()) {
if (cookie.expiresAt <= System.currentTimeMillis()) {
iter.remove()
continue
}

View File

@@ -0,0 +1,77 @@
/*
* 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.content.ContentResolver
import android.content.Context
import android.content.Intent
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.davdroid.settings.AccountSettings
import at.bitfire.ical4android.TaskProvider.ProviderName.OpenTasks
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class OpenTasksWatcher(
context: Context
): PackageChangedReceiver(context) {
companion object {
@WorkerThread
fun updateTaskSync(context: Context) {
val tasksInstalled = LocalTaskList.tasksProviderAvailable(context)
Logger.log.info("App was launched or package was (in)installed; OpenTasks provider now available = $tasksInstalled")
var enabledAnyAccount = false
// check all accounts and (de)activate OpenTasks if a CalDAV service is defined
val db = AppDatabase.getInstance(context)
for (service in db.serviceDao().getByType(Service.TYPE_CALDAV)) {
val account = Account(service.accountName, context.getString(R.string.account_type))
try {
val accountSettings = AccountSettings(context, account)
val currentSyncable = ContentResolver.getIsSyncable(account, OpenTasks.authority)
if (tasksInstalled) {
if (currentSyncable <= 0) {
Logger.log.info("Enabling OpenTasks sync for $account")
ContentResolver.setIsSyncable(account, OpenTasks.authority, 1)
accountSettings.setSyncInterval(OpenTasks.authority, Constants.DEFAULT_SYNC_INTERVAL)
enabledAnyAccount = true
}
} else if (currentSyncable != 0) {
Logger.log.info("Disabling OpenTasks sync for $account")
ContentResolver.setIsSyncable(account, OpenTasks.authority, 0)
}
} catch (e: InvalidAccountException) {
// account which is still mentioned in DB doesn't exist (anymore)
}
}
if (enabledAnyAccount && !PermissionUtils.havePermissions(context, PermissionUtils.TASKS_PERMISSIONS)) {
Logger.log.warning("Tasks sync is now enabled for at least one account, but OpenTasks permissions are not granted")
PermissionUtils.notifyPermissions(context, null)
}
}
}
override fun onReceive(context: Context, intent: Intent) {
CoroutineScope(Dispatchers.Default).launch {
updateTaskSync(context)
}
}
}

View File

@@ -1,61 +1,25 @@
/*
* 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.content.BroadcastReceiver
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.ServiceDB.Services
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.ical4android.TaskProvider
import android.content.IntentFilter
class PackageChangedReceiver: BroadcastReceiver() {
abstract class PackageChangedReceiver(
val context: Context
): BroadcastReceiver(), AutoCloseable {
companion object {
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
ServiceDB.OpenHelper(context).use { dbHelper ->
val db = dbHelper.readableDatabase
db.query(Services._TABLE, arrayOf(Services.ACCOUNT_NAME),
"${Services.SERVICE}=?", arrayOf(Services.SERVICE_CALDAV), null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
val account = Account(cursor.getString(0), context.getString(R.string.account_type))
if (tasksInstalled) {
if (ContentResolver.getIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority) <= 0) {
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 1)
ContentResolver.setSyncAutomatically(account, TaskProvider.ProviderName.OpenTasks.authority, true)
ContentResolver.addPeriodicSync(account, TaskProvider.ProviderName.OpenTasks.authority, Bundle(), Constants.DEFAULT_SYNC_INTERVAL)
}
} else
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.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) {
updateTaskSync(context)
override fun close() {
context.unregisterReceiver(this)
}
}
}

View File

@@ -0,0 +1,72 @@
package at.bitfire.davdroid
import android.Manifest
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.PermissionsActivity
import at.bitfire.ical4android.TaskProvider
object PermissionUtils {
val CONTACT_PERMSSIONS = arrayOf(
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_CONTACTS
)
val CALENDAR_PERMISSIONS = arrayOf(
Manifest.permission.READ_CALENDAR,
Manifest.permission.WRITE_CALENDAR
)
val TASKS_PERMISSIONS = arrayOf(
TaskProvider.PERMISSION_READ_TASKS,
TaskProvider.PERMISSION_WRITE_TASKS
)
/**
* Checks whether at least one of the given permissions is granted.
*
* @param context context to check
* @param permissions array of permissions to check
*
* @return whether at least one of [permissions] is granted
*/
fun haveAnyPermission(context: Context, permissions: Array<String>) =
permissions.any { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED }
/**
* Checks whether all given permissions are granted.
*
* @param context context to check
* @param permissions array of permissions to check
*
* @return whether all [permissions] are granted
*/
fun havePermissions(context: Context, permissions: Array<String>) =
permissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED }
/**
* Shows a notification about missing permissions.
*
* @param context notification context
* @param intent will be set as content Intent; if null, an Intent to launch PermissionsActivity will be used
*/
fun notifyPermissions(context: Context, intent: Intent?) {
val contentIntent = intent ?: Intent(context, PermissionsActivity::class.java)
val notify = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_ERRORS)
.setSmallIcon(R.drawable.ic_sync_problem_notify)
.setContentTitle(context.getString(R.string.sync_error_permissions))
.setContentText(context.getString(R.string.sync_error_permissions_text))
.setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setAutoCancel(true)
.build()
NotificationManagerCompat.from(context)
.notify(NotificationUtils.NOTIFY_PERMISSIONS, notify)
}
}

View File

@@ -8,45 +8,51 @@
package at.bitfire.davdroid.log
import android.app.Application
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Process
import android.preference.PreferenceManager
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.FileProvider
import androidx.preference.PreferenceManager
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.AppSettingsActivity
import at.bitfire.davdroid.ui.NotificationUtils
import org.apache.commons.lang3.time.DateFormatUtils
import java.io.File
import java.io.IOException
import java.util.logging.FileHandler
import java.util.logging.Level
object Logger {
object Logger : SharedPreferences.OnSharedPreferenceChangeListener {
const val LOG_TO_EXTERNAL_STORAGE = "log_to_external_storage"
val log = java.util.logging.Logger.getLogger("davdroid")!!
private const val LOG_TO_FILE = "log_to_file"
val log: java.util.logging.Logger = java.util.logging.Logger.getLogger("davx5")
private lateinit var context: Application
private lateinit var preferences: SharedPreferences
fun initialize(context: Context) {
fun initialize(app: Application) {
context = app
preferences = PreferenceManager.getDefaultSharedPreferences(context)
preferences.registerOnSharedPreferenceChangeListener { _, s ->
if (s == LOG_TO_EXTERNAL_STORAGE)
reinitialize(context.applicationContext)
}
preferences.registerOnSharedPreferenceChangeListener(this)
reinitialize(context.applicationContext)
reinitialize()
}
private fun reinitialize(context: Context) {
val logToFile = preferences.getBoolean(LOG_TO_EXTERNAL_STORAGE, false)
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if (key == LOG_TO_FILE) {
log.info("Logging settings changed; re-initializing logger")
reinitialize()
}
}
private fun reinitialize() {
val logToFile = preferences.getBoolean(LOG_TO_FILE, false)
val logVerbose = logToFile || Log.isLoggable(log.name, Log.DEBUG)
log.info("Verbose logging: $logVerbose; to file: $logToFile")
@@ -64,43 +70,67 @@ object Logger {
// log to external file according to preferences
if (logToFile) {
val builder = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_DEBUG)
builder .setSmallIcon(R.drawable.ic_sd_storage_notification)
.setContentTitle(context.getString(R.string.logging_davdroid_file_logging))
.setLocalOnly(true)
builder .setSmallIcon(R.drawable.ic_sd_card_notify)
.setContentTitle(context.getString(R.string.logging_notification_title))
val dir = context.getExternalFilesDir(null)
if (dir != null)
try {
val fileName = File(dir, "davdroid-${Process.myPid()}-${DateFormatUtils.format(System.currentTimeMillis(), "yyyyMMdd-HHmmss")}.txt").toString()
log.info("Logging to $fileName")
val logDir = debugDir(context) ?: return
val logFile = File(logDir, "davx5-log.txt")
val fileHandler = FileHandler(fileName)
fileHandler.formatter = PlainTextFormatter.DEFAULT
rootLogger.addHandler(fileHandler)
try {
val fileHandler = FileHandler(logFile.toString(), true)
fileHandler.formatter = PlainTextFormatter.DEFAULT
rootLogger.addHandler(fileHandler)
val prefIntent = Intent(context, AppSettingsActivity::class.java)
prefIntent.putExtra(AppSettingsActivity.EXTRA_SCROLL_TO, LOG_TO_EXTERNAL_STORAGE)
val prefIntent = Intent(context, AppSettingsActivity::class.java)
prefIntent.putExtra(AppSettingsActivity.EXTRA_SCROLL_TO, LOG_TO_FILE)
prefIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
builder .setContentText(dir.path)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(R.string.logging_to_external_storage, dir.path)))
.setOngoing(true)
builder .setContentText(logDir.path)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentText(context.getString(R.string.logging_notification_text))
.setContentIntent(PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setOngoing(true)
} catch(e: IOException) {
log.log(Level.SEVERE, "Couldn't create external log file", e)
val message = context.getString(R.string.logging_couldnt_create_file, e.localizedMessage)
builder .setContentText(message)
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
.setCategory(NotificationCompat.CATEGORY_ERROR)
}
else
builder.setContentText(context.getString(R.string.logging_no_external_storage))
// add "Share" action
val logFileUri = FileProvider.getUriForFile(context, context.getString(R.string.authority_debug_provider), logFile)
log.fine("Now logging to file: $logFile -> $logFileUri")
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_SUBJECT, "DAVx⁵ logs")
shareIntent.putExtra(Intent.EXTRA_STREAM, logFileUri)
shareIntent.type = "text/plain"
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
val chooserIntent = Intent.createChooser(shareIntent, null)
val shareAction = NotificationCompat.Action.Builder(R.drawable.ic_share_notify,
context.getString(R.string.logging_notification_send_log),
PendingIntent.getActivity(context, 0, chooserIntent, PendingIntent.FLAG_UPDATE_CURRENT))
builder.addAction(shareAction.build())
} catch(e: IOException) {
log.log(Level.SEVERE, "Couldn't create log file", e)
Toast.makeText(context, context.getString(R.string.logging_couldnt_create_file), Toast.LENGTH_LONG).show()
}
nm.notify(NotificationUtils.NOTIFY_EXTERNAL_FILE_LOGGING, builder.build())
} else
} else {
nm.cancel(NotificationUtils.NOTIFY_EXTERNAL_FILE_LOGGING)
// delete old logs
debugDir(context)?.deleteRecursively()
}
}
private fun debugDir(context: Context): File? {
val dir = File(context.filesDir, "debug")
if (dir.exists() && dir.isDirectory)
return dir
if (dir.mkdir())
return dir
Toast.makeText(context, context.getString(R.string.logging_couldnt_create_file), Toast.LENGTH_LONG).show()
return null
}
}

View File

@@ -0,0 +1,216 @@
package at.bitfire.davdroid.model
import android.content.Context
import android.database.sqlite.SQLiteException
import android.database.sqlite.SQLiteQueryBuilder
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import at.bitfire.davdroid.AndroidSingleton
import at.bitfire.davdroid.log.Logger
@Suppress("ClassName")
@Database(entities = [
Service::class,
HomeSet::class,
Collection::class
], exportSchema = true, version = 7)
@TypeConverters(Converters::class)
abstract class AppDatabase: RoomDatabase() {
abstract fun serviceDao(): ServiceDao
abstract fun homeSetDao(): HomeSetDao
abstract fun collectionDao(): CollectionDao
companion object: AndroidSingleton<AppDatabase>() {
override fun createInstance(context: Context): AppDatabase =
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "services.db")
.addMigrations(
Migration1_2,
Migration2_3,
Migration3_4,
Migration4_5,
Migration5_6,
Migration6_7
)
.fallbackToDestructiveMigration() // as a last fallback, recreate database instead of crashing
.build()
}
fun dump(sb: StringBuilder) {
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 table = cursorTables.getString(0)
sb.append(table).append("\n")
db.query("SELECT * FROM $table").use { cursor ->
// print columns
val cols = cursor.columnCount
sb.append("\t| ")
for (i in 0 until cols)
sb .append(" ")
.append(cursor.getColumnName(i))
.append(" |")
sb.append("\n")
// print rows
while (cursor.moveToNext()) {
sb.append("\t| ")
for (i in 0 until cols) {
sb.append(" ")
try {
val value = cursor.getString(i)
if (value != null)
sb.append(value
.replace("\r", "<CR>")
.replace("\n", "<LF>"))
else
sb.append("<null>")
} catch (e: SQLiteException) {
sb.append("<unprintable>")
}
sb.append(" |")
}
sb.append("\n")
}
sb.append("----------\n")
}
}
db.endTransaction()
}
}
// migrations
object Migration6_7: 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 Migration5_6: 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 Migration4_5: 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 Migration3_4: Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE collections ADD COLUMN forceReadOnly INTEGER DEFAULT 0 NOT NULL")
}
}
object Migration2_3: 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 Migration1_2: 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"))
}
}
}

View File

@@ -0,0 +1,160 @@
package at.bitfire.davdroid.model
import androidx.room.*
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.dav4jvm.property.*
import at.bitfire.davdroid.DavUtils
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@Entity(tableName = "collection",
foreignKeys = [
ForeignKey(entity = Service::class, parentColumns = arrayOf("id"), childColumns = arrayOf("serviceId"), onDelete = ForeignKey.CASCADE)
],
indices = [
Index("serviceId","type")
]
)
data class Collection(
@PrimaryKey(autoGenerate = true)
override var id: Long = 0,
var serviceId: Long = 0,
var type: String,
var url: HttpUrl,
var privWriteContent: Boolean = true,
var privUnbind: Boolean = true,
var forceReadOnly: Boolean = false,
var displayName: String? = null,
var description: String? = null,
// CalDAV only
var color: Int? = null,
/** timezone definition (full VTIMEZONE) - not a TZID! **/
var timezone: String? = null,
/** whether the collection supports VEVENT; in case of calendars: null means true */
var supportsVEVENT: Boolean? = null,
/** whether the collection supports VTODO; in case of calendars: null means true */
var supportsVTODO: Boolean? = null,
/** whether the collection supports VJOURNAL; in case of calendars: null means true */
var supportsVJOURNAL: Boolean? = null,
/** Webcal subscription source URL */
var source: HttpUrl? = null,
/** whether this collection has been selected for synchronization */
var sync: Boolean = false
): IdEntity() {
companion object {
const val TYPE_ADDRESSBOOK = "ADDRESS_BOOK"
const val TYPE_CALENDAR = "CALENDAR"
const val TYPE_WEBCAL = "WEBCAL"
/**
* Generates a collection entity from a WebDAV response.
* @param dav WebDAV response
* @return null if the response doesn't represent a collection
*/
fun fromDavResponse(dav: Response): Collection? {
val url = UrlUtils.withTrailingSlash(dav.href)
val type: String = dav[ResourceType::class.java]?.let { resourceType ->
when {
resourceType.types.contains(ResourceType.ADDRESSBOOK) -> TYPE_ADDRESSBOOK
resourceType.types.contains(ResourceType.CALENDAR) -> TYPE_CALENDAR
resourceType.types.contains(ResourceType.SUBSCRIBED) -> TYPE_WEBCAL
else -> null
}
} ?: return null
var privWriteContent = true
var privUnbind = true
dav[CurrentUserPrivilegeSet::class.java]?.let { privilegeSet ->
privWriteContent = privilegeSet.mayWriteContent
privUnbind = privilegeSet.mayUnbind
}
var displayName: String? = null
dav[DisplayName::class.java]?.let {
if (!it.displayName.isNullOrEmpty())
displayName = it.displayName
}
var description: String? = null
var color: Int? = null
var timezone: String? = null
var supportsVEVENT: Boolean? = null
var supportsVTODO: Boolean? = null
var supportsVJOURNAL: Boolean? = null
var source: HttpUrl? = null
when (type) {
TYPE_ADDRESSBOOK -> {
dav[AddressbookDescription::class.java]?.let { description = it.description }
}
TYPE_CALENDAR, TYPE_WEBCAL -> {
dav[CalendarDescription::class.java]?.let { description = it.description }
dav[CalendarColor::class.java]?.let { color = it.color }
dav[CalendarTimezone::class.java]?.let { timezone = it.vTimeZone }
if (type == TYPE_CALENDAR) {
supportsVEVENT = true
supportsVTODO = true
supportsVJOURNAL = true
dav[SupportedCalendarComponentSet::class.java]?.let {
supportsVEVENT = it.supportsEvents
supportsVTODO = it.supportsTasks
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://")
href.toHttpUrlOrNull()
}
}
supportsVEVENT = true
}
}
}
return Collection(
type = type,
url = url,
privWriteContent = privWriteContent,
privUnbind = privUnbind,
displayName = displayName,
description = description,
color = color,
timezone = timezone,
supportsVEVENT = supportsVEVENT,
supportsVTODO = supportsVTODO,
supportsVJOURNAL = supportsVJOURNAL,
source = source
)
}
}
// non-persistent properties
@Ignore
var confirmed: Boolean = false
// calculated properties
fun title() = displayName ?: DavUtils.lastSegmentOfUrl(url)
fun readOnly() = forceReadOnly || !privWriteContent
}

View File

@@ -0,0 +1,43 @@
package at.bitfire.davdroid.model
import androidx.lifecycle.LiveData
import androidx.paging.DataSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface CollectionDao: SyncableDao<Collection> {
@Query("SELECT * FROM collection WHERE id=:id")
fun get(id: Long): Collection?
@Query("SELECT * FROM collection WHERE serviceId=:serviceId")
fun getByService(serviceId: Long): List<Collection>
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type=:type")
fun getByServiceAndType(serviceId: Long, type: String): List<Collection>
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type=:type ORDER BY displayName, url")
fun pageByServiceAndType(serviceId: Long, type: String): DataSource.Factory<Int, Collection>
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND sync ORDER BY displayName, url")
fun getByServiceAndSync(serviceId: Long): List<Collection>
@Query("SELECT COUNT(*) FROM collection WHERE serviceId=:serviceId AND sync")
fun observeHasSyncByService(serviceId: Long): LiveData<Boolean>
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND supportsVEVENT AND sync ORDER BY displayName, url")
fun getSyncCalendars(serviceId: Long): List<Collection>
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND supportsVTODO AND sync ORDER BY displayName, url")
fun getSyncTaskLists(serviceId: Long): List<Collection>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(collection: Collection)
@Insert
fun insert(collection: Collection)
}

View File

@@ -1,243 +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.model
import android.content.ContentValues
import android.os.Parcel
import android.os.Parcelable
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.UrlUtils
import at.bitfire.dav4android.property.*
import at.bitfire.davdroid.model.ServiceDB.Collections
import okhttp3.HttpUrl
/**
* Represents a WebDAV collection.
*
* @constructor always appends a trailing slash to the URL
*/
data class CollectionInfo(
/**
* URL of the collection (including trailing slash)
*/
val url: HttpUrl,
var id: Long? = null,
var serviceID: Long? = null,
var type: Type? = null,
var readOnly: Boolean = false,
var forceReadOnly: Boolean = false,
var displayName: String? = null,
var description: String? = null,
var color: Int? = null,
var timeZone: String? = null,
var supportsVEVENT: Boolean = false,
var supportsVTODO: Boolean = false,
var selected: Boolean = false,
// subscriptions
var source: String? = null,
// non-persistent properties
var confirmed: Boolean = false
): Parcelable {
enum class Type {
ADDRESS_BOOK,
CALENDAR,
WEBCAL // iCalendar subscription
}
constructor(dav: Response): this(UrlUtils.withTrailingSlash(dav.href)) {
dav[ResourceType::class.java]?.let { type ->
when {
type.types.contains(ResourceType.ADDRESSBOOK) -> this.type = Type.ADDRESS_BOOK
type.types.contains(ResourceType.CALENDAR) -> this.type = Type.CALENDAR
type.types.contains(ResourceType.SUBSCRIBED) -> this.type = Type.WEBCAL
}
}
dav[CurrentUserPrivilegeSet::class.java]?.let { privilegeSet ->
readOnly = !privilegeSet.mayWriteContent
}
dav[DisplayName::class.java]?.let {
if (!it.displayName.isNullOrEmpty())
displayName = it.displayName
}
when (type) {
Type.ADDRESS_BOOK -> {
dav[AddressbookDescription::class.java]?.let { description = it.description }
}
Type.CALENDAR, Type.WEBCAL -> {
dav[CalendarDescription::class.java]?.let { description = it.description }
dav[CalendarColor::class.java]?.let { color = it.color }
dav[CalendarTimezone::class.java]?.let { timeZone = it.vTimeZone }
if (type == Type.CALENDAR) {
supportsVEVENT = true
supportsVTODO = true
dav[SupportedCalendarComponentSet::class.java]?.let {
supportsVEVENT = it.supportsEvents
supportsVTODO = it.supportsTasks
}
} else { // Type.WEBCAL
dav[Source::class.java]?.let { source = it.hrefs.firstOrNull() }
supportsVEVENT = true
}
}
}
}
constructor(values: ContentValues): this(UrlUtils.withTrailingSlash(HttpUrl.parse(values.getAsString(Collections.URL))!!)) {
id = values.getAsLong(Collections.ID)
serviceID = values.getAsLong(Collections.SERVICE_ID)
type = try {
Type.valueOf(values.getAsString(Collections.TYPE))
} catch (e: Exception) {
null
}
readOnly = values.getAsInteger(Collections.READ_ONLY) != 0
forceReadOnly = values.getAsInteger(Collections.FORCE_READ_ONLY) != 0
displayName = values.getAsString(Collections.DISPLAY_NAME)
description = values.getAsString(Collections.DESCRIPTION)
color = values.getAsInteger(Collections.COLOR)
timeZone = values.getAsString(Collections.TIME_ZONE)
supportsVEVENT = getAsBooleanOrNull(values, Collections.SUPPORTS_VEVENT) ?: false
supportsVTODO = getAsBooleanOrNull(values, Collections.SUPPORTS_VTODO) ?: false
source = values.getAsString(Collections.SOURCE)
selected = values.getAsInteger(Collections.SYNC) != 0
}
fun toDB(): ContentValues {
val values = ContentValues()
// Collections.SERVICE_ID is never changed
type?.let { values.put(Collections.TYPE, it.name) }
values.put(Collections.URL, url.toString())
values.put(Collections.READ_ONLY, if (readOnly) 1 else 0)
values.put(Collections.FORCE_READ_ONLY, if (forceReadOnly) 1 else 0)
values.put(Collections.DISPLAY_NAME, displayName)
values.put(Collections.DESCRIPTION, description)
values.put(Collections.COLOR, color)
values.put(Collections.TIME_ZONE, timeZone)
values.put(Collections.SUPPORTS_VEVENT, if (supportsVEVENT) 1 else 0)
values.put(Collections.SUPPORTS_VTODO, if (supportsVTODO) 1 else 0)
values.put(Collections.SOURCE, source)
values.put(Collections.SYNC, if (selected) 1 else 0)
return values
}
private fun getAsBooleanOrNull(values: ContentValues, field: String): Boolean? {
val i = values.getAsInteger(field)
return if (i == null)
null
else
(i != 0)
}
override fun describeContents(): Int = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
fun<T> writeOrNull(value: T?, write: (T) -> Unit) {
if (value == null)
dest.writeByte(0)
else {
dest.writeByte(1)
write(value)
}
}
dest.writeString(url.toString())
writeOrNull(id) { dest.writeLong(it) }
writeOrNull(serviceID) { dest.writeLong(it) }
dest.writeString(type?.name)
dest.writeByte(if (readOnly) 1 else 0)
dest.writeByte(if (forceReadOnly) 1 else 0)
dest.writeString(displayName)
dest.writeString(description)
writeOrNull(color) { dest.writeInt(it) }
dest.writeString(timeZone)
dest.writeByte(if (supportsVEVENT) 1 else 0)
dest.writeByte(if (supportsVTODO) 1 else 0)
dest.writeByte(if (selected) 1 else 0)
dest.writeString(source)
dest.writeByte(if (confirmed) 1 else 0)
}
companion object CREATOR : Parcelable.Creator<CollectionInfo> {
val DAV_PROPERTIES = arrayOf(
ResourceType.NAME,
CurrentUserPrivilegeSet.NAME,
DisplayName.NAME,
AddressbookDescription.NAME, SupportedAddressData.NAME,
CalendarDescription.NAME, CalendarColor.NAME, SupportedCalendarComponentSet.NAME,
Source.NAME
)
override fun createFromParcel(parcel: Parcel): CollectionInfo {
fun<T> readOrNull(parcel: Parcel, read: () -> T): T? {
return if (parcel.readByte() == 0.toByte())
null
else
read()
}
return CollectionInfo(
HttpUrl.parse(parcel.readString())!!,
readOrNull(parcel) { parcel.readLong() },
readOrNull(parcel) { parcel.readLong() },
parcel.readString()?.let { Type.valueOf(it) },
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readString(),
parcel.readString(),
readOrNull(parcel) { parcel.readInt() },
parcel.readString(),
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readString(),
parcel.readByte() != 0.toByte()
)
}
override fun newArray(size: Int) = arrayOfNulls<CollectionInfo>(size)
}
}

View File

@@ -0,0 +1,17 @@
package at.bitfire.davdroid.model
import androidx.room.TypeConverter
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
class Converters {
@TypeConverter
fun httpUrlToString(url: HttpUrl?) =
url?.toString()
@TypeConverter
fun stringToHttpUrl(url: String?): HttpUrl? =
url?.let { it.toHttpUrlOrNull() }
}

View File

@@ -8,13 +8,11 @@
package at.bitfire.davdroid.model
import java.io.Serializable
class Credentials(
val userName: String? = null,
val password: String? = null,
val certificateAlias: String? = null
): Serializable {
) {
enum class Type {
UsernamePassword,

View File

@@ -0,0 +1,41 @@
package at.bitfire.davdroid.model
import at.bitfire.davdroid.log.Logger
import java.util.logging.Level
class DaoTools<T: IdEntity>(dao: SyncableDao<T>): SyncableDao<T> by dao {
/**
* Synchronizes a list of "old" elements with a list of "new" elements so that the list
* only contain equal elements.
*
* @param allOld list of old elements
* @param allNew map of new elements (stored in key map)
* @param selectKey generates a unique key from the element (will be called on old elements)
* @param prepareNew prepares new elements (can be used to take over properties of old elements)
*/
fun <K> syncAll(allOld: List<T>, allNew: Map<K,T>, selectKey: (T) -> K, prepareNew: (new: T, old: T) -> Unit = { _, _ -> }) {
Logger.log.log(Level.FINE, "Syncing tables", arrayOf(allOld, allNew))
val remainingNew = allNew.toMutableMap()
allOld.forEach { old ->
val key = selectKey(old)
val matchingNew = remainingNew[key]
if (matchingNew != null) {
// keep this old item, but maybe update it
matchingNew.id = old.id // identity is proven by key
prepareNew(matchingNew, old)
if (matchingNew != old)
update(matchingNew)
// remove from remainingNew
remainingNew -= key
} else {
// this old item is not present anymore, delete it
delete(old)
}
}
insert(remainingNew.values.toList())
}
}

View File

@@ -0,0 +1,28 @@
package at.bitfire.davdroid.model
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import okhttp3.HttpUrl
@Entity(tableName = "homeset",
foreignKeys = [
ForeignKey(entity = Service::class, parentColumns = arrayOf("id"), childColumns = arrayOf("serviceId"), onDelete = ForeignKey.CASCADE)
],
indices = [
// index by service; no duplicate URLs per service
Index("serviceId", "url", unique = true)
]
)
data class HomeSet(
@PrimaryKey(autoGenerate = true)
override var id: Long,
var serviceId: Long,
var url: HttpUrl,
var privBind: Boolean = true,
var displayName: String? = null
): IdEntity()

View File

@@ -0,0 +1,21 @@
package at.bitfire.davdroid.model
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface HomeSetDao: SyncableDao<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>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(homeSet: HomeSet): Long
}

View File

@@ -0,0 +1,5 @@
package at.bitfire.davdroid.model
abstract class IdEntity {
abstract var id: Long
}

View File

@@ -0,0 +1,28 @@
package at.bitfire.davdroid.model
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import okhttp3.HttpUrl
@Entity(tableName = "service",
indices = [
// only one service per type and account
Index("accountName", "type", unique = true)
])
data class Service(
@PrimaryKey(autoGenerate = true)
var id: Long,
var accountName: String,
var type: String,
var principal: HttpUrl?
) {
companion object {
const val TYPE_CALDAV = "caldav"
const val TYPE_CARDDAV = "carddav"
}
}

View File

@@ -1,223 +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.model
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteException
import android.database.sqlite.SQLiteOpenHelper
import android.preference.PreferenceManager
import at.bitfire.davdroid.App
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.StartupDialogFragment
import java.util.logging.Level
class ServiceDB {
object Services {
const val _TABLE = "services"
const val ID = "_id"
const val ACCOUNT_NAME = "accountName"
const val SERVICE = "service"
const val PRINCIPAL = "principal"
// allowed values for SERVICE column
const val SERVICE_CALDAV = "caldav"
const val SERVICE_CARDDAV = "carddav"
}
object HomeSets {
const val _TABLE = "homesets"
const val ID = "_id"
const val SERVICE_ID = "serviceID"
const val URL = "url"
}
object Collections {
const val _TABLE = "collections"
const val ID = "_id"
const val TYPE = "type"
const val SERVICE_ID = "serviceID"
const val URL = "url"
const val READ_ONLY = "readOnly"
const val FORCE_READ_ONLY = "forceReadOnly"
const val DISPLAY_NAME = "displayName"
const val DESCRIPTION = "description"
const val COLOR = "color"
const val TIME_ZONE = "timezone"
const val SUPPORTS_VEVENT = "supportsVEVENT"
const val SUPPORTS_VTODO = "supportsVTODO"
const val SOURCE = "source"
const val SYNC = "sync"
}
companion object {
fun onRenameAccount(db: SQLiteDatabase, oldName: String, newName: String) {
val values = ContentValues(1)
values.put(Services.ACCOUNT_NAME, newName)
db.updateWithOnConflict(Services._TABLE, values, Services.ACCOUNT_NAME + "=?", arrayOf(oldName), SQLiteDatabase.CONFLICT_REPLACE)
}
}
class OpenHelper(
val context: Context
): SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION), AutoCloseable {
companion object {
const val DATABASE_NAME = "services.db"
const val DATABASE_VERSION = 4
}
override fun onConfigure(db: SQLiteDatabase) {
setWriteAheadLoggingEnabled(true)
db.setForeignKeyConstraintsEnabled(true)
}
override fun onCreate(db: SQLiteDatabase) {
Logger.log.info("Creating database " + db.path)
db.execSQL("CREATE TABLE ${Services._TABLE}(" +
"${Services.ID} INTEGER PRIMARY KEY AUTOINCREMENT," +
"${Services.ACCOUNT_NAME} TEXT NOT NULL," +
"${Services.SERVICE} TEXT NOT NULL," +
"${Services.PRINCIPAL} TEXT NULL)")
db.execSQL("CREATE UNIQUE INDEX services_account ON ${Services._TABLE} (${Services.ACCOUNT_NAME},${Services.SERVICE})")
db.execSQL("CREATE TABLE ${HomeSets._TABLE}(" +
"${HomeSets.ID} INTEGER PRIMARY KEY AUTOINCREMENT," +
"${HomeSets.SERVICE_ID} INTEGER NOT NULL REFERENCES ${Services._TABLE} ON DELETE CASCADE," +
"${HomeSets.URL} TEXT NOT NULL)")
db.execSQL("CREATE UNIQUE INDEX homesets_service_url ON ${HomeSets._TABLE}(${HomeSets.SERVICE_ID},${HomeSets.URL})")
db.execSQL("CREATE TABLE ${Collections._TABLE}(" +
"${Collections.ID} INTEGER PRIMARY KEY AUTOINCREMENT," +
"${Collections.SERVICE_ID} INTEGER NOT NULL REFERENCES ${Services._TABLE} ON DELETE CASCADE," +
"${Collections.TYPE} TEXT NOT NULL," +
"${Collections.URL} TEXT NOT NULL," +
"${Collections.READ_ONLY} INTEGER DEFAULT 0 NOT NULL," +
"${Collections.FORCE_READ_ONLY} INTEGER DEFAULT 0 NOT NULL," +
"${Collections.DISPLAY_NAME} TEXT NULL," +
"${Collections.DESCRIPTION} TEXT NULL," +
"${Collections.COLOR} INTEGER NULL," +
"${Collections.TIME_ZONE} TEXT NULL," +
"${Collections.SUPPORTS_VEVENT} INTEGER NULL," +
"${Collections.SUPPORTS_VTODO} INTEGER NULL," +
"${Collections.SOURCE} TEXT NULL," +
"${Collections.SYNC} INTEGER DEFAULT 0 NOT NULL)")
db.execSQL("CREATE UNIQUE INDEX collections_service_url ON ${Collections._TABLE}(${Collections.SERVICE_ID},${Collections.URL})")
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
for (upgradeFrom in oldVersion until newVersion) {
val upgradeTo = upgradeFrom + 1
Logger.log.info("Upgrading database from version $upgradeFrom to $upgradeTo")
try {
val upgradeProc = this::class.java.getDeclaredMethod("upgrade_${upgradeFrom}_$upgradeTo", SQLiteDatabase::class.java)
upgradeProc.invoke(this, db)
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't upgrade database", e)
}
}
}
@Suppress("unused")
private fun upgrade_3_4(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE ${Collections._TABLE} ADD COLUMN ${Collections.FORCE_READ_ONLY} INTEGER DEFAULT 0 NOT NULL")
}
@Suppress("unused")
private fun upgrade_2_3(db: SQLiteDatabase) {
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()
}
}
@Suppress("unused")
private fun upgrade_1_2(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE ${Collections._TABLE} ADD COLUMN ${Collections.TYPE} TEXT NOT NULL DEFAULT ''")
db.execSQL("ALTER TABLE ${Collections._TABLE} ADD COLUMN ${Collections.SOURCE} TEXT NULL")
db.execSQL("UPDATE ${Collections._TABLE} SET ${Collections.TYPE}=(" +
"SELECT CASE ${Services.SERVICE} WHEN ? THEN ? ELSE ? END " +
"FROM ${Services._TABLE} WHERE ${Services.ID}=${Collections._TABLE}.${Collections.SERVICE_ID}" +
")",
arrayOf(Services.SERVICE_CALDAV, CollectionInfo.Type.CALENDAR, CollectionInfo.Type.ADDRESS_BOOK))
}
fun dump(sb: StringBuilder) {
val db = readableDatabase
db.beginTransactionNonExclusive()
// iterate through all tables
db.query("sqlite_master", arrayOf("name"), "type='table'", null, null, null, null).use { cursorTables ->
while (cursorTables.moveToNext()) {
val table = cursorTables.getString(0)
sb.append(table).append("\n")
db.query(table, null, null, null, null, null, null).use { cursor ->
// print columns
val cols = cursor.columnCount
sb.append("\t| ")
for (i in 0 until cols)
sb .append(" ")
.append(cursor.getColumnName(i))
.append(" |")
sb.append("\n")
// print rows
while (cursor.moveToNext()) {
sb.append("\t| ")
for (i in 0 until cols) {
sb.append(" ")
try {
val value = cursor.getString(i)
if (value != null)
sb.append(value
.replace("\r", "<CR>")
.replace("\n", "<LF>"))
else
sb.append("<null>")
} catch (e: SQLiteException) {
sb.append("<unprintable>")
}
sb.append(" |")
}
sb.append("\n")
}
sb.append("----------\n")
}
}
db.endTransaction()
}
}
}
}

View File

@@ -0,0 +1,35 @@
package at.bitfire.davdroid.model
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface ServiceDao {
@Query("SELECT * FROM service WHERE accountName=:accountName AND type=:type")
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?
@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
@Query("DELETE FROM service")
fun deleteAll()
@Query("DELETE FROM service WHERE accountName NOT IN (:accountNames)")
fun deleteExceptAccounts(accountNames: Array<String>)
@Query("UPDATE service SET accountName=:newName WHERE accountName=:oldName")
fun renameAccount(oldName: String, newName: String)
}

View File

@@ -8,7 +8,7 @@
package at.bitfire.davdroid.model
import at.bitfire.dav4android.property.SyncToken
import at.bitfire.dav4jvm.property.SyncToken
import org.json.JSONException
import org.json.JSONObject

View File

@@ -0,0 +1,18 @@
package at.bitfire.davdroid.model
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Update
interface SyncableDao<T: IdEntity> {
@Insert
fun insert(items: List<T>)
@Update
fun update(item: T)
@Delete
fun delete(item: T)
}

View File

@@ -9,7 +9,6 @@ package at.bitfire.davdroid.resource
import android.accounts.Account
import android.accounts.AccountManager
import android.annotation.TargetApi
import android.content.*
import android.os.Build
import android.os.Bundle
@@ -22,7 +21,7 @@ import android.util.Base64
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.model.SyncState
import at.bitfire.vcard4android.*
import java.io.ByteArrayOutputStream
@@ -31,9 +30,9 @@ import java.util.logging.Level
/**
* A local address book. Requires an own Android account, because Android manages contacts per
* account and there is no such thing as "address books". So, DAVdroid creates a "DAVdroid
* account and there is no such thing as "address books". So, DAVx5 creates a "DAVx5
* address book" account for every CardDAV address book. These accounts are bound to a
* DAVdroid main account.
* DAVx5 main account.
*/
class LocalAddressBook(
private val context: Context,
@@ -48,14 +47,37 @@ class LocalAddressBook(
const val USER_DATA_URL = "url"
const val USER_DATA_READ_ONLY = "read_only"
fun create(context: Context, provider: ContentProviderClient, mainAccount: Account, info: CollectionInfo): LocalAddressBook {
private fun verifyUserData(context: Context, account: Account, userData: Bundle): Boolean {
val accountManager = AccountManager.get(context)
userData.keySet().forEach { key ->
val stored = accountManager.getUserData(account, key)
val expected = userData.getString(key)
if (stored != expected) {
Logger.log.warning("Stored user data \"${stored}\" differs from expected data \"${expected}\" for ${key}")
return false
}
}
return true
}
fun create(context: Context, provider: ContentProviderClient, mainAccount: Account, info: Collection): LocalAddressBook {
val accountManager = AccountManager.get(context)
val account = Account(accountName(mainAccount, info), context.getString(R.string.account_type_address_book))
if (!accountManager.addAccountExplicitly(account, null, initialUserData(mainAccount, info.url.toString())))
val userData = initialUserData(mainAccount, info.url.toString())
Logger.log.log(Level.INFO, "Creating local address book $account", userData)
if (!accountManager.addAccountExplicitly(account, null, userData))
throw IllegalStateException("Couldn't create address book account")
if (!verifyUserData(context, account, userData))
// Android seems to lose the initial user data sometimes, so set it a second time if that happens
// https://forums.bitfire.at/post/11644
userData.keySet().forEach { key ->
accountManager.setUserData(account, key, userData.getString(key))
}
val addressBook = LocalAddressBook(context, account, provider)
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1)
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
// initialize Contacts Provider Settings
@@ -63,22 +85,34 @@ class LocalAddressBook(
values.put(ContactsContract.Settings.SHOULD_SYNC, 1)
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
addressBook.settings = values
addressBook.readOnly = !info.privWriteContent || info.forceReadOnly
return addressBook
}
fun findAll(context: Context, provider: ContentProviderClient, mainAccount: Account?) = AccountManager.get(context)
fun findAll(context: Context, provider: ContentProviderClient?, mainAccount: Account) = AccountManager.get(context)
.getAccountsByType(context.getString(R.string.account_type_address_book))
.map { LocalAddressBook(context, it, provider) }
.filter { mainAccount == null || it.mainAccount == mainAccount }
.filter {
try {
it.mainAccount == mainAccount
} catch(e: IllegalStateException) {
false
}
}
.toList()
fun accountName(mainAccount: Account, info: CollectionInfo): String {
fun accountName(mainAccount: Account, info: Collection): String {
val baos = ByteArrayOutputStream()
baos.write(info.url.hashCode())
val hash = Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP or Base64.NO_PADDING)
val sb = StringBuilder(if (info.displayName.isNullOrEmpty()) DavUtils.lastSegmentOfUrl(info.url) else info.displayName)
val sb = StringBuilder(info.displayName.let {
if (it.isNullOrEmpty())
DavUtils.lastSegmentOfUrl(info.url)
else
it
})
sb.append(" (${mainAccount.name} $hash)")
return sb.toString()
}
@@ -103,6 +137,9 @@ class LocalAddressBook(
}
override val tag: String
get() = "contacts-${account.name}"
override val title = account.name!!
/**
@@ -115,6 +152,10 @@ class LocalAddressBook(
var includeGroups = true
private var _mainAccount: Account? = null
/**
* The associated main account which this address book accounts belongs to.
* @throws IllegalStateException when no main account is assigned
*/
var mainAccount: Account
get() {
_mainAccount?.let { return it }
@@ -125,7 +166,7 @@ class LocalAddressBook(
if (name != null && type != null)
return Account(name, type)
else
throw IllegalStateException("Address book doesn't exist anymore")
throw IllegalStateException("No main account assigned to address book account")
}
}
set(newMainAccount) {
@@ -142,7 +183,7 @@ class LocalAddressBook(
?: throw IllegalStateException("Address book has no URL")
set(url) = AccountManager.get(context).setUserData(account, USER_DATA_URL, url)
var readOnly: Boolean
override var readOnly: Boolean
get() = AccountManager.get(context).getUserData(account, USER_DATA_READ_ONLY) != null
set(readOnly) = AccountManager.get(context).setUserData(account, USER_DATA_READ_ONLY, if (readOnly) "1" else null)
@@ -163,7 +204,7 @@ class LocalAddressBook(
if (includeGroups) {
values.clear()
values.put(LocalGroup.COLUMN_FLAGS, flags)
number += provider.update(groupsSyncUri(), values, "${Groups.DIRTY}=0", null)
number += provider.update(groupsSyncUri(), values, "NOT ${Groups.DIRTY}", null)
}
return number
@@ -171,19 +212,18 @@ class LocalAddressBook(
override fun removeNotDirtyMarked(flags: Int): Int {
var number = provider!!.delete(rawContactsSyncUri(),
"${RawContacts.DIRTY}=0 AND ${LocalContact.COLUMN_FLAGS}=?", arrayOf(flags.toString()))
"NOT ${RawContacts.DIRTY} AND ${LocalContact.COLUMN_FLAGS}=?", arrayOf(flags.toString()))
if (includeGroups)
number += provider.delete(groupsSyncUri(),
"${Groups.DIRTY}=0 AND ${LocalGroup.COLUMN_FLAGS}=?", arrayOf(flags.toString()))
"NOT ${Groups.DIRTY} AND ${LocalGroup.COLUMN_FLAGS}=?", arrayOf(flags.toString()))
return number
}
fun update(info: CollectionInfo) {
fun update(info: Collection) {
val newAccountName = accountName(mainAccount, info)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
if (account.name != newAccountName && Build.VERSION.SDK_INT >= 21) {
// no need to re-assign contacts to new account, because they will be deleted by contacts provider in any case
val accountManager = AccountManager.get(context)
@@ -191,15 +231,34 @@ class LocalAddressBook(
account = future.result
}
Constants.log.info("Address book read-only? = ${info.readOnly}")
readOnly = info.readOnly || info.forceReadOnly
val nowReadOnly = !info.privWriteContent || info.forceReadOnly
if (nowReadOnly != readOnly) {
Constants.log.info("Address book now read-only = $nowReadOnly, updating contacts")
// update address book itself
readOnly = nowReadOnly
// update raw contacts
val rawContactValues = ContentValues(1)
rawContactValues.put(RawContacts.RAW_CONTACT_IS_READ_ONLY, if (nowReadOnly) 1 else 0)
provider!!.update(rawContactsSyncUri(), rawContactValues, null, null)
// update data rows
val dataValues = ContentValues(1)
dataValues.put(ContactsContract.Data.IS_READ_ONLY, if (nowReadOnly) 1 else 0)
provider.update(syncAdapterURI(ContactsContract.Data.CONTENT_URI), dataValues, null, null)
}
// make sure it will still be synchronized when contacts are updated
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
if (ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) <= 0)
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1)
if (!ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY))
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
}
fun delete() {
val accountManager = AccountManager.get(context)
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= 22)
accountManager.removeAccount(account, null, null, null)
else
@@ -228,8 +287,8 @@ class LocalAddressBook(
else
findDeletedContacts()
fun findDeletedContacts() = queryContacts("${RawContacts.DELETED}!=0", null)
fun findDeletedGroups() = queryGroups("${Groups.DELETED}!=0", null)
fun findDeletedContacts() = queryContacts(RawContacts.DELETED, null)
fun findDeletedGroups() = queryGroups(Groups.DELETED, null)
/**
* Returns an array of local contacts/groups which have been changed locally (DIRTY != 0).
@@ -240,16 +299,30 @@ class LocalAddressBook(
findDirtyContacts() + findDirtyGroups()
else
findDirtyContacts()
fun findDirtyContacts() = queryContacts(RawContacts.DIRTY, null)
fun findDirtyGroups() = queryGroups(Groups.DIRTY, null)
fun findDirtyContacts() = queryContacts("${RawContacts.DIRTY}!=0", null)
fun findDirtyGroups() = queryGroups("${Groups.DIRTY}!=0", null)
override fun findDirtyWithoutNameOrUid() =
if (includeGroups)
findDirtyContactsWithoutNameOrUid() + findDirtyGroupsWithoutNameOrUid()
else
findDirtyContactsWithoutNameOrUid()
private fun findDirtyContactsWithoutNameOrUid() = queryContacts(
"${RawContacts.DIRTY} AND (${AndroidContact.COLUMN_FILENAME} IS NULL OR ${AndroidContact.COLUMN_UID} IS NULL)",
null)
private fun findDirtyGroupsWithoutNameOrUid() = queryGroups(
"${Groups.DIRTY} AND (${AndroidGroup.COLUMN_FILENAME} IS NULL OR ${AndroidGroup.COLUMN_UID} IS NULL)",
null)
private fun queryContactsGroups(whereContacts: String?, whereArgsContacts: Array<String>?, whereGroups: String?, whereArgsGroups: Array<String>?): List<LocalAddress> {
val contacts = queryContacts(whereContacts, whereArgsContacts)
return if (includeGroups)
contacts + queryGroups(whereGroups, whereArgsGroups)
else
contacts
override fun forgetETags() {
if (includeGroups) {
val values = ContentValues(1)
values.putNull(AndroidGroup.COLUMN_ETAG)
provider!!.update(groupsSyncUri(), values, null, null)
}
val values = ContentValues(1)
values.putNull(AndroidContact.COLUMN_ETAG)
provider!!.update(rawContactsSyncUri(), values, null, null)
}
@@ -318,7 +391,7 @@ class LocalAddressBook(
val values = ContentValues(1)
values.put(Groups.TITLE, title)
val uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values)
val uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values) ?: throw RemoteException("Couldn't create contact group")
return ContentUris.parseId(uri)
}

View File

@@ -14,12 +14,12 @@ import android.content.ContentProviderOperation
import android.content.ContentUris
import android.content.ContentValues
import android.net.Uri
import android.provider.CalendarContract
import android.provider.CalendarContract.*
import android.provider.CalendarContract.Calendars
import android.provider.CalendarContract.Events
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.model.SyncState
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.AndroidCalendarFactory
@@ -38,12 +38,15 @@ class LocalCalendar private constructor(
private const val COLUMN_SYNC_STATE = Calendars.CAL_SYNC1
fun create(account: Account, provider: ContentProviderClient, info: CollectionInfo): Uri {
fun create(account: Account, provider: ContentProviderClient, info: Collection): Uri {
val values = valuesFromCollectionInfo(info, true)
// ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash.
values.put(Calendars.ACCOUNT_NAME, account.name)
values.put(Calendars.ACCOUNT_TYPE, account.type)
// Email address for scheduling. Used by the calendar provider to determine whether the
// user is ORGANIZER/ATTENDEE for a certain event.
values.put(Calendars.OWNER_ACCOUNT, account.name)
// flag as visible & synchronizable at creation, might be changed by user at any time
@@ -52,7 +55,7 @@ class LocalCalendar private constructor(
return create(account, provider, values)
}
private fun valuesFromCollectionInfo(info: CollectionInfo, withColor: Boolean): ContentValues {
private fun valuesFromCollectionInfo(info: Collection, withColor: Boolean): ContentValues {
val values = ContentValues()
values.put(Calendars.NAME, info.url.toString())
values.put(Calendars.CALENDAR_DISPLAY_NAME, if (info.displayName.isNullOrBlank()) DavUtils.lastSegmentOfUrl(info.url) else info.displayName)
@@ -60,15 +63,14 @@ class LocalCalendar private constructor(
if (withColor)
values.put(Calendars.CALENDAR_COLOR, info.color ?: Constants.DAVDROID_GREEN_RGBA)
if (info.readOnly || info.forceReadOnly)
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
else {
if (info.privWriteContent && !info.forceReadOnly) {
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1)
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1)
}
} else
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
info.timeZone?.let { tzData ->
info.timezone?.let { tzData ->
try {
val timeZone = DateUtils.parseVTimeZone(tzData)
timeZone.timeZoneId?.let { tzId ->
@@ -78,18 +80,22 @@ class LocalCalendar private constructor(
Logger.log.log(Level.WARNING, "Couldn't parse calendar default time zone", e)
}
}
values.put(Calendars.ALLOWED_REMINDERS, "${Reminders.METHOD_ALERT},${Reminders.METHOD_EMAIL}")
values.put(Calendars.ALLOWED_AVAILABILITY, "${Reminders.AVAILABILITY_TENTATIVE},${Reminders.AVAILABILITY_FREE},${Reminders.AVAILABILITY_BUSY}")
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, "${CalendarContract.Attendees.TYPE_OPTIONAL},${CalendarContract.Attendees.TYPE_REQUIRED},${CalendarContract.Attendees.TYPE_RESOURCE}")
// add base values for Calendars
values.putAll(calendarBaseValues)
return values
}
}
override val tag: String
get() = "events-${account.name}-$id"
override val title: String
get() = displayName ?: id.toString()
override var lastSyncState: SyncState?
get() = provider.query(calendarSyncURI(), arrayOf(COLUMN_SYNC_STATE), null, null, null)?.let { cursor ->
get() = provider.query(calendarSyncURI(), arrayOf(COLUMN_SYNC_STATE), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
return SyncState.fromString(cursor.getString(0))
else
@@ -102,30 +108,48 @@ class LocalCalendar private constructor(
}
fun update(info: CollectionInfo, updateColor: Boolean) =
fun update(info: Collection, updateColor: Boolean) =
update(valuesFromCollectionInfo(info, updateColor))
override fun findDeleted() =
queryEvents("${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null)
queryEvents("${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NULL", null)
override fun findDirty(): List<LocalEvent> {
val dirty = LinkedList<LocalEvent>()
// get dirty events which are required to have an increased SEQUENCE value
for (localEvent in queryEvents("${Events.DIRTY}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null)) {
val event = localEvent.event!!
val sequence = event.sequence
if (event.sequence == null) // sequence has not been assigned yet (i.e. this event was just locally created)
event.sequence = 0
else if (localEvent.weAreOrganizer)
event.sequence = sequence!! + 1
/*
* RFC 5545 3.8.7.4. Sequence Number
* When a calendar component is created, its sequence number is 0. It is monotonically incremented by the "Organizer's"
* CUA each time the "Organizer" makes a significant revision to the calendar component.
*/
for (localEvent in queryEvents("${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL", null)) {
try {
val event = requireNotNull(localEvent.event)
val nonGroupScheduled = event.attendees.isEmpty()
val weAreOrganizer = localEvent.weAreOrganizer
val sequence = event.sequence
if (sequence == null)
// sequence has not been assigned yet (i.e. this event was just locally created)
event.sequence = 0
else if (nonGroupScheduled || weAreOrganizer) // increase sequence
event.sequence = sequence + 1
} catch(e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't check/increase sequence", e)
}
dirty += localEvent
}
return dirty
}
override fun findDirtyWithoutNameOrUid() =
queryEvents("${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL AND " +
"(${Events._SYNC_ID} IS NULL OR ${Events.UID_2445} IS NULL)", null)
override fun findByName(name: String) =
queryEvents("${Events._SYNC_ID}=?", arrayOf(name)).firstOrNull()
@@ -134,14 +158,36 @@ class LocalCalendar private constructor(
val values = ContentValues(1)
values.put(LocalEvent.COLUMN_FLAGS, flags)
return provider.update(eventsSyncURI(), values,
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY}=0 AND ${Events.ORIGINAL_ID} IS NULL",
"${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL",
arrayOf(id.toString()))
}
override fun removeNotDirtyMarked(flags: Int) =
provider.delete(eventsSyncURI(),
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY}=0 AND ${Events.ORIGINAL_ID} IS NULL AND ${LocalEvent.COLUMN_FLAGS}=?",
arrayOf(id.toString(), flags.toString()))
override fun removeNotDirtyMarked(flags: Int): Int {
var deleted = 0
// list all non-dirty events with the given flags and delete every row + its exceptions
provider.query(eventsSyncURI(), arrayOf(Events._ID),
"${Events.CALENDAR_ID}=? AND NOT ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NULL AND ${LocalEvent.COLUMN_FLAGS}=?",
arrayOf(id.toString(), flags.toString()), null)?.use { cursor ->
val batch = BatchOperation(provider)
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
// delete event and possible exceptions (content provider doesn't delete exceptions itself)
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newDelete(eventsSyncURI())
.withSelection("${Events._ID}=? OR ${Events.ORIGINAL_ID}=?", arrayOf(id.toString(), id.toString()))
))
}
deleted = batch.commit()
}
return deleted
}
override fun forgetETags() {
val values = ContentValues(1)
values.putNull(LocalEvent.COLUMN_ETAG)
provider.update(eventsSyncURI(), values, "${Events.CALENDAR_ID}=?",
arrayOf(id.toString()))
}
fun processDirtyExceptions() {
@@ -150,7 +196,7 @@ class LocalCalendar private constructor(
provider.query(
syncAdapterURI(Events.CONTENT_URI),
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
"${Events.CALENDAR_ID}=? AND ${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NOT NULL",
"${Events.CALENDAR_ID}=? AND ${Events.DELETED} AND ${Events.ORIGINAL_ID} IS NOT NULL",
arrayOf(id.toString()), null)?.use { cursor ->
while (cursor.moveToNext()) {
Logger.log.fine("Found deleted exception, removing and re-scheduling original event (if available)")
@@ -190,7 +236,7 @@ class LocalCalendar private constructor(
provider.query(
syncAdapterURI(Events.CONTENT_URI),
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY}!=0 AND ${Events.ORIGINAL_ID} IS NOT NULL",
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY} AND ${Events.ORIGINAL_ID} IS NOT NULL",
arrayOf(id.toString()), null)?.use { cursor ->
while (cursor.moveToNext()) {
Logger.log.fine("Found dirty exception, increasing SEQUENCE to re-schedule")

View File

@@ -8,31 +8,81 @@
package at.bitfire.davdroid.resource
import android.provider.CalendarContract.Events
import at.bitfire.davdroid.model.SyncState
interface LocalCollection<out T: LocalResource<*>> {
/** a tag that uniquely identifies the collection (DAVx5-wide) */
val tag: String
/** collection title (used for user notifications etc.) **/
val title: String
var lastSyncState: SyncState?
/**
* Finds local resources of this collection which have been marked as *deleted* by the user
* or an app acting on their behalf.
*
* @return list of resources marked as *deleted*
*/
fun findDeleted(): List<T>
/**
* Finds local resources of this collection which have been marked as *dirty*, i.e. resources
* which have been modified by the user or an app acting on their behalf.
*
* @return list of resources marked as *dirty*
*/
fun findDirty(): List<T>
/**
* Finds local resources of this collection which do not have a file name and/or UID, but
* need one for synchronization.
*
* For instance, exceptions of recurring events are local resources but do not need their
* own file name/UID because they're sent with the same UID as the main event.
*
* @return list of resources which need file name and UID for synchronization, but don't have both of them
*/
fun findDirtyWithoutNameOrUid(): List<T>
/**
* Finds a local resource of this collection with a given file name. (File names are assigned
* by the sync adapter.)
*
* @param name file name to look for
* @return resource with the given name, or null if none
*/
fun findByName(name: String): T?
/**
* Marks all entries which are not dirty with the given flags only.
* @return number of marked entries
**/
* Sets the [LocalEvent.COLUMN_FLAGS] value for entries which are not dirty ([Events.DIRTY] is 0)
* and have an [Events.ORIGINAL_ID] of null.
*
* @param flags value of flags to set (for instance, [LocalResource.FLAG_REMOTELY_PRESENT]])
*
* @return number of marked entries
*/
fun markNotDirty(flags: Int): Int
/**
* Removes all entries with are not dirty and are marked with exactly the given flags.
* @return number of removed entries
* Removes entries which are not dirty ([Events.DIRTY] is 0 and an [Events.ORIGINAL_ID] is null) with
* a given flag combination.
*
* @param flags exact flags value to remove entries with (for instance, if this is [LocalResource.FLAG_REMOTELY_PRESENT]],
* all entries with exactly this flag will be removed)
*
* @return number of removed entries
*/
fun removeNotDirtyMarked(flags: Int): Int
/**
* Forgets the ETags of all members so that they will be reloaded from the server during sync.
*/
fun forgetETags()
}

View File

@@ -27,7 +27,7 @@ class LocalContact: AndroidContact, LocalAddress {
companion object {
init {
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ez-vcard/" + Ezvcard.VERSION
Contact.productID = "+//IDN bitfire.at//${BuildConfig.userAgent}/${BuildConfig.VERSION_NAME} ez-vcard/" + Ezvcard.VERSION
}
const val COLUMN_FLAGS = ContactsContract.RawContacts.SYNC4
@@ -37,6 +37,10 @@ class LocalContact: AndroidContact, LocalAddress {
private val cachedGroupMemberships = HashSet<Long>()
private val groupMemberships = HashSet<Long>()
override var scheduleTag: String?
get() = null
set(value) = throw NotImplementedError()
override var flags: Int = 0
constructor(addressBook: AndroidAddressBook<LocalContact,*>, values: ContentValues)
@@ -50,20 +54,34 @@ class LocalContact: AndroidContact, LocalAddress {
}
override fun assignNameAndUID() {
val uid = UUID.randomUUID().toString()
val newFileName = "$uid.vcf"
override fun prepareForFirstUpload(): String {
var uid: String? = null
addressBook.provider!!.query(rawContactSyncURI(), arrayOf(COLUMN_UID), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
uid = cursor.getString(0)
}
val values = ContentValues(2)
values.put(COLUMN_FILENAME, newFileName)
values.put(COLUMN_UID, uid)
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
if (uid == null) {
// generate new UID
uid = UUID.randomUUID().toString()
fileName = newFileName
val values = ContentValues(1)
values.put(COLUMN_UID, uid)
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
contact!!.uid = uid
}
return "$uid.vcf"
}
override fun clearDirty(eTag: String?) {
val values = ContentValues(3)
override fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String?) {
if (scheduleTag != null)
throw IllegalArgumentException("Contacts must not have a Schedule-Tag")
val values = ContentValues(4)
if (fileName != null)
values.put(COLUMN_FILENAME, fileName)
values.put(COLUMN_ETAG, eTag)
values.put(ContactsContract.RawContacts.DIRTY, 0)
@@ -76,6 +94,8 @@ class LocalContact: AndroidContact, LocalAddress {
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
if (fileName != null)
this.fileName = fileName
this.eTag = eTag
}
@@ -93,7 +113,7 @@ class LocalContact: AndroidContact, LocalAddress {
override fun updateFlags(flags: Int) {
val values = ContentValues(1)
values.put(LocalContact.COLUMN_FLAGS, flags)
values.put(COLUMN_FLAGS, flags)
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
this.flags = flags
@@ -214,7 +234,7 @@ class LocalContact: AndroidContact, LocalAddress {
/**
* Returns the IDs of all groups the contact was member of (cached memberships).
* Cached memberships are kept in sync with memberships by DAVdroid and are used to determine
* Cached memberships are kept in sync with memberships by DAVx5 and are used to determine
* whether a membership has been deleted/added when a raw contact is dirty.
* @return set of {@link GroupMembership#GROUP_ROW_ID} (may be empty)
* @throws FileNotFoundException if the current contact can't be found

View File

@@ -10,7 +10,6 @@ package at.bitfire.davdroid.resource
import android.content.ContentProviderOperation
import android.content.ContentValues
import android.provider.CalendarContract
import android.provider.CalendarContract.Events
import at.bitfire.davdroid.BuildConfig
import at.bitfire.ical4android.*
@@ -21,39 +20,43 @@ class LocalEvent: AndroidEvent, LocalResource<Event> {
companion object {
init {
ICalendar.prodId = ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4j/" + Constants.ical4jVersion)
ICalendar.prodId = ProdId("${BuildConfig.userAgent}/${BuildConfig.VERSION_NAME} ical4j/" + Ical4Android.ical4jVersion)
}
const val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1
const val COLUMN_FLAGS = CalendarContract.Events.SYNC_DATA2
const val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3
const val COLUMN_ETAG = Events.SYNC_DATA1
const val COLUMN_FLAGS = Events.SYNC_DATA2
const val COLUMN_SEQUENCE = Events.SYNC_DATA3
const val COLUMN_SCHEDULE_TAG = Events.SYNC_DATA4
}
override var fileName: String? = null
private set
override var eTag: String? = null
override var scheduleTag: String? = null
override var flags: Int = 0
private set
var weAreOrganizer = true
var weAreOrganizer = false
private set
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?, flags: Int): super(calendar, event) {
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?, scheduleTag: String?, flags: Int): super(calendar, event) {
this.fileName = fileName
this.eTag = eTag
this.scheduleTag = scheduleTag
this.flags = flags
}
private constructor(calendar: AndroidCalendar<*>, values: ContentValues): super(calendar, values) {
fileName = values.getAsString(Events._SYNC_ID)
eTag = values.getAsString(COLUMN_ETAG)
scheduleTag = values.getAsString(COLUMN_SCHEDULE_TAG)
flags = values.getAsInteger(COLUMN_FLAGS) ?: 0
}
override fun populateEvent(row: ContentValues) {
super.populateEvent(row)
override fun populateEvent(row: ContentValues, groupScheduled: Boolean) {
val event = requireNotNull(event)
event.uid = row.getAsString(Events.UID_2445)
@@ -61,6 +64,8 @@ class LocalEvent: AndroidEvent, LocalResource<Event> {
val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER)
weAreOrganizer = isOrganizer != null && isOrganizer != 0
super.populateEvent(row, groupScheduled)
}
override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) {
@@ -72,46 +77,54 @@ class LocalEvent: AndroidEvent, LocalResource<Event> {
builder .withValue(Events.UID_2445, event.uid)
.withValue(COLUMN_SEQUENCE, eventToBuild.sequence)
.withValue(CalendarContract.Events.DIRTY, 0)
.withValue(CalendarContract.Events.DELETED, 0)
.withValue(LocalEvent.COLUMN_FLAGS, flags)
.withValue(Events.DIRTY, 0)
.withValue(Events.DELETED, 0)
.withValue(COLUMN_FLAGS, flags)
if (buildException)
builder .withValue(Events.ORIGINAL_SYNC_ID, fileName)
else
builder .withValue(Events._SYNC_ID, fileName)
.withValue(COLUMN_ETAG, eTag)
.withValue(COLUMN_SCHEDULE_TAG, scheduleTag)
}
override fun assignNameAndUID() {
override fun prepareForFirstUpload(): String {
var uid: String? = null
calendar.provider.query(eventSyncURI(), arrayOf(Events.UID_2445), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
uid = cursor.getString(0)
}
if (uid == null)
if (uid == null) {
// generate new UID
uid = UUID.randomUUID().toString()
val newFileName = "$uid.ics"
val values = ContentValues(1)
values.put(Events.UID_2445, uid)
calendar.provider.update(eventSyncURI(), values, null, null)
val values = ContentValues(2)
values.put(Events._SYNC_ID, newFileName)
values.put(Events.UID_2445, uid)
calendar.provider.update(eventSyncURI(), values, null, null)
event!!.uid = uid
}
fileName = newFileName
event!!.uid = uid
return "$uid.ics"
}
override fun clearDirty(eTag: String?) {
val values = ContentValues(2)
values.put(CalendarContract.Events.DIRTY, 0)
override fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String?) {
val values = ContentValues(5)
if (fileName != null)
values.put(Events._SYNC_ID, fileName)
values.put(COLUMN_ETAG, eTag)
values.put(COLUMN_SCHEDULE_TAG, scheduleTag)
values.put(COLUMN_SEQUENCE, event!!.sequence)
values.put(Events.DIRTY, 0)
calendar.provider.update(eventSyncURI(), values, null, null)
if (fileName != null)
this.fileName = fileName
this.eTag = eTag
this.scheduleTag = scheduleTag
}
override fun updateFlags(flags: Int) {

View File

@@ -20,7 +20,6 @@ import android.provider.ContactsContract.CommonDataKinds.GroupMembership
import android.provider.ContactsContract.Groups
import android.provider.ContactsContract.RawContacts
import android.provider.ContactsContract.RawContacts.Data
import at.bitfire.dav4android.Constants
import at.bitfire.vcard4android.*
import java.util.*
@@ -101,6 +100,10 @@ class LocalGroup: AndroidGroup, LocalAddress {
}
override var scheduleTag: String?
get() = null
set(value) = throw NotImplementedError()
override var flags: Int = 0
@@ -130,27 +133,43 @@ class LocalGroup: AndroidGroup, LocalAddress {
}
override fun assignNameAndUID() {
val uid = UUID.randomUUID().toString()
val newFileName = "$uid.vcf"
override fun prepareForFirstUpload(): String {
var uid: String? = null
addressBook.provider!!.query(groupSyncUri(), arrayOf(AndroidContact.COLUMN_UID), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
uid = cursor.getString(0)
}
val values = ContentValues(2)
values.put(COLUMN_FILENAME, newFileName)
values.put(COLUMN_UID, uid)
update(values)
if (uid == null) {
// generate new UID
uid = UUID.randomUUID().toString()
fileName = newFileName
val values = ContentValues(1)
values.put(AndroidContact.COLUMN_UID, uid)
addressBook.provider!!.update(groupSyncUri(), values, null, null)
contact!!.uid = uid
}
return "$uid.vcf"
}
override fun clearDirty(eTag: String?) {
override fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String?) {
if (scheduleTag != null)
throw IllegalArgumentException("Contact groups must not have a Schedule-Tag")
val id = requireNotNull(id)
val values = ContentValues(2)
values.put(Groups.DIRTY, 0)
val values = ContentValues(3)
if (fileName != null)
values.put(COLUMN_FILENAME, fileName)
values.put(COLUMN_ETAG, eTag)
this.eTag = eTag
values.put(Groups.DIRTY, 0)
update(values)
if (fileName != null)
this.fileName = fileName
this.eTag = eTag
// update cached group memberships
val batch = BatchOperation(addressBook.provider!!)
@@ -200,7 +219,7 @@ class LocalGroup: AndroidGroup, LocalAddress {
override fun updateFlags(flags: Int) {
val values = ContentValues(1)
values.put(LocalGroup.COLUMN_FLAGS, flags)
values.put(COLUMN_FLAGS, flags)
addressBook.provider!!.update(groupSyncUri(), values, null, null)
this.flags = flags

View File

@@ -28,14 +28,44 @@ interface LocalResource<in TData: Any> {
*/
val id: Long?
/**
* Remote file name for the resource, for instance `mycontact.vcf`. Also used to determine whether
* a dirty record has just been created (in this case, [fileName] is *null*) or modified
* (in this case, [fileName] is the remote file name).
*/
val fileName: String?
var eTag: String?
var scheduleTag: String?
val flags: Int
fun assignNameAndUID()
fun clearDirty(eTag: String?)
/**
* Prepares the resource for the first upload.
*
* 1. If the resource doesn't have an UID yet, this method generates one and writes it to the content provider.
* 2. The new file name which should be used for the upload is derived from the UID and returned, but not
* saved to the content provider.
*
* @return new file name of the resource (like "<uid>.vcf")
*/
fun prepareForFirstUpload(): String
/**
* Unsets the /dirty/ field of the resource. Typically used after successfully uploading a
* locally modified resource.
*
* @param fileName If this argument is not *null*, [LocalResource.fileName] will be set to its value.
* @param eTag ETag of the uploaded resource as returned by the server (null if the server didn't return one)
* @param scheduleTag CalDAV Schedule-Tag of the uploaded resource as returned by the server (null if not applicable or if the server didn't return one)
*/
fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String? = null)
/**
* Sets (local) flags of the resource. At the moment, the only allowed values are
* 0 and [FLAG_REMOTELY_PRESENT].
*/
fun updateFlags(flags: Int)
/**
* Adds the data object to the content provider and ensures that the dirty flag is clear.
* @return content URI of the created row (e.g. event URI)

View File

@@ -10,10 +10,8 @@ package at.bitfire.davdroid.resource
import android.content.ContentProviderOperation
import android.content.ContentValues
import at.bitfire.ical4android.AndroidTask
import at.bitfire.ical4android.AndroidTaskFactory
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.Task
import android.provider.CalendarContract
import at.bitfire.ical4android.*
import org.dmfs.tasks.contract.TaskContract.Tasks
import java.util.*
@@ -25,6 +23,8 @@ class LocalTask: AndroidTask, LocalResource<Task> {
}
override var fileName: String? = null
override var scheduleTag: String? = null
override var eTag: String? = null
override var flags = 0
@@ -50,7 +50,6 @@ class LocalTask: AndroidTask, LocalResource<Task> {
override fun buildTask(builder: ContentProviderOperation.Builder, update: Boolean) {
super.buildTask(builder, update)
val task = requireNotNull(task)
builder .withValue(Tasks._SYNC_ID, fileName)
.withValue(COLUMN_ETAG, eTag)
@@ -60,27 +59,41 @@ class LocalTask: AndroidTask, LocalResource<Task> {
/* custom queries */
override fun assignNameAndUID() {
val uid = UUID.randomUUID().toString()
val newFileName = "$uid.ics"
override fun prepareForFirstUpload(): String {
var uid: String? = null
taskList.provider.client.query(taskSyncURI(), arrayOf(Tasks._UID), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
uid = cursor.getString(0)
}
val values = ContentValues(2)
values.put(Tasks._SYNC_ID, newFileName)
values.put(Tasks._UID, uid)
taskList.provider.client.update(taskSyncURI(), values, null, null)
if (uid == null) {
// generate new UID
uid = UUID.randomUUID().toString()
fileName = newFileName
val values = ContentValues(1)
values.put(Tasks._UID, uid)
taskList.provider.client.update(taskSyncURI(), values, null, null)
task!!.uid = uid
task!!.uid = uid
}
return "$uid.ics"
}
override fun clearDirty(eTag: String?) {
val values = ContentValues(3)
values.put(Tasks._DIRTY, 0)
override fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String?) {
if (scheduleTag != null)
Ical4Android.log.fine("Schedule-Tag for tasks not supported yet, won't save")
val values = ContentValues(4)
if (fileName != null)
values.put(Tasks._SYNC_ID, fileName)
values.put(COLUMN_ETAG, eTag)
values.put(Tasks.SYNC_VERSION, task!!.sequence)
values.put(Tasks._DIRTY, 0)
taskList.provider.client.update(taskSyncURI(), values, null, null)
if (fileName != null)
this.fileName = fileName
this.eTag = eTag
}

View File

@@ -9,6 +9,7 @@
package at.bitfire.davdroid.resource
import android.accounts.Account
import android.annotation.SuppressLint
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.ContentValues
@@ -17,8 +18,9 @@ import android.net.Uri
import android.os.Build
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.closeCompat
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.model.SyncState
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.AndroidTaskListFactory
@@ -49,7 +51,7 @@ class LocalTaskList private constructor(
return false
}
fun create(account: Account, provider: TaskProvider, info: CollectionInfo): Uri {
fun create(account: Account, provider: TaskProvider, info: Collection): Uri {
val values = valuesFromCollectionInfo(info, true)
values.put(TaskLists.OWNER, account.name)
values.put(TaskLists.SYNC_ENABLED, 1)
@@ -57,6 +59,7 @@ class LocalTaskList private constructor(
return create(account, provider, values)
}
@SuppressLint("Recycle")
@Throws(Exception::class)
fun onRenameAccount(resolver: ContentResolver, oldName: String, newName: String) {
var client: ContentProviderClient? = null
@@ -68,14 +71,11 @@ class LocalTaskList private constructor(
it.update(Tasks.getContentUri(TaskProvider.ProviderName.OpenTasks.authority), values, "${Tasks.ACCOUNT_NAME}=?", arrayOf(oldName))
}
} finally {
if (Build.VERSION.SDK_INT >= 24)
client?.close()
else
client?.release()
client?.closeCompat()
}
}
private fun valuesFromCollectionInfo(info: CollectionInfo, withColor: Boolean): ContentValues {
private fun valuesFromCollectionInfo(info: Collection, withColor: Boolean): ContentValues {
val values = ContentValues(3)
values.put(TaskLists._SYNC_ID, info.url.toString())
values.put(TaskLists.LIST_NAME, if (info.displayName.isNullOrBlank()) DavUtils.lastSegmentOfUrl(info.url) else info.displayName)
@@ -88,6 +88,9 @@ class LocalTaskList private constructor(
}
override val tag: String
get() = "tasks-${account.name}-$id"
override val title: String
get() = name ?: id.toString()
@@ -113,25 +116,32 @@ class LocalTaskList private constructor(
}
fun update(info: CollectionInfo, updateColor: Boolean) =
fun update(info: Collection, updateColor: Boolean) =
update(valuesFromCollectionInfo(info, updateColor))
override fun findDeleted() = queryTasks("${Tasks._DELETED}!=0", null)
override fun findDeleted() = queryTasks(Tasks._DELETED, null)
override fun findDirty(): List<LocalTask> {
val tasks = queryTasks("${Tasks._DIRTY}!=0", null)
val tasks = queryTasks(Tasks._DIRTY, null)
for (localTask in tasks) {
val task = requireNotNull(localTask.task)
val sequence = task.sequence
if (sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created)
task.sequence = 0
else
task.sequence = sequence + 1
try {
val task = requireNotNull(localTask.task)
val sequence = task.sequence
if (sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created)
task.sequence = 0
else // task was modified, increase sequence
task.sequence = sequence + 1
} catch(e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't check/increase sequence", e)
}
}
return tasks
}
override fun findDirtyWithoutNameOrUid() =
queryTasks("${Tasks._DIRTY} AND (${Tasks._SYNC_ID} IS NULL OR ${Tasks._UID} IS NULL)", null)
override fun findByName(name: String) =
queryTasks("${Tasks._SYNC_ID}=?", arrayOf(name)).firstOrNull()
@@ -146,9 +156,16 @@ class LocalTaskList private constructor(
override fun removeNotDirtyMarked(flags: Int) =
provider.client.delete(tasksSyncUri(),
"${Tasks.LIST_ID}=? AND ${Tasks._DIRTY}=0 AND ${LocalTask.COLUMN_FLAGS}=?",
"${Tasks.LIST_ID}=? AND NOT ${Tasks._DIRTY} AND ${LocalTask.COLUMN_FLAGS}=?",
arrayOf(id.toString(), flags.toString()))
override fun forgetETags() {
val values = ContentValues(1)
values.putNull(LocalEvent.COLUMN_ETAG)
provider.client.update(tasksSyncUri(), values, "${Tasks.LIST_ID}=?",
arrayOf(id.toString()))
}
object Factory: AndroidTaskListFactory<LocalTaskList> {

View File

@@ -5,39 +5,38 @@
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid
package at.bitfire.davdroid.settings
import android.accounts.Account
import android.accounts.AccountManager
import android.annotation.SuppressLint
import android.content.*
import android.os.Build
import android.content.ContentResolver
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Parcel
import android.os.RemoteException
import android.provider.CalendarContract
import android.provider.ContactsContract
import androidx.core.content.ContextCompat
import at.bitfire.davdroid.*
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.model.Credentials
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.ServiceDB.*
import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.model.SyncState
import at.bitfire.davdroid.model.Service
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.resource.LocalTask
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.TaskProvider
import at.bitfire.ical4android.TaskProvider.ProviderName.OpenTasks
import at.bitfire.vcard4android.ContactsStorageException
import at.bitfire.vcard4android.GroupMethod
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.apache.commons.lang3.StringUtils
import org.dmfs.tasks.contract.TaskContract
import java.util.*
import java.util.logging.Level
/**
@@ -45,15 +44,15 @@ import java.util.logging.Level
*
* @throws InvalidAccountException on construction when the account doesn't exist (anymore)
*/
@Suppress("FunctionName")
class AccountSettings(
val context: Context,
val settings: ISettings,
val account: Account
) {
companion object {
const val CURRENT_VERSION = 8
const val CURRENT_VERSION = 10
const val KEY_SETTINGS_VERSION = "version"
const val KEY_USERNAME = "user_name"
@@ -62,20 +61,28 @@ class AccountSettings(
const val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false)
const val KEY_WIFI_ONLY_SSIDS = "wifi_only_ssids" // restrict sync to specific WiFi SSIDs
/** Time range limitation to the past [in days]
value = null default value (DEFAULT_TIME_RANGE_PAST_DAYS)
< 0 (-1) no limit
>= 0 entries more than n days in the past won't be synchronized
/** Time range limitation to the past [in days]. Values:
*
* - null: default value (DEFAULT_TIME_RANGE_PAST_DAYS)
* - <0 (typically -1): no limit
* - n>0: entries more than n days in the past won't be synchronized
*/
const val KEY_TIME_RANGE_PAST_DAYS = "time_range_past_days"
const val DEFAULT_TIME_RANGE_PAST_DAYS = 90
/* Whether DAVdroid sets the local calendar color to the value from service DB at every sync
/**
* Whether a default alarm shall be assigned to received events/tasks which don't have an alarm.
* Value can be null (no default alarm) or an integer (default alarm shall be created this
* number of minutes before the event/task).
*/
const val KEY_DEFAULT_ALARM = "default_alarm"
/* Whether DAVx5 sets the local calendar color to the value from service DB at every sync
value = null (not existing) true (default)
"0" false */
const val KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors"
/* Whether DAVdroid populates and uses CalendarContract.Colors
/* Whether DAVx5 populates and uses CalendarContract.Colors
value = null (not existing) false (default)
"1" true */
const val KEY_EVENT_COLORS = "event_colors"
@@ -103,9 +110,10 @@ class AccountSettings(
}
}
val accountManager: AccountManager = AccountManager.get(context)
val settings = SettingsManager.getInstance(context)
init {
synchronized(AccountSettings::class.java) {
@@ -159,15 +167,15 @@ class AccountSettings(
}
}
fun getSyncWifiOnly() = if (settings.has(KEY_WIFI_ONLY))
settings.getBoolean(KEY_WIFI_ONLY, false)
fun getSyncWifiOnly() = if (settings.containsKey(KEY_WIFI_ONLY))
settings.getBoolean(KEY_WIFI_ONLY)
else
accountManager.getUserData(account, KEY_WIFI_ONLY) != null
fun setSyncWiFiOnly(wiFiOnly: Boolean) =
accountManager.setUserData(account, KEY_WIFI_ONLY, if (wiFiOnly) "1" else null)
fun getSyncWifiOnlySSIDs(): List<String>? = (if (settings.has(KEY_WIFI_ONLY_SSIDS))
settings.getString(KEY_WIFI_ONLY_SSIDS, null)
fun getSyncWifiOnlySSIDs(): List<String>? = (if (settings.containsKey(KEY_WIFI_ONLY_SSIDS))
settings.getString(KEY_WIFI_ONLY_SSIDS)
else
accountManager.getUserData(account, KEY_WIFI_ONLY_SSIDS))?.split(',')
fun setSyncWifiOnlySSIDs(ssids: List<String>?) =
@@ -179,8 +187,11 @@ class AccountSettings(
fun getTimeRangePastDays(): Int? {
val strDays = accountManager.getUserData(account, KEY_TIME_RANGE_PAST_DAYS)
return if (strDays != null) {
val days = Integer.valueOf(strDays)
if (days < 0) null else days
val days = strDays.toInt()
if (days < 0)
null
else
days
} else
DEFAULT_TIME_RANGE_PAST_DAYS
}
@@ -188,15 +199,44 @@ class AccountSettings(
fun setTimeRangePastDays(days: Int?) =
accountManager.setUserData(account, KEY_TIME_RANGE_PAST_DAYS, (days ?: -1).toString())
fun getManageCalendarColors() = if (settings.has(KEY_MANAGE_CALENDAR_COLORS))
settings.getBoolean(KEY_MANAGE_CALENDAR_COLORS, false)
/**
* Takes the default alarm setting (in this order) from
*
* 1. the local account settings
* 2. the settings provider (unless the value is -1 there).
*
* @return A default reminder shall be created this number of minutes before the start of every
* non-full-day event without reminder. *null*: No default reminders shall be created.
*/
fun getDefaultAlarm() =
accountManager.getUserData(account, KEY_DEFAULT_ALARM)?.toInt() ?:
settings.getIntOrNull(KEY_DEFAULT_ALARM)?.takeIf { it != -1 }
/**
* Sets the default alarm value in the local account settings, if the new value differs
* from the value of the settings provider. If the new value is the same as the value of
* the settings provider, the local setting will be deleted, so that the settings provider
* value applies.
*
* @param minBefore The number of minutes a default reminder shall be created before the
* start of every non-full-day event without reminder. *null*: No default reminders shall be created.
*/
fun setDefaultAlarm(minBefore: Int?) =
accountManager.setUserData(account, KEY_DEFAULT_ALARM,
if (minBefore == settings.getIntOrNull(KEY_DEFAULT_ALARM)?.takeIf { it != -1 })
null
else
minBefore?.toString())
fun getManageCalendarColors() = if (settings.containsKey(KEY_MANAGE_CALENDAR_COLORS))
settings.getBoolean(KEY_MANAGE_CALENDAR_COLORS)
else
accountManager.getUserData(account, KEY_MANAGE_CALENDAR_COLORS) == null
fun setManageCalendarColors(manage: Boolean) =
accountManager.setUserData(account, KEY_MANAGE_CALENDAR_COLORS, if (manage) null else "0")
fun getEventColors() = if (settings.has(KEY_EVENT_COLORS))
settings.getBoolean(KEY_EVENT_COLORS, false)
fun getEventColors() = if (settings.containsKey(KEY_EVENT_COLORS))
settings.getBoolean(KEY_EVENT_COLORS)
else
accountManager.getUserData(account, KEY_EVENT_COLORS) != null
fun setEventColors(useColors: Boolean) =
@@ -205,7 +245,7 @@ class AccountSettings(
// CardDAV settings
fun getGroupMethod(): GroupMethod {
val name = settings.getString(KEY_CONTACT_GROUP_METHOD, null) ?:
val name = settings.getString(KEY_CONTACT_GROUP_METHOD) ?:
accountManager.getUserData(account, KEY_CONTACT_GROUP_METHOD)
if (name != null)
try {
@@ -224,7 +264,7 @@ class AccountSettings(
// update from previous account settings
private fun update(baseVersion: Int) {
for (toVersion in baseVersion+1 .. CURRENT_VERSION) {
for (toVersion in baseVersion+1 ..CURRENT_VERSION) {
val fromVersion = toVersion-1
Logger.log.info("Updating account ${account.name} from version $fromVersion to $toVersion")
try {
@@ -239,19 +279,59 @@ class AccountSettings(
}
}
@Suppress("unused")
@Suppress("unused","FunctionName")
/**
* Task synchronization now handles alarms, categories, relations and unknown properties.
* Setting task ETags to null will cause them to be downloaded (and parsed) again.
*
* Also update the allowed reminder types for calendars.
**/
private fun update_9_10() {
TaskProvider.acquire(context, OpenTasks)?.use { provider ->
val tasksUri = TaskProvider.syncAdapterUri(provider.tasksUri(), account)
val emptyETag = ContentValues(1)
emptyETag.putNull(LocalTask.COLUMN_ETAG)
provider.client.update(tasksUri, emptyETag, "${TaskContract.Tasks._DIRTY}=0 AND ${TaskContract.Tasks._DELETED}=0", null)
}
@SuppressLint("Recycle")
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED)
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { provider ->
provider.update(AndroidCalendar.syncAdapterURI(CalendarContract.Calendars.CONTENT_URI, account),
AndroidCalendar.calendarBaseValues, null, null)
provider.closeCompat()
}
}
@Suppress("unused","FunctionName")
/**
* It seems that somehow some non-CalDAV accounts got OpenTasks syncable, which caused battery problems.
* Disable it on those accounts for the future.
*/
private fun update_8_9() {
val db = AppDatabase.getInstance(context)
val hasCalDAV = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV) != null
if (!hasCalDAV && ContentResolver.getIsSyncable(account, OpenTasks.authority) != 0) {
Logger.log.info("Disabling OpenTasks sync for $account")
ContentResolver.setIsSyncable(account, OpenTasks.authority, 0)
}
}
@Suppress("unused","FunctionName")
@SuppressLint("Recycle")
/**
* There is a mistake in this method. [TaskContract.Tasks.SYNC_VERSION] is used to store the
* SEQUENCE and should not be used for the eTag.
*/
private fun update_7_8() {
TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)?.let { provider ->
TaskProvider.acquire(context, OpenTasks)?.use { provider ->
// ETag is now in sync_version instead of sync1
// UID is now in _uid instead of sync2
provider.client.query(TaskProvider.syncAdapterUri(provider.tasksUri(), account),
arrayOf(TaskContract.Tasks._ID, TaskContract.Tasks.SYNC1, TaskContract.Tasks.SYNC2),
"${TaskContract.Tasks.ACCOUNT_TYPE}=? AND ${TaskContract.Tasks.ACCOUNT_NAME}=?",
arrayOf(account.type, account.name), null).use { cursor ->
arrayOf(account.type, account.name), null)!!.use { cursor ->
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
val eTag = cursor.getString(1)
@@ -271,16 +351,14 @@ class AccountSettings(
}
@Suppress("unused")
@SuppressLint("Recycle")
private fun update_6_7() {
// add calendar colors
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { provider ->
try {
AndroidCalendar.insertColors(provider, account)
} finally {
if (Build.VERSION.SDK_INT >= 24)
provider.close()
else
provider.release()
provider.closeCompat()
}
}
@@ -291,7 +369,7 @@ class AccountSettings(
}
@Suppress("unused")
@SuppressLint("ParcelClassLoader")
@SuppressLint("Recycle", "ParcelClassLoader")
private fun update_5_6() {
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.let { provider ->
val parcel = Parcel.obtain()
@@ -308,15 +386,13 @@ class AccountSettings(
else {
parcel.unmarshall(raw, 0, raw.size)
parcel.setDataPosition(0)
val params = parcel.readBundle()
val url = params.getString("url")?.let { HttpUrl.parse(it) }
val params = parcel.readBundle()!!
val url = params.getString("url")?.toHttpUrlOrNull()
if (url == null)
Logger.log.info("No address book URL, ignoring account")
else {
// create new address book
val info = CollectionInfo(url)
info.type = CollectionInfo.Type.ADDRESS_BOOK
info.displayName = account.name
val info = Collection(url = url, type = Collection.TYPE_ADDRESSBOOK, displayName = account.name)
Logger.log.log(Level.INFO, "Creating new address book account", url)
val addressBookAccount = Account(LocalAddressBook.accountName(account, info), context.getString(R.string.account_type_address_book))
if (!accountManager.addAccountExplicitly(addressBookAccount, null, LocalAddressBook.initialUserData(account, info.url.toString())))
@@ -343,10 +419,7 @@ class AccountSettings(
throw ContactsStorageException("Couldn't migrate contacts to new address book", e)
} finally {
parcel.recycle()
if (Build.VERSION.SDK_INT >= 24)
provider.close()
else
provider.release()
provider.closeCompat()
}
}
@@ -362,7 +435,7 @@ class AccountSettings(
@Suppress("unused")
private fun update_4_5() {
// call PackageChangedReceiver which then enables/disables OpenTasks sync when it's (not) available
PackageChangedReceiver.updateTaskSync(context)
OpenTasksWatcher.updateTaskSync(context)
}
@Suppress("unused")
@@ -370,171 +443,6 @@ class AccountSettings(
setGroupMethod(GroupMethod.CATEGORIES)
}
@Suppress("unused")
private fun update_2_3() {
// Don't show a warning for Android updates anymore
accountManager.setUserData(account, "last_android_version", null)
var serviceCardDAV: Long? = null
var serviceCalDAV: Long? = null
ServiceDB.OpenHelper(context).use { dbHelper ->
val db = dbHelper.writableDatabase
// we have to create the WebDAV Service database only from the old address book, calendar and task list URLs
// CardDAV: migrate address books
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.let { client ->
try {
val addrBook = LocalAddressBook(context, account, client)
val url = addrBook.url
Logger.log.fine("Migrating address book $url")
// insert CardDAV service
val values = ContentValues(3)
values.put(Services.ACCOUNT_NAME, account.name)
values.put(Services.SERVICE, Services.SERVICE_CARDDAV)
serviceCardDAV = db.insert(Services._TABLE, null, values)
// insert address book
values.clear()
values.put(Collections.SERVICE_ID, serviceCardDAV)
values.put(Collections.URL, url)
values.put(Collections.SYNC, 1)
db.insert(Collections._TABLE, null, values)
// insert home set
HttpUrl.parse(url)?.let {
val homeSet = it.resolve("../")
values.clear()
values.put(HomeSets.SERVICE_ID, serviceCardDAV)
values.put(HomeSets.URL, homeSet.toString())
db.insert(HomeSets._TABLE, null, values)
}
} catch (e: ContactsStorageException) {
Logger.log.log(Level.SEVERE, "Couldn't migrate address book", e)
} finally {
if (Build.VERSION.SDK_INT >= 24)
client.close()
else
@Suppress("deprecation")
client.release()
}
}
// CalDAV: migrate calendars + task lists
val collections = HashSet<String>()
val homeSets = HashSet<HttpUrl>()
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { client ->
try {
val calendars = AndroidCalendar.find(account, client, LocalCalendar.Factory, null, null)
for (calendar in calendars)
calendar.name?.let { url ->
Logger.log.fine("Migrating calendar $url")
collections.add(url)
HttpUrl.parse(url)?.resolve("../")?.let { homeSets.add(it) }
}
} catch (e: CalendarStorageException) {
Logger.log.log(Level.SEVERE, "Couldn't migrate calendars", e)
} finally {
if (Build.VERSION.SDK_INT >= 24)
client.close()
else
@Suppress("deprecation")
client.release()
}
}
AndroidTaskList.acquireTaskProvider(context)?.use { provider ->
try {
val taskLists = AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null)
for (taskList in taskLists)
taskList.syncId?.let { url ->
Logger.log.fine("Migrating task list $url")
collections.add(url)
HttpUrl.parse(url)?.resolve("../")?.let { homeSets.add(it) }
}
} catch (e: CalendarStorageException) {
Logger.log.log(Level.SEVERE, "Couldn't migrate task lists", e)
}
}
if (!collections.isEmpty()) {
// insert CalDAV service
val values = ContentValues(3)
values.put(Services.ACCOUNT_NAME, account.name)
values.put(Services.SERVICE, Services.SERVICE_CALDAV)
serviceCalDAV = db.insert(Services._TABLE, null, values)
// insert collections
for (url in collections) {
values.clear()
values.put(Collections.SERVICE_ID, serviceCalDAV)
values.put(Collections.URL, url)
values.put(Collections.SYNC, 1)
db.insert(Collections._TABLE, null, values)
}
// insert home sets
for (homeSet in homeSets) {
values.clear()
values.put(HomeSets.SERVICE_ID, serviceCalDAV)
values.put(HomeSets.URL, homeSet.toString())
db.insert(HomeSets._TABLE, null, values)
}
}
}
// initiate service detection (refresh) to get display names, colors etc.
val refresh = Intent(context, DavService::class.java)
refresh.action = DavService.ACTION_REFRESH_COLLECTIONS
serviceCardDAV?.let {
refresh.putExtra(DavService.EXTRA_DAV_SERVICE_ID, it)
context.startService(refresh)
}
serviceCalDAV?.let {
refresh.putExtra(DavService.EXTRA_DAV_SERVICE_ID, it)
context.startService(refresh)
}
}
@Suppress("unused")
@SuppressLint("Recycle")
private fun update_1_2() {
/* - KEY_ADDRESSBOOK_URL ("addressbook_url"),
- KEY_ADDRESSBOOK_CTAG ("addressbook_ctag"),
- KEY_ADDRESSBOOK_VCARD_VERSION ("addressbook_vcard_version") are not used anymore (now stored in ContactsContract.SyncState)
- KEY_LAST_ANDROID_VERSION ("last_android_version") has been added
*/
// move previous address book info to ContactsContract.SyncState
val provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY) ?:
throw ContactsStorageException("Couldn't access Contacts provider")
try {
val addr = LocalAddressBook(context, account, provider)
// until now, ContactsContract.Settings.UNGROUPED_VISIBLE was not set explicitly
val values = ContentValues()
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
addr.settings = values
val url = accountManager.getUserData(account, "addressbook_url")
if (!url.isNullOrEmpty())
addr.url = url
accountManager.setUserData(account, "addressbook_url", null)
val cTag = accountManager.getUserData (account, "addressbook_ctag")
if (!cTag.isNullOrEmpty())
addr.lastSyncState = SyncState(SyncState.Type.CTAG, cTag)
accountManager.setUserData(account, "addressbook_ctag", null)
} finally {
if (Build.VERSION.SDK_INT >= 24)
provider.close()
else
@Suppress("deprecation")
provider.release()
}
}
// updates from AccountSettings version 2 and below are not supported anymore
}

View File

@@ -8,67 +8,99 @@
package at.bitfire.davdroid.settings
import at.bitfire.davdroid.App
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.Build
import androidx.core.content.getSystemService
open class DefaultsProvider(
private val allowOverride: Boolean = true
): Provider {
val context: Context,
val settingsManager: SettingsManager
): SettingsProvider {
open val booleanDefaults = mapOf(
Pair(App.DISTRUST_SYSTEM_CERTIFICATES, false),
Pair(App.OVERRIDE_PROXY, false)
open val booleanDefaults = mutableMapOf(
Pair(Settings.DISTRUST_SYSTEM_CERTIFICATES, false),
Pair(Settings.OVERRIDE_PROXY, false)
)
open val intDefaults = mapOf(
Pair(App.OVERRIDE_PROXY_PORT, App.OVERRIDE_PROXY_PORT_DEFAULT)
Pair(Settings.OVERRIDE_PROXY_PORT, 8118)
)
open val longDefaults = mapOf<String, Long>()
open val stringDefaults = mapOf(
Pair(App.OVERRIDE_PROXY_HOST, App.OVERRIDE_PROXY_HOST_DEFAULT)
Pair(Settings.OVERRIDE_PROXY_HOST, "localhost")
)
val dataSaverChangedListener by lazy {
object: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
evaluateDataSaver(true)
}
}
}
override fun close() {
init {
if (Build.VERSION.SDK_INT >= 24) {
val dataSaverChangedFilter = IntentFilter(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED)
context.registerReceiver(dataSaverChangedListener, dataSaverChangedFilter)
evaluateDataSaver()
}
}
override fun forceReload() {
evaluateDataSaver()
}
override fun close() {
if (Build.VERSION.SDK_INT >= 24)
context.unregisterReceiver(dataSaverChangedListener)
}
private fun hasKey(key: String) =
fun evaluateDataSaver(notify: Boolean = false) {
if (Build.VERSION.SDK_INT >= 24) {
context.getSystemService<ConnectivityManager>()?.let { connectivityManager ->
if (connectivityManager.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED)
booleanDefaults[AccountSettings.KEY_WIFI_ONLY] = true
else
booleanDefaults -= AccountSettings.KEY_WIFI_ONLY
}
if (notify)
settingsManager.onSettingsChanged()
}
}
override fun canWrite() = false
override fun contains(key: String) =
booleanDefaults.containsKey(key) ||
intDefaults.containsKey(key) ||
longDefaults.containsKey(key) ||
stringDefaults.containsKey(key)
override fun has(key: String): Pair<Boolean, Boolean> {
val has = hasKey(key)
return Pair(has, allowOverride || !has)
override fun getBoolean(key: String) = booleanDefaults[key]
override fun getInt(key: String) = intDefaults[key]
override fun getLong(key: String) = longDefaults[key]
override fun getString(key: String) = stringDefaults[key]
override fun putBoolean(key: String, value: Boolean?) = throw NotImplementedError()
override fun putInt(key: String, value: Int?) = throw NotImplementedError()
override fun putLong(key: String, value: Long?) = throw NotImplementedError()
override fun putString(key: String, value: String?) = throw NotImplementedError()
override fun remove(key: String) = throw NotImplementedError()
class Factory : SettingsProviderFactory {
override fun getProviders(context: Context, settingsManager: SettingsManager) =
listOf(DefaultsProvider(context, settingsManager))
}
override fun getBoolean(key: String) =
Pair(booleanDefaults[key], allowOverride || !booleanDefaults.containsKey(key))
override fun getInt(key: String) =
Pair(intDefaults[key], allowOverride || !intDefaults.containsKey(key))
override fun getLong(key: String) =
Pair(longDefaults[key], allowOverride || !longDefaults.containsKey(key))
override fun getString(key: String) =
Pair(stringDefaults[key], allowOverride || !stringDefaults.containsKey(key))
override fun isWritable(key: String) = Pair(false, allowOverride || !hasKey(key))
override fun putBoolean(key: String, value: Boolean?) = false
override fun putInt(key: String, value: Int?) = false
override fun putLong(key: String, value: Long?) = false
override fun putString(key: String, value: String?) = false
override fun remove(key: String) = false
}

View File

@@ -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 java.io.Closeable
interface Provider: Closeable {
fun forceReload()
fun has(key: String): Pair<Boolean, Boolean>
fun getBoolean(key: String): Pair<Boolean?, Boolean>
fun getInt(key: String): Pair<Int?, Boolean>
fun getLong(key: String): Pair<Long?, Boolean>
fun getString(key: String): Pair<String?, Boolean>
fun isWritable(key: String): Pair<Boolean, Boolean>
fun putBoolean(key: String, value: Boolean?): Boolean
fun putInt(key: String, value: Int?): Boolean
fun putLong(key: String, value: Long?): Boolean
fun putString(key: String, value: String?): Boolean
fun remove(key: String): Boolean
interface Observer {
fun onReload()
}
}

View File

@@ -1,289 +1,11 @@
/*
* 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 android.annotation.TargetApi
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import at.bitfire.davdroid.log.Logger
import java.lang.ref.WeakReference
import java.util.*
import java.util.logging.Level
object Settings {
class Settings: Service(), Provider.Observer {
const val DISTRUST_SYSTEM_CERTIFICATES = "distrust_system_certs"
private val providers = LinkedList<Provider>()
private val observers = LinkedList<WeakReference<ISettingsObserver>>()
const val OVERRIDE_PROXY = "override_proxy"
const val OVERRIDE_PROXY_HOST = "override_proxy_host"
const val OVERRIDE_PROXY_PORT = "override_proxy_port"
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
override fun onCreate() {
Logger.log.info("Initializing Settings service")
// always add a defaults provider first
providers.add(DefaultsProvider())
// load flavor-specific providers
val providersLoader = ServiceLoader.load(ISettingsProviderFactory::class.java)!!
providersLoader.forEach { factory ->
factory.getProviders(this).forEach {
Logger.log.info("Registering settings provider: ${it.javaClass.name}")
providers.add(it)
}
}
// always add a provider for local preferences
providers.add(SharedPreferencesProvider(this))
}
override fun onDestroy() {
Logger.log.info("Shutting down Settings service")
providers.forEach { it.close() }
providers.clear()
}
override fun onTrimMemory(level: Int) {
stopSelf()
}
fun forceReload() {
providers.forEach { it.forceReload() }
}
override fun onReload() {
observers.forEach {
Handler(Looper.getMainLooper()).post {
it.get()?.onSettingsChanged()
}
}
}
private fun has(key: String): Boolean {
Logger.log.fine("Looking for setting $key")
var result = false
for (provider in providers)
try {
val (value, further) = provider.has(key)
Logger.log.finer("${provider::class.java.simpleName}: has $key = $value, continue: $further")
if (value) {
result = true
break
}
if (!further)
break
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't look up setting in $provider", e)
}
Logger.log.fine("Looking for setting $key -> $result")
return result
}
private fun<T> getValue(key: String, reader: (Provider) -> Pair<T?, Boolean>): T? {
Logger.log.fine("Looking up setting $key")
var result: T? = null
for (provider in providers)
try {
val (value, further) = reader(provider)
Logger.log.finer("${provider::class.java.simpleName}: value = $value, continue: $further")
value?.let { result = it }
if (!further)
break
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't read setting from $provider", e)
}
Logger.log.fine("Looked up setting $key -> $result")
return result
}
fun getBoolean(key: String) =
getValue(key) { provider -> provider.getBoolean(key) }
fun getInt(key: String) =
getValue(key) { provider -> provider.getInt(key) }
fun getLong(key: String) =
getValue(key) { provider -> provider.getLong(key) }
fun getString(key: String) =
getValue(key) { provider -> provider.getString(key) }
fun isWritable(key: String): Boolean {
for (provider in providers) {
val (value, further) = provider.isWritable(key)
if (value)
return true
if (!further)
return false
}
return false
}
private fun<T> putValue(key: String, value: T?, writer: (Provider) -> Boolean): Boolean {
Logger.log.fine("Trying to write setting $key = $value")
for (provider in providers) {
val (writable, further) = provider.isWritable(key)
Logger.log.finer("${provider::class.java.simpleName}: writable = $writable, continue: $further")
if (writable)
return try {
writer(provider)
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't write setting to $provider", e)
false
}
if (!further)
return false
}
return false
}
fun putBoolean(key: String, value: Boolean?) =
putValue(key, value) { provider -> provider.putBoolean(key, value) }
fun putInt(key: String, value: Int?) =
putValue(key, value) { provider -> provider.putInt(key, value) }
fun putLong(key: String, value: Long?) =
putValue(key, value) { provider -> provider.putLong(key, value) }
fun putString(key: String, value: String?) =
putValue(key, value) { provider -> provider.putString(key, value) }
fun remove(key: String): Boolean {
var deleted = false
providers.forEach { deleted = deleted || it.remove(key) }
return deleted
}
val binder = object: ISettings.Stub() {
override fun forceReload() =
this@Settings.forceReload()
override fun has(key: String) =
this@Settings.has(key)
override fun getBoolean(key: String, defaultValue: Boolean) =
this@Settings.getBoolean(key) ?: defaultValue
override fun getInt(key: String, defaultValue: Int) =
this@Settings.getInt(key) ?: defaultValue
override fun getLong(key: String, defaultValue: Long) =
this@Settings.getLong(key) ?: defaultValue
override fun getString(key: String, defaultValue: String?) =
this@Settings.getString(key) ?: defaultValue
override fun isWritable(key: String) =
this@Settings.isWritable(key)
override fun remove(key: String) =
this@Settings.remove(key)
override fun putBoolean(key: String, value: Boolean) =
this@Settings.putBoolean(key, value)
override fun putString(key: String, value: String?) =
this@Settings.putString(key, value)
override fun putInt(key: String, value: Int) =
this@Settings.putInt(key, value)
override fun putLong(key: String, value: Long) =
this@Settings.putLong(key, value)
override fun registerObserver(observer: ISettingsObserver) {
observers += WeakReference(observer)
}
override fun unregisterObserver(observer: ISettingsObserver) {
observers.removeAll { it.get() == observer }
}
}
override fun onBind(intent: Intent?) = binder
class Stub(
delegate: ISettings,
private val context: Context,
private val serviceConn: ServiceConnection?
): ISettings by delegate, AutoCloseable {
override fun close() {
serviceConn?.let {
try {
context.unbindService(it)
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't unbind Settings service", e)
}
}
}
}
companion object {
fun getInstance(context: Context): Stub? {
if (context is Settings)
return Stub(context.binder, context, null)
if (Looper.getMainLooper().thread == Thread.currentThread())
throw IllegalStateException("Must not be called from main thread")
var service: ISettings? = null
val serviceLock = Object()
val serviceConn = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
synchronized(serviceLock) {
service = ISettings.Stub.asInterface(binder)
serviceLock.notify()
}
}
override fun onServiceDisconnected(name: ComponentName) {
service = null
}
}
if (!context.bindService(Intent(context, Settings::class.java), serviceConn, Context.BIND_AUTO_CREATE or Context.BIND_IMPORTANT))
return null
synchronized(serviceLock) {
if (service == null)
try {
serviceLock.wait()
} catch(e: InterruptedException) {
}
if (service == null) {
try {
context.unbindService(serviceConn)
} catch (e: IllegalArgumentException) {
}
return null
}
}
return Stub(service!!, context, serviceConn)
}
}
}
}

View File

@@ -0,0 +1,164 @@
/*
* 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 android.content.Context
import android.util.NoSuchPropertyException
import androidx.annotation.AnyThread
import at.bitfire.davdroid.AndroidSingleton
import at.bitfire.davdroid.log.Logger
import java.lang.ref.WeakReference
import java.util.*
import java.util.logging.Level
/**
* Settings manager which coordinates [SettingsProvider]s to read/write
* application settings.
*/
class SettingsManager private constructor(
context: Context
) {
companion object: AndroidSingleton<SettingsManager>() {
override fun createInstance(context: Context) = SettingsManager(context)
}
private val providers = LinkedList<SettingsProvider>()
private var writeProvider: SettingsProvider? = null
private val observers = LinkedList<WeakReference<OnChangeListener>>()
init {
val factories = ServiceLoader.load(SettingsProviderFactory::class.java, context.classLoader)
Logger.log.fine("Loading settings providers from ${factories.count()} factories")
for (factory in factories)
providers.addAll(factory.getProviders(context, this))
writeProvider = providers.first { it.canWrite() }
Logger.log.fine("Changed settings are handled by $writeProvider")
}
/**
* Requests all providers to reload their settings.
*/
fun forceReload() {
for (provider in providers)
provider.forceReload()
onSettingsChanged()
}
/*** OBSERVERS ***/
fun addOnChangeListener(observer: OnChangeListener) {
synchronized(observers) {
observers += WeakReference(observer)
}
}
fun removeOnChangeListener(observer: OnChangeListener) {
synchronized(observers) {
observers.removeAll { it.get() == null || it.get() == observer }
}
}
/**
* Notifies registered listeners about changes in the configuration.
* Should be called by config providers when settings have changed.
*/
fun onSettingsChanged() {
synchronized(observers) {
for (observer in observers.mapNotNull { it.get() })
observer.onSettingsChanged()
}
}
/*** SETTINGS ACCESS ***/
fun containsKey(key: String) = providers.any { it.contains(key) }
private fun<T> getValue(key: String, reader: (SettingsProvider) -> T?): T? {
Logger.log.fine("Looking up setting $key")
var result: T? = null
for (provider in providers)
try {
val value = reader(provider)
Logger.log.finer("${provider::class.java.simpleName}: value = $value")
if (value != null) {
Logger.log.fine("Looked up setting $key -> $value")
return value
}
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't read setting from $provider", e)
}
Logger.log.fine("Looked up setting $key -> no result")
return result
}
fun getBooleanOrNull(key: String): Boolean? = getValue(key) { provider -> provider.getBoolean(key) }
fun getBoolean(key: String): Boolean = getBooleanOrNull(key) ?: throw NoSuchPropertyException(key)
fun getIntOrNull(key: String): Int? = getValue(key) { provider -> provider.getInt(key) }
fun getInt(key: String): Int = getIntOrNull(key) ?: throw NoSuchPropertyException(key)
fun getLongOrNull(key: String): Long? = getValue(key) { provider -> provider.getLong(key) }
fun getLong(key: String) = getLongOrNull(key) ?: throw NoSuchPropertyException(key)
fun getString(key: String) = getValue(key) { provider -> provider.getString(key) }
fun isWritable(key: String): Boolean {
for (provider in providers) {
if (provider.canWrite() == true)
return true
else if (provider.contains(key))
// non-writeable provider contains this key -> setting will always be provided by this read-only provider
return false
}
return false
}
private fun<T> putValue(key: String, value: T?, writer: (SettingsProvider) -> Unit) {
Logger.log.fine("Trying to write setting $key = $value")
val provider = writeProvider ?: return
try {
writer(provider)
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't write setting to $writeProvider", e)
}
}
fun putBoolean(key: String, value: Boolean?) =
putValue(key, value) { provider -> provider.putBoolean(key, value) }
fun putInt(key: String, value: Int?) =
putValue(key, value) { provider -> provider.putInt(key, value) }
fun putLong(key: String, value: Long?) =
putValue(key, value) { provider -> provider.putLong(key, value) }
fun putString(key: String, value: String?) =
putValue(key, value) { provider -> provider.putString(key, value) }
fun remove(key: String) = putString(key, null)
interface OnChangeListener {
/**
* Will be called when something has changed in a [SettingsProvider].
* May run in worker thread!
*/
@AnyThread
fun onSettingsChanged()
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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
/**
* Defines a settings provider, which provides settings from a certain source
* to the [SettingsManager].
*
* Implementations must be thread-safe and synchronize get/put operations on their own.
*/
interface SettingsProvider {
/**
* Whether this provider can write settings.
*
* If this method returns false, the put...() methods will never be called for this provider.
*
* @return true = this provider provides read/write settings;
* false = this provider provides read-only settings
*/
fun canWrite(): Boolean
fun close()
fun forceReload()
fun contains(key: String): Boolean
fun getBoolean(key: String): Boolean?
fun getInt(key: String): Int?
fun getLong(key: String): Long?
fun getString(key: String): String?
fun putBoolean(key: String, value: Boolean?)
fun putInt(key: String, value: Int?)
fun putLong(key: String, value: Long?)
fun putString(key: String, value: String?)
fun remove(key: String)
}

View File

@@ -8,8 +8,10 @@
package at.bitfire.davdroid.settings
interface ISettingsProviderFactory {
import android.content.Context
fun getProviders(settings: Settings): List<Provider>
interface SettingsProviderFactory {
fun getProviders(context: Context, settingsManager: SettingsManager): Iterable<SettingsProvider>
}

View File

@@ -11,13 +11,15 @@ package at.bitfire.davdroid.settings
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import android.preference.PreferenceManager
import android.util.NoSuchPropertyException
import androidx.preference.PreferenceManager
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.AppDatabase
class SharedPreferencesProvider(
context: Context
): Provider {
val context: Context,
val settingsManager: SettingsManager
): SettingsProvider, SharedPreferences.OnSharedPreferenceChangeListener {
companion object {
private const val META_VERSION = "version"
@@ -34,52 +36,57 @@ class SharedPreferencesProvider(
firstCall(context)
meta.edit().putInt(META_VERSION, CURRENT_VERSION).apply()
}
}
override fun close() {
preferences.registerOnSharedPreferenceChangeListener(this)
}
override fun forceReload() {
}
override fun has(key: String) =
Pair(preferences.contains(key), true)
private fun<T> getValue(key: String, reader: (SharedPreferences) -> T): Pair<T?, Boolean> {
if (preferences.contains(key))
return Pair(
try { reader(preferences) } catch(e: ClassCastException) { null },
true)
return Pair(null, true)
override fun close() {
preferences.unregisterOnSharedPreferenceChangeListener(this)
}
override fun getBoolean(key: String): Pair<Boolean?, Boolean> =
override fun canWrite() = true
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
settingsManager.onSettingsChanged()
}
override fun contains(key: String) = preferences.contains(key)
private fun<T> getValue(key: String, reader: (SharedPreferences) -> T): T? =
try {
if (preferences.contains(key))
reader(preferences)
else
null
} catch(e: ClassCastException) {
null
}
override fun getBoolean(key: String) =
getValue(key) { preferences -> preferences.getBoolean(key, /* will never be used: */ false) }
override fun getInt(key: String): Pair<Int?, Boolean> =
override fun getInt(key: String) =
getValue(key) { preferences -> preferences.getInt(key, /* will never be used: */ -1) }
override fun getLong(key: String): Pair<Long?, Boolean> =
override fun getLong(key: String) =
getValue(key) { preferences -> preferences.getLong(key, /* will never be used: */ -1) }
override fun getString(key: String): Pair<String?, Boolean> =
getValue(key) { preferences -> preferences.getString(key, /* will never be used: */ null) }
override fun getString(key: String): String? =
preferences.getString(key, /* will never be used: */ null)
override fun isWritable(key: String) =
Pair(true, true)
private fun<T> putValue(key: String, value: T?, writer: (SharedPreferences.Editor, T) -> Unit): Boolean {
return if (value == null)
private fun<T> putValue(key: String, value: T?, writer: (SharedPreferences.Editor, T) -> Unit) {
if (value == null)
remove(key)
else {
Logger.log.fine("Writing setting $key = $value")
val edit = preferences.edit()
writer(edit, value)
edit.apply()
true
}
}
@@ -95,12 +102,9 @@ class SharedPreferencesProvider(
override fun putString(key: String, value: String?) =
putValue(key, value) { editor, v -> editor.putString(key, v) }
override fun remove(key: String): Boolean {
override fun remove(key: String) {
Logger.log.fine("Removing setting $key")
preferences.edit()
.remove(key)
.apply()
return true
preferences.edit().remove(key).apply()
}
@@ -114,7 +118,12 @@ class SharedPreferencesProvider(
edit.apply()
// open ServiceDB to upgrade it and possibly migrate settings
ServiceDB.OpenHelper(context).use { it.readableDatabase }
AppDatabase.getInstance(context)
}
class Factory : SettingsProviderFactory {
override fun getProviders(context: Context, settingsManager: SettingsManager) = listOf(SharedPreferencesProvider(context, settingsManager))
}
}

View File

@@ -11,59 +11,57 @@ import android.accounts.*
import android.app.Service
import android.content.Context
import android.content.Intent
import android.database.DatabaseUtils
import android.os.Bundle
import androidx.annotation.WorkerThread
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.ui.setup.LoginActivity
import java.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.logging.Level
/**
* Account authenticator for the main DAVdroid account type.
* Account authenticator for the main DAVx5 account type.
*
* Gets started when a DAVdroid account is removed, too, so it also watches for account removals
* Gets started when a DAVx5 account is removed, too, so it also watches for account removals
* and contains the corresponding cleanup code.
*/
class AccountAuthenticatorService: Service(), OnAccountsUpdateListener {
companion object {
@WorkerThread
@Synchronized
fun cleanupAccounts(context: Context) {
Logger.log.info("Cleaning up orphaned accounts")
ServiceDB.OpenHelper(context).use { dbHelper ->
val db = dbHelper.writableDatabase
val accountManager = AccountManager.get(context)
val accountNames = accountManager.getAccountsByType(context.getString(R.string.account_type))
.map { it.name }
val sqlAccountNames = LinkedList<String>()
val accountNames = HashSet<String>()
val accountManager = AccountManager.get(context)
for (account in accountManager.getAccountsByType(context.getString(R.string.account_type))) {
sqlAccountNames.add(DatabaseUtils.sqlEscapeString(account.name))
accountNames += account.name
}
// delete orphaned address book accounts
accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
.map { LocalAddressBook(context, it, null) }
.forEach {
try {
if (!accountNames.contains(it.mainAccount.name))
it.delete()
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e)
}
// delete orphaned address book accounts
accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
.map { LocalAddressBook(context, it, null) }
.forEach {
try {
if (!accountNames.contains(it.mainAccount.name))
it.delete()
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e)
}
}
// delete orphaned services in DB
if (sqlAccountNames.isEmpty())
db.delete(ServiceDB.Services._TABLE, null, null)
else
db.delete(ServiceDB.Services._TABLE, "${ServiceDB.Services.ACCOUNT_NAME} NOT IN (${sqlAccountNames.joinToString(",")})", null)
}
// delete orphaned services in DB
val db = AppDatabase.getInstance(context)
val serviceDao = db.serviceDao()
if (accountNames.isEmpty())
serviceDao.deleteAll()
else
serviceDao.deleteExceptAccounts(accountNames.toTypedArray())
}
}
@@ -85,11 +83,13 @@ class AccountAuthenticatorService: Service(), OnAccountsUpdateListener {
}
override fun onBind(intent: Intent?) =
accountAuthenticator.iBinder.takeIf { intent?.action == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT }
accountAuthenticator.iBinder.takeIf { intent?.action == AccountManager.ACTION_AUTHENTICATOR_INTENT }
override fun onAccountsUpdated(accounts: Array<out Account>?) {
cleanupAccounts(this)
CoroutineScope(Dispatchers.Default).launch {
cleanupAccounts(this@AccountAuthenticatorService)
}
}

View File

@@ -12,13 +12,14 @@ import android.content.ContentProvider
import android.content.ContentValues
import android.net.Uri
@Suppress("ImplicitNullableNothingType")
class AddressBookProvider: ContentProvider() {
override fun onCreate() = false
override fun insert(p0: Uri?, p1: ContentValues?) = null
override fun query(p0: Uri?, p1: Array<out String>?, p2: String?, p3: Array<out String>?, p4: String?) = null
override fun update(p0: Uri?, p1: ContentValues?, p2: String?, p3: Array<out String>?) = 0
override fun delete(p0: Uri?, p1: String?, p2: Array<out String>?) = 0
override fun getType(p0: Uri?) = null
override fun insert(p0: Uri, p1: ContentValues?) = null
override fun query(p0: Uri, p1: Array<out String>?, p2: String?, p3: Array<out String>?, p4: String?) = null
override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array<out String>?) = 0
override fun delete(p0: Uri, p1: String?, p2: Array<out String>?) = 0
override fun getType(p0: Uri) = null
}

View File

@@ -9,24 +9,23 @@ package at.bitfire.davdroid.syncadapter
import android.Manifest
import android.accounts.Account
import android.accounts.AccountManager
import android.content.*
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.Context
import android.content.SyncResult
import android.content.pm.PackageManager
import android.database.DatabaseUtils
import android.os.Build
import android.os.Bundle
import android.provider.ContactsContract
import android.support.v4.content.ContextCompat
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.R
import androidx.core.content.ContextCompat
import at.bitfire.davdroid.closeCompat
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.model.Service
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.ui.AccountActivity
import at.bitfire.davdroid.settings.AccountSettings
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.util.logging.Level
class AddressBooksSyncAdapterService : SyncAdapterService() {
@@ -38,9 +37,9 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
context: Context
) : SyncAdapter(context) {
override fun sync(settings: ISettings, account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
try {
val accountSettings = AccountSettings(context, settings, account)
val accountSettings = AccountSettings(context, account)
/* don't run sync if
- sync conditions (e.g. "sync only in WiFi") are not met AND
@@ -49,16 +48,14 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
return
updateLocalAddressBooks(provider, account, syncResult)
val accountManager = AccountManager.get(context)
for (addressBookAccount in accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))) {
Logger.log.log(Level.INFO, "Running sync for address book", addressBookAccount)
val syncExtras = Bundle(extras)
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true)
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true)
ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras)
}
if (updateLocalAddressBooks(account, syncResult))
for (addressBookAccount in LocalAddressBook.findAll(context, null, account).map { it.account }) {
Logger.log.log(Level.INFO, "Running sync for address book", addressBookAccount)
val syncExtras = Bundle(extras)
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true)
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true)
ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras)
}
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't sync address books", e)
}
@@ -66,94 +63,61 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
Logger.log.info("Address book sync complete")
}
private fun updateLocalAddressBooks(provider: ContentProviderClient, account: Account, syncResult: SyncResult) {
ServiceDB.OpenHelper(context).use { dbHelper ->
val db = dbHelper.readableDatabase
private fun updateLocalAddressBooks(account: Account, syncResult: SyncResult): Boolean {
val db = AppDatabase.getInstance(context)
val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CARDDAV)
fun getService() =
db.query(ServiceDB.Services._TABLE, arrayOf(ServiceDB.Services.ID),
"${ServiceDB.Services.ACCOUNT_NAME}=? AND ${ServiceDB.Services.SERVICE}=?",
arrayOf(account.name, ServiceDB.Services.SERVICE_CARDDAV), null, null, null)?.use { c ->
if (c.moveToNext())
c.getLong(0)
else
null
}
val remoteAddressBooks = mutableMapOf<HttpUrl, Collection>()
if (service != null)
for (collection in db.collectionDao().getByServiceAndSync(service.id))
remoteAddressBooks[collection.url] = collection
fun remoteAddressBooks(service: Long?): MutableMap<HttpUrl, CollectionInfo> {
val collections = mutableMapOf<HttpUrl, CollectionInfo>()
service?.let {
db.query(Collections._TABLE, null,
Collections.SERVICE_ID + "=? AND " + Collections.SYNC, arrayOf(service.toString()), null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
val values = ContentValues(cursor.columnCount)
DatabaseUtils.cursorRowToContentValues(cursor, values)
val info = CollectionInfo(values)
collections[info.url] = info
}
}
}
return collections
}
// enumerate remote and local address books
val service = getService()
val remote = remoteAddressBooks(service)
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
if (remote.isEmpty()) {
Logger.log.info("No contacts permission, but no address book selected for synchronization")
return
} else {
// no contacts permission, but address books should be synchronized -> show notification
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
notifyPermissions(intent)
}
}
val contactsProvider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
try {
if (contactsProvider == null) {
Logger.log.severe("Couldn't access contacts provider")
syncResult.databaseError = true
return
}
// delete/update local address books
for (addressBook in LocalAddressBook.findAll(context, contactsProvider, account)) {
val url = HttpUrl.parse(addressBook.url)!!
val info = remote[url]
if (info == null) {
Logger.log.log(Level.INFO, "Deleting obsolete local address book", url)
addressBook.delete()
} else {
// remote CollectionInfo found for this local collection, update data
try {
Logger.log.log(Level.FINE, "Updating local address book $url", info)
addressBook.update(info)
} catch (e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't rename address book account", e)
}
// we already have a local address book for this remote collection, don't take into consideration anymore
remote -= url
}
}
// create new local address books
for ((_, info) in remote) {
Logger.log.log(Level.INFO, "Adding local address book", info)
LocalAddressBook.create(context, contactsProvider, account, info)
}
} finally {
if (Build.VERSION.SDK_INT >= 24)
contactsProvider?.close()
else
contactsProvider?.release()
}
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
if (remoteAddressBooks.isEmpty())
Logger.log.info("No contacts permission, but no address book selected for synchronization")
else
Logger.log.warning("No contacts permission, but address books are selected for synchronization")
return false
}
val contactsProvider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
try {
if (contactsProvider == null) {
Logger.log.severe("Couldn't access contacts provider")
syncResult.databaseError = true
return false
}
// delete/update local address books
for (addressBook in LocalAddressBook.findAll(context, contactsProvider, account)) {
val url = addressBook.url.toHttpUrl()
val info = remoteAddressBooks[url]
if (info == null) {
Logger.log.log(Level.INFO, "Deleting obsolete local address book", url)
addressBook.delete()
} else {
// remote CollectionInfo found for this local collection, update data
try {
Logger.log.log(Level.FINE, "Updating local address book $url", info)
addressBook.update(info)
} catch (e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't rename address book account", e)
}
// we already have a local address book for this remote collection, don't take into consideration anymore
remoteAddressBooks -= url
}
}
// create new local address books
for ((_, info) in remoteAddressBooks) {
Logger.log.log(Level.INFO, "Adding local address book", info)
LocalAddressBook.create(context, contactsProvider, account, info)
}
} finally {
contactsProvider?.closeCompat()
}
return true
}
}

View File

@@ -12,27 +12,31 @@ import android.accounts.Account
import android.content.Context
import android.content.SyncResult
import android.os.Bundle
import at.bitfire.dav4android.DavCalendar
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.DavResponseCallback
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.property.*
import at.bitfire.davdroid.AccountSettings
import at.bitfire.dav4jvm.DavCalendar
import at.bitfire.dav4jvm.DavResponseCallback
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.property.*
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.SyncState
import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.resource.LocalEvent
import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.ical4android.DateUtils
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.InvalidCalendarException
import net.fortuna.ical4j.model.component.VAlarm
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.ByteArrayOutputStream
import java.io.Reader
import java.io.StringReader
import java.time.Duration
import java.util.*
import java.util.logging.Level
@@ -41,17 +45,16 @@ import java.util.logging.Level
*/
class CalendarSyncManager(
context: Context,
settings: ISettings,
account: Account,
accountSettings: AccountSettings,
extras: Bundle,
authority: String,
syncResult: SyncResult,
localCalendar: LocalCalendar
): SyncManager<LocalEvent, LocalCalendar, DavCalendar>(context, settings, account, accountSettings, extras, authority, syncResult, localCalendar) {
): SyncManager<LocalEvent, LocalCalendar, DavCalendar>(context, account, accountSettings, extras, authority, syncResult, localCalendar) {
override fun prepare(): Boolean {
collectionURL = HttpUrl.parse(localCollection.name ?: return false) ?: return false
collectionURL = (localCollection.name ?: return false).toHttpUrlOrNull() ?: return false
davCollection = DavCalendar(httpClient.okHttpClient, collectionURL)
// if there are dirty exceptions for events, mark their master events as dirty, too
@@ -61,14 +64,13 @@ class CalendarSyncManager(
}
override fun queryCapabilities(): SyncState? =
useRemoteCollection {
remoteExceptionContext {
var syncState: SyncState? = null
it.propfind(0, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation ->
if (relation == Response.HrefRelation.SELF) {
response[SupportedReportSet::class.java]?.let {
hasCollectionSync = it.reports.contains(SupportedReportSet.SYNC_COLLECTION)
response[SupportedReportSet::class.java]?.let { supported ->
hasCollectionSync = supported.reports.contains(SupportedReportSet.SYNC_COLLECTION)
}
syncState = syncState(response)
}
}
@@ -82,17 +84,14 @@ class CalendarSyncManager(
else
SyncAlgorithm.COLLECTION_SYNC
override fun prepareUpload(resource: LocalEvent): RequestBody = useLocal(resource) {
override fun generateUpload(resource: LocalEvent): RequestBody = localExceptionContext(resource) {
val event = requireNotNull(resource.event)
Logger.log.log(Level.FINE, "Preparing upload of event ${resource.fileName}", event)
val os = ByteArrayOutputStream()
event.write(os)
RequestBody.create(
DavCalendar.MIME_ICALENDAR_UTF8,
os.toByteArray()
)
os.toByteArray().toRequestBody(DavCalendar.MIME_ICALENDAR_UTF8)
}
override fun listAllRemote(callback: DavResponseCallback) {
@@ -104,7 +103,7 @@ class CalendarSyncManager(
limitStart = calendar.time
}
return useRemoteCollection { remote ->
return remoteExceptionContext { remote ->
Logger.log.info("Querying events since $limitStart")
remote.calendarQuery("VEVENT", limitStart, null, callback)
}
@@ -112,36 +111,26 @@ class CalendarSyncManager(
override fun downloadRemote(bunch: List<HttpUrl>) {
Logger.log.info("Downloading ${bunch.size} iCalendars: $bunch")
if (bunch.size == 1) {
val remote = bunch.first()
// only one contact, use GET
useRemote(DavResource(httpClient.okHttpClient, remote)) { resource ->
resource.get(DavCalendar.MIME_ICALENDAR.toString()) { response ->
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
val eTag = response.header("ETag")?.let { GetETag(it).eTag }
?: throw DavException("Received CalDAV GET response without ETag")
response.body()!!.use {
processVEvent(resource.fileName(), eTag, it.charStream())
}
}
}
} else
// multiple iCalendars, use calendar-multi-get
useRemoteCollection {
it.multiget(bunch) { response, _ ->
useRemote(response) {
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val calendarData = response[CalendarData::class.java]
val iCal = calendarData?.iCalendar
?: throw DavException("Received multi-get response without address data")
processVEvent(DavUtils.lastSegmentOfUrl(response.href), eTag, StringReader(iCal))
remoteExceptionContext {
it.multiget(bunch) { response, _ ->
responseExceptionContext(response) {
if (!response.isSuccess()) {
Logger.log.warning("Received non-successful multiget response for ${response.href}")
return@responseExceptionContext
}
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val scheduleTag = response[ScheduleTag::class.java]?.scheduleTag
val calendarData = response[CalendarData::class.java]
val iCal = calendarData?.iCalendar
?: throw DavException("Received multi-get response without address data")
processVEvent(DavUtils.lastSegmentOfUrl(response.href), eTag, scheduleTag, StringReader(iCal))
}
}
}
}
override fun postProcess() {
@@ -150,28 +139,38 @@ class CalendarSyncManager(
// helpers
private fun processVEvent(fileName: String, eTag: String, reader: Reader) {
private fun processVEvent(fileName: String, eTag: String, scheduleTag: String?, reader: Reader) {
val events: List<Event>
try {
events = Event.fromReader(reader)
events = Event.eventsFromReader(reader)
} catch (e: InvalidCalendarException) {
Logger.log.log(Level.SEVERE, "Received invalid iCalendar, ignoring", e)
notifyInvalidResource(e, fileName)
return
}
if (events.size == 1) {
val newData = events.first()
val event = events.first()
// delete local event, if it exists
useLocal(localCollection.findByName(fileName)) { local ->
// set default reminder for non-full-day events, if requested
val defaultAlarmMinBefore = accountSettings.getDefaultAlarm()
if (defaultAlarmMinBefore != null && DateUtils.isDateTime(event.dtStart) && event.alarms.isEmpty()) {
val alarm = VAlarm(Duration.ofMinutes(-defaultAlarmMinBefore.toLong()))
Logger.log.log(Level.FINE, "${event.uid}: Adding default alarm", alarm)
event.alarms += alarm
}
// update local event, if it exists
localExceptionContext(localCollection.findByName(fileName)) { local ->
if (local != null) {
Logger.log.info("Updating $fileName in local calendar")
Logger.log.log(Level.INFO, "Updating $fileName in local calendar", event)
local.eTag = eTag
local.update(newData)
local.scheduleTag = scheduleTag
local.update(event)
syncResult.stats.numUpdates++
} else {
Logger.log.info("Adding $fileName to local calendar")
useLocal(LocalEvent(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
Logger.log.log(Level.INFO, "Adding $fileName to local calendar", event)
localExceptionContext(LocalEvent(localCollection, event, fileName, eTag, scheduleTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
it.add()
}
syncResult.stats.numInserts++
@@ -181,4 +180,7 @@ class CalendarSyncManager(
Logger.log.info("Received VCALENDAR with not exactly one VEVENT with UID and without RECURRENCE-ID; ignoring $fileName")
}
override fun notifyInvalidResourceTitle(): String =
context.getString(R.string.sync_invalid_event)
}

View File

@@ -8,19 +8,21 @@
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.content.*
import android.database.DatabaseUtils
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.Context
import android.content.SyncResult
import android.os.Bundle
import android.provider.CalendarContract
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.model.AppDatabase
import at.bitfire.davdroid.model.Collection
import at.bitfire.davdroid.model.Service
import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.ical4android.AndroidCalendar
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.util.logging.Level
class CalendarsSyncAdapterService: SyncAdapterService() {
@@ -32,9 +34,9 @@ class CalendarsSyncAdapterService: SyncAdapterService() {
context: Context
): SyncAdapter(context) {
override fun sync(settings: ISettings, account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
try {
val accountSettings = AccountSettings(context, settings, account)
val accountSettings = AccountSettings(context, account)
/* don't run sync if
- sync conditions (e.g. "sync only in WiFi") are not met AND
@@ -50,9 +52,13 @@ class CalendarsSyncAdapterService: SyncAdapterService() {
updateLocalCalendars(provider, account, accountSettings)
for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, "${CalendarContract.Calendars.SYNC_EVENTS}!=0", null)) {
val priorityCalendars = priorityCollections(extras)
val calendars = AndroidCalendar
.find(account, provider, LocalCalendar.Factory, "${CalendarContract.Calendars.SYNC_EVENTS}!=0", null)
.sortedByDescending { priorityCalendars.contains(it.id) }
for (calendar in calendars) {
Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}")
CalendarSyncManager(context, settings, account, accountSettings, extras, authority, syncResult, calendar).use {
CalendarSyncManager(context, account, accountSettings, extras, authority, syncResult, calendar).use {
it.performSync()
}
}
@@ -63,64 +69,37 @@ class CalendarsSyncAdapterService: SyncAdapterService() {
}
private fun updateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
ServiceDB.OpenHelper(context).use { dbHelper ->
val db = dbHelper.readableDatabase
val db = AppDatabase.getInstance(context)
val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
fun getService() =
db.query(ServiceDB.Services._TABLE, arrayOf(ServiceDB.Services.ID),
"${ServiceDB.Services.ACCOUNT_NAME}=? AND ${ServiceDB.Services.SERVICE}=?",
arrayOf(account.name, ServiceDB.Services.SERVICE_CALDAV), null, null, null)?.use { c ->
if (c.moveToNext())
c.getLong(0)
else
null
}
fun remoteCalendars(service: Long?): MutableMap<HttpUrl, CollectionInfo> {
val collections = mutableMapOf<HttpUrl, CollectionInfo>()
service?.let {
db.query(Collections._TABLE, null,
"${Collections.SERVICE_ID}=? AND ${Collections.SUPPORTS_VEVENT}!=0 AND ${Collections.SYNC}",
arrayOf(service.toString()), null, null, null).use { cursor ->
while (cursor.moveToNext()) {
val values = ContentValues(cursor.columnCount)
DatabaseUtils.cursorRowToContentValues(cursor, values)
val info = CollectionInfo(values)
collections[info.url] = info
}
}
}
return collections
val remoteCalendars = mutableMapOf<HttpUrl, Collection>()
if (service != null)
for (collection in db.collectionDao().getSyncCalendars(service.id)) {
remoteCalendars[collection.url] = collection
}
// enumerate remote and local calendars
val service = getService()
val remote = remoteCalendars(service)
// delete/update local calendars
val updateColors = settings.getManageCalendarColors()
for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null))
calendar.name?.let {
val url = HttpUrl.parse(it)!!
val info = remote[url]
if (info == null) {
Logger.log.log(Level.INFO, "Deleting obsolete local calendar", url)
calendar.delete()
} else {
// remote CollectionInfo found for this local collection, update data
Logger.log.log(Level.FINE, "Updating local calendar $url", info)
calendar.update(info, updateColors)
// we already have a local calendar for this remote collection, don't take into consideration anymore
remote -= url
}
// delete/update local calendars
val updateColors = settings.getManageCalendarColors()
for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null))
calendar.name?.let {
val url = it.toHttpUrl()
val info = remoteCalendars[url]
if (info == null) {
Logger.log.log(Level.INFO, "Deleting obsolete local calendar", url)
calendar.delete()
} else {
// remote CollectionInfo found for this local collection, update data
Logger.log.log(Level.FINE, "Updating local calendar $url", info)
calendar.update(info, updateColors)
// we already have a local calendar for this remote collection, don't take into consideration anymore
remoteCalendars -= url
}
// create new local calendars
for ((_, info) in remote) {
Logger.log.log(Level.INFO, "Adding local calendar", info)
LocalCalendar.create(account, provider, info)
}
// create new local calendars
for ((_, info) in remoteCalendars) {
Logger.log.log(Level.INFO, "Adding local calendar", info)
LocalCalendar.create(account, provider, info)
}
}

View File

@@ -15,10 +15,9 @@ import android.content.Context
import android.content.SyncResult
import android.os.Bundle
import android.provider.ContactsContract
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.settings.AccountSettings
import java.util.logging.Level
class ContactsSyncAdapterService: SyncAdapterService() {
@@ -34,10 +33,10 @@ class ContactsSyncAdapterService: SyncAdapterService() {
context: Context
): SyncAdapter(context) {
override fun sync(settings: ISettings, account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
override fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
try {
val addressBook = LocalAddressBook(context, account, provider)
val accountSettings = AccountSettings(context, settings, addressBook.mainAccount)
val accountSettings = AccountSettings(context, addressBook.mainAccount)
// handle group method change
val groupMethod = accountSettings.getGroupMethod().name
@@ -55,13 +54,17 @@ class ContactsSyncAdapterService: SyncAdapterService() {
}
accountSettings.accountManager.setUserData(account, PREVIOUS_GROUP_METHOD, groupMethod)
/* don't run sync if
- sync conditions (e.g. "sync only in WiFi") are not met AND
- this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
*/
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
return
Logger.log.info("Synchronizing address book: ${addressBook.url}")
Logger.log.info("Taking settings from: ${addressBook.mainAccount}")
ContactsSyncManager(context, settings, account, accountSettings, extras, authority, syncResult, provider, addressBook).use {
ContactsSyncManager(context, account, accountSettings, extras, authority, syncResult, provider, addressBook).use {
it.performSync()
}
} catch(e: Exception) {

View File

@@ -13,29 +13,28 @@ import android.content.*
import android.os.Build
import android.os.Bundle
import android.provider.ContactsContract.Groups
import android.support.v4.app.NotificationCompat
import at.bitfire.dav4android.DavAddressBook
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.DavResponseCallback
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.property.*
import at.bitfire.davdroid.AccountSettings
import at.bitfire.dav4jvm.DavAddressBook
import at.bitfire.dav4jvm.DavResponseCallback
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.property.*
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.SyncState
import at.bitfire.davdroid.resource.*
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.vcard4android.BatchOperation
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.GroupMethod
import ezvcard.VCardVersion
import ezvcard.io.CannotParseException
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.*
import java.util.logging.Level
@@ -59,10 +58,10 @@ import java.util.logging.Level
* to be checked whether its group memberships have changed. In this case, the respective
* groups have to be set to dirty. For instance, if contact A is in group G and H, and then
* group membership of G is removed, the contact will be set to dirty because of the changed
* [android.provider.ContactsContract.CommonDataKinds.GroupMembership]. DAVdroid will
* [android.provider.ContactsContract.CommonDataKinds.GroupMembership]. DAVx5 will
* then have to check whether the group memberships have actually changed, and if so,
* all affected groups have to be set to dirty. To detect changes in group memberships,
* DAVdroid always mirrors all [android.provider.ContactsContract.CommonDataKinds.GroupMembership]
* DAVx5 always mirrors all [android.provider.ContactsContract.CommonDataKinds.GroupMembership]
* data rows in respective [at.bitfire.vcard4android.CachedGroupMembership] rows.
* If the cached group memberships are not the same as the current group member ships, the
* difference set (in our example G, because its in the cached memberships, but not in the
@@ -76,7 +75,6 @@ import java.util.logging.Level
*/
class ContactsSyncManager(
context: Context,
settings: ISettings,
account: Account,
accountSettings: AccountSettings,
extras: Bundle,
@@ -84,14 +82,13 @@ class ContactsSyncManager(
syncResult: SyncResult,
val provider: ContentProviderClient,
localAddressBook: LocalAddressBook
): SyncManager<LocalAddress, LocalAddressBook, DavAddressBook>(context, settings, account, accountSettings, extras, authority, syncResult, localAddressBook) {
): SyncManager<LocalAddress, LocalAddressBook, DavAddressBook>(context, account, accountSettings, extras, authority, syncResult, localAddressBook) {
companion object {
infix fun <T> Set<T>.disjunct(other: Set<T>) = (this - other) union (other - this)
}
private val readOnly = localAddressBook.readOnly
private var numDiscarded = 0
private var hasVCard4 = false
private val groupMethod = accountSettings.getGroupMethod()
@@ -113,7 +110,7 @@ class ContactsSyncManager(
}
}
collectionURL = HttpUrl.parse(localCollection.url) ?: return false
collectionURL = localCollection.url.toHttpUrlOrNull() ?: return false
davCollection = DavAddressBook(httpClient.okHttpClient, collectionURL)
resourceDownloader = ResourceDownloader(davCollection.location)
@@ -126,18 +123,16 @@ class ContactsSyncManager(
// in case of GROUP_VCARDs, treat groups as contacts in the local address book
localCollection.includeGroups = groupMethod == GroupMethod.GROUP_VCARDS
return useRemoteCollection {
return remoteExceptionContext {
var syncState: SyncState? = null
it.propfind(0, SupportedAddressData.NAME, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation ->
if (relation == Response.HrefRelation.SELF) {
response[SupportedAddressData::class.java]?.let {
hasVCard4 = it.hasVCard4()
response[SupportedAddressData::class.java]?.let { supported ->
hasVCard4 = supported.hasVCard4()
}
response[SupportedReportSet::class.java]?.let {
hasCollectionSync = it.reports.contains(SupportedReportSet.SYNC_COLLECTION)
response[SupportedReportSet::class.java]?.let { supported ->
hasCollectionSync = supported.reports.contains(SupportedReportSet.SYNC_COLLECTION)
}
syncState = syncState(response)
}
}
@@ -154,45 +149,35 @@ class ContactsSyncManager(
else
SyncAlgorithm.PROPFIND_REPORT
override fun processLocallyDeleted(): Boolean {
if (readOnly) {
for (group in localCollection.findDeletedGroups()) {
Logger.log.warning("Restoring locally deleted group (read-only address book!)")
useLocal(group) { it.resetDeleted() }
numDiscarded++
}
override fun processLocallyDeleted() =
if (readOnly) {
for (group in localCollection.findDeletedGroups()) {
Logger.log.warning("Restoring locally deleted group (read-only address book!)")
localExceptionContext(group) { it.resetDeleted() }
}
for (contact in localCollection.findDeletedContacts()) {
Logger.log.warning("Restoring locally deleted contact (read-only address book!)")
useLocal(contact) { it.resetDeleted() }
numDiscarded++
}
for (contact in localCollection.findDeletedContacts()) {
Logger.log.warning("Restoring locally deleted contact (read-only address book!)")
localExceptionContext(contact) { it.resetDeleted() }
}
if (numDiscarded > 0)
notifyDiscardedChange()
return false
} else
// mirror deletions to remote collection (DELETE)
return super.processLocallyDeleted()
}
false
} else
// mirror deletions to remote collection (DELETE)
super.processLocallyDeleted()
override fun uploadDirty(): Boolean {
if (readOnly) {
for (group in localCollection.findDirtyGroups()) {
Logger.log.warning("Resetting locally modified group to ETag=null (read-only address book!)")
useLocal(group) { it.clearDirty(null) }
numDiscarded++
localExceptionContext(group) { it.clearDirty(null, null) }
}
for (contact in localCollection.findDirtyContacts()) {
Logger.log.warning("Resetting locally modified contact to ETag=null (read-only address book!)")
useLocal(contact) { it.clearDirty(null) }
numDiscarded++
localExceptionContext(contact) { it.clearDirty(null, null) }
}
if (numDiscarded > 0)
notifyDiscardedChange()
} else {
if (groupMethod == GroupMethod.CATEGORIES) {
/* groups memberships are represented as contact CATEGORIES */
@@ -202,15 +187,15 @@ class ContactsSyncManager(
Logger.log.fine("Finally removing group $group")
// useless because Android deletes group memberships as soon as a group is set to DELETED:
// group.markMembersDirty()
useLocal(group) { it.delete() }
localExceptionContext(group) { it.delete() }
}
// groups with DIRTY=1: mark all memberships as dirty, then clean DIRTY flag of group
for (group in localCollection.findDirtyGroups()) {
Logger.log.fine("Marking members of modified group $group as dirty")
useLocal(group) {
localExceptionContext(group) {
it.markMembersDirty()
it.clearDirty(null)
it.clearDirty(null, null)
}
}
} else {
@@ -241,21 +226,7 @@ class ContactsSyncManager(
return super.uploadDirty()
}
private fun notifyDiscardedChange() {
val notification = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_STATUS)
.setSmallIcon(R.drawable.ic_delete_notification)
.setContentTitle(context.getString(R.string.sync_contacts_read_only_address_book))
.setContentText(context.resources.getQuantityString(R.plurals.sync_contacts_local_contact_changes_discarded, numDiscarded, numDiscarded))
.setNumber(numDiscarded)
.setSubText(account.name)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setLocalOnly(true)
.build()
notificationManager.notify("discarded_${account.name}", 0, notification)
}
override fun prepareUpload(resource: LocalAddress): RequestBody = useLocal(resource) {
override fun generateUpload(resource: LocalAddress): RequestBody = localExceptionContext(resource) {
val contact: Contact
if (resource is LocalContact) {
contact = resource.contact!!
@@ -285,49 +256,37 @@ class ContactsSyncManager(
val os = ByteArrayOutputStream()
contact.write(if (hasVCard4) VCardVersion.V4_0 else VCardVersion.V3_0, groupMethod, os)
RequestBody.create(
if (hasVCard4) DavAddressBook.MIME_VCARD4 else DavAddressBook.MIME_VCARD3_UTF8,
os.toByteArray()
os.toByteArray().toRequestBody(
if (hasVCard4) DavAddressBook.MIME_VCARD4 else DavAddressBook.MIME_VCARD3_UTF8
)
}
override fun listAllRemote(callback: DavResponseCallback) =
useRemoteCollection {
remoteExceptionContext {
it.propfind(1, ResourceType.NAME, GetETag.NAME, callback = callback)
}
override fun downloadRemote(bunch: List<HttpUrl>) {
Logger.log.info("Downloading ${bunch.size} vCards: $bunch")
if (bunch.size == 1) {
val remote = bunch.first()
// only one contact, use GET
useRemote(DavResource(httpClient.okHttpClient, remote)) { resource ->
resource.get("text/vcard;version=4.0, text/vcard;charset=utf-8;q=0.8, text/vcard;q=0.5") { response ->
// CardDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc6352#section-6.3.2.3]
val eTag = response.header("ETag")?.let { GetETag(it).eTag }
?: throw DavException("Received CardDAV GET response without ETag")
response.body()!!.use {
processVCard(resource.fileName(), eTag, it.charStream(), resourceDownloader)
}
}
}
} else
// multiple vCards, use addressbook-multi-get
useRemoteCollection {
it.multiget(bunch, hasVCard4) { response, _ ->
useRemote(response) {
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val addressData = response[AddressData::class.java]
val vCard = addressData?.vCard
?: throw DavException("Received multi-get response without address data")
processVCard(DavUtils.lastSegmentOfUrl(response.href), eTag, StringReader(vCard), resourceDownloader)
Logger.log.info("Downloading ${bunch.size} vCard(s): $bunch")
remoteExceptionContext {
it.multiget(bunch, hasVCard4) { response, _ ->
responseExceptionContext(response) {
if (!response.isSuccess()) {
Logger.log.warning("Received non-successful multiget response for ${response.href}")
return@responseExceptionContext
}
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val addressData = response[AddressData::class.java]
val vCard = addressData?.vCard
?: throw DavException("Received multi-get response without address data")
processVCard(DavUtils.lastSegmentOfUrl(response.href), eTag, StringReader(vCard), resourceDownloader)
}
}
}
}
override fun postProcess() {
@@ -350,22 +309,30 @@ class ContactsSyncManager(
private fun processVCard(fileName: String, eTag: String, reader: Reader, downloader: Contact.Downloader) {
Logger.log.info("Processing CardDAV resource $fileName")
val contacts = Contact.fromReader(reader, downloader)
val contacts = try {
Contact.fromReader(reader, downloader)
} catch (e: CannotParseException) {
Logger.log.log(Level.SEVERE, "Received invalid vCard, ignoring", e)
notifyInvalidResource(e, fileName)
return
}
if (contacts.isEmpty()) {
Logger.log.warning("Received VCard without data, ignoring")
Logger.log.warning("Received vCard without data, ignoring")
return
} else if (contacts.size > 1)
Logger.log.warning("Received multiple VCards, using first one")
Logger.log.warning("Received multiple vCards, using first one")
val newData = contacts.first()
if (groupMethod == GroupMethod.CATEGORIES && newData.group) {
Logger.log.warning("Received group VCard although group method is CATEGORIES. Saving as regular contact")
Logger.log.warning("Received group vCard although group method is CATEGORIES. Saving as regular contact")
newData.group = false
}
// update local contact, if it exists
useLocal(localCollection.findByName(fileName)) {
localExceptionContext(localCollection.findByName(fileName)) {
var local = it
if (local != null) {
Logger.log.log(Level.INFO, "Updating $fileName in local address book", newData)
@@ -394,15 +361,15 @@ class ContactsSyncManager(
if (local == null) {
if (newData.group) {
Logger.log.log(Level.INFO, "Creating local group", newData)
useLocal(LocalGroup(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
it.add()
local = it
localExceptionContext(LocalGroup(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) { group ->
group.add()
local = group
}
} else {
Logger.log.log(Level.INFO, "Creating local contact", newData)
useLocal(LocalContact(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
it.add()
local = it
localExceptionContext(LocalContact(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) { contact ->
contact.add()
local = contact
}
}
syncResult.stats.numInserts++
@@ -438,20 +405,14 @@ class ContactsSyncManager(
): Contact.Downloader {
override fun download(url: String, accepts: String): ByteArray? {
val httpUrl = HttpUrl.parse(url)
val httpUrl = url.toHttpUrlOrNull()
if (httpUrl == null) {
Logger.log.log(Level.SEVERE, "Invalid external resource URL", url)
return null
}
val host = httpUrl.host()
if (host == null) {
Logger.log.log(Level.SEVERE, "External resource URL doesn't specify a host name", url)
return null
}
// authenticate only against a certain host, and only upon request
val builder = HttpClient.Builder(context, baseUrl.host(), accountSettings.credentials())
val builder = HttpClient.Builder(context, baseUrl.host, accountSettings.credentials())
// allow redirects
builder.followRedirects(true)
@@ -464,7 +425,7 @@ class ContactsSyncManager(
.build()).execute()
if (response.isSuccessful)
return response.body()?.bytes()
return response.body?.bytes()
else
Logger.log.warning("Couldn't download external resource")
} catch(e: IOException) {
@@ -476,4 +437,7 @@ class ContactsSyncManager(
}
}
override fun notifyInvalidResourceTitle(): String =
context.getString(R.string.sync_invalid_contact)
}

View File

@@ -1,30 +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.syncadapter
import android.content.Context
import android.content.SyncResult
import at.bitfire.davdroid.settings.ISettings
interface ISyncPlugin {
/**
* Called before synchronization within a sync adapter is started. Can be used for
* license checks etc. Must be thread-safe.
* @return whether synchronization shall take place (false to abort)
*/
fun beforeSync(context: Context, settings: ISettings, syncResult: SyncResult): Boolean
/**
* Called at the end of a synchronization adapter call, regardless of whether the synchronization
* was actually run (i.e. what [beforeSync] had returned). Must be thread-safe.
*/
fun afterSync(context: Context, settings: ISettings, syncResult: SyncResult)
}

View File

@@ -26,7 +26,7 @@ class NullAuthenticatorService: Service() {
}
override fun onBind(intent: Intent?) =
accountAuthenticator.iBinder.takeIf { intent?.action == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT }
accountAuthenticator.iBinder.takeIf { intent?.action == AccountManager.ACTION_AUTHENTICATOR_INTENT }
private class AccountAuthenticator(

View File

@@ -10,25 +10,20 @@ package at.bitfire.davdroid.syncadapter
import android.Manifest
import android.accounts.Account
import android.app.PendingIntent
import android.app.Service
import android.content.*
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import android.support.v4.content.ContextCompat
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.R
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import at.bitfire.davdroid.PermissionUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.ui.AccountActivity
import at.bitfire.davdroid.ui.AccountSettingsActivity
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.account.SettingsActivity
import java.lang.ref.WeakReference
import java.util.*
import java.util.logging.Level
@@ -41,6 +36,38 @@ abstract class SyncAdapterService: Service() {
* is terminated and the `finally` block which cleans up [runningSyncs] is not
* executed. */
private val runningSyncs = mutableListOf<WeakReference<Pair<String, Account>>>()
/**
* Specifies an list of IDs which are requested to be synchronized before
* the other collections. For instance, if some calendars of a CalDAV
* account are visible in the calendar app and others are hidden, the visible calendars can
* be synchronized first, so that the "Refresh" action in the calendar app is more responsive.
*
* Extra type: String (comma-separated list of IDs)
*
* In case of calendar sync, the extra value is a list of Android calendar IDs.
* In case of task sync, the extra value is an a list of OpenTask task list IDs.
*/
const val SYNC_EXTRAS_PRIORITY_COLLECTIONS = "priority_collections"
/**
* Requests a re-synchronization of all entries. For instance, if this extra is
* set for a calendar sync, all remote events will be listed and checked for remote
* changes again.
*
* Useful if settings which modify the remote resource list (like the CalDAV setting
* "sync events n days in the past") have been changed.
*/
const val SYNC_EXTRAS_RESYNC = "resync"
/**
* Requests a full re-synchronization of all entries. For instance, if this extra is
* set for an address book sync, all contacts will be downloaded again and updated in the
* local storage.
*
* Useful if settings which modify parsing/local behavior have been changed.
*/
const val SYNC_EXTRAS_FULL_RESYNC = "full_resync"
}
protected abstract fun syncAdapter(): AbstractThreadedSyncAdapter
@@ -53,17 +80,21 @@ abstract class SyncAdapterService: Service() {
): AbstractThreadedSyncAdapter(context, false) {
companion object {
private val syncPluginLoader = ServiceLoader.load(ISyncPlugin::class.java)
fun priorityCollections(extras: Bundle): Set<Long> {
val ids = mutableSetOf<Long>()
extras.getString(SYNC_EXTRAS_PRIORITY_COLLECTIONS)?.let { rawIds ->
for (rawId in rawIds.split(','))
try {
ids += rawId.toLong()
} catch (e: NumberFormatException) {
Logger.log.log(Level.WARNING, "Couldn't parse SYNC_EXTRAS_PRIORITY_COLLECTIONS", e)
}
}
return ids
}
}
private val syncPlugins = syncPluginLoader.iterator().asSequence().toList()
init {
syncPlugins.forEach { Logger.log.info("Registered sync plugin: ${it::class.java.name}") }
}
abstract fun sync(settings: ISettings, account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult)
abstract fun sync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult)
override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
Logger.log.log(Level.INFO, "$authority sync of $account has been initiated", extras.keySet().joinToString(", "))
@@ -78,27 +109,12 @@ abstract class SyncAdapterService: Service() {
runningSyncs += WeakReference(currentSync)
}
// required for ServiceLoader -> ical4j -> ical4android
Thread.currentThread().contextClassLoader = context.classLoader
try {
// required for dav4android (ServiceLoader)
Thread.currentThread().contextClassLoader = context.classLoader
// load app settings
Settings.getInstance(context).use { settings ->
if (settings == null) {
syncResult.databaseError = true
Logger.log.severe("Couldn't connect to Settings service, aborting sync")
return
}
val runSync = syncPlugins.all { it.beforeSync(context, settings, syncResult) }
if (runSync) {
SyncManager.cancelNotifications(NotificationManagerCompat.from(context), authority, account)
sync(settings, account, extras, authority, provider, syncResult)
}
syncPlugins.forEach { it.afterSync(context, settings, syncResult) }
}
if (true)
sync(account, extras, authority, provider, syncResult)
} finally {
synchronized(runningSyncs) {
runningSyncs.removeAll { it.get() == null || it.get() == currentSync }
@@ -110,37 +126,51 @@ abstract class SyncAdapterService: Service() {
override fun onSecurityException(account: Account, extras: Bundle, authority: String, syncResult: SyncResult) {
Logger.log.log(Level.WARNING, "Security exception when opening content provider for $authority")
syncResult.databaseError = true
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
notifyPermissions(intent)
}
override fun onSyncCanceled() {
Logger.log.info("Sync thread cancelled! Interrupting sync")
super.onSyncCanceled()
}
override fun onSyncCanceled(thread: Thread) {
Logger.log.info("Sync thread ${thread.id} cancelled! Interrupting sync")
super.onSyncCanceled(thread)
}
protected fun checkSyncConditions(settings: AccountSettings): Boolean {
if (settings.getSyncWifiOnly()) {
val connectivityManager = context.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetworkInfo
if (network == null || network.type != ConnectivityManager.TYPE_WIFI || !network.isConnected) {
// WiFi required
val connectivityManager = context.getSystemService<ConnectivityManager>()!!
// check for connected WiFi network
var wifiAvailable = false
connectivityManager.allNetworks.forEach { network ->
connectivityManager.getNetworkCapabilities(network)?.let { capabilities ->
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))
wifiAvailable = true
}
}
if (!wifiAvailable) {
Logger.log.info("Not on connected WiFi, stopping")
return false
}
// if execution reaches this point, we're on a connected WiFi
settings.getSyncWifiOnlySSIDs()?.let { onlySSIDs ->
// getting the WiFi name requires location permission (and active location services) since Android 8.1
// see https://issuetracker.google.com/issues/70633700
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 &&
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
val intent = Intent(context, AccountSettingsActivity::class.java)
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, settings.account)
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
val intent = Intent(context, SettingsActivity::class.java)
intent.putExtra(SettingsActivity.EXTRA_ACCOUNT, settings.account)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
notifyPermissions(intent)
PermissionUtils.notifyPermissions(context, intent)
}
val wifi = context.applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
val wifi = context.getSystemService<WifiManager>()!!
val info = wifi.connectionInfo
if (info == null || !onlySSIDs.contains(info.ssid.trim('"'))) {
Logger.log.info("Connected to wrong WiFi network (${info.ssid}), ignoring")
@@ -151,18 +181,6 @@ abstract class SyncAdapterService: Service() {
return true
}
protected fun notifyPermissions(intent: Intent) {
val notify = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_ERRORS)
.setSmallIcon(R.drawable.ic_sync_error_notification)
.setContentTitle(context.getString(R.string.sync_error_permissions))
.setContentText(context.getString(R.string.sync_error_permissions_text))
.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setAutoCancel(true)
.build()
NotificationManagerCompat.from(context).notify(NotificationUtils.NOTIFY_PERMISSIONS, notify)
}
}
}

View File

@@ -10,33 +10,43 @@ package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.app.PendingIntent
import android.content.*
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.content.SyncResult
import android.net.Uri
import android.os.Bundle
import android.os.RemoteException
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import at.bitfire.dav4android.*
import at.bitfire.dav4android.exception.*
import at.bitfire.dav4android.property.GetCTag
import at.bitfire.dav4android.property.GetETag
import at.bitfire.dav4android.property.SyncToken
import at.bitfire.davdroid.*
import at.bitfire.davdroid.BuildConfig
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import at.bitfire.dav4jvm.*
import at.bitfire.dav4jvm.exception.*
import at.bitfire.dav4jvm.property.GetCTag
import at.bitfire.dav4jvm.property.GetETag
import at.bitfire.dav4jvm.property.ScheduleTag
import at.bitfire.dav4jvm.property.SyncToken
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.DavService
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.SyncState
import at.bitfire.davdroid.resource.*
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.ui.AccountSettingsActivity
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.account.SettingsActivity
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.Ical4Android
import at.bitfire.ical4android.TaskProvider
import at.bitfire.ical4android.UsesThreadContextClassLoader
import at.bitfire.vcard4android.ContactsStorageException
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.HttpUrl
import okhttp3.RequestBody
import org.apache.commons.lang3.exception.ContextedException
@@ -46,15 +56,19 @@ import java.io.InterruptedIOException
import java.net.HttpURLConnection
import java.security.cert.CertificateException
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level
import javax.net.ssl.SSLHandshakeException
import kotlin.math.min
@Suppress("MemberVisibilityCanBePrivate")
@UsesThreadContextClassLoader
abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: LocalCollection<ResourceType>, RemoteType: DavCollection>(
val context: Context,
val settings: ISettings,
val account: Account,
val accountSettings: AccountSettings,
val extras: Bundle,
@@ -69,28 +83,31 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
}
companion object {
val MAX_PROCESSING_THREADS = Math.max(Runtime.getRuntime().availableProcessors(), 4)
val MAX_DOWNLOAD_THREADS = Math.max(Runtime.getRuntime().availableProcessors(), 4)
const val MAX_MULTIGET_RESOURCES = 10
fun cancelNotifications(manager: NotificationManagerCompat, authority: String, account: Account) =
manager.cancel(notificationTag(authority, account), NotificationUtils.NOTIFY_SYNC_ERROR)
private fun notificationTag(authority: String, account: Account) =
"$authority-${account.name}".hashCode().toString()
}
init {
// required for ServiceLoader -> ical4j -> ical4android
Ical4Android.checkThreadContextClassLoader()
}
/**
* We use our own dispatcher to make sure that all threads have [Thread.getContextClassLoader] set,
* which is required for dav4jvm and ical4j (because they rely on [ServiceLoader]).
*/
private val workDispatcher = Executors.newFixedThreadPool(
// number of threads = number of CPUs, but max. 4
min(Runtime.getRuntime().availableProcessors(), 4)
).asCoroutineDispatcher()
private val mainAccount = if (localCollection is LocalAddressBook)
localCollection.mainAccount
else
account
protected val notificationManager = NotificationManagerCompat.from(context)
protected val notificationTag = notificationTag(authority, mainAccount)
protected val notificationTag = localCollection.tag
protected val httpClient = HttpClient.Builder(context, settings, accountSettings).build()
protected val httpClient = HttpClient.Builder(context, accountSettings).build()
protected lateinit var collectionURL: HttpUrl
protected lateinit var davCollection: RemoteType
@@ -112,16 +129,24 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
Logger.log.info("No reason to synchronize, aborting")
return@unwrapExceptions
}
abortIfCancelled()
Logger.log.info("Querying server capabilities")
var remoteSyncState = queryCapabilities()
abortIfCancelled()
Logger.log.info("Sending local deletes/updates to server")
val modificationsSent = processLocallyDeleted() ||
uploadDirty()
abortIfCancelled()
if (extras.containsKey(SyncAdapterService.SYNC_EXTRAS_FULL_RESYNC)) {
Logger.log.info("Forcing re-synchronization of all entries")
// forget sync state of collection (→ initial sync in case of SyncAlgorithm.COLLECTION_SYNC)
localCollection.lastSyncState = null
remoteSyncState = null
// forget sync state of members (→ download all members again and update them locally)
localCollection.forgetETags()
}
if (modificationsSent || syncRequired(remoteSyncState))
when (syncAlgorithm()) {
@@ -140,7 +165,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
}
Logger.log.info("Deleting entries which are not present remotely anymore")
syncResult.stats.numDeletes += deleteNotPresentRemotely()
deleteNotPresentRemotely()
Logger.log.info("Post-processing")
postProcess()
@@ -149,9 +174,9 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
localCollection.lastSyncState = remoteSyncState
}
SyncAlgorithm.COLLECTION_SYNC -> {
var initialSync = false
var syncState = localCollection.lastSyncState?.takeIf { it.type == SyncState.Type.SYNC_TOKEN }
var initialSync = false
if (syncState == null) {
Logger.log.info("Starting initial sync")
initialSync = true
@@ -170,7 +195,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
syncState = SyncState.fromSyncToken(result.first, initialSync)
furtherChanges = result.second
} catch(e: HttpException) {
if (e.errors.any { it.name == Property.Name(XmlUtils.NS_WEBDAV, "valid-sync-token") }) {
if (e.errors.contains(Error.VALID_SYNC_TOKEN)) {
Logger.log.info("Sync token invalid, performing initial sync")
initialSync = true
resetPresentRemotely()
@@ -181,11 +206,11 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
} else
throw e
}
Logger.log.log(Level.INFO, "Saving sync state", syncState)
localCollection.lastSyncState = syncState
}
Logger.log.log(Level.INFO, "Saving sync state", syncState)
localCollection.lastSyncState = syncState
Logger.log.info("Server has further changes: $furtherChanges")
} while(furtherChanges)
@@ -219,7 +244,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
Logger.log.log(Level.WARNING, "SSL handshake failed", e)
// when a certificate is rejected by cert4android, the cause will be a CertificateException
if (!BuildConfig.customCerts || e.cause !is CertificateException)
if (e.cause !is CertificateException)
notifyException(e, local, remote)
}
@@ -264,15 +289,16 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
// but only if they don't have changed on the server. Then finally remove them from the local address book.
val localList = localCollection.findDeleted()
for (local in localList) {
abortIfCancelled()
useLocal(local) {
localExceptionContext(local) {
val fileName = local.fileName
if (fileName != null) {
Logger.log.info("$fileName has been deleted locally -> deleting from server (ETag ${local.eTag})")
val lastScheduleTag = local.scheduleTag
val lastETag = if (lastScheduleTag == null) local.eTag else null
Logger.log.info("$fileName has been deleted locally -> deleting from server (ETag $lastETag / schedule-tag $lastScheduleTag)")
useRemote(DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build())) { remote ->
remoteExceptionContext(DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build())) { remote ->
try {
remote.delete(local.eTag) {}
remote.delete(ifETag = lastETag, ifScheduleTag = lastScheduleTag) {}
numDeleted++
} catch (e: HttpException) {
Logger.log.warning("Couldn't delete $fileName from server; ignoring (may be downloaded again)")
@@ -289,63 +315,89 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
}
/**
* Uploads locally modified resources to the server (HTTP `PUT`).
* Uploads locally modified resources to the server.
*
* @return whether resources have been uploaded
*/
protected open fun uploadDirty(): Boolean {
var numUploaded = 0
// upload dirty contacts
for (local in localCollection.findDirty())
useLocal(local) {
abortIfCancelled()
if (local.fileName == null) {
Logger.log.fine("Generating file name/UID for local record #${local.id}")
local.assignNameAndUID()
// upload dirty resources (parallelized)
runBlocking(workDispatcher) {
for (local in localCollection.findDirty())
launch {
uploadDirty(local)
numUploaded++
}
val fileName = local.fileName!!
useRemote(DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build())) { remote ->
// generate entity to upload (VCard, iCal, whatever)
val body = prepareUpload(local)
var eTag: String? = null
val processETag: (response: okhttp3.Response) -> Unit = {
it.header("ETag")?.let {
eTag = GetETag(it).eTag
}
}
try {
if (local.eTag == null) {
Logger.log.info("Uploading new record $fileName")
remote.put(body, null, true, processETag)
} else {
Logger.log.info("Uploading locally modified record $fileName")
remote.put(body, local.eTag, false, processETag)
}
numUploaded++
} catch(e: ConflictException) {
// we can't interact with the user to resolve the conflict, so we treat 409 like 412
Logger.log.log(Level.INFO, "Edit conflict, ignoring", e)
} catch(e: PreconditionFailedException) {
Logger.log.log(Level.INFO, "Resource has been modified on the server before upload, ignoring", e)
}
if (eTag != null)
Logger.log.fine("Received new ETag=$eTag after uploading")
else
Logger.log.fine("Didn't receive new ETag after uploading, setting to null")
local.clearDirty(eTag)
}
}
}
syncResult.stats.numEntries += numUploaded
Logger.log.info("Sent $numUploaded record(s) to server")
return numUploaded > 0
}
protected abstract fun prepareUpload(resource: ResourceType): RequestBody
protected fun uploadDirty(local: ResourceType) {
val existingFileName = local.fileName
var newFileName: String? = null
var eTag: String? = null
var scheduleTag: String? = null
val readTagsFromResponse: (okhttp3.Response) -> Unit = { response ->
eTag = GetETag.fromResponse(response)?.eTag
scheduleTag = ScheduleTag.fromResponse(response)?.scheduleTag
}
try {
if (existingFileName == null) { // new resource
newFileName = local.prepareForFirstUpload()
val uploadUrl = collectionURL.newBuilder().addPathSegment(newFileName).build()
remoteExceptionContext(DavResource(httpClient.okHttpClient, uploadUrl)) { remote ->
Logger.log.info("Uploading new record ${local.id} -> $newFileName")
remote.put(generateUpload(local), ifNoneMatch = true, callback = readTagsFromResponse)
}
} else /* existingFileName != null */ { // updated resource
val uploadUrl = collectionURL.newBuilder().addPathSegment(existingFileName).build()
remoteExceptionContext(DavResource(httpClient.okHttpClient, uploadUrl)) { remote ->
val lastScheduleTag = local.scheduleTag
val lastETag = if (lastScheduleTag == null) local.eTag else null
Logger.log.info("Uploading modified record ${local.id} -> $newFileName (ETag=$lastETag, Schedule-Tag=$lastScheduleTag)")
remote.put(generateUpload(local), ifETag = lastETag, ifScheduleTag = lastScheduleTag, callback = readTagsFromResponse)
}
}
} catch(e: ForbiddenException) {
// HTTP 403 Forbidden
// If and only if the upload failed because of missing permissions, treat it like 412.
if (e.errors.contains(Error.NEED_PRIVILEGES))
Logger.log.log(Level.INFO, "Couldn't upload because of missing permissions, ignoring", e)
else
throw e
} catch(e: ConflictException) {
// HTTP 409 Conflict
// We can't interact with the user to resolve the conflict, so we treat 409 like 412.
Logger.log.log(Level.INFO, "Edit conflict, ignoring", e)
} catch(e: PreconditionFailedException) {
// HTTP 412 Precondition failed: Resource has been modified on the server in the meanwhile.
// Ignore this condition so that the resource can be downloaded and reset again.
Logger.log.log(Level.INFO, "Resource has been modified on the server before upload, ignoring", e)
}
if (eTag != null)
Logger.log.fine("Received new ETag=$eTag after uploading")
else
Logger.log.fine("Didn't receive new ETag after uploading, setting to null")
local.clearDirty(newFileName, eTag, scheduleTag)
}
/**
* Generates the request body (iCalendar or vCard) from a local resource.
*
* @param resource local resource to generate the body from
*
* @return iCalendar or vCard (content + Content-Type) that can be uploaded to the server
*/
protected abstract fun generateUpload(resource: ResourceType): RequestBody
/**
* Determines whether a sync is required because there were changes on the server.
@@ -356,16 +408,18 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
* [uploadDirty] were true), a sync is always required and this method
* should *not* be evaluated.
*
* Will return _true_ if [SyncAdapterService.SYNC_EXTRAS_RESYNC] and/or
* [SyncAdapterService.SYNC_EXTRAS_FULL_RESYNC] is set in [extras].
*
* @param state remote sync state to compare local sync state with
*
* @return whether data has been changed on the server, i.e. whether running the
* sync algorithm is required
*/
protected open fun syncRequired(state: SyncState?): Boolean {
if (syncAlgorithm() == SyncAlgorithm.PROPFIND_REPORT && extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL)) {
Logger.log.info("Manual sync in PROPFIND/REPORT mode, forcing sync")
if (extras.containsKey(SyncAdapterService.SYNC_EXTRAS_RESYNC) ||
extras.containsKey(SyncAdapterService.SYNC_EXTRAS_FULL_RESYNC))
return true
}
val localState = localCollection.lastSyncState
Logger.log.info("Local sync state = $localState, remote sync state = $state")
@@ -404,115 +458,94 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
Logger.log.info("Number of local non-dirty entries: $number")
}
/**
* Calls a callback to list remote resources. All resources from the returned
* list are downloaded and processed.
*
* @param listRemote function to list remote resources (for instance, all since a certain sync-token)
*/
protected open fun syncRemote(listRemote: (DavResponseCallback) -> Unit) {
// results must be processed in main thread because exceptions must be thrown in main
// thread, so that they can be catched by SyncManager
val results = ConcurrentLinkedQueue<Future<*>>()
// thread-safe sync stats
val nInserted = AtomicInteger()
val nUpdated = AtomicInteger()
val nDeleted = AtomicInteger()
val nSkipped = AtomicInteger()
// download queue
val toDownload = LinkedBlockingQueue<HttpUrl>()
runBlocking(workDispatcher) {
// download queue
val toDownload = LinkedBlockingQueue<HttpUrl>()
fun download(url: HttpUrl?) {
if (url != null)
toDownload += url
// tasks from this executor create the download tasks (if necessary)
val processor = ThreadPoolExecutor(1, MAX_PROCESSING_THREADS,
10, TimeUnit.SECONDS,
LinkedBlockingQueue(MAX_PROCESSING_THREADS), // accept up to MAX_PROCESSING_THREADS processing tasks
ThreadPoolExecutor.CallerRunsPolicy() // if the queue is full, run task in submitting thread
)
// this executor runs the actual download tasks
val downloader = ThreadPoolExecutor(0, MAX_DOWNLOAD_THREADS,
10, TimeUnit.SECONDS,
LinkedBlockingQueue(MAX_DOWNLOAD_THREADS), // accept up to MAX_DOWNLOAD_THREADS download tasks
ThreadPoolExecutor.CallerRunsPolicy() // if the queue is full, run task in submitting thread
)
fun downloadBunch() {
val bunch = LinkedList<HttpUrl>()
toDownload.drainTo(bunch, MAX_MULTIGET_RESOURCES)
results += downloader.submit {
downloadRemote(bunch)
}
}
listRemote { response, relation ->
// ignore non-members
if (relation != Response.HrefRelation.MEMBER)
return@listRemote
// ignore collections
if (response[at.bitfire.dav4android.property.ResourceType::class.java]?.types?.contains(at.bitfire.dav4android.property.ResourceType.COLLECTION) == true)
return@listRemote
val name = response.hrefName()
if (response.isSuccess()) {
Logger.log.fine("Found remote resource: $name")
results += processor.submit {
useLocal(localCollection.findByName(name)) { local ->
if (local == null) {
Logger.log.info("$name has been added remotely")
toDownload += response.href
nInserted.incrementAndGet()
} else {
val localETag = local.eTag
val remoteETag = response[GetETag::class.java]?.eTag ?: throw DavException("Server didn't provide ETag")
if (localETag == remoteETag) {
Logger.log.info("$name has not been changed on server (ETag still $remoteETag)")
nSkipped.incrementAndGet()
} else {
Logger.log.info("$name has been changed on server (current ETag=$remoteETag, last known ETag=$localETag)")
toDownload += response.href
nUpdated.incrementAndGet()
}
// mark as remotely present, so that this resource won't be deleted at the end
local.updateFlags(LocalResource.FLAG_REMOTELY_PRESENT)
if (toDownload.size >= MAX_MULTIGET_RESOURCES || url == null) {
while (toDownload.size > 0) {
val bunch = LinkedList<HttpUrl>()
toDownload.drainTo(bunch, MAX_MULTIGET_RESOURCES)
launch {
downloadRemote(bunch)
}
}
synchronized(processor) {
if (toDownload.size >= MAX_MULTIGET_RESOURCES)
// download another bunch of MAX_MULTIGET_RESOURCES resources
downloadBunch()
}
}
}
} else if (response.status?.code == HttpURLConnection.HTTP_NOT_FOUND) {
// collection sync: resource has been deleted on remote server
results += processor.submit {
useLocal(localCollection.findByName(name)) { local ->
Logger.log.info("$name has been deleted on server, deleting locally")
local?.delete()
nDeleted.incrementAndGet()
coroutineScope {
listRemote { response, relation ->
// ignore non-members
if (relation != Response.HrefRelation.MEMBER)
return@listRemote
// ignore collections
if (response[at.bitfire.dav4jvm.property.ResourceType::class.java]?.types?.contains(at.bitfire.dav4jvm.property.ResourceType.COLLECTION) == true)
return@listRemote
val name = response.hrefName()
if (response.isSuccess()) {
Logger.log.fine("Found remote resource: $name")
launch {
localExceptionContext(localCollection.findByName(name)) { local ->
if (local == null) {
Logger.log.info("$name has been added remotely, queueing download")
download(response.href)
nInserted.incrementAndGet()
} else {
val localETag = local.eTag
val remoteETag = response[GetETag::class.java]?.eTag
?: throw DavException("Server didn't provide ETag")
if (localETag == remoteETag) {
Logger.log.info("$name has not been changed on server (ETag still $remoteETag)")
nSkipped.incrementAndGet()
} else {
Logger.log.info("$name has been changed on server (current ETag=$remoteETag, last known ETag=$localETag)")
download(response.href)
nUpdated.incrementAndGet()
}
// mark as remotely present, so that this resource won't be deleted at the end
local.updateFlags(LocalResource.FLAG_REMOTELY_PRESENT)
}
}
}
} else if (response.status?.code == HttpURLConnection.HTTP_NOT_FOUND) {
// collection sync: resource has been deleted on remote server
launch {
localExceptionContext(localCollection.findByName(name)) { local ->
Logger.log.info("$name has been deleted on server, deleting locally")
local?.delete()
nDeleted.incrementAndGet()
}
}
}
}
}
// check already available results for exceptions so that they don't become too many
checkResults(results)
// download remaining resources
download(null)
}
// process remaining responses
processor.shutdown()
processor.awaitTermination(5, TimeUnit.MINUTES)
// download remaining resources
if (toDownload.isNotEmpty())
downloadBunch()
// signal end of queue and wait for download thread
downloader.shutdown()
downloader.awaitTermination(5, TimeUnit.MINUTES)
// check remaining results for exceptions
checkResults(results)
// update sync stats
with(syncResult.stats) {
numInserts += nInserted.get()
@@ -553,6 +586,24 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
return Pair(syncToken!!, furtherResults)
}
/**
* Downloads and processes resources, given as a list of URLs. Will be called with a list
* of changed/new remote resources.
*
* Implementations should not use GET to fetch single resources, but always multi-get, even
* for single resources for these reasons:
*
* 1. GET can only be used without HTTP compression, because it may change the ETag.
* multi-get sends the ETag in the XML body, so there's no problem with compression.
* 2. Some servers are wrongly configured to suppress the ETag header in the response.
* With multi-get, the ETag is in the XML body, so it won't be affected by that.
* 3. If there are two methods to download resources (GET and multi-get), both methods
* have to be implemented, tested and maintained. Given that multi-get is required
* in any case, it's better to have only one method.
* 4. For users, it's strange behavior when DAVx5 can download multiple remote changes,
* but not a single one (or vice versa). So only one method is more user-friendly.
* 5. March 2020: iCloud now crashes with HTTP 500 upon CardDAV GET requests.
*/
protected abstract fun downloadRemote(bunch: List<HttpUrl>)
/**
@@ -563,10 +614,10 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
* Used together with [resetPresentRemotely] when a full listing has been received from
* the server to locally delete resources which are not present remotely (anymore).
*/
protected open fun deleteNotPresentRemotely(): Int {
protected open fun deleteNotPresentRemotely() {
val removed = localCollection.removeNotDirtyMarked(0)
Logger.log.info("Removed $removed local resources which are not present on the server anymore")
return removed
syncResult.stats.numDeletes += removed
}
/**
@@ -577,17 +628,6 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
// sync helpers
/**
* Throws an [InterruptedException] if the current thread has been interrupted,
* most probably because synchronization was cancelled by the user.
*
* @throws InterruptedException (which will be caught by [performSync])
* */
protected fun abortIfCancelled() {
if (Thread.interrupted())
throw InterruptedException("Sync was cancelled")
}
protected fun syncState(dav: Response) =
dav[SyncToken::class.java]?.token?.let {
SyncState(SyncState.Type.SYNC_TOKEN, it)
@@ -643,25 +683,16 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
val contentIntent: Intent
var viewItemAction: NotificationCompat.Action? = null
if (e is UnauthorizedException) {
contentIntent = Intent(context, AccountSettingsActivity::class.java)
contentIntent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account)
contentIntent = Intent(context, SettingsActivity::class.java)
contentIntent.putExtra(SettingsActivity.EXTRA_ACCOUNT,
if (authority == ContactsContract.AUTHORITY)
mainAccount
else
account)
} else {
contentIntent = Intent(context, DebugInfoActivity::class.java)
contentIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e)
contentIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account)
contentIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority)
// use current local/remote resource
if (local != null) {
// pass local resource info to debug info
contentIntent.putExtra(DebugInfoActivity.KEY_LOCAL_RESOURCE, local.toString())
// generate "view item" action
contentIntent = buildDebugInfoIntent(e, local, remote)
if (local != null)
viewItemAction = buildViewItemAction(local)
}
if (remote != null)
contentIntent.putExtra(DebugInfoActivity.KEY_REMOTE_RESOURCE, remote.toString())
}
// to make the PendingIntent unique
@@ -678,7 +709,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
}
val builder = NotificationUtils.newBuilder(context, channel)
builder .setSmallIcon(R.drawable.ic_sync_error_notification)
builder .setSmallIcon(R.drawable.ic_sync_problem_notify)
.setContentTitle(localCollection.title)
.setContentText(message)
.setStyle(NotificationCompat.BigTextStyle(builder).bigText(message))
@@ -693,6 +724,19 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
notificationManager.notify(notificationTag, NotificationUtils.NOTIFY_SYNC_ERROR, builder.build())
}
private fun buildDebugInfoIntent(e: Throwable, local: ResourceType?, remote: HttpUrl?) =
Intent(context, DebugInfoActivity::class.java).apply {
putExtra(DebugInfoActivity.KEY_ACCOUNT, account)
putExtra(DebugInfoActivity.KEY_AUTHORITY, authority)
putExtra(DebugInfoActivity.KEY_THROWABLE, e)
// pass current local/remote resource
if (local != null)
putExtra(DebugInfoActivity.KEY_LOCAL_RESOURCE, local.toString())
if (remote != null)
putExtra(DebugInfoActivity.KEY_REMOTE_RESOURCE, remote.toString())
}
private fun buildRetryAction(): NotificationCompat.Action {
val retryIntent = Intent(context, DavService::class.java)
retryIntent.action = DavService.ACTION_FORCE_SYNC
@@ -740,6 +784,8 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
null
}
@Deprecated("Use Kotlin coroutines instead")
fun checkResults(results: MutableCollection<Future<*>>) {
val iter = results.iterator()
while (iter.hasNext()) {
@@ -755,7 +801,24 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
}
}
protected fun<T: ResourceType?, R> useLocal(local: T, body: (T) -> R): R {
protected fun notifyInvalidResource(e: Throwable, fileName: String) {
val intent = buildDebugInfoIntent(e, null, collectionURL.resolve(fileName))
val builder = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_WARNINGS)
builder .setSmallIcon(R.drawable.ic_warning_notify)
.setContentTitle(notifyInvalidResourceTitle())
.setContentText(context.getString(R.string.sync_invalid_resources_ignoring))
.setSubText(mainAccount.name)
.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.priority = NotificationCompat.PRIORITY_LOW
notificationManager.notify(notificationTag, NotificationUtils.NOTIFY_INVALID_RESOURCE, builder.build())
}
protected abstract fun notifyInvalidResourceTitle(): String
protected fun<T: ResourceType?, R> localExceptionContext(local: T, body: (T) -> R): R {
try {
return body(local)
} catch (e: ContextedException) {
@@ -769,7 +832,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
}
}
protected fun<T: DavResource, R> useRemote(remote: T, body: (T) -> R): R {
protected fun<T: DavResource, R> remoteExceptionContext(remote: T, body: (T) -> R): R {
try {
return body(remote)
} catch (e: ContextedException) {
@@ -780,7 +843,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
}
}
protected fun<T> useRemote(remote: Response, body: (Response) -> T): T {
protected fun<T> responseExceptionContext(remote: Response, body: (Response) -> T): T {
try {
return body(remote)
} catch (e: ContextedException) {
@@ -791,8 +854,8 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
}
}
protected fun<R> useRemoteCollection(body: (RemoteType) -> R) =
useRemote(davCollection, body)
protected fun<R> remoteExceptionContext(body: (RemoteType) -> R) =
remoteExceptionContext(davCollection, body)
private fun unwrapExceptions(body: () -> Unit, handler: (e: Throwable, local: ResourceType?, remote: HttpUrl?) -> Unit) {
var ex: Throwable? = null

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