Compare commits

...

228 Commits

Author SHA1 Message Date
Ricki Hirner
02bcb2579b Update gradle plugin 2017-06-21 12:58:51 +02:00
Ricki Hirner
abed9bf8bb SDK/build tools level 26 2017-06-21 00:03:17 +02:00
Ricki Hirner
0e1eb13fca Version bump to 1.6.2 2017-06-20 15:16:12 +02:00
Ricki Hirner
8d6764a16b Update libs; update Commons Lang to 3.6 2017-06-20 15:13:53 +02:00
Ricki Hirner
79037d7940 License check for GPlay version 2017-06-20 11:50:39 +02:00
Ricki Hirner
3ca636c2b2 Lib updates, okhttp/3.8.1 2017-06-19 17:11:22 +02:00
Ricki Hirner
350c85e161 ical4android: Kotlin 2017-06-17 23:08:19 +02:00
Ricki Hirner
eec0a64c03 Version bump to 1.6.1.1 (151) 2017-06-16 18:56:21 +02:00
Ricki Hirner
8c9de140cf MultiSync: force app-specific passwords in UI 2017-06-16 18:44:53 +02:00
Ricki Hirner
f05252882c Lib updates 2017-06-16 17:53:50 +02:00
Ricki Hirner
77c0d3f6a8 Fetch translations from Transifex 2017-06-09 16:34:03 +02:00
Ricki Hirner
ea5b9e3853 Version bump to 1.6.1 2017-06-09 16:31:53 +02:00
Ricki Hirner
d98b109ec5 dav4android: Kotlin, cert4android: handle InterruptedException 2017-06-09 00:39:10 +02:00
Ricki Hirner
004887d642 Google Play version code 2017-06-05 15:01:12 +02:00
Ricki Hirner
c20c9b26db Birthdays without years, again 2017-06-05 14:35:13 +02:00
Ricki Hirner
14cacb4c51 Fetch translations from Transifex 2017-06-03 19:28:11 +02:00
Ricki Hirner
f2a87b88ba Version bump to 1.6 2017-06-03 19:26:08 +02:00
Ricki Hirner
60d69c205a Improve resource detection
* scan unrequested responses for useful auto-detection information
* cert4android: Kotlin
2017-06-03 19:16:52 +02:00
Ricki Hirner
0849466450 Support for birthdays without year 2017-06-02 00:37:27 +02:00
Ricki Hirner
f54bb0f010 Remove "ical4android" from iCal PRODID (same format as for VCard) 2017-05-14 13:36:08 +02:00
Ricki Hirner
73a2d6d20f okhttp 3.8.0 ProGuard settings 2017-05-14 13:00:29 +02:00
Ricki Hirner
9363f243fd Fetch translations from Transifex 2017-05-14 12:19:11 +02:00
Ricki Hirner
19d05bad91 Version bump to 1.5.2 2017-05-14 12:18:28 +02:00
Ricki Hirner
20709d4f61 Remove "vcard4android" from VCard PRODID to avoid folding for better compatibility 2017-05-14 12:11:06 +02:00
Ricki Hirner
848616c7e2 Upgrade to okhttp/3.8.0 2017-05-14 11:32:09 +02:00
Ricki Hirner
eb97773770 Use UUIDs for newly generated event/task UIDs (RFC 7986 5.3 UID Property) 2017-04-27 13:35:14 +02:00
Ricki Hirner
9ff9a6b935 Fetch translations from Transifex 2017-04-25 14:12:11 +02:00
Ricki Hirner
9efa0199c5 Version bump to 1.5.1 2017-04-25 14:10:59 +02:00
Ricki Hirner
2de8c116a9 Use untranslated User-Agent string
* refactoring: don't use global string variables
2017-04-24 22:52:52 +02:00
Ricki Hirner
75c03074df Check sync conditions for contacts sync, too 2017-04-23 18:09:34 +02:00
Ricki Hirner
f8896b3e24 Remove unnecessary InvalidAccountException 2017-04-22 21:37:08 +02:00
Ricki Hirner
30bff9062d Allow null values for IS_ORGANIZER 2017-04-18 23:53:25 +02:00
Ricki Hirner
950ce6eaec README updates 2017-04-18 23:53:21 +02:00
Ricki Hirner
f7154bd778 Version bump to 1.5.0.3 2017-04-16 15:51:28 +02:00
Ricki Hirner
f96689da65 Fetch translations from Transifex 2017-04-16 15:49:22 +02:00
Ricki Hirner
ce1262357e Upgrade libraries 2017-04-16 14:31:22 +02:00
Ricki Hirner
01e6e28384 Add Soldupe signing config 2017-04-11 17:58:15 +02:00
Ricki Hirner
988b6f7c7c Branding strings
* HTTP client: use app name as User-Agent
* use string resources for homepage URLs
* MultiSync: add FAQ
2017-04-11 16:23:38 +02:00
Ricki Hirner
494fe0e702 MultiSync: version bump to 1.5.0.2-cloud1 2017-04-08 20:27:37 +02:00
Ricki Hirner
a74e159558 Billing: ignore DeadObjectExceptions
* update Android gradle plugin to 2.3.1
* dav4android update: follow redirects on DELETE
2017-04-08 20:24:43 +02:00
Ricki Hirner
16c8bbc471 Soldupe branding 2017-04-03 14:41:10 +02:00
Ricki Hirner
7c800db81d MultiSync: show DebugInfo on BillingException 2017-04-03 13:38:16 +02:00
Ricki Hirner
91cea341cd Version bump to 1.5.0.2 2017-04-02 19:22:05 +02:00
Ricki Hirner
ba3c741f95 Open DAVdroid main activity when add a "DAVdroid Address book" account is added over Settings 2017-03-30 21:22:46 +02:00
Ricki Hirner
e0b0fe112d Account settings: restart loader after sync interval update
* debug info: add signature
2017-03-29 12:39:26 +02:00
Ricki Hirner
afc8b22843 Version bump to 1.5.0.1 2017-03-27 11:42:23 +02:00
Ricki Hirner
89a936856c Hotfix: don't crash on empty address book displayName 2017-03-27 11:41:27 +02:00
Ricki Hirner
134e60784b Varianten-Strings 2017-03-26 20:09:02 +02:00
Ricki Hirner
e024bd56f9 Version bump to 1.5 2017-03-26 19:31:00 +02:00
Ricki Hirner
dccd68962e Fetch translations from Transifex 2017-03-26 19:31:00 +02:00
Ricki Hirner
b42c72dc96 Improve address book details in debug info 2017-03-26 19:04:38 +02:00
Ricki Hirner
51fb655e57 Add more debug information
* power saving status
* permissions
* address book accounts
2017-03-25 20:12:08 +01:00
Ricki Hirner
f4ca7b4a8b Enable SSL_RSA_WITH_3DES_EDE_CBC_SHA for all Android versions
* refactor cipher selection
2017-03-25 19:51:11 +01:00
Ricki Hirner
3f303c4718 Version bump to 1.5-beta1 2017-03-19 20:42:33 +01:00
Ricki Hirner
6f86544e45 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:21:43 +01:00
Ricki Hirner
af1d9ac962 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-16 19:59:47 +01:00
Ricki Hirner
6e610382fa Unify action bar icon colors 2017-03-12 13:24:33 +01:00
Ricki Hirner
5723170e01 Fix maven 2017-03-03 13:25:24 +01:00
Ricki Hirner
8808bca856 Update translations from Transifex 2017-03-03 12:53:30 +01:00
Ricki Hirner
771293727c Fix cardview colors 2017-03-03 12:47:31 +01:00
Ricki Hirner
fe44128861 Update gradle 2017-03-03 12:38:16 +01:00
Ricki Hirner
1e81964ce1 Fix cardview background 2017-03-03 00:14:49 +01:00
Ricki Hirner
1aa18490c1 Version bump to 1.4.1 2017-03-02 23:43:46 +01:00
Ricki Hirner
07e9e9b169 Fetch translations from Transifex 2017-03-02 23:43:46 +01:00
Ricki Hirner
65a6583657 AccountsActivity: show message when global sync is disabled 2017-03-02 23:43:41 +01:00
Ricki Hirner
9791a4b730 Soldupe/MultiSync branding 2017-02-28 14:21:04 +01:00
Ricki Hirner
a9336b90a9 Continue Soldupe branding 2017-02-27 20:39:21 +01:00
Ricki Hirner
e2edad18f4 Gradle update to 3.4 2017-02-27 17:36:46 +01:00
Ricki Hirner
b0ea4166f0 Begin Soldupe branding 2017-02-23 13:24:27 +01:00
Ricki Hirner
c333133bcb Update ez-vcard 2017-02-23 11:09:47 +01:00
Ricki Hirner
fb83488c78 Fetch translations from Transifex 2017-02-17 13:55:36 +01:00
Ricki Hirner
266dc50f4f Version bump to 1.4.0.3 2017-02-17 13:51:55 +01:00
Ricki Hirner
0cb51e06ad Don't use uid2445 column on Android <4.2; alarm ACTION: compare only value (ignore parameters) 2017-02-17 09:55:01 +01:00
Ricki Hirner
722d981c91 Version bump to 1.4.0.2 2017-02-12 18:59:05 +01:00
Ricki Hirner
2215b47f2c Improve Android 7 workaround behavior in combination with CATEGORIES/VCard4 contact groups 2017-02-12 18:57:45 +01:00
Ricki Hirner
a2d866b5bb 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 17:56:41 +01:00
Ricki Hirner
27e59a6e6e Android 7 workaround: update hash after group membership operations 2017-02-10 16:52:25 +01:00
Ricki Hirner
0836859ea5 Version bump to 1.4.0.1 2017-02-06 11:57:01 +01:00
Ricki Hirner
24722bfca9 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:55:18 +01:00
Ricki Hirner
8983ace0b1 Version bump to 1.4.0 2017-02-05 17:19:42 +01:00
Ricki Hirner
0d3de671a2 Use contact hash codes only on Android 7+ (workaround)
vcard4android: don't hash CATEGORIES, more verbose logging
2017-02-05 17:18:36 +01:00
Ricki Hirner
f7527ddd8a 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:18:50 +01:00
Ricki Hirner
c2414ab805 AccountSettingsActivity: use loader
* use Loader for AccountSettingsActivity sync intervals (fixes Android 7 display "issues")
* SyncManager: allow prepare() to skip synchronization
2017-02-01 01:14:19 +01:00
Ricki Hirner
6046bb2229 Upgrade to okhttp 3.6.0 2017-01-30 22:26:54 +01:00
Ricki Hirner
e14c4da890 Version bump to 1.3.8 2017-01-29 20:06:49 +01:00
Ricki Hirner
ca1e438e30 Fetch translations from Transifex (fixes crash in Spanish version) 2017-01-29 19:22:47 +01:00
Ricki Hirner
3bc37d9a66 Add information about current local and remote resource to debug info 2017-01-29 19:13:54 +01:00
Ricki Hirner
6c4b8436f6 Upgrade to gradle 3.3 2017-01-29 16:43:51 +01:00
Ricki Hirner
1be370daca Version bump to 1.3.7.1 2017-01-09 21:27:50 +01:00
Ricki Hirner
3ca962c677 Use isAlwaysSyncable for contacts/calendars again because of buggy Android firmwares 2017-01-09 21:26:41 +01:00
Ricki Hirner
d463efc461 Version bump to 1.3.7 (132) 2017-01-09 00:00:28 +01:00
Ricki Hirner
d6756e31ca Change authentication restriction to domains instead of host names 2017-01-08 19:09:12 +01:00
Ricki Hirner
bd5857c055 Delete local contacts when no CardDAV collection is selected 2017-01-06 15:51:44 +01:00
Ricki Hirner
2faf5cae40 Cloud fix 2017-01-01 12:55:23 +01:00
Ricki Hirner
08f74f2eb0 Version bump to 1.3.6 2017-01-01 12:43:20 +01:00
Ricki Hirner
ea084ef374 Fetch translations from Transifex 2017-01-01 12:26:43 +01:00
Ricki Hirner
a267a6210e Update to ez-vcard 0.10.1
* fix REV and PREF problems
2017-01-01 01:12:27 +01:00
Ricki Hirner
c4642c4aac Fix permissions notification
* ical4android: remove ORGANIZER from all VEVENT components if there are not attendees
2016-12-31 14:15:27 +01:00
Ricki Hirner
ada9f328c8 AccountSettings version 5: enable/disable OpenTasks by availability (Android 7.1.1 fix)
* better handling of setIsSyncable
2016-12-30 14:20:09 +01:00
Ricki Hirner
aae701d9ee Don't show warning on AccountSettings version updates 2016-12-30 13:50:33 +01:00
Ricki Hirner
caec01ddba Update to SDK level 25 2016-12-30 02:58:01 +01:00
Ricki Hirner
19e7967405 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:11:47 +01:00
Ricki Hirner
4478ae2c5a gradle: use $HOME for signing 2016-12-23 16:42:14 +01:00
Ricki Hirner
ca737f58e7 Fetch translations from Transifex 2016-12-23 15:55:50 +01:00
Ricki Hirner
d755da2159 Version bump to 1.3.5 2016-12-23 15:49:53 +01:00
Ricki Hirner
6617a7bcaa Address book selection changed: update URL as soon as possible 2016-12-19 18:57:45 +01:00
Ricki Hirner
033c4d658b Update gradle to 3.2.1; ical4android/vcard4android updates 2016-12-18 22:01:46 +01:00
Ricki Hirner
2b6c9e42c7 Update okhttp to 3.5.0 2016-12-02 15:00:46 +01:00
Ricki Hirner
4a36edfe9d Log group assignments more verbosely 2016-11-25 21:37:04 +01:00
Ricki Hirner
9f7f4e8411 Rename account: don't crash when content providers are not accessible 2016-11-17 19:58:08 +01:00
Ricki Hirner
07f665b245 Version bump to 1.3.4.1 2016-11-14 18:47:58 +01:00
Ricki Hirner
068e05d41e Fetch translations from Transifex 2016-11-14 18:41:24 +01:00
Ricki Hirner
fb9ce81a99 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:38:15 +01:00
Ricki Hirner
6237aacd96 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:12:41 +01:00
Ricki Hirner
f9a97da7f4 Fetch translations from Transifex 2016-11-13 20:34:28 +01:00
Ricki Hirner
14959ce869 Update build tools to 25.0.0, fix WiFiManager leak 2016-11-13 20:21:29 +01:00
Ricki Hirner
c8df1dc10a Fetch translations from Transifex
ical4android: fix for events without dtend/duration
2016-11-06 17:36:39 +01:00
Ricki Hirner
6712c24482 Version bump to 1.3.4
* library updates
2016-11-04 11:59:48 +01:00
Ricki Hirner
d7b3f89513 Add app-wide HTTP proxy setting 2016-10-30 22:21:11 +01:00
Ricki Hirner
4c4857db6d Versions: -gplay1, -cloud1 2016-10-22 16:08:52 +02:00
Ricki Hirner
f343691877 Debug info: send inline on Android <4.1 and when creating an attachment doesn't work 2016-10-22 02:05:14 +02:00
Ricki Hirner
1426a047e1 Version bump to 1.3.3.1 2016-10-21 20:10:18 +02:00
Ricki Hirner
3ac92a00b0 Fetch translations from Transifex 2016-10-21 20:10:18 +02:00
Ricki Hirner
208936415d Library updates
* dav4android: disable compression for GET requests because it may change the ETag
* better logging for ical4j messages
* tests
2016-10-21 10:37:48 +02:00
Ricki Hirner
9a3b510193 ProGuard update; signing config 2016-10-18 12:30:47 +02:00
Ricki Hirner
3d43919995 Use string resource for logging file provider authority; vcard4android update 2016-10-17 23:56:45 +02:00
Ricki Hirner
e919fc8794 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:45:59 +02:00
Ricki Hirner
bc4bf8ebbf Fetch translations from Transifex 2016-10-14 21:19:56 +02:00
Ricki Hirner
e26c61ba5a Version bump to 1.3.3 2016-10-14 20:57:11 +02:00
Ricki Hirner
9074b74632 dav4android: always use UTF-8 for Basic/Digest auth credentials 2016-10-13 15:22:50 +02:00
Ricki Hirner
9a9ad8ec17 MultiSync SubscribtionActivity: button for "try again" on error; UI fix 2016-10-13 15:22:46 +02:00
Ricki Hirner
a6d11b1a42 MultiSync improvements
* use Loader for license info
* dark action bar for About activity
* version bump to 1.3.2.2-cloud1
2016-10-12 21:03:13 +02:00
Ricki Hirner
eca941a78d MultiSync: ignore IllegalArgumentException on unbindService 2016-10-12 18:58:27 +02:00
Ricki Hirner
29e0917a73 ical4android: ignore invalid DUE < DTSTART for tasks 2016-10-12 17:03:17 +02:00
Ricki Hirner
822d6f8c6f Remove VCard RFC6868 setting (always enabled now; setting not needed for Posteo compatibility anymore) 2016-10-12 16:41:08 +02:00
Ricki Hirner
46b6d06b6a Gitlab CI: install OpenTasks before tests 2016-10-12 13:03:43 +02:00
Ricki Hirner
87ecd3c182 vcard4android: ez-vcard 0.10.0 2016-10-11 23:35:47 +02:00
Ricki Hirner
b02bdc6ecd Test adaptions 2016-10-11 00:26:50 +02:00
Ricki Hirner
f3af584494 Switch to JUnit4 2016-10-10 21:03:32 +02:00
Ricki Hirner
0cf5a758ba Add Gitlab CI 2016-10-10 20:18:05 +02:00
Ricki Hirner
840c7e741a Improve tests 2016-10-07 14:39:40 +02:00
Ricki Hirner
da97c1f2b9 Fix NPE in "is refreshing progress bar" 2016-10-07 14:39:35 +02:00
Ricki Hirner
859ef6d29f Version bump to 1.3.2.2 2016-10-05 11:15:48 +02:00
Ricki Hirner
351f5e8447 Enable verbose logging of allow loggers (for instance, okhttp) / dav4android update 2016-10-04 22:41:52 +02:00
Ricki Hirner
80b1bb8ee2 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:12:44 +02:00
Ricki Hirner
e558c45139 Version bump to 1.3.2 2016-10-03 20:55:42 +02:00
Ricki Hirner
a2284de509 Fetch translations from Transifex 2016-10-03 20:43:33 +02:00
Ricki Hirner
a61fe817ca Avoid "no transaction" exception 2016-10-03 20:11:18 +02:00
Ricki Hirner
15349b3d23 Use an own ISyncPlugin instance per SyncAdapter 2016-10-03 12:13:00 +02:00
Ricki Hirner
6eea640647 Minimal layout change 2016-10-03 12:12:38 +02:00
Ricki Hirner
91864f76e4 Show progress bar when synchronization is active 2016-09-26 22:57:45 +02:00
Ricki Hirner
3c303ff760 Increase SEQUENCE only when we're ORGANIZER 2016-09-24 22:23:25 +02:00
Ricki Hirner
dec4f81faa Query/use CalDAV email address as account name, if available 2016-09-24 21:33:59 +02:00
Ricki Hirner
3e691e6aef MultiSync for Cloud: one-time payment instead of subscription 2016-09-24 15:53:06 +02:00
Ricki Hirner
66ae1ddc8d Always increase SEQUENCE 2016-09-23 13:55:45 +02:00
Ricki Hirner
d757f45e8b lint: don't keep references to Context in static fields 2016-09-21 21:59:17 +02:00
Ricki Hirner
374acf1c70 Version bump to 1.3.1
* some cert4android tests
2016-09-18 17:37:08 +02:00
Ricki Hirner
b1ab14f311 Import strings from Transifex 2016-09-18 16:51:05 +02:00
Ricki Hirner
d65a021536 Always use PROPFIND instead of REPORT addressbook-query 2016-09-18 16:38:41 +02:00
Ricki Hirner
8e61320225 Add About activity for iCloud flavor 2016-09-18 16:36:11 +02:00
Ricki Hirner
ef698102f1 New icon, show license notification again 2016-09-06 23:07:00 +02:00
Ricki Hirner
47e417158f Reinitialize certificate manager when needed 2016-09-06 21:56:44 +02:00
Ricki Hirner
65d3986257 README changes 2016-09-02 12:21:11 +02:00
Ricki Hirner
c350baa863 Fetch translations from Transifex 2016-09-02 12:15:17 +02:00
Ricki Hirner
4222a5d2c6 lint optimizations
* permissions: declare AUTHENTICATE_ACCOUNTS, GET_ACCOUNTS and MANAGE_ACCOUNTS only until SDK level 22
* minor optimizations and bug fixes
2016-09-02 11:57:16 +02:00
Ricki Hirner
718bcebf20 Version bump to 1.3
* vcard4android: fix bug concerning generated formatted postal addresses
2016-09-02 00:56:06 +02:00
Ricki Hirner
c6d3370dd3 gplay version: remove donation link
Fix icons again
2016-09-02 00:56:00 +02:00
Ricki Hirner
f03e1d7948 New launcher logo
* new launcher logo (contributed by Christoph Scheidl)
2016-09-01 22:48:12 +02:00
Ricki Hirner
cff047c4cb 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-01 22:03:38 +02:00
Ricki Hirner
7a3a9047e6 iCloud: UI and strings 2016-08-14 20:57:31 +02:00
Ricki Hirner
7edd960d47 Navigation drawer, read-only settings 2016-08-14 17:49:18 +02:00
Ricki Hirner
46a5a8a25a White icons for Apple® iCloud®, hallelujah™ 2016-08-14 13:18:45 +02:00
Ricki Hirner
a3f2c23a3c Accept intent extras for LoginActivity 2016-08-13 23:06:28 +02:00
Ricki Hirner
aaee3fbd9b Fetch translations from Transifex 2016-08-06 00:11:40 +02:00
Ricki Hirner
b80608be98 Fix OpenTasks regression bug
* version bump to 1.2.3
* enable OpenTasks sync on Android <6 again
2016-08-05 23:17:55 +02:00
Ricki Hirner
531440d5a9 Improve HTTP authentication
* use preemptive Basic auth automatically for HTTPS connections
* cache auth parameters (Basic/Digest)
2016-08-05 23:17:32 +02:00
Ricki Hirner
be8657433e Fetch translations from Transifex 2016-08-02 19:27:14 +02:00
Ricki Hirner
c5af9a735b 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:27:07 +02:00
Ricki Hirner
3a582c4534 Avoid sync error when OpenTasks is not installed 2016-08-01 21:49:20 +02:00
Ricki Hirner
bd3d27f883 Clean up launcher icon
* clean up launcher icon
* update dependencies
2016-08-01 21:03:21 +02:00
Ricki Hirner
601cfff788 Basic subscription management
* SDK version 24
* Subscription management and GUI
2016-08-01 20:24:32 +02:00
Ricki Hirner
2fec3e3cb8 iCloud: implement free trial 2016-07-29 15:21:17 +02:00
Ricki Hirner
c4725a9b17 Initial iCloud version
* new gradle configField: useMTM
* new gradle source dir: davdroid (for DAVdroid OSE + DAVdroid variants)
* move strings and default login fragment to davdroid source dir
* iCloud AndroidManifest: add billing permission and SubscriptionActivity
* add sync plugins
* iCloud flavor + sync plugin: in-app billing
* iCloud flavor: login credentials fragment + font
* iCloud flavor: new strings
* use account type from string assets instead of hardcoded constant
2016-07-29 14:27:05 +02:00
Ricki Hirner
70bec8e980 Allow large transactions
* version bump to 1.2.1-ose
* upgrade to okhttp 3.4.1
* ical4android/vcard4android: split oversized transactions
2016-07-27 18:24:30 +02:00
Ricki Hirner
0d0341fd62 Minor changes
* MTM: use multi-process activity
* change library repos to private ones
* upgrade Android gradle plugin
2016-07-14 22:57:00 +02:00
Ricki Hirner
b49bdda7e8 Fix NPE, add intent to view FAQ to upgrade notification 2016-07-11 12:55:55 +02:00
Ricki Hirner
19b54748cd Version bump to 1.2
* move ETag requirement from vcard4android to davdroid
* more debug info
* vcard4android: support for custom labels (X-ABLabel)
2016-07-02 10:32:12 +02:00
Ricki Hirner
41ce609237 Support X-ABLabel for custom types
* vcard4android: support X-ABLabel for custom types
2016-07-01 22:10:20 +02:00
Ricki Hirner
aafcc36c4d Version bump to 1.1.1.2
* allow ProGuard optimization to remove non-relevant bytecode from flavors
* check flavors with direct comparison instead of .equals() to allwo optimizations
* store cookies per HttpClient, and not per DAVdroid instance (allows multiple sessions for parallel syncs)
* fetch translations from Transifex
2016-06-24 13:39:26 +02:00
Ricki Hirner
2496a3bf05 Add standard and gplay product flavor 2016-06-24 00:06:43 +02:00
Ricki Hirner
54e6426dc4 Version bump to 1.1.1.1
* add yield points to allow processing of groups with many contacts
* new script to generate contacts for testing
2016-06-23 11:42:12 +02:00
Ricki Hirner
4542da7d89 Version bump to 1.1.1
* fetch translations from Transifex
2016-06-21 21:05:18 +02:00
Ricki Hirner
977409511a Handle cookies correctly using a name/domain/path MultiKeyMap 2016-06-21 20:51:52 +02:00
Ricki Hirner
ad8c832819 Version bump to 1.1
* fetch translations from Transifex
* fix account settings version update routine
2016-06-19 19:15:38 +02:00
Ricki Hirner
389af2b738 Better group support
* change group methods to less specific values
* new account settings version: change group method to CATEGORIES for updated accounts
* change group method from CATEGORIES to GROUP_VCARDS automatically when a group VCard is received

GUI:
* AccountSettings: disable CalDAV/CardDAV options when the corresponding service is not available
* AccountSettings: new option to choose contact group method
* account setup: allow to choose contact group method at account creation
2016-06-19 18:52:56 +02:00
Ricki Hirner
be2e15e463 Merge branch 'master' into vcard4-groups 2016-06-12 15:52:07 +02:00
Ricki Hirner
c7c13520f9 Version bump to 1.0.9.2
* version bump to 1.0.9.2
* fetch translations from Transifex
2016-06-11 09:06:21 +02:00
Ricki Hirner
317144630c Make use of RFC6868 optional
* make use of RFC6868 for VCards optional because some defect servers don't accept it
* minor UI improvements (thanks biociahi)
2016-06-11 08:53:29 +02:00
Ricki Hirner
34bc27fa79 Switch from preference-v7 to preference-v14 to make preferences look more Material (thanks biociahi!) 2016-06-11 08:53:15 +02:00
Ricki Hirner
210735a500 Switch from preference-v7 to preference-v14 to make preferences look more Material (thanks biociahi!) 2016-06-09 10:13:47 +02:00
Ricki Hirner
b30733c64b Basic support for VCard4-style groups
* rewritten contact group support to support VCard3 CATEGORIES and VCard4-style KIND/MEMBER groups
* new account setting: contact group method (VCard3/VCard4/Apple "VCard4-as-VCard3")
* keep unknown properties when saving/generating VCards
2016-06-08 21:44:31 +02:00
Ricki Hirner
91234a688f Upgrade to okhttp 3.3.1 2016-05-30 12:02:04 +02:00
Ricki Hirner
5675e544b5 Better alarm handling
* ical4android: better alarm handling
* API change: pass OutputStream instead of returning it
2016-05-28 16:00:23 +02:00
Ricki Hirner
42a261b84e Revert "Contact/event/task upload: use streams directly without extra byte[] array"
This reverts commit 3bde3758fc.
Version bump to 1.0.9.1

Reason: A server MAY reject a request that contains a message body but not a Content-Length
by responding with 411 (Length Required). (RFC 7230 3.3.3 Message Body Length)
2016-05-26 22:08:22 +02:00
Ricki Hirner
0d1825cbf3 Revert "Contact/event/task upload: use streams directly without extra byte[] array"
This reverts commit 3bde3758fc.
2016-05-26 22:04:03 +02:00
Ricki Hirner
9b8fc983cd Version bump to 1.0.9
* upgrade to okhttp 3.3 to reduce HTTP/2 incompatibilties
* vcard4android: enable RFC 6868 support in ez-vcard
* minor improvements and bug fixes
* new translations from Transifex
* version bump to 1.0.9
2016-05-25 21:37:43 +02:00
Ricki Hirner
3bde3758fc Contact/event/task upload: use streams directly without extra byte[] array 2016-05-23 15:10:35 +02:00
Ricki Hirner
fd1f59d124 Logs: show which contact/event/task is being prepared for upload 2016-05-23 14:28:27 +02:00
Ricki Hirner
9886507b7d Minor improvements
* use weak references for DavService RefreshingStatusListener
* additional null checks for cases which shouldn't appear, but apparently appear
* additional database conflict handling for cases which shouldn't appear, but apparently appear
* setup by URL: null check for empty host names
* vcard4android: upgrade to ezvcard 0.9.10
2016-05-20 21:38:04 +02:00
Ricki Hirner
144643d6af Work around unexpected IllegalArgumentException when user enters garbage host name 2016-04-30 12:39:19 +02:00
Ricki Hirner
14875f63ea Always update all task fields (including null values)
* ical4android: always update all task fields (including null values)
* lint optimizations
* fetch translations from Transifex
* version bump to 1.0.8
2016-04-26 23:32:24 +02:00
Ricki Hirner
28e567cf78 Introduce local unit tests
* split tests into Android tests and local unit tests
* LoginCredentialsFragment: check for empty host before doing IDN conversion
2016-04-26 13:10:42 +02:00
Ricki Hirner
7997606550 Minor optimizations
* catch IllegalArgumentException from HttpUrl in DavResourceFinder (caused crash when logging in with email "test@server/withslash")
* use IteratorChain in DavService collection enumeration
2016-04-19 21:56:05 +02:00
Ricki Hirner
fb0552de46 Find collections when they're identical with their home set 2016-04-18 00:04:18 +02:00
Ricki Hirner
03c15a6924 Task list synchronization conditions, tests
* task list synchronization: don't set VISIBLE=1 and SYNC_ENABLED=1 at every sync, but only at creation
* task list synchronization: sync only task lists which are SYNC_ENABLED
* honor "manage calendar colors" account setting for task list colors, too
* add run-tests-connected.sh, to be used as pre-commit hook
* ical4android/vcard4android tests
* fetch translations from Transifex
* version bump to 1.0.7
2016-04-14 20:37:22 +02:00
Ricki Hirner
c3b2929f88 Changes in ical4android and vcard4android
* don't set ORGANIZER for events without attendees
* make some lists public final instead of @Getter private
* PermissionsActivity: call refresh in onResume() instead of onCreate()
2016-04-13 14:15:34 +02:00
Ricki Hirner
eb2537a278 Version bump to 1.0.7-beta1
* fetch translations from Transifex
2016-04-10 20:43:01 +02:00
Ricki Hirner
0b9727cca6 More detailled OpenTasks installation hint
* show "reinstall DAVdroid" hint only for Android <6
* fetch translations from Transifex
2016-04-10 18:50:42 +02:00
Ricki Hirner
61231b4233 Implement Android 6-style permissions
* increase target API level to 23 (Android 6), which makes Android 6-style permissions mandatory
* AUTHENTICATE_ACCOUNTS permission is only required up to API level 22
* new activity: PermissionsActivity which shows missing permissions and provides buttons to request them
* DavService: Android shouldn't send a null Intent, but sometimes it does, so implement null check
* LocalTaskList: tasksProviderAvailable may return true on API level 23+ even if permissions are not sufficient
* SyncAdapterService: show a notification (with Intent for PermissionsActivity) when permissions are not sufficient
* when creating accounts, set OpenTasks sync always to true if API level is 23+ (even if OpenTasks is not installed [yet])
* update Lombok
2016-04-10 15:55:11 +02:00
Ricki Hirner
59252d7471 Fetch translations from Transifex
* fetch translations
* minor changes (lint)
2016-04-07 08:38:09 +02:00
Ricki Hirner
6ffa6fa9a7 New feature: only sync in WiFi
* new setting: only sync in WiFi (or when sync is triggered manually)
* new setting: only sync in specific WiFI (by SSID)
* lower default sync interval when account is created to 4 hours (was 1 day)
* version bump to 1.0.6
2016-04-06 21:04:16 +02:00
Ricki Hirner
03ee9a037b Various tests 2016-04-05 23:25:18 +02:00
Ricki Hirner
7ab13d648e Check for migrations only when package is replaced, DB fixes
* AccountSettings$AppUpdatedReceiver: check for migrations only when package is replaced
* SyncAdapter: move DB helper from service to SyncAdapter to prevent databases from being closed too early
* Manual sync button: run sync immediately (without queueing)
2016-04-05 16:52:43 +02:00
Ricki Hirner
25c54cce62 SyncManager notifications: create a unique notification for every synced collection 2016-04-01 17:45:57 +02:00
Ricki Hirner
f0e45c71f5 Add account setting: manage calendar colors 2016-03-31 20:07:36 +02:00
Ricki Hirner
fa528a64e9 Sync database optimizations
* enable WAL as early as possible
* don't close database in SyncAdapter but only in SyncService
* version bump to 1.0.4
2016-03-31 14:08:48 +02:00
Ricki Hirner
c6aed90c96 OOM handling, DB transactions, calandar VISIBLE, service refresh notification
* handle and show OutOfMemoryErrors correctly (they're not Exceptions)
* use db.beginTransactionNonExclusive() because WAL is enabled
* set calendar VISIBLE=1 AND SYNC=1 only at creation and not at every sync
* update PendingIntent of service refresh notification
2016-03-31 12:47:43 +02:00
Ricki Hirner
2280f899ee Use last path segment as collection display name if there's no DAV:displayName
* use last path segment as collection display name if there's no DAV:displayName
* add Contacts Provider Settings again to show contacts without groups in all clients (bluetooth cars etc.)
2016-03-31 00:52:10 +02:00
199 changed files with 10525 additions and 2664 deletions

20
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,20 @@
image: registry.gitlab.com/bitfireat/davdroid:latest
before_script:
- git submodule update --init --recursive
- export GRADLE_USER_HOME=`pwd`/.gradle; chmod +x gradlew
- emulator64-arm -avd test -no-audio -no-window & wait-for-emulator.sh
- wget -q https://f-droid.org/repo/org.dmfs.tasks_103.apk && adb install org.dmfs.tasks_103.apk
cache:
paths:
- .gradle/
test:
script:
- ./gradlew check mergeAndroidReports
artifacts:
paths:
- app/build/outputs/lint-results-debug.html
- app/build/reports
- build/reports

13
.gitmodules vendored
View File

@@ -1,13 +1,12 @@
[submodule "dav4android"]
path = dav4android
url = https://gitlab.com/bitfireAT/dav4android.git
url = ../dav4android.git
[submodule "ical4android"]
path = ical4android
url = https://gitlab.com/bitfireAT/ical4android.git
url = ../ical4android.git
[submodule "vcard4android"]
path = vcard4android
url = https://gitlab.com/bitfireAT/vcard4android.git
[submodule "MemorizingTrustManager"]
path = MemorizingTrustManager
url = https://github.com/ge0rg/MemorizingTrustManager
ignore = dirty
url = ../vcard4android.git
[submodule "cert4android"]
path = cert4android
url = ../cert4android.git

View File

@@ -1,4 +1,7 @@
[![build status](https://gitlab.com/bitfireAT/davdroid/badges/master/build.svg)](https://gitlab.com/bitfireAT/davdroid/commits/master)
DAVdroid
========
@@ -7,18 +10,21 @@ comprehensive information about DAVdroid.
DAVdroid is licensed under the [GPLv3 License](LICENSE).
News and updates: [@davdroidapp](https://twitter.com/davdroidapp)
News and updates: [@davdroidapp](https://twitter.com/davdroidapp) on Twitter /
[davdroid-announce](https://davdroid.bitfire.at/download/newsletter/) mailing list
Help and discussion: [DAVdroid forums](https://davdroid.bitfire.at/forums)
**If you want to support DAVdroid, please consider [donating to DAVdroid](https://davdroid.bitfire.at/donate/)
or [purchasing it](https://davdroid.bitfire.at/download/).**
Parts of DAVdroid have been outsourced into these libraries:
* [cert4android](https://gitlab.com/bitfireAT/cert4android) custom certificate management
* [dav4android](https://gitlab.com/bitfireAT/dav4android) 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
[![Flattr this!](https://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=bitfire&url=https://davdroid.bitfire.at&title=DAVdroid&category=software)
USED THIRD-PARTY LIBRARIES
==========================
@@ -28,7 +34,5 @@ Those libraries are used by DAVdroid (alphabetically):
* [dnsjava](http://www.xbill.org/dnsjava/) [BSD License](http://www.xbill.org/dnsjava/dnsjava-current/LICENSE)
* [ez-vcard](https://code.google.com/p/ez-vcard/) [New BSD License](http://opensource.org/licenses/BSD-3-Clause)
* [iCal4j](http://ical4j.sourceforge.net/) [New BSD License](http://sourceforge.net/p/ical4j/ical4j/ci/default/tree/LICENSE)
* [MemorizingTrustManager](https://github.com/ge0rg/MemorizingTrustManager) [MIT License](https://raw.githubusercontent.com/ge0rg/MemorizingTrustManager/master/LICENSE.txt)
* [okhttp](https://square.github.io/okhttp/) [Apache License, Version 2.0](https://square.github.io/okhttp/#license)
* [Project Lombok](http://projectlombok.org/) [MIT License](http://opensource.org/licenses/mit-license.php)
* [SLF4J](http://www.slf4j.org/) [MIT License](http://www.slf4j.org/license.html)

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013 2016 Ricki Hirner (bitfire web engineering).
* Copyright (c) Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
@@ -9,18 +9,72 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
compileSdkVersion 26
buildToolsVersion '26.0.0'
defaultConfig {
applicationId "at.bitfire.davdroid"
minSdkVersion 14
targetSdkVersion 22
versionCode 92
versionName "1.0.2"
resValue "string", "packageID", applicationId
versionCode 152
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
minSdkVersion 15
targetSdkVersion 25
buildConfigField "boolean", "customCerts", "false"
buildConfigField "at.bitfire.vcard4android.GroupMethod", "settingContactGroupMethod", "null"
}
productFlavors {
standard {
versionName "1.6.2"
buildConfigField "boolean", "customCerts", "true"
}
gplay {
versionName "1.6.2-gplay"
buildConfigField "boolean", "customCerts", "true"
}
icloud {
applicationId "at.bitfire.cloudsync"
resValue "string", "packageID", applicationId
versionName "1.6.2-cloud"
buildConfigField "at.bitfire.vcard4android.GroupMethod", "settingContactGroupMethod", "at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS"
}
soldupe {
applicationId "com.soldupe.cloudsync"
resValue "string", "packageID", applicationId
minSdkVersion 21
versionName "1.6.2-soldupe"
buildConfigField "boolean", "customCerts", "true"
buildConfigField "at.bitfire.vcard4android.GroupMethod", "settingContactGroupMethod", "at.bitfire.vcard4android.GroupMethod.CATEGORIES"
}
}
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" ]
}
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'
}
}
buildTypes {
@@ -30,6 +84,9 @@ android {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
signingConfig signingConfigs.bitfire
productFlavors.soldupe.signingConfig signingConfigs.soldupe
}
}
@@ -40,8 +97,8 @@ android {
disable 'IconColors'
disable 'IconLauncherShape'
disable 'IconMissingDensityFolder'
disable 'MissingTranslation'
disable 'OldTargetApi' // Android 6 permission model not implemented yet
disable 'ImpliedQuantity', 'MissingQuantity'
disable 'MissingTranslation', 'ExtraTranslation' // translations from Transifex are not always up to date
disable 'Recycle' // doesn't understand Lombok's @Cleanup
disable 'RtlEnabled'
disable 'RtlHardcoded'
@@ -52,28 +109,43 @@ android {
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE.txt'
}
}
configurations.all {
exclude module: 'commons-logging' // undocumented part of Android
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
dependencies {
provided 'org.projectlombok:lombok:1.16.6'
compile project(':cert4android')
compile project(':dav4android')
compile project(':ical4android')
compile project(':vcard4android')
compile project(':market_licensing')
compile 'com.android.support:appcompat-v7:23.+'
compile 'com.android.support:cardview-v7:23.+'
compile 'com.android.support:design:23.+'
compile 'com.android.support:preference-v7:23.+'
compile 'com.android.support:appcompat-v7:26.+'
compile 'com.android.support:cardview-v7:26.+'
compile 'com.android.support:design:26.+'
compile 'com.android.support:preference-v14:26.+'
compile 'com.github.yukuku:ambilwarna:2.0.1'
compile project(':MemorizingTrustManager')
androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.2.0'
compile 'dnsjava:dnsjava:2.1.7'
compile 'org.apache.commons:commons-lang3:3.4'
compile 'com.squareup.okhttp3:logging-interceptor:3.8.1'
compile 'commons-io:commons-io:2.5'
compile 'dnsjava:dnsjava:2.1.8'
compile 'org.apache.commons:commons-lang3:3.6'
compile 'org.apache.commons:commons-collections4:4.1'
provided 'org.projectlombok:lombok:1.16.16'
// for tests
androidTestCompile('com.android.support.test:runner:+') {
exclude group: 'com.android.support', module: 'support-annotations'
}
androidTestCompile('com.android.support.test:rules:+') {
exclude group: 'com.android.support', module: 'support-annotations'
}
androidTestCompile 'junit:junit:4.12'
androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.8.1'
testCompile 'junit:junit:4.12'
testCompile 'com.squareup.okhttp3:mockwebserver:3.8.1'
}

View File

@@ -1,36 +1,38 @@
# ProGuard usage for DAVdroid:
# shrinking yes - main reason for using ProGuard
# optimization no - too risky
# obfuscation no - DAVdroid is open-source
# preverification no (Android default)
# 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
# ez-vcard
-dontwarn com.fasterxml.jackson.** # Jackson JSON Processor (for jCards) not used
-dontwarn freemarker.** # freemarker templating library (for creating hCards) not used
-dontwarn org.jsoup.** # jsoup library (for hCard parsing) not used
-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
-dontwarn sun.misc.Perf
-keep class ezvcard.property.** { *; } # keep all VCard properties (created at runtime)
-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 groovy.** # Groovy-based ContentBuilder not used
-dontwarn org.codehaus.groovy.**
-dontwarn org.apache.commons.logging.** # Commons logging is not available
-dontwarn net.fortuna.ical4j.model.** # ignore warnings from Groovy dependency
-keep class net.fortuna.ical4j.model.** { *; } # keep all model classes (properties/factories, created at runtime)
-dontwarn net.fortuna.ical4j.model.** # ignore warnings from Groovy dependency
-keep class net.fortuna.ical4j.** { *; } # keep all model classes (properties/factories, created at runtime)
# okhttp
-dontwarn java.nio.file.** # not available on Android
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
# MemorizingTrustManager
-dontwarn de.duenndns.ssl.MemorizingTrustManager
-dontwarn okio.**
-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault
# dnsjava
-dontwarn sun.net.spi.nameservice.** # not available on Android
-dontwarn sun.net.spi.nameservice.** # not available on Android
# DAVdroid + libs
-keep class at.bitfire.** { *; } # all DAVdroid code is required
-keep class at.bitfire.** { *; } # all DAVdroid code is required

View File

@@ -1,39 +0,0 @@
/*
* Copyright © 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
*/
package at.bitfire.davdroid;
import junit.framework.TestCase;
import java.util.Arrays;
public class ArrayUtilsTest extends TestCase {
public void testPartition() {
// n == 0
assertTrue(Arrays.deepEquals(
new Long[0][0],
ArrayUtils.partition(new Long[] { }, 5)));
// n < max
assertTrue(Arrays.deepEquals(
new Long[][] { { 1l, 2l } },
ArrayUtils.partition(new Long[] { 1l, 2l }, 5)));
// n == max
assertTrue(Arrays.deepEquals(
new Long[][] { { 1l, 2l }, { 3l, 4l } },
ArrayUtils.partition(new Long[] { 1l, 2l, 3l, 4l }, 2)));
// n > max
assertTrue(Arrays.deepEquals(
new Long[][] { { 1l, 2l, 3l, 4l, 5l }, { 6l, 7l, 8l, 9l, 10l }, { 11l } },
ArrayUtils.partition(new Long[] { 1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l, 9l, 10l, 11l }, 5)));
}
}

View File

@@ -9,36 +9,43 @@
package at.bitfire.davdroid;
import android.os.Build;
import android.test.InstrumentationTestCase;
import android.support.test.runner.AndroidJUnit4;
import junit.framework.TestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.net.Socket;
import javax.net.ssl.SSLSocket;
import de.duenndns.ssl.MemorizingTrustManager;
import okhttp3.OkHttpClient;
import at.bitfire.cert4android.CustomCertManager;
import okhttp3.mockwebserver.MockWebServer;
public class SSLSocketFactoryCompatTest extends InstrumentationTestCase {
import static android.support.test.InstrumentationRegistry.getTargetContext;
import static junit.framework.TestCase.assertFalse;
import static org.junit.Assert.assertTrue;
public class SSLSocketFactoryCompatTest {
SSLSocketFactoryCompat factory;
MockWebServer server = new MockWebServer();
@Override
protected void setUp() throws Exception {
factory = new SSLSocketFactoryCompat(new MemorizingTrustManager(getInstrumentation().getTargetContext().getApplicationContext()));
@Before
public void startServer() throws Exception {
factory = new SSLSocketFactoryCompat(new CustomCertManager(getTargetContext().getApplicationContext(), true));
server.start();
}
@Override
protected void tearDown() throws Exception {
@After
public void stopServer() throws Exception {
server.shutdown();
}
@Test
public void testUpgradeTLS() throws IOException {
Socket s = factory.createSocket(server.getHostName(), server.getPort());
assertTrue(s instanceof SSLSocket);

View File

@@ -1,27 +0,0 @@
/*
* Copyright © 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
*/
package at.bitfire.davdroid;
import android.util.Log;
import java.net.URI;
import java.net.URISyntaxException;
public class TestConstants {
public static final String ROBOHYDRA_BASE = "http://192.168.0.11:3000/";
public static URI roboHydra;
static {
try {
roboHydra = new URI(ROBOHYDRA_BASE);
} catch(URISyntaxException e) {
Log.wtf("davdroid.test.Constants", "Invalid RoboHydra base URL");
}
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright © 2013 2016 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.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.property.ResourceType;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.model.ServiceDB.Collections;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class CollectionInfoTest {
MockWebServer server = new MockWebServer();
@Test
public void testFromDavResource() throws IOException, HttpException, DavException {
// r/w address book
server.enqueue(new MockResponse()
.setResponseCode(207)
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
"<response>" +
" <href>/</href>" +
" <propstat><prop>" +
" <resourcetype><collection/><CARD:addressbook/></resourcetype>" +
" <displayname>My Contacts</displayname>" +
" <CARD:addressbook-description>My Contacts Description</CARD:addressbook-description>" +
" </prop></propstat>" +
"</response>" +
"</multistatus>"));
DavResource dav = new DavResource(HttpClient.create(null), server.url("/"));
dav.propfind(0, ResourceType.NAME);
CollectionInfo info = CollectionInfo.fromDavResource(dav);
assertEquals(CollectionInfo.Type.ADDRESS_BOOK, info.type);
assertFalse(info.readOnly);
assertEquals("My Contacts", info.displayName);
assertEquals("My Contacts Description", info.description);
// read-only calendar, no display name
server.enqueue(new MockResponse()
.setResponseCode(207)
.setBody("<multistatus xmlns='DAV:' xmlns:CAL='urn:ietf:params:xml:ns:caldav' xmlns:ICAL='http://apple.com/ns/ical/'>" +
"<response>" +
" <href>/</href>" +
" <propstat><prop>" +
" <resourcetype><collection/><CAL:calendar/></resourcetype>" +
" <current-user-privilege-set><privilege><read/></privilege></current-user-privilege-set>" +
" <CAL:calendar-description>My Calendar</CAL:calendar-description>" +
" <CAL:calendar-timezone>tzdata</CAL:calendar-timezone>" +
" <ICAL:calendar-color>#ff0000</ICAL:calendar-color>" +
" </prop></propstat>" +
"</response>" +
"</multistatus>"));
dav = new DavResource(HttpClient.create(null), server.url("/"));
dav.propfind(0, ResourceType.NAME);
info = CollectionInfo.fromDavResource(dav);
assertEquals(CollectionInfo.Type.CALENDAR, info.type);
assertTrue(info.readOnly);
assertNull(info.displayName);
assertEquals("My Calendar", info.description);
assertEquals(0xFFFF0000, (int)info.color);
assertEquals("tzdata", info.timeZone);
assertTrue(info.supportsVEVENT);
assertTrue(info.supportsVTODO);
}
@Test
public void testFromDB() {
ContentValues values = new ContentValues();
values.put(Collections.ID, 1);
values.put(Collections.SERVICE_ID, 1);
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);
CollectionInfo info = CollectionInfo.fromDB(values);
assertEquals(1, info.id);
assertEquals(1, (long)info.serviceID);
assertEquals("http://example.com", info.url);
assertTrue(info.readOnly);
assertEquals("display name", info.displayName);
assertEquals("description", info.description);
assertEquals(0xFFFF0000, (int)info.color);
assertEquals("tzdata", info.timeZone);
assertTrue(info.supportsVEVENT);
assertTrue(info.supportsVTODO);
assertTrue(info.selected);
}
}

View File

@@ -1,90 +0,0 @@
package at.bitfire.davdroid.resource;
import android.test.InstrumentationTestCase;
import java.io.IOException;
import java.net.URI;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.davdroid.ui.setup.DavResourceFinder;
import at.bitfire.davdroid.ui.setup.LoginCredentials;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
public class DavResourceFinderTest extends InstrumentationTestCase {
MockWebServer server = new MockWebServer();
@Override
protected void setUp() throws Exception {
server.start();
}
@Override
protected void tearDown() throws Exception {
server.shutdown();
}
public void testGetCurrentUserPrincipal() throws IOException, HttpException, DavException {
HttpUrl url = server.url("/dav");
LoginCredentials credentials = new LoginCredentials(url.uri(), "admin", "12345", true);
DavResourceFinder finder = new DavResourceFinder(getInstrumentation().getTargetContext().getApplicationContext(), credentials);
// positive test case
server.enqueue(new MockResponse() // PROPFIND response
.setResponseCode(207)
.setHeader("Content-Type", "application/xml;charset=utf-8")
.setBody("<multistatus xmlns='DAV:'>" +
" <response>" +
" <href>/dav</href>" +
" <propstat>" +
" <prop>" +
" <current-user-principal><href>/principals/myself</href></current-user-principal>" +
" </prop>" +
" <status>HTTP/1.0 200 OK</status>" +
" </propstat>" +
" </response>" +
"</multistatus>"));
server.enqueue(new MockResponse() // OPTIONS response
.setResponseCode(200)
.setHeader("DAV", "addressbook"));
URI principal = finder.getCurrentUserPrincipal(url, DavResourceFinder.Service.CARDDAV);
assertEquals(url.resolve("/principals/myself").uri(), principal);
// negative test case: no current-user-principal
server.enqueue(new MockResponse()
.setResponseCode(207)
.setHeader("Content-Type", "application/xml;charset=utf-8")
.setBody("<multistatus xmlns='DAV:'>" +
" <response>" +
" <href>/dav</href>" +
" <status>HTTP/1.0 200 OK</status>" +
" </response>" +
"</multistatus>"));
assertNull(finder.getCurrentUserPrincipal(url, DavResourceFinder.Service.CARDDAV));
// negative test case: requested service not available
server.enqueue(new MockResponse() // PROPFIND response
.setResponseCode(207)
.setHeader("Content-Type", "application/xml;charset=utf-8")
.setBody("<multistatus xmlns='DAV:'>" +
" <response>" +
" <href>/dav</href>" +
" <propstat>" +
" <prop>" +
" <current-user-principal><href>/principals/myself</href></current-user-principal>" +
" </prop>" +
" <status>HTTP/1.0 200 OK</status>" +
" </propstat>" +
" </response>" +
"</multistatus>"));
server.enqueue(new MockResponse() // OPTIONS response
.setResponseCode(200)
.setHeader("DAV", "addressbook"));
assertNull(finder.getCurrentUserPrincipal(url, DavResourceFinder.Service.CALDAV));
}
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright © 2013 2016 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.support.test.runner.AndroidJUnit4;
import android.test.InstrumentationTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.net.URI;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.property.AddressbookHomeSet;
import at.bitfire.dav4android.property.ResourceType;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.ui.setup.DavResourceFinder;
import at.bitfire.davdroid.ui.setup.DavResourceFinder.Configuration.ServiceInfo;
import at.bitfire.davdroid.ui.setup.LoginCredentials;
import okhttp3.OkHttpClient;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import static android.support.test.InstrumentationRegistry.getTargetContext;
import static junit.framework.TestCase.assertFalse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class DavResourceFinderTest {
MockWebServer server = new MockWebServer();
DavResourceFinder finder;
OkHttpClient client;
LoginCredentials credentials;
private static final String
PATH_NO_DAV = "/nodav",
PATH_CALDAV = "/caldav",
PATH_CARDDAV = "/carddav",
PATH_CALDAV_AND_CARDDAV = "/both-caldav-carddav",
SUBPATH_PRINCIPAL = "/principal",
SUBPATH_ADDRESSBOOK_HOMESET = "/addressbooks",
SUBPATH_ADDRESSBOOK = "/addressbooks/private-contacts";
@Before
public void initServerAndClient() throws Exception {
server.setDispatcher(new TestDispatcher());
server.start();
credentials = new LoginCredentials(URI.create("/"), "mock", "12345");
finder = new DavResourceFinder(getTargetContext(), credentials);
client = HttpClient.create(null);
client = HttpClient.addAuthentication(client, credentials.userName, credentials.password);
}
@After
public void stopServer() throws Exception {
server.shutdown();
}
@Test
public void testRememberIfAddressBookOrHomeset() throws IOException, HttpException, DavException {
ServiceInfo info;
// before dav.propfind(), no info is available
DavResource dav = new DavResource(client, server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL));
finder.rememberIfAddressBookOrHomeset(dav, info = new ServiceInfo());
assertEquals(0, info.collections.size());
assertEquals(0, info.homeSets.size());
// recognize home set
dav.propfind(0, AddressbookHomeSet.NAME);
finder.rememberIfAddressBookOrHomeset(dav, info = new ServiceInfo());
assertEquals(0, info.collections.size());
assertEquals(1, info.homeSets.size());
assertEquals(server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET + "/").uri(), info.homeSets.iterator().next());
// recognize address book
dav = new DavResource(client, server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK));
dav.propfind(0, ResourceType.NAME);
finder.rememberIfAddressBookOrHomeset(dav, info = new ServiceInfo());
assertEquals(1, info.collections.size());
assertEquals(server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK + "/").uri(), info.collections.keySet().iterator().next());
assertEquals(0, info.homeSets.size());
}
@Test
public void testProvidesService() throws IOException {
assertFalse(finder.providesService(server.url(PATH_NO_DAV), DavResourceFinder.Service.CALDAV));
assertFalse(finder.providesService(server.url(PATH_NO_DAV), DavResourceFinder.Service.CARDDAV));
assertTrue(finder.providesService(server.url(PATH_CALDAV), DavResourceFinder.Service.CALDAV));
assertFalse(finder.providesService(server.url(PATH_CALDAV), DavResourceFinder.Service.CARDDAV));
assertTrue(finder.providesService(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV));
assertFalse(finder.providesService(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV));
assertTrue(finder.providesService(server.url(PATH_CALDAV_AND_CARDDAV), DavResourceFinder.Service.CALDAV));
assertTrue(finder.providesService(server.url(PATH_CALDAV_AND_CARDDAV), DavResourceFinder.Service.CARDDAV));
}
@Test
public void testGetCurrentUserPrincipal() throws IOException, HttpException, DavException {
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_NO_DAV), DavResourceFinder.Service.CALDAV));
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_NO_DAV), DavResourceFinder.Service.CARDDAV));
assertEquals(
server.url(PATH_CALDAV + SUBPATH_PRINCIPAL).uri(),
finder.getCurrentUserPrincipal(server.url(PATH_CALDAV), DavResourceFinder.Service.CALDAV)
);
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CALDAV), DavResourceFinder.Service.CARDDAV));
assertEquals(
server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL).uri(),
finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV)
);
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV));
}
// mock server
public class TestDispatcher extends Dispatcher {
@Override
public MockResponse dispatch(RecordedRequest rq) throws InterruptedException {
if (!checkAuth(rq)) {
MockResponse authenticate = new MockResponse().setResponseCode(401);
authenticate.setHeader("WWW-Authenticate", "Basic realm=\"test\"");
return authenticate;
}
String path = rq.getPath();
if ("OPTIONS".equalsIgnoreCase(rq.getMethod())) {
String dav = null;
if (path.startsWith(PATH_CALDAV))
dav = "calendar-access";
else if (path.startsWith(PATH_CARDDAV))
dav = "addressbook";
else if (path.startsWith(PATH_CALDAV_AND_CARDDAV))
dav = "calendar-access, addressbook";
MockResponse response = new MockResponse().setResponseCode(200);
if (dav != null)
response.addHeader("DAV", dav);
return response;
} else if ("PROPFIND".equalsIgnoreCase(rq.getMethod())) {
String props = null;
switch (path) {
case PATH_CALDAV:
case PATH_CARDDAV:
props = "<current-user-principal><href>" + path + SUBPATH_PRINCIPAL + "</href></current-user-principal>";
break;
case PATH_CARDDAV + SUBPATH_PRINCIPAL:
props = "<CARD:addressbook-home-set>" +
" <href>" + PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET + "</href>" +
"</CARD:addressbook-home-set>";
break;
case PATH_CARDDAV + SUBPATH_ADDRESSBOOK:
props = "<resourcetype>" +
" <collection/>" +
" <CARD:addressbook/>" +
"</resourcetype>";
break;
}
App.log.info("Sending props: " + props);
return new MockResponse()
.setResponseCode(207)
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
"<response>" +
" <href>" + rq.getPath() + "</href>" +
" <propstat><prop>" + props + "</prop></propstat>" +
"</response>" +
"</multistatus>");
}
return new MockResponse().setResponseCode(404);
}
private boolean checkAuth(RecordedRequest rq) {
return "Basic bW9jazoxMjM0NQ==".equals(rq.getHeader("Authorization"));
}
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,50 @@
package at.bitfire.davdroid.ui;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.view.MenuItem;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
public class DefaultAccountsDrawerHandler implements IAccountsDrawerHandler {
@Override
public boolean onNavigationItemSelected(@NonNull Activity activity, MenuItem item) {
switch (item.getItemId()) {
case R.id.nav_about:
activity.startActivity(new Intent(activity, AboutActivity.class));
break;
case R.id.nav_app_settings:
activity.startActivity(new Intent(activity, AppSettingsActivity.class));
break;
case R.id.nav_twitter:
activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://twitter.com/davdroidapp")));
break;
case R.id.nav_website:
activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))));
break;
case R.id.nav_faq:
activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.navigation_drawer_faq_url))));
break;
case R.id.nav_forums:
activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))
.buildUpon().appendEncodedPath("forums/").build()));
break;
case R.id.nav_donate:
if (BuildConfig.FLAVOR != App.FLAVOR_GOOGLE_PLAY)
activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))
.buildUpon().appendEncodedPath("donate/").build()));
break;
default:
return false;
}
return true;
}
}

View File

@@ -8,27 +8,33 @@
package at.bitfire.davdroid.ui.setup;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import org.apache.commons.lang3.StringUtils;
import java.net.IDN;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.logging.Level;
import at.bitfire.dav4android.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.ui.widget.EditPassword;
public class LoginCredentialsFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
public class DefaultLoginCredentialsFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
RadioButton radioUseEmail;
LinearLayout emailDetails;
@@ -39,9 +45,9 @@ public class LoginCredentialsFragment extends Fragment implements CompoundButton
LinearLayout urlDetails;
EditText editBaseURL, editUserName;
EditPassword editUrlPassword;
CheckBox checkPreemptiveAuth;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.login_credentials_fragment, container, false);
@@ -56,13 +62,35 @@ public class LoginCredentialsFragment extends Fragment implements CompoundButton
editBaseURL = (EditText)v.findViewById(R.id.base_url);
editUserName = (EditText)v.findViewById(R.id.user_name);
editUrlPassword = (EditPassword)v.findViewById(R.id.url_password);
checkPreemptiveAuth = (CheckBox)v.findViewById(R.id.preemptive_auth);
radioUseEmail.setOnCheckedChangeListener(this);
radioUseURL.setOnCheckedChangeListener(this);
if (savedInstanceState == null)
radioUseEmail.setChecked(true);
if (savedInstanceState == null) {
// first call
Activity activity = getActivity();
Intent intent = (activity != null) ? activity.getIntent() : null;
if (intent != null) {
// we've got initial login data
String url = intent.getStringExtra(LoginActivity.EXTRA_URL),
username = intent.getStringExtra(LoginActivity.EXTRA_USERNAME),
password = intent.getStringExtra(LoginActivity.EXTRA_PASSWORD);
if (url != null) {
radioUseURL.setChecked(true);
editBaseURL.setText(url);
editUserName.setText(username);
editUrlPassword.setText(password);
} else {
radioUseEmail.setChecked(true);
editEmailAddress.setText(username);
editEmailPassword.setText(password);
}
} else
radioUseEmail.setChecked(true);
}
final Button login = (Button)v.findViewById(R.id.login);
login.setOnClickListener(new View.OnClickListener() {
@@ -83,6 +111,7 @@ public class LoginCredentialsFragment extends Fragment implements CompoundButton
boolean loginByEmail = buttonView == radioUseEmail;
emailDetails.setVisibility(loginByEmail ? View.VISIBLE : View.GONE);
urlDetails.setVisibility(loginByEmail ? View.GONE : View.VISIBLE);
(loginByEmail ? editEmailAddress : editBaseURL).requestFocus();
}
}
@@ -109,7 +138,7 @@ public class LoginCredentialsFragment extends Fragment implements CompoundButton
valid = false;
}
return valid ? new LoginCredentials(uri, email, password, true) : null;
return valid ? new LoginCredentials(uri, email, password) : null;
} else if (radioUseURL.isChecked()) {
URI uri = null;
@@ -118,11 +147,16 @@ public class LoginCredentialsFragment extends Fragment implements CompoundButton
Uri baseUrl = Uri.parse(editBaseURL.getText().toString());
String scheme = baseUrl.getScheme();
if ("https".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme)) {
String host = IDN.toASCII(baseUrl.getHost());
if (host.isEmpty()) {
String host = baseUrl.getHost();
if (StringUtils.isEmpty(host)) {
editBaseURL.setError(getString(R.string.login_url_host_name_required));
valid = false;
}
} else
try {
host = IDN.toASCII(host);
} catch(IllegalArgumentException e) {
Constants.log.log(Level.WARNING, "Host name not conforming to RFC 3490", e);
}
String path = baseUrl.getEncodedPath();
int port = baseUrl.getPort();
@@ -149,10 +183,20 @@ public class LoginCredentialsFragment extends Fragment implements CompoundButton
valid = false;
}
return valid ? new LoginCredentials(uri, userName, password, checkPreemptiveAuth.isChecked()) : null;
return valid ? new LoginCredentials(uri, userName, password) : null;
}
return null;
}
public static class Factory implements ILoginCredentialsFragment {
@Override
public Fragment getFragment() {
return new DefaultLoginCredentialsFragment();
}
}
}

View File

@@ -94,13 +94,6 @@
android:layout_height="wrap_content"
android:hint="@string/login_password"/>
<CheckBox
android:id="@+id/preemptive_auth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_auth_preemptive"
android:checked="true"/>
</LinearLayout>
</RadioGroup>

View File

@@ -10,12 +10,13 @@
<!--DavService-->
<!--AppSettingsActivity-->
<!--AccountActivity-->
<!--PermissionsActivity-->
<!--AddAccountActivity-->
<string name="login_type_email">Entra amb una adreça de correu electrònic</string>
<string name="login_type_url">Entra amb una URL i un nom d\'usuari</string>
<string name="login_auth_preemptive">Autentificació preferent (recomanat però incompatible amb l\'autentificació Digest)</string>
<!--AccountSettingsActivity-->
<!--collection management-->
<!--ExceptionInfoFragment-->
<!--sync errors and DebugInfoActivity-->
<!--cert4android-->
</resources>

View File

@@ -0,0 +1,246 @@
<?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">Optimalizace využití baterie</string>
<string name="startup_battery_optimization_message">Android může po několika dnech vypnout/prodloužit interval synchronizování DAVdroid. Chcete-li tomuto zabránit, vypněte optimalizaci baterie.</string>
<string name="startup_battery_optimization_disable">Vypnout pro DAVdroid</string>
<string name="startup_dont_show_again">Již nezobrazovat</string>
<string name="startup_development_version">DAVdroid preview vydání</string>
<string name="startup_development_version_message">Toto je vývojová verze aplikace DAVdroid. Mějte na paměti, že vše nemusí správně fungovat. Budeme rádi za konstruktivní zpětnou vazbu, která pomůže vylepšit DAVdroid.</string>
<string name="startup_development_version_give_feedback">Dát zpětnou vazbu</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_google_play_accounts_removed_more_info">Více informací</string>
<string name="startup_opentasks_not_installed">OpenTasks není nainstalován</string>
<string name="startup_opentasks_not_installed_message">Aplikace OpenTasks není dostupná, proto nebude DAVdroid moci synchronizovat seznam úkolů.</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_terms">Licenční podmínky</string>
<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_to_external_storage_warning">Smazat logy jak nejdříve možno!</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_forums">Komunita</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>
<!--PermissionsActivity-->
<string name="permissions_title">DAVdroid oprávnění</string>
<string name="permissions_calendar">Oprávnění pro kalendáře</string>
<string name="permissions_calendar_details">Pro synchronizaci CalDAV událostí s místním kalendářem potřebuje DAVdroid oprávnění přistupovat ke kalendářům.</string>
<string name="permissions_calendar_request">Vyžádat oprávnění kalendáře</string>
<string name="permissions_contacts">Oprávnění pro kontakty</string>
<string name="permissions_contacts_details">Pro synchronizaci CardDAV adresářů s místními kontakty potřebuje DAVdroid oprávnění přistupovat ke kontaktům.</string>
<string name="permissions_contacts_request">Vyžádat oprávnění kontaktů</string>
<string name="permissions_opentasks">Oprávnění pro OpenTasks</string>
<string name="permissions_opentasks_details">Pro synchronizaci CalDAV událostí s místním seznamem úkolů potřebuje DAVdroid oprávnění přistupovat k OpenTasks.</string>
<string name="permissions_opentasks_request">Vyžádat oprávnění OpenTasks</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>
<string name="login_view_logs">Prohlížet logy</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_summary_not_available">Nedostupný</string>
<string name="settings_sync_interval_calendars">Interval synchronizace kalendáře</string>
<string name="settings_sync_interval_tasks">Interval synchronizace úkolů</string>
<string-array name="settings_sync_interval_seconds">
<item>-1</item>
<item>300</item>
<item>600</item>
<item>900</item>
<item>3600</item>
<item>7200</item>
<item>14400</item>
<item>86400</item>
</string-array>
<string-array name="settings_sync_interval_names">
<item>Pouze manuálně</item>
<item>Každých 5 minut</item>
<item>Každých 10 minut</item>
<item>Každých 15 minut</item>
<item>Každou hodinu</item>
<item>Každé 2 hodiny</item>
<item>Každé 4 hodiny</item>
<item>Jednou za den</item>
</string-array>
<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_sync_wifi_only_ssid">Omezení WiFi SSID</string>
<string name="settings_sync_wifi_only_ssid_on">Synchronizace pouze přes %s</string>
<string name="settings_sync_wifi_only_ssid_off">Použít všechna WiFi připojení</string>
<string name="settings_sync_wifi_only_ssid_message">Zadejte jméno WiFi sítě (SSID) pro omezení synchronizace na tutu síť, nebo ponechte prázdné pro použití všech WiFi připojení.</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="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 errors 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>
<string name="sync_error_calendar">Synchronizace kalendáře selhala (%s)</string>
<string name="sync_error_contacts">Synchronizace adresáře selhala (%s)</string>
<string name="sync_error_tasks">Synchronizace úkolu selhala (%s)</string>
<string name="sync_error">Chyba při %s</string>
<string name="sync_error_http_dav">Chyba serveru při %s</string>
<string name="sync_error_local_storage">Chyba databáze při %s</string>
<string-array name="sync_error_phases">
<item>příprava synchronizace</item>
<item>dotazování možností</item>
<item>zpracovávání místně smazaných záznamů</item>
<item>příprava vytvořených/upravených záznamů</item>
<item>nahrávání vytvořených/upravených záznamů</item>
<item>kontrola stavu synchronizace</item>
<item>výpis místních záznamů</item>
<item>výpis vzdálených záznamů</item>
<item>porovnání místních/vzdálených záznamů</item>
<item>stahování vzdálených záznamů</item>
<item>uzavírání procesu</item>
<item>ukládání stavu synchronizace</item>
</string-array>
<string name="sync_error_unauthorized">Chybné uživatelské jméno/heslo</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

@@ -2,11 +2,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">DAVdroid Addressebog</string>
<string name="address_books_authority_title">Adressebøger</string>
<string name="help">Hjælp</string>
<string name="manage_accounts">Administrer konti</string>
<string name="please_wait">Vent venligst -</string>
<string name="please_wait">Vent venligst ...</string>
<string name="send">Send</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Batterioptimering</string>
<string name="startup_battery_optimization_message">Android kan deaktivere/reducere DAVDroid synkronisering efter et par dage. For at undgå dette, slå batterioptimering fra.</string>
<string name="startup_battery_optimization_disable">Deaktiver DAVdroid</string>
<string name="startup_dont_show_again">Vis ikke igen</string>
<string name="startup_development_version">DAVdroid Preview</string>
<string name="startup_development_version_message">Dette er udviklingsversionen af DAVdroid. Bemærk, at nogle ting måske ikke fungerer, som man forventer. Giv os gerne konstruktiv kritik for at forbedre DAVdroid.</string>
@@ -19,7 +24,8 @@
<string name="startup_google_play_accounts_removed_message">Under visse tekniske omstændigheder kan DRM fra Play Store bevirke, at alle DAVdroid-konti er væk efter en genstart eller opgradering af DAVdroid. Hvis du er udsat for dette problem (og ellers ikke), opfordres du til at installere DAVDroid JB Workaround\" fra Play Store.</string>
<string name="startup_google_play_accounts_removed_more_info">Yderligere oplysninger</string>
<string name="startup_opentasks_not_installed">OpenTasks ikke installeret</string>
<string name="startup_opentasks_not_installed_message">OpenTasks-app\'en er ikke installeret eller kan ikke tilgås, så DAVdroid vil ikke være i stand til at opdatere opgavelister. Geninstaller DAVdroid og tilføj dine konti igen efter installation af OpenTasks.</string>
<string name="startup_opentasks_not_installed_message">OpenTasks er ikke til rådighedm så DAVdroid vil ikke kunne synkronisere opgavelister.</string>
<string name="startup_opentasks_reinstall_davdroid">Efter at have installeret OpenTasks, vil du være nødt til at GENINSTALLERE DAVdroid og dine konti igen (en fejl i Android).</string>
<string name="startup_opentasks_not_installed_install">Installer OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Licensforhold</string>
@@ -43,6 +49,8 @@
<string name="navigation_drawer_forums">Community</string>
<string name="navigation_drawer_donate">Donation</string>
<string name="account_list_empty">Velkommen til DAVdroid!\n\nDu kan nu tilføje en CaDAV/CardDAV-konto.</string>
<string name="accounts_global_sync_disabled">Automatisk synkronisering på tværs af systemet er deaktiveret</string>
<string name="accounts_global_sync_enable">Aktiver</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Registrering af tjeneste kunne ikke foretages</string>
<string name="dav_service_refresh_couldnt_refresh">Kunne opdatere liste over sæt</string>
@@ -52,13 +60,19 @@
<string name="app_settings_reset_hints">Nulstil vejledende popups</string>
<string name="app_settings_reset_hints_summary">Genaktiverer hjælp, som er blevet lukket tidligere</string>
<string name="app_settings_reset_hints_success">Al vejledning vil blive vist igen</string>
<string name="app_settings_connection">Forbindelse</string>
<string name="app_settings_override_proxy">Tilsidesæt proxyindstillinger</string>
<string name="app_settings_override_proxy_on">Brug brugerdefinerede proxyindstillinger</string>
<string name="app_settings_override_proxy_off">Brug systemstandard proxyindstillinger</string>
<string name="app_settings_override_proxy_host">HTTP proxy værtsnavn</string>
<string name="app_settings_override_proxy_port">HTTP proxy port</string>
<string name="app_settings_security">Sikkerhed</string>
<string name="app_settings_reset_trusted_certificates">Nulstil verificerede certifikater</string>
<string name="app_settings_reset_trusted_certificates_summary">Glemmer alle certifikater, der er blevet godkendt tidligere</string>
<plurals name="app_settings_reset_trusted_certificates_success">
<item quantity="one">Ikke-accepteret certifikat</item>
<item quantity="other">%d kke-accepterede certifikater</item>
</plurals>
<string name="app_settings_distrust_system_certs">Stol ikke på systemcertifikater</string>
<string name="app_settings_distrust_system_certs_on">System og brugertilføjede CA\'er vil ikke blive betroet</string>
<string name="app_settings_distrust_system_certs_off">System og brugertilføjede CA\'er vil blive betroet (anbefalet)</string>
<string name="app_settings_reset_certificates">Nulstil (ikke-)betroede certifikater</string>
<string name="app_settings_reset_certificates_summary">Nulstiller tilliden til brugerdefinerede certifikater</string>
<string name="app_settings_reset_certificates_success">Alle brugerdefinerede certifikater er blevet rydet</string>
<string name="app_settings_debug">Debugging</string>
<string name="app_settings_log_to_external_storage">Log til ekstern fil</string>
<string name="app_settings_log_to_external_storage_on">Logger til eksternt lager (hvis muligt)</string>
@@ -69,6 +83,9 @@
<string name="account_synchronize_now">Synkroniser nu</string>
<string name="account_synchronizing_now">Synkroniserer nu</string>
<string name="account_settings">Opsætning af konti</string>
<string name="account_rename">Omdøb konto</string>
<string name="account_rename_new_name">Lokaldata der ikke er gemt kan gå tabt. Eftersynkronisering er krævet efter omdøbning. Nyt kontonavn: </string>
<string name="account_rename_rename">Omdøb</string>
<string name="account_delete">Slet konto</string>
<string name="account_delete_confirmation_title">Ønsker du at slette konto?</string>
<string name="account_delete_confirmation_text">Alle lokale kopier af addessebøger, kalendere og opgavelister vil blive slettet.</string>
@@ -76,6 +93,17 @@
<string name="account_create_new_address_book">Opret ny adressebog</string>
<string name="account_refresh_calendar_list">Opdater kalenderliste</string>
<string name="account_create_new_calendar">Opret ny kalender</string>
<!--PermissionsActivity-->
<string name="permissions_title">DAVdroid adgangsrettigheder</string>
<string name="permissions_calendar">Kalenderadgange</string>
<string name="permissions_calendar_details">For at synkronisere CalDAV-begivenheder med dine lokale kalendere, skal DAVdroid have adgang til dine kalendere.</string>
<string name="permissions_calendar_request">Anmod om kalenderadgang</string>
<string name="permissions_contacts">Kontakter: Adgangsrettigheder</string>
<string name="permissions_contacts_details">DAVdroid er nødt til at have adgang til dine kontakter, hvis CardDAV-adressebøger skal kunne synkronisere med dine kontakter.</string>
<string name="permissions_contacts_request">Anmod om adgang til kontakter</string>
<string name="permissions_opentasks">OpenTasks: Adgangsrettigheder</string>
<string name="permissions_opentasks_details">DAVdroid er nødt til at have adgang til OpenTasks, hvis CalDAV-opgaver skal kunne synkronisere med dine lokale opgavelister.</string>
<string name="permissions_opentasks_request">Anmod om adgang til OpenTasks</string>
<!--AddAccountActivity-->
<string name="login_title">Tilføj konto</string>
<string name="login_type_email">Log ind med emailadresse</string>
@@ -89,12 +117,12 @@
<string name="login_user_name">Brugernavn</string>
<string name="login_user_name_required">Brugernavn påkrævet</string>
<string name="login_base_url">Basis-URL</string>
<string name="login_auth_preemptive">Forhåndsgodkendelse (preemptive authentication - anbefalet, men ikke kompatiblelt med Digest auth)</string>
<string name="login_login">Login</string>
<string name="login_back">Tilbage</string>
<string name="login_create_account">Opret konto</string>
<string name="login_account_name">Kontonavn</string>
<string name="login_account_name_info">Brug emailadressen som kontonavn, for Android vil bruge kontonavnet til ORGANIZER-feltet for aktiviteter, som du opretter. Du kan ikke have to konti med samme navn.</string>
<string name="login_account_contact_group_method">Gruppering af kontakter:</string>
<string name="login_account_name_required">Kontonavn påkrævet</string>
<string name="login_account_not_created">Konto kunne ikke oprettes</string>
<string name="login_configuration_detection">Check konfiguration</string>
@@ -109,9 +137,6 @@
<string name="settings_password">Adgangskode</string>
<string name="settings_password_summary">Opdater adgangskoden, så den svarer til din server.</string>
<string name="settings_enter_password">Indtast adgangskode:</string>
<string name="settings_preemptive">Forhåndsgodkendelse</string>
<string name="settings_preemptive_on">Loginoplysninger sendes ved hver anmodning (anbefalet)</string>
<string name="settings_preemptive_off">Loginoplysninger sendes efter server anmoder om dem</string>
<string name="settings_sync">Synkronisering</string>
<string name="settings_sync_interval_contacts">Synkroniseringsinterval for kontakter</string>
<string name="settings_sync_summary_manually">Kun manuelt</string>
@@ -139,6 +164,24 @@
<item>En gang hver 4. time</item>
<item>En gang om dagen</item>
</string-array>
<string name="settings_sync_wifi_only">Synkroniser kun over WiFi</string>
<string name="settings_sync_wifi_only_on">Synkronisering er begrænset til WiFi-forbindelser</string>
<string name="settings_sync_wifi_only_off">Forbindelsestypen har ingen betydning</string>
<string name="settings_sync_wifi_only_ssid">Begræsning til WiFi-SSID</string>
<string name="settings_sync_wifi_only_ssid_on">Vil kun blive synkroniseret over %s</string>
<string name="settings_sync_wifi_only_ssid_off">Alle WiFi-forbindelse vil kunne bruges</string>
<string name="settings_sync_wifi_only_ssid_message">Indtast navnet på et WiFi-netværk (SSID) for at begrænse synkronisering til dette netværk, eller efterlad feltet blank for at acceptere alle WiFi-forbindelser.</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Gruppering af kontakter</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>Grupper er særskilte VCards</item>
<item>Grupper er kategorier pr. kontakt</item>
</string-array>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Tidsafgrænsning for tidligere begivenheder</string>
<string name="settings_sync_time_range_past_none">Alle begivenheder vil blive synkroniseret</string>
<plurals name="settings_sync_time_range_past_days">
@@ -146,9 +189,9 @@
<item quantity="other">Begivenheder, der er mere end %d dage gamle, vil blive ignoreret</item>
</plurals>
<string name="settings_sync_time_range_past_message">Begivenheder, som er mere end dette antal dage gamle vil blive ignoreret (kan også være 0). Hvis feltet ikke er udfyldt, vil alle begivenheder blive synkroniseret.</string>
<string name="settings_version_update">Versionsopdatering af DAVdroid</string>
<string name="settings_version_update_settings_updated">Interne indstillinger er blevet opdateret.</string>
<string name="settings_version_update_install_hint">Problemer? Afinstaller DAVdroid og geninstaller.</string>
<string name="settings_manage_calendar_colors">Administrer farver for kalender</string>
<string name="settings_manage_calendar_colors_on">Kalenderfarver administreres af DAVdroid</string>
<string name="settings_manage_calendar_colors_off">Kalenderfarver sættes ikke fra DAVdroid</string>
<!--collection management-->
<string name="create_addressbook">Opret adressebog</string>
<string name="create_addressbook_display_name_hint">Min adressebog</string>
@@ -177,6 +220,8 @@
<string name="exception_show_details">Vis detaljer</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Debug-info</string>
<string name="sync_error_permissions">DAVdroid-rettigheder</string>
<string name="sync_error_permissions_text">Yderligere adgang påkrævet</string>
<string name="sync_error_calendar">Synkronisering af kalenderen lykkedes ikke (%s)</string>
<string name="sync_error_contacts">Synkronisering af adressebogen lykkedes ikke (%s)</string>
<string name="sync_error_tasks">Synkronisering af opgaver lykkedes ikke (%s)</string>
@@ -185,16 +230,20 @@
<string name="sync_error_local_storage">Databasefejl under %s</string>
<string-array name="sync_error_phases">
<item>forbereder synkronisering</item>
<item>checker funktioner</item>
<item>checker understøttelse</item>
<item>behandler poster, der er slettet lokalt</item>
<item>behandler poster, der er oprettet/ændret lokalt</item>
<item>uploader poster, der er oprettet/ændret lokalt</item>
<item>behandler poster, der er blevet oprettet/redigeret</item>
<item>uploader poster, der er blevet oprettet/redigeret</item>
<item>checker synkroniseringsstatus</item>
<item>oplister lokale poster</item>
<item>oplister poster på server</item>
<item>laver liste af lokale poster</item>
<item>laver lister af poster på server</item>
<item>sammenligner poster lokalt og på server</item>
<item>downloader poster på server</item>
<item>efterbehandler</item>
<item>gemmer synkroniseringsstatus</item>
</string-array>
<string name="sync_error_unauthorized">Fejl i brugernavn/adgangskode</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: Forbindelsessikkerhed</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid er stødt på et ukendt certifikat. Vil du stole på det? </string>
</resources>

View File

@@ -7,7 +7,11 @@
<string name="please_wait">Por favor, espere...</string>
<string name="send">Enviar</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Optimización de batería</string>
<string name="startup_battery_optimization_message">Android puede desactivar/reducir la sincronización de DAVdroid después de unos días. Para prevenir esto, desactiva la optimización.</string>
<string name="startup_battery_optimization_disable">Apagar para DAVdroid</string>
<string name="startup_dont_show_again">No mostrar de nuevo</string>
<string name="startup_development_version">Versión candidata final de DAVdroid</string>
<string name="startup_development_version_message">Esta es una versión de desarrollo de DAVdroid. Tenga presente que puede que no todo funcione como espera. Por favor, denos una retroalimentación constructiva para mejorar DAVdroid.</string>
<string name="startup_development_version_give_feedback">Dar retroalimentación</string>
<string name="startup_donate">Información de código abierto</string>
@@ -18,9 +22,12 @@
<string name="startup_google_play_accounts_removed_message">Bajo ciertas condiciones, el DRM de Play Store puede causar que todas las cuentas de DAVdroid se desconfiguren tras un reinicio o una actualización de DAVdroid. Si esto le afecta (y sólo en ese caso), por favor, instale \"DAVdroid JB Workaround\" desde Play Store.</string>
<string name="startup_google_play_accounts_removed_more_info">Más información</string>
<string name="startup_opentasks_not_installed">OpenTasks no está instalado</string>
<string name="startup_opentasks_not_installed_message">La aplicación OpenTasks no está instalada o accesible, por lo que DAVdroid no podrá sincronizar las listas de tareas. Reinstale DAVdroid y añada sus cuentas de nuevo después de instalar OpenTasks.</string>
<string name="startup_opentasks_not_installed_message">La aplicación OpenTasks no está disponible. DAVdroid no podrá sincronizar listas de tareas.</string>
<string name="startup_opentasks_reinstall_davdroid">Tras instalar OpenTasks, tendrás que re-instalar DAVdroid y añadir tus cuentas de nuevo (por un error de Android).</string>
<string name="startup_opentasks_not_installed_install">Instalar OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Términos de la licencia</string>
<string name="about_license_info_no_warranty">Este programa viene sin NINGÚN TIPO DE GARANTÍA. Es software libre, y cualquier contribución es bienvenida y redistribuida bajo ciertas condiciones.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">Archivo de registro de DAVdroid</string>
<string name="logging_to_external_storage">Registrar en almacenamiento externo: %s</string>
@@ -28,34 +35,71 @@
<string name="logging_couldnt_create_file">No se puede crear el archivo de registro externo: %s</string>
<string name="logging_no_external_storage">Almacenamiento externo no encontrado</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">Abrir panel de navegación</string>
<string name="navigation_drawer_close">Cerrar panel de navegación</string>
<string name="navigation_drawer_subtitle">Adaptador de sincronización CalDAV/CardDAV</string>
<string name="navigation_drawer_about">Acerca de / Licencia</string>
<string name="navigation_drawer_settings">Ajustes</string>
<string name="navigation_drawer_news_updates">Noticias y actualizaciones</string>
<string name="navigation_drawer_external_links">Enlaces externos</string>
<string name="navigation_drawer_website">Sitio web</string>
<string name="navigation_drawer_faq">Preguntas frequentes</string>
<string name="navigation_drawer_forums">Comunidad</string>
<string name="navigation_drawer_donate">Donar</string>
<string name="account_list_empty">Bienvenido a DAVdroid!\n\nAhora puede añadir una cuenta CalDAV/CardDAV.</string>
<string name="account_list_empty">Bienvenido a DAVdroid!\n\nAhora puedes añadir una cuenta CalDAV/CardDAV.</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Falló la detección del servicio</string>
<string name="dav_service_refresh_couldnt_refresh">No se pudo refrescar lista de colección</string>
<!--AppSettingsActivity-->
<string name="app_settings">Ajustes</string>
<string name="app_settings_user_interface">Interfaz de usuario</string>
<string name="app_settings_reset_hints">Restablecer advertencias</string>
<string name="app_settings_reset_hints_summary">Habilita las advertencias que han sido rechazadas con anterioridad</string>
<string name="app_settings_reset_hints_success">Todas las advertencias se mostrarán nuevamente</string>
<string name="app_settings_connection">Conexión</string>
<string name="app_settings_override_proxy">Anular ajustes del proxy</string>
<string name="app_settings_override_proxy_on">Usar ajustes personalizados del proxy</string>
<string name="app_settings_override_proxy_off">Usar ajustes del proxy predefinidos por el sistema</string>
<string name="app_settings_override_proxy_host">Nombre del host del proxy HTTP</string>
<string name="app_settings_override_proxy_port">Puerto del proxy HTTP</string>
<string name="app_settings_security">Seguridad</string>
<string name="app_settings_reset_trusted_certificates">Restablecer certificados de confianza</string>
<string name="app_settings_reset_trusted_certificates_summary">Olvidar todos los certificados aceptados previamente</string>
<string name="app_settings_distrust_system_certs">Invalidar los certificados del sistema</string>
<string name="app_settings_distrust_system_certs_on">Los CA del sistema y los añadidos por el usuario no serán válidos</string>
<string name="app_settings_distrust_system_certs_off">Los CA del sistema y los añadidos por el usuario serán usados y de confianza (recomendado)</string>
<string name="app_settings_reset_certificates">Reiniciar certificados (in)validados</string>
<string name="app_settings_reset_certificates_summary">Reinicia la validez de todos los certificados particulares</string>
<string name="app_settings_reset_certificates_success">Todos los certificados particulares han sido limpiados</string>
<string name="app_settings_debug">Depuración</string>
<string name="app_settings_log_to_external_storage">Registrar en fichero externo</string>
<string name="app_settings_log_to_external_storage_on">Registro en almacenamiento externo (si está disponible)</string>
<string name="app_settings_log_to_external_storage_off">El archivo de registro externo está deshabilitado</string>
<string name="app_settings_show_debug_info">Mostrar la información de depuración</string>
<string name="app_settings_show_debug_info_details">Ver/compartir detalles de software y configuración</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Sincronizar ahora</string>
<string name="account_synchronizing_now">Sincronizando...</string>
<string name="account_settings">Ajustes de cuenta</string>
<string name="account_rename">Renombrar cuenta</string>
<string name="account_rename_new_name">Información local no guardada puede ser desechada. Se requiere resincronizar después de renombrar. Nuevo nombre de cuenta:</string>
<string name="account_rename_rename">Renombrar</string>
<string name="account_delete">Eliminar cuenta</string>
<string name="account_delete_confirmation_title">¿Seguro que desea eliminar la cuenta?</string>
<string name="account_delete_confirmation_text">Todas las copias locales de sus contactos, calendarios y tareas serán eliminadas.</string>
<string name="account_delete_confirmation_title">¿Seguro que deseas eliminar la cuenta?</string>
<string name="account_delete_confirmation_text">Todas las copias locales de tus contactos, calendarios y tareas serán eliminadas.</string>
<string name="account_refresh_address_book_list">Refrescar contactos</string>
<string name="account_create_new_address_book">Crear nueva lista de contactos</string>
<string name="account_refresh_calendar_list">Refrescar calendario</string>
<string name="account_create_new_calendar">Crear nuevo calendario</string>
<!--PermissionsActivity-->
<string name="permissions_title">Permisos de DAVdroid</string>
<string name="permissions_calendar">Permisos de calendario</string>
<string name="permissions_calendar_details">Para sincronizar eventos CalDAV con tus calendarios locales, DAVdroid necesita acceder a los mismos.</string>
<string name="permissions_calendar_request">Solicitar permisos sobre calendario</string>
<string name="permissions_contacts">Permisos de contactos</string>
<string name="permissions_contacts_details">Para sincronizar libretas de contactos CadDAV con tus contactos locales, DAVdroid necesita acceder a los mismos.</string>
<string name="permissions_contacts_request">Solicitar permisos sobre contactos</string>
<string name="permissions_opentasks">Permisos de OpenTasks</string>
<string name="permissions_opentasks_details">Para sincronizar listas de tareas CalDAV con tus listas de tareas locales, DAVdroid necesita acceder a OpenTasks.</string>
<string name="permissions_opentasks_request">Solicitar permisos sobre OpenTasks</string>
<!--AddAccountActivity-->
<string name="login_title">Añadir cuenta</string>
<string name="login_type_email">Acceder con cuenta de correo</string>
@@ -69,17 +113,17 @@
<string name="login_user_name">Nombre de usuario</string>
<string name="login_user_name_required">Nombre de usuario requerido</string>
<string name="login_base_url">URL base</string>
<string name="login_auth_preemptive">Autenticación preferente (recomendado, pero incompatible con Digest auth)</string>
<string name="login_login">Registrar</string>
<string name="login_back">Volver</string>
<string name="login_create_account">Crear cuenta</string>
<string name="login_account_name">Nombre de cuenta</string>
<string name="login_account_name_info">Use su dirección de correo como nombre de su cuenta puesto que Android usará el nombre de la cuenta como campo de \"organizador\" en los eventos que cree. No puede tener dos cuentas con el mismo nombre.</string>
<string name="login_account_name_info">Usa tu dirección de correo como nombre de cuenta puesto que Android usará el nombre de la cuenta como campo de \"organizador\" en los eventos que cree. No puedes tener dos cuentas con el mismo nombre.</string>
<string name="login_account_contact_group_method">Método de contacto de grupo:</string>
<string name="login_account_name_required">Nombre de cuenta requerido</string>
<string name="login_account_not_created">La cuenta no pudo ser creada</string>
<string name="login_configuration_detection">Detectar configuración</string>
<string name="login_querying_server">Por favor espere, consultando al servidor...</string>
<string name="login_no_caldav_carddav">No se pudo encontrar servicio CalDAV o CardDAV.</string>
<string name="login_querying_server">Por favor espera, consultando al servidor...</string>
<string name="login_no_caldav_carddav">No se pudo encontrar el servicio CalDAV o CardDAV.</string>
<string name="login_view_logs">Ver registros</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Ajustes: %s</string>
@@ -89,9 +133,6 @@
<string name="settings_password">Contraseña</string>
<string name="settings_password_summary">Actualiza la contraseña de acuerdo a tu servidor.</string>
<string name="settings_enter_password">Introduce tu contraseña:</string>
<string name="settings_preemptive">Autenticación preferente</string>
<string name="settings_preemptive_on">Las credenciales se envían en cada petición (recomendado)</string>
<string name="settings_preemptive_off">Las credenciales se envían después de que el servidor las pida</string>
<string name="settings_sync">Sincronización</string>
<string name="settings_sync_interval_contacts">Intervalo de sincronización de contactos</string>
<string name="settings_sync_summary_manually">Solo manualmente</string>
@@ -119,16 +160,34 @@
<item>Cada 4 horas</item>
<item>Una vez al día</item>
</string-array>
<string name="settings_sync_wifi_only">Sincronizar sólo sobre WiFi</string>
<string name="settings_sync_wifi_only_on">La sincronización está restringida a conexiones WiFi</string>
<string name="settings_sync_wifi_only_off">Tipo de conexión no tenido en cuenta</string>
<string name="settings_sync_wifi_only_ssid">Restricción WiFi SSID</string>
<string name="settings_sync_wifi_only_ssid_on">Sólo se sincronizará sobre %s</string>
<string name="settings_sync_wifi_only_ssid_off">Todas las conexiones WiFi pueden ser usadas</string>
<string name="settings_sync_wifi_only_ssid_message">Introduce el nombre de una red WiFi (SSID) para restringir la sincronización a esta red, o deja el campo en blanco para usar todas las conexiones WiFi.</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Método de contacto de grupo</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>Los groups tienen VCards separadas</item>
<item>Los groups tienen una categoría por contacto</item>
</string-array>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Límite de tiempo de eventos pasados</string>
<string name="settings_sync_time_range_past_none">Todos los eventos serán sincronizados</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Los eventos anteriores a un día serán ignorados</item>
<item quantity="other">Los eventos anteriores a %d días serán ignorados</item>
</plurals>
<string name="settings_sync_time_range_past_message">Los eventos anteriores a este número de días serán ignorados (puede ser 0). Deje en blanco el campo para sincronizar todos los eventos.</string>
<string name="settings_version_update">Actualización de la versión de Android</string>
<string name="settings_version_update_settings_updated">Los ajustes internos han sido actualizados.</string>
<string name="settings_version_update_install_hint">¿Problemas? Desinstala y vuelve a instalar DAVdroid.</string>
<string name="settings_sync_time_range_past_message">Los eventos anteriores a este número de días serán ignorados (puede ser 0). Deja en blanco el campo para sincronizar todos los eventos.</string>
<string name="settings_manage_calendar_colors">Colores de calendario</string>
<string name="settings_manage_calendar_colors_on">Los colores de los calendarios son administrados por DAVdroid</string>
<string name="settings_manage_calendar_colors_off">Los colores de los calendarios no son establecidos por DAVdroid</string>
<!--collection management-->
<string name="create_addressbook">Crear nueva lista de contactos</string>
<string name="create_addressbook_display_name_hint">Agendas</string>
@@ -144,9 +203,10 @@
<string name="create_collection_display_name">Nombre mostrado (título):</string>
<string name="create_collection_display_name_required">Título requerido</string>
<string name="create_collection_description">Descripción (opcional):</string>
<string name="create_collection_home_set">Establecer localización:</string>
<string name="create_collection_create">Crear</string>
<string name="delete_collection">Eliminar colección</string>
<string name="delete_collection_confirm_title">¿Está seguro/a?</string>
<string name="delete_collection_confirm_title">¿Estás seguro/a?</string>
<string name="delete_collection_confirm_warning">Esta colección (%s) y toda su información será eliminada del servidor.</string>
<string name="delete_collection_deleting_collection">Eliminando colección</string>
<!--ExceptionInfoFragment-->
@@ -156,6 +216,8 @@
<string name="exception_show_details">Mostrar detalles</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Información de depuración</string>
<string name="sync_error_permissions">Permisos de DAVdroid</string>
<string name="sync_error_permissions_text">Permisos adicionales requeridos</string>
<string name="sync_error_calendar">La sincronización de calendario falló (%s)</string>
<string name="sync_error_contacts">La sincronización de agenda falló (%s)</string>
<string name="sync_error_tasks">La sincronización de tareas falló (%s)</string>
@@ -163,17 +225,21 @@
<string name="sync_error_http_dav">Error de servidor al %s</string>
<string name="sync_error_local_storage">Error de base de datos al %s</string>
<string-array name="sync_error_phases">
<item>preparar sincronización</item>
<item>consultar capacidades</item>
<item>procesar entradas borradas localmente</item>
<item>preparar entradas creadas/modificadas</item>
<item>cargar entradas creadas/modificadas</item>
<item>comprobar estado de sincronización</item>
<item>listar entradas locales</item>
<item>listar entradas remotas</item>
<item>comparar entradas locaesl/remotas</item>
<item>descargar entradas remotas</item>
<item>guardar estado de sincronización</item>
<item>preparando sincronización</item>
<item>buscando capacidades</item>
<item>procesando entradas borradas localmente</item>
<item>preparando entradas creadas/modificadas</item>
<item>cargando entradas creadas/modificadas</item>
<item>comprobando estado de sincronización</item>
<item>enumerando entradas locales</item>
<item>enumerando entradas remotas</item>
<item>comparando entradas locales/remotas</item>
<item>descargando entradas remotas</item>
<item>post-procesando</item>
<item>guardando estado de sincronización</item>
</string-array>
<string name="sync_error_unauthorized">Nombre de usuario/contraseña erróneo</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: Seguridad de conexión</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid ha encontrado un certificado desconocido. ¿Quieres que sea válido?</string>
</resources>

View File

@@ -0,0 +1,250 @@
<?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">SVP attendez ...</string>
<string name="send">Envoyer</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Optimisation de la batterie</string>
<string name="startup_battery_optimization_message">Android peut désactiver/réduire la synchronisation de DAVdroid après quelques jours. Pour éviter cela, désactivez l\'optimisation de la batterie.</string>
<string name="startup_battery_optimization_disable">Désactiver pour DAVdroid</string>
<string name="startup_dont_show_again">Ne plus afficher</string>
<string name="startup_development_version">Pré-version de DAVdroid</string>
<string name="startup_development_version_message">Il s\'agit d\'une version de développement de DAVdroid. Il se peut que les choses ne fonctionnent pas comme prévu. Sil vous plaît faites-nous un retour pour améliorer DAVdroid.</string>
<string name="startup_development_version_give_feedback">Faire un commentaire</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_google_play_accounts_removed_more_info">Plus d\'informations</string>
<string name="startup_opentasks_not_installed">OpenTasks n\'est pas installé</string>
<string name="startup_opentasks_not_installed_message">L\'application OpenTasks n\'est pas disponible, donc DAVdroid ne pourra pas synchroniser des listes de tâches.</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_terms">Conditions d\'utilisation</string>
<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_to_external_storage_warning">Supprimer les journaux dès que possible!</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é &amp; mise à jour</string>
<string name="navigation_drawer_external_links">Liens externes</string>
<string name="navigation_drawer_website">Site Web</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_forums">Communauté</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/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_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>
<!--PermissionsActivity-->
<string name="permissions_title">Autorisations DAVdroid</string>
<string name="permissions_calendar">Autorisations calendrier</string>
<string name="permissions_calendar_details">Pour synchroniser les événements CalDAV avec vos calendriers locaux, DAVdroid a besoin d\'accéder à vos calendriers.</string>
<string name="permissions_calendar_request">Demande d\'autorisations d\'accéder au calendrier</string>
<string name="permissions_contacts">Autorisations contacts</string>
<string name="permissions_contacts_details">Pour synchroniser les carnets d\'adresses de CardDAV avec votre carnet d\'adresses local, DAVdroid a besoin d\'accéder à vos contacts.</string>
<string name="permissions_contacts_request">Demande d\'autorisations d\'accéder aux contacts</string>
<string name="permissions_opentasks">Autorisations OpenTasks</string>
<string name="permissions_opentasks_details">Pour synchroniser les tâches de CalDAV avec vos listes de tâches locales, DAVdroid a besoin d\'accéder à OpenTasks.</string>
<string name="permissions_opentasks_request">Demande d\'autorisations d\'accéder à OpenTasks</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://davdroid.bitfire.at/configuration/?pk_campaign=davdroid-app</string>
<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">S\'il vous plaît patienter, nous interrogeons le serveur ...</string>
<string name="login_no_caldav_carddav">Aucun service CalDAV ou CardDAV trouvé.</string>
<string name="login_view_logs">Voir infos de débogage</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_summary_not_available">Indisponible</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_seconds">
<item>-1</item>
<item>300</item>
<item>600</item>
<item>900</item>
<item>3600</item>
<item>7200</item>
<item>14400</item>
<item>86400</item>
</string-array>
<string-array name="settings_sync_interval_names">
<item>Manuellement</item>
<item>Toutes les 5 minutes</item>
<item>Toutes les 10 minutes</item>
<item>Toutes les 15 minutes</item>
<item>Toutes les heures</item>
<item>Toutes les 2 heures</item>
<item>Toutes les 4 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_ssid">Restriction WiFi SSID</string>
<string name="settings_sync_wifi_only_ssid_on">Sera seulement synchroniser avec %s</string>
<string name="settings_sync_wifi_only_ssid_off">Toutes les connexions WiFi peuvent être utilisées</string>
<string name="settings_sync_wifi_only_ssid_message">Entrez le nom d\'un réseau WiFi (SSID) pour restreindre la synchronisation à ce réseau, ou laisser vide pour autoriser toutes les connexions WiFi.</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>
<!--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 errors 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>
<string name="sync_error_calendar">Échec de la synchronisation du calendrier (%s)</string>
<string name="sync_error_contacts">Échec de la synchronisation du carnet d\'adresse (%s)</string>
<string name="sync_error_tasks">Échec de la synchronisation (%s)</string>
<string name="sync_error">Erreur durant %s</string>
<string name="sync_error_http_dav">Erreur de serveur durant %s</string>
<string name="sync_error_local_storage">Erreur de base de donnée durant %s</string>
<string-array name="sync_error_phases">
<item>prépare la synchronisation</item>
<item>demande les autorisations</item>
<item>procède à la suppression des entrées locales</item>
<item>prépare les entrées créées/modifiées</item>
<item>envoi les entrées créées/modifiées</item>
<item>vérifie l\'état de la synchronisation</item>
<item>liste les entrées locales</item>
<item>liste les entrées distantes</item>
<item>compare les entrées locales/distantes</item>
<item>télécharge les entrées distantes</item>
<item>post-traitement</item>
<item>enregistre l\'état de la synchronisation</item>
</string-array>
<string name="sync_error_unauthorized">Nom d\'utilisateur ou mot de passe erroné</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

@@ -2,11 +2,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">DAVdroid címjegyzék</string>
<string name="address_books_authority_title">Címjegyzékek</string>
<string name="help">Súgó</string>
<string name="manage_accounts">Fiókok kezelése</string>
<string name="please_wait">Kérjük, várjon ...</string>
<string name="send">Küldés</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Akkumulátoroptimalizálás </string>
<string name="startup_battery_optimization_message">Az operációs rendszer a DAVdroid szinkronizálást pár nap után leállíthatja vagy visszafoghatja. Ennek elkerülésére kapcsolja ki az akkumulátoroptimalizálást.</string>
<string name="startup_battery_optimization_disable">Kikapcsolás a DAVdroid kapcsán</string>
<string name="startup_dont_show_again">Ne jelenjen meg többet</string>
<string name="startup_development_version">DAVdroid előzetes kiadás</string>
<string name="startup_development_version_message">Ez a DAVdroid egy fejlesztői verziója. Elképzelhető, hogy nem minden működik úgy, ahogyan kellene. Ha így lenne, kérjük küldjön a tapasztaltakról visszajelzést.</string>
@@ -19,7 +24,8 @@
<string name="startup_google_play_accounts_removed_message">Bizonyos körülmények között a Play Áruház DRM okozhatja azt, hogy az eszköz újraindítását vagy a DAVdroid frissítését követően a DAVdroid fiókok eltűnnek. Amennyiben (és csak amennyiben) érinti Önt ez a probléma, telepítse a \"DAVdroid JB Workaround\" alkalmazást Play Áruházból.</string>
<string name="startup_google_play_accounts_removed_more_info">További információk</string>
<string name="startup_opentasks_not_installed">Az OpenTasks nincs telepítve</string>
<string name="startup_opentasks_not_installed_message">Az OpenTasks alkalmazás nincs telepítve vagy nem elérhető, ezért a DAVdroid nem lesz képes a feladatlisták szinkronizálására. Az OpenTasks telepítése után telepítse újra a DAVdroid-ot majd hozza létre a fiókot újra.</string>
<string name="startup_opentasks_not_installed_message">Az OpenTasks alkalmazás nincs telepítve, így a DAVdroid nem lesz képes szinkronizálni feladatlistákat.</string>
<string name="startup_opentasks_reinstall_davdroid">Az OpenTasks telepítését követően újra kell telepíteni a DAVdroit alkalmazást és újra fel kell venni a fiókokat (Android hiba).</string>
<string name="startup_opentasks_not_installed_install">Az OpenTasks telepítése</string>
<!--AboutActivity-->
<string name="about_license_terms">Licencfeltételek</string>
@@ -43,6 +49,8 @@
<string name="navigation_drawer_forums">Közösség</string>
<string name="navigation_drawer_donate">Támogatás</string>
<string name="account_list_empty">Üdvözöljük a DAVdroid felhasználók között!\n\nMost már felvehet CalDAV/CardDav fiókokat.</string>
<string name="accounts_global_sync_disabled">A rendszerszintű automatikus szinkronizálás ki van kapcsolva</string>
<string name="accounts_global_sync_enable">Bekapcsolás</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Szolgáltatások felderítése nem sikerült</string>
<string name="dav_service_refresh_couldnt_refresh">Gyűjteménylista frissítése nem sikerült</string>
@@ -52,13 +60,19 @@
<string name="app_settings_reset_hints">Tippek visszaállítása</string>
<string name="app_settings_reset_hints_summary">Újra jelenjen meg az összes tipp</string>
<string name="app_settings_reset_hints_success">Az összes tipp újra meg fog jelenni</string>
<string name="app_settings_connection">Kapcsolat</string>
<string name="app_settings_override_proxy">Proxybeállítások felülírása</string>
<string name="app_settings_override_proxy_on">Egyedi proxybeállítások</string>
<string name="app_settings_override_proxy_off">Az alapértelmezett proxybeállítás használata</string>
<string name="app_settings_override_proxy_host">HTTP proxyállomás neve</string>
<string name="app_settings_override_proxy_port">HTTP proxy port</string>
<string name="app_settings_security">Biztonság</string>
<string name="app_settings_reset_trusted_certificates">A tanúsítványtár visszaállítása</string>
<string name="app_settings_reset_trusted_certificates_summary">Az összes korábban megbízhatónak kiválasztott tanúsítvány törlése</string>
<plurals name="app_settings_reset_trusted_certificates_success">
<item quantity="one">Egy tanúsítvány megbízhatósága törölve</item>
<item quantity="other">%d tanúsítvány megbízhatósága törölve</item>
</plurals>
<string name="app_settings_distrust_system_certs">A rendszertanúsítványok elfogadása</string>
<string name="app_settings_distrust_system_certs_on">A rendszer által kezelt, előre vagy felhasználó által telepített tanúsítványok figyelmen kívül lesznek hagyva</string>
<string name="app_settings_distrust_system_certs_off">A rendszer által kezelt, előre vagy felhasználó által telepített tanúsítványok megbízhatóak (javasolt)</string>
<string name="app_settings_reset_certificates">A tanúsítványok megbízhatóságának törlésére</string>
<string name="app_settings_reset_certificates_summary">A tanúsítványok megbízhatóságával kapcsolatos beállítások törlésére</string>
<string name="app_settings_reset_certificates_success">A tanúsítványok megbízhatóságával kapcsolatos beállítások törölve</string>
<string name="app_settings_debug">Hibakeresés</string>
<string name="app_settings_log_to_external_storage">Naplózás fájlba</string>
<string name="app_settings_log_to_external_storage_on">Naplózás külső tárhelyre (ha elérhető)</string>
@@ -69,6 +83,9 @@
<string name="account_synchronize_now">Szinkronizálás most</string>
<string name="account_synchronizing_now">Szinkronizálás</string>
<string name="account_settings">Fiókbeállítások</string>
<string name="account_rename">Fiók átnevezése</string>
<string name="account_rename_new_name">Az elmentetlen helyben tárolt adatok elvesznek. Az átnevezés után szinkronizálásra lesz szükség. Új fióknév:</string>
<string name="account_rename_rename">Átnevez</string>
<string name="account_delete">Fiók törlése</string>
<string name="account_delete_confirmation_title">Valóban törölni akarja a fiókot?</string>
<string name="account_delete_confirmation_text">Az összes címjegyzék, naptár és feladatlista helyi példányai törölve lesznek.</string>
@@ -76,6 +93,17 @@
<string name="account_create_new_address_book">Új címjegyzék létrehozása</string>
<string name="account_refresh_calendar_list">Naptárlista frissítése</string>
<string name="account_create_new_calendar">Új naptár létrehozása</string>
<!--PermissionsActivity-->
<string name="permissions_title">DAVdroid engedélyek </string>
<string name="permissions_calendar">Naptárengedély</string>
<string name="permissions_calendar_details">A CalDAV naptárak és a helyi naptárak szinkronizálásához a DAVdroid naptárhozzáférést igényel.</string>
<string name="permissions_calendar_request">Naptárhozzáférés igénylése</string>
<string name="permissions_contacts">Névjegyengedélyek</string>
<string name="permissions_contacts_details">A CardDAV címlisták és a helyi címlisták szinkronizálásához a névjegyhozzáférést igényel.</string>
<string name="permissions_contacts_request">Névjegyengedélyek igénylése</string>
<string name="permissions_opentasks">OpenTasks engedélyek</string>
<string name="permissions_opentasks_details">A CalDAV feladatlisták és a helyi feladatlisták szinkronizálásához a DAVdroid OpenTasks hozzáférést igényel.</string>
<string name="permissions_opentasks_request">OpenTasks engedélyek igénylése</string>
<!--AddAccountActivity-->
<string name="login_title">Fiók hozzáadása</string>
<string name="login_type_email">Bejelentkezés email cím segítségével</string>
@@ -89,12 +117,12 @@
<string name="login_user_name">Felhasználónév</string>
<string name="login_user_name_required">A felhasználónév megadása feltétlenül szükséges</string>
<string name="login_base_url">URL-törzs</string>
<string name="login_auth_preemptive">Preemptív authentikáció (ajánlott, de Digest authentikációval nem működik)</string>
<string name="login_login">Bejelentkezés</string>
<string name="login_back">Vissza</string>
<string name="login_create_account">Fiók létrehozása</string>
<string name="login_account_name">A fiók neve</string>
<string name="login_account_name_info">Használja az email címet fióknévként, mert később a létrehozandó események szervezőjeként (ORGANIZER mező) az Android ezt fogja használni. Két fiókot nem lehet azonos néven létrehozni.</string>
<string name="login_account_contact_group_method">A csoportok kezelésének módja:</string>
<string name="login_account_name_required">A fióknév megadása feltétlenül szükséges</string>
<string name="login_account_not_created">A fiók létrehozása nem sikerült</string>
<string name="login_configuration_detection">A konfiguráció felderítése</string>
@@ -109,9 +137,6 @@
<string name="settings_password">Jelszó</string>
<string name="settings_password_summary">Adja meg a szerveren érvényes új jelszót.</string>
<string name="settings_enter_password">Adja meg a jelszót:</string>
<string name="settings_preemptive">Preemptív authentikáció</string>
<string name="settings_preemptive_on">A hitelesítő adatok elküldése minden kérésnél (ajánlott)</string>
<string name="settings_preemptive_off">A hitelesítő adatok elküldése, csak ha a szerver azt igényli</string>
<string name="settings_sync">Szinkronizálás</string>
<string name="settings_sync_interval_contacts">Névjegyszinkronizálás sűrűsége</string>
<string name="settings_sync_summary_manually">Manuális</string>
@@ -139,6 +164,24 @@
<item>4 óránként</item>
<item>Naponta</item>
</string-array>
<string name="settings_sync_wifi_only">Szinkronizálás csak WIFI-n</string>
<string name="settings_sync_wifi_only_on">Csak WIFI kapcsolat keresztül történjen szinkronizálás</string>
<string name="settings_sync_wifi_only_off">Szinkronizálás a kapcsolat típusától függetlenül</string>
<string name="settings_sync_wifi_only_ssid">WIFI SSID szűkítés</string>
<string name="settings_sync_wifi_only_ssid_on">Szinkronizálás csak a(z) %s hálózatra kapcsolódva</string>
<string name="settings_sync_wifi_only_ssid_off">Szinkronizálás bármely WIFI hálózaton</string>
<string name="settings_sync_wifi_only_ssid_message">Adja meg a WIFI hálózat nevét (SSID) a szinkronizálás egy hálózatra való korlátozához, vagy hagyja üresen, ha nem akar ilyen szűkítést.</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">A csoportok kezelésének módja</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>Minden csoport egy különálló VCard</item>
<item>A csoportok a kapcsolatonkéni kategóriák</item>
</string-array>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Múltbéli események időkorlátja</string>
<string name="settings_sync_time_range_past_none">Minden esemény szinkronizálása</string>
<plurals name="settings_sync_time_range_past_days">
@@ -146,9 +189,9 @@
<item quantity="other">A %d napnál régebbi események figyelmen kívül hagyása</item>
</plurals>
<string name="settings_sync_time_range_past_message">Az ennyi napnál (lehet 0) régebbi események figyelmen kívül lesznek hagyva. Hagyja üresen, ha minden múltbéli eseményt szinkronizálni akar.</string>
<string name="settings_version_update">DAVdroid frissítése</string>
<string name="settings_version_update_settings_updated">A belső beállítások frissítve lettek.</string>
<string name="settings_version_update_install_hint">Probléma? Próbálja meg törölni majd újratelepíteni a DAVdroid-ot.</string>
<string name="settings_manage_calendar_colors">Naptárszínek kezelése</string>
<string name="settings_manage_calendar_colors_on">A naptárszíneket a DAVdroid kezeli</string>
<string name="settings_manage_calendar_colors_off">A naptárszíneket nem a DAVdroid kezeli</string>
<!--collection management-->
<string name="create_addressbook">Címjegyzék létrehozása</string>
<string name="create_addressbook_display_name_hint">Új címjegyzék</string>
@@ -177,6 +220,8 @@
<string name="exception_show_details">Részletek megjelenítése</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Hibakeresési információ</string>
<string name="sync_error_permissions">DAVdroid engedélyek </string>
<string name="sync_error_permissions_text">További engedélyek szükségesek</string>
<string name="sync_error_calendar">A naptár szinkronizálása nem sikerült (%s)</string>
<string name="sync_error_contacts">A címjegyzék szinkronizálása nem sikerült (%s)</string>
<string name="sync_error_tasks">A feladatok szinkronizálása nem sikerült (%s)</string>
@@ -184,17 +229,21 @@
<string name="sync_error_http_dav">Szerver oldali hiba az alábbi művelet közben: %s</string>
<string name="sync_error_local_storage">Adatbázishiba az alábbi művelet közben: %s</string>
<string-array name="sync_error_phases">
<item>felkészülés a szinkronizálásra</item>
<item>szolgáltatások keresése</item>
<item>szinkronizáció előkészítése </item>
<item>szerver képességeinek lekérdezése</item>
<item>a helyben törölt bejegyzések feldolgozása</item>
<item>az új vagy módosított bejegyzések összegyűjtése</item>
<item>az új vagy módosított bejegyzések gyűjtése </item>
<item>az új vagy módosított bejegyzések feltöltése</item>
<item>a szinkronizációs állapot ellenőrzése</item>
<item>szinkronizációs állapot ellenőrzése</item>
<item>helyi bejegyzések listázása</item>
<item>távoli bejegyzések listázása</item>
<item>helyi és távoli bejegyzések összehasonlítása</item>
<item>távoli bejegyzések letöltése</item>
<item>utófeldolgozás</item>
<item>szinkronizációs állapot mentése</item>
</string-array>
<string name="sync_error_unauthorized">A felhasználónév vagy jelszó hibás</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: kapcsolatbiztonság</string>
<string name="trust_certificate_unknown_certificate_found">Egy eddig ismeretlen tanúsítvány érkezett. Megbízhatónak kívánja elfogadni?</string>
</resources>

View File

@@ -0,0 +1,249 @@
<?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">Rubrica degli indirizzi DAVdroid</string>
<string name="address_books_authority_title">Rubriche degli indirizzi</string>
<string name="help">Aiuto</string>
<string name="manage_accounts">Gestione account</string>
<string name="please_wait">Attendere prego …</string>
<string name="send">Invia</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Ottimizazione della batteria</string>
<string name="startup_battery_optimization_message">Android può ridurre o disabilitare la sincronizzazione di DAVdroid dopo alcuni giorni. Per prevenire questo comportamento disabilita l\'ottimizzazione della batteria</string>
<string name="startup_battery_optimization_disable">Disabilita per DAVdroid</string>
<string name="startup_dont_show_again">Non mostrare più</string>
<string name="startup_development_version">Versione di sviluppo di DAVdroid</string>
<string name="startup_development_version_message">Questa è una versione di sviluppo di DAVdroid. Attenzione perché potrebbe avere malfunzionamenti. Inviateci consigli utili per migliorarlo.</string>
<string name="startup_development_version_give_feedback">Invia un rapporto</string>
<string name="startup_donate">Informazioni sull\'Open-Source</string>
<string name="startup_donate_message">Siamo soddisfatti del tuo uso di DAVdroid, programma Open-Source (GPLv3). Poiché lo sviluppo è un\'iniziativa complessa che comporta molte ore di lavoro ti invitiamo a fare una donazione.</string>
<string name="startup_donate_now">Mostra pagina donazioni</string>
<string name="startup_donate_later">Più tardi</string>
<string name="startup_google_play_accounts_removed">Informazioni sul bug del DRM di Play Store</string>
<string name="startup_google_play_accounts_removed_message">In alcune condizioni il DRM di Play Store può causare la perdita di tutti gli account DAVdroid dopo un riavvio o dopo un aggiornamento di DAVdroid. Se verificate questo problema installate successivamente \"DAVdroid JB Workaround\" da Play Store.</string>
<string name="startup_google_play_accounts_removed_more_info">Maggiori informazioni</string>
<string name="startup_opentasks_not_installed">OpenTasks non installata</string>
<string name="startup_opentasks_not_installed_message">L\'applicazione OpenTasks non è installata: di conseguenza DAVdroid non potrà sincronizzare l\'elenco delle attività.</string>
<string name="startup_opentasks_reinstall_davdroid">Dopo l\'installazione di OpenTasks è necessario INSTALLARE NUOVAMENTE DAVdroid e aggiungere ancora gli account (per un bug di Android).</string>
<string name="startup_opentasks_not_installed_install">Installa OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Termini di licenza</string>
<string name="about_license_info_no_warranty">Il programma è distribuito SENZA ALCUNA GARANZIA. È software libero e può essere redistribuito sotto alcune condizioni.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">Invio del log di DAVdroid su file</string>
<string name="logging_to_external_storage">Log su dispositivo esterno: %s</string>
<string name="logging_to_external_storage_warning">Cancellare prima possibile i file di log!</string>
<string name="logging_couldnt_create_file">Non riesco a creare il file di log esterno: %s</string>
<string name="logging_no_external_storage">Dispositivo esterno non disponibile</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">Apri barra di navigazione</string>
<string name="navigation_drawer_close">Chiudi barra di navigazione</string>
<string name="navigation_drawer_subtitle">CalDAV/CardDAV adattatore di sincronizzazione</string>
<string name="navigation_drawer_about">Informazioni / Licenza</string>
<string name="navigation_drawer_settings">Impostazioni</string>
<string name="navigation_drawer_news_updates">Notizie &amp; aggiornamenti</string>
<string name="navigation_drawer_external_links">Link esterni</string>
<string name="navigation_drawer_website">Sito web</string>
<string name="navigation_drawer_faq">Domande Frequenti</string>
<string name="navigation_drawer_forums">Comunità</string>
<string name="navigation_drawer_donate">Donazione</string>
<string name="account_list_empty">Benvenuto a DAVdroid!\n\nÈ ora possibile aggiungere account CalDAV/CardDAV.</string>
<string name="accounts_global_sync_disabled">La sincronizzazione automatica dell\'intero sistema è disabilitata</string>
<string name="accounts_global_sync_enable">Attiva</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Fallita l\'individuazione dei servizi</string>
<string name="dav_service_refresh_couldnt_refresh">Impossibile aggiornare la lista delle raccolte</string>
<!--AppSettingsActivity-->
<string name="app_settings">Impostazioni</string>
<string name="app_settings_user_interface">Interfaccia utente</string>
<string name="app_settings_reset_hints">Reimposta i suggerimenti</string>
<string name="app_settings_reset_hints_summary">Riabilita i suggerimenti precedentemente disabilitati</string>
<string name="app_settings_reset_hints_success">I suggerimenti verranno mostrati</string>
<string name="app_settings_connection">Connessione</string>
<string name="app_settings_override_proxy">Non rispettare la impostazioni del proxy</string>
<string name="app_settings_override_proxy_on">Impostazioni personalizzate del proxy</string>
<string name="app_settings_override_proxy_off">Usa le impostazioni di sistema del proxy</string>
<string name="app_settings_override_proxy_host">Nome host del proxy HTTP</string>
<string name="app_settings_override_proxy_port">Porta del proxy HTTP</string>
<string name="app_settings_security">Sicurezza</string>
<string name="app_settings_distrust_system_certs">Non ti fidare dei certificati di sistema</string>
<string name="app_settings_distrust_system_certs_on">Le CA di sistema e quelle aggiunte dall\'utente non sono affidabili</string>
<string name="app_settings_distrust_system_certs_off">Le CA di sistema e quelle aggiunte dall\'utente sono affidabili (raccomandato)</string>
<string name="app_settings_reset_certificates">Reimposta la fiducia in tutti i certificati</string>
<string name="app_settings_reset_certificates_summary">Reimposta la fiducia nei certificati aggiunti</string>
<string name="app_settings_reset_certificates_success">Sono stati cancellati tutti i certificati aggiunti</string>
<string name="app_settings_debug">Debug</string>
<string name="app_settings_log_to_external_storage">Log su file esterno</string>
<string name="app_settings_log_to_external_storage_on">Log su dispositivo esterno (se disponibile)</string>
<string name="app_settings_log_to_external_storage_off">Log su file esterno disabilitato</string>
<string name="app_settings_show_debug_info">Mostra informazioni di debug</string>
<string name="app_settings_show_debug_info_details">Mostra e condividi i dettagli del programma e della configurazione</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Sincronizza adesso</string>
<string name="account_synchronizing_now">Sincronizzazione in corso</string>
<string name="account_settings">Impostazioni account</string>
<string name="account_rename">Rinomina account</string>
<string name="account_rename_new_name">I dati locali non salvati potrebbero essere rimossi. È necessario effettuare la risincronizzazione dopo la rinomina. Nuovo nome dell\'account:</string>
<string name="account_rename_rename">Rinomina</string>
<string name="account_delete">Elimina account</string>
<string name="account_delete_confirmation_title">Cancellare l\'account?</string>
<string name="account_delete_confirmation_text">Tutte le copie locali delle rubriche, dei calendari e degli elenchi attività verranno eliminate.</string>
<string name="account_refresh_address_book_list">Aggiorna elenco degli indirizzari</string>
<string name="account_create_new_address_book">Crea un nuovo indirizzario</string>
<string name="account_refresh_calendar_list">Aggiorna lista calendari</string>
<string name="account_create_new_calendar">Crea nuovo calendario</string>
<!--PermissionsActivity-->
<string name="permissions_title">Permessi DAVdroid</string>
<string name="permissions_calendar">Permessi calendario</string>
<string name="permissions_calendar_details">Per sincronizzare gli eventi CalDAV con i calendari locali DAVdroid deve avere l\'accesso ai tuoi calendari.</string>
<string name="permissions_calendar_request">Richiesta autorizzazione al calendario</string>
<string name="permissions_contacts">Permessi Contatti</string>
<string name="permissions_contacts_details">Per sincronizzare l\'indirizzario CardDAV con i contatti locali DAVdroid deve avere l\'accesso ai tuoi contatti.</string>
<string name="permissions_contacts_request">Richiesta autorizzazione ai contatti</string>
<string name="permissions_opentasks">Permessi OpenTasks</string>
<string name="permissions_opentasks_details">Per sincronizzazione l\'elenco attività di CalDAV con l\'elenco locale DAVdroid deve avere l\'accesso ad OpenTasks.</string>
<string name="permissions_opentasks_request">Richiesta autorizzazione ad OpenTasks</string>
<!--AddAccountActivity-->
<string name="login_title">Aggiungi account</string>
<string name="login_type_email">Accedi con indirizzo email</string>
<string name="login_email_address">Indirizzo email</string>
<string name="login_email_address_error">È necessario un indirizzo email valido</string>
<string name="login_password">Password</string>
<string name="login_password_required">Password richiesta</string>
<string name="login_type_url">Accedi con URL e nome utente</string>
<string name="login_url_must_be_http_or_https">L\'URL deve iniziare con http(s)://</string>
<string name="login_url_host_name_required">Nome host richiesto</string>
<string name="login_user_name">Nome utente</string>
<string name="login_user_name_required">Nome utente richiesto</string>
<string name="login_base_url">Base URL</string>
<string name="login_login">Login</string>
<string name="login_back">Indietro</string>
<string name="login_create_account">Crea account</string>
<string name="login_account_name">Nome account</string>
<string name="login_account_name_info">Inserisci il tuo indirizzo email come nome dell\'account in quanto Android userà il nome dell\'account nel campo ORGANIZER degli eventi creati. Non è possibile avere due account con nome uguale.</string>
<string name="login_account_contact_group_method">Metodo del contact group:</string>
<string name="login_account_name_required">Richiesto il nome dell\'account</string>
<string name="login_account_not_created">L\'account non può essere creato</string>
<string name="login_configuration_detection">Rilevazione configurazione</string>
<string name="login_querying_server">Attendere, invio richiesta al server...</string>
<string name="login_no_caldav_carddav">Impossibile trovare servizi CalDAV o CardDAV.</string>
<string name="login_view_logs">Vedi i log</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Impostazioni: %s</string>
<string name="settings_authentication">Autenticazione</string>
<string name="settings_username">Nome utente</string>
<string name="settings_enter_username">Inserisci nome utente:</string>
<string name="settings_password">Password</string>
<string name="settings_password_summary">Aggiorna la password come sul tuo server.</string>
<string name="settings_enter_password">Inserisci la tua password:</string>
<string name="settings_sync">Sincronizzazione</string>
<string name="settings_sync_interval_contacts">Intervallo sincr. Contatti</string>
<string name="settings_sync_summary_manually">Solo manualmente</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Ogni %d minuti e a seguito di ogni cambiamento locale</string>
<string name="settings_sync_summary_not_available">Non disponile</string>
<string name="settings_sync_interval_calendars">Intervallo sincr. calendari</string>
<string name="settings_sync_interval_tasks">Intervallo sincr. attività</string>
<string-array name="settings_sync_interval_seconds">
<item>-1</item>
<item>300</item>
<item>600</item>
<item>900</item>
<item>3600</item>
<item>7200</item>
<item>14400</item>
<item>86400</item>
</string-array>
<string-array name="settings_sync_interval_names">
<item>Solo manualmente</item>
<item>Ogni 5 minuti</item>
<item>Ogni 10 minuti</item>
<item>Ogni 15 minuti</item>
<item>Ogni ora</item>
<item>Ogni 2 ore</item>
<item>Ogni 4 ore</item>
<item>Una volta al giorno</item>
</string-array>
<string name="settings_sync_wifi_only">Sincr. solo tramite WiFi</string>
<string name="settings_sync_wifi_only_on">La sincronizzazione è limitata alle connessioni WiFi</string>
<string name="settings_sync_wifi_only_off">Il tipo di connessione non è preso in considerazione</string>
<string name="settings_sync_wifi_only_ssid">Restrizione sul SSID del WiFi</string>
<string name="settings_sync_wifi_only_ssid_on">La sincronizzazione avverrà solo con %s</string>
<string name="settings_sync_wifi_only_ssid_off">Verranno usate tutte le connessioni WiFi</string>
<string name="settings_sync_wifi_only_ssid_message">Inserisci il nome di una rete WiFi (SSID) per limitare la sincronizzazione solo con questa rete o lasciare in bianco per usare tutte le connessioni WiFi.</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Organizzazione dei gruppi di contatto</string>
<string-array name="settings_contact_group_method_values">
<item>GROUP_VCARDS</item>
<item>CATEGORIES</item>
</string-array>
<string-array name="settings_contact_group_method_entries">
<item>I gruppi sono VCards separate</item>
<item>I gruppi sono categorie per ogni contatto</item>
</string-array>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Limite di tempo per gli eventi trascorsi</string>
<string name="settings_sync_time_range_past_none">Verranno sincronizzati tutti gli eventi</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Eventi più vecchi di un giorno saranno ignorati</item>
<item quantity="other">Eventi più vecchi di %d giorni saranno ignorati</item>
</plurals>
<string name="settings_sync_time_range_past_message">Eventi più vecchi di questo numero di giorni verranno ignorati(può anche essere 0). Lasciare in bianco per sincronizzare tutti gli eventi.</string>
<string name="settings_manage_calendar_colors">Cambia il colore del calendario</string>
<string name="settings_manage_calendar_colors_on">I colori dei calendari sono gestiti da DAVdroid</string>
<string name="settings_manage_calendar_colors_off">I colori dei calendari non sono gestiti da DAVdroid</string>
<!--collection management-->
<string name="create_addressbook">Crea indirizzario</string>
<string name="create_addressbook_display_name_hint">Il mio indirizzario</string>
<string name="create_calendar">Crea raccolta CalDAV</string>
<string name="create_calendar_display_name_hint">Mio calendario</string>
<string name="create_calendar_time_zone">Fuso orario:</string>
<string name="create_calendar_type">Tipo di raccolta:</string>
<string name="create_calendar_type_only_events">Calendario (solo eventi)</string>
<string name="create_calendar_type_only_tasks">Elenco attività (solo attività)</string>
<string name="create_calendar_type_events_and_tasks">Combinato (eventi e attività)</string>
<string name="create_collection_color">Imposta colore della raccolta</string>
<string name="create_collection_creating">Crea una raccolta</string>
<string name="create_collection_display_name">Mostra il nome (titolo) di questa raccolta:</string>
<string name="create_collection_display_name_required">Il titolo è richiesto</string>
<string name="create_collection_description">Descrizione (opzionale):</string>
<string name="create_collection_home_set">Imposta la home:</string>
<string name="create_collection_create">Crea</string>
<string name="delete_collection">Elimina raccolta</string>
<string name="delete_collection_confirm_title">Sei sicuro?</string>
<string name="delete_collection_confirm_warning">Questa raccolta (%s) e tutti i suoi dati saranno rimossi dal server.</string>
<string name="delete_collection_deleting_collection">Cancellazione della raccolta</string>
<!--ExceptionInfoFragment-->
<string name="exception">Si è verificato un errore.</string>
<string name="exception_httpexception">Si è verificato un errore HTTP.</string>
<string name="exception_ioexception">Si è verificato un errore di I/O.</string>
<string name="exception_show_details">Mostra dettagli</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Informazioni di debug</string>
<string name="sync_error_permissions">Autorizzazioni DAVdroid</string>
<string name="sync_error_permissions_text">Autorizzazioni addizionali richieste</string>
<string name="sync_error_calendar">Sincronizzazione del calendario fallita (%s)</string>
<string name="sync_error_contacts">Sincronizzazione dell\'indirizzario fallita (%s)</string>
<string name="sync_error_tasks">Sincronizzazione delle attività fallita (%s)</string>
<string name="sync_error">Errore nel %s</string>
<string name="sync_error_http_dav">Errore del server nel %s</string>
<string name="sync_error_local_storage">Errore del database nel %s</string>
<string-array name="sync_error_phases">
<item>inizio sincronizzazione</item>
<item>controllo caratteristiche del server</item>
<item>elaborazione voci cancellate in locale</item>
<item>elaborazione voci create o modificate</item>
<item>invio voci create o modificate</item>
<item>controllo stato della sincronizzazione</item>
<item>elenco voci locali</item>
<item>elenco voci remote</item>
<item>confronto voci locali e remote</item>
<item>download voci remote</item>
<item>post-processing</item>
<item>salvataggio stato della sincronizzazione</item>
</string-array>
<string name="sync_error_unauthorized">Nome utente o password errati</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: sicurezza della connessione</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid ha trovato un certificato sconosciuto. Ritenerlo affidabile?</string>
</resources>

View File

@@ -2,11 +2,17 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">DAVdroid アドレス帳</string>
<string name="address_books_authority_title">アドレス帳</string>
<string name="help">ヘルプ</string>
<string name="manage_accounts">アカウントの管理</string>
<string name="please_wait">しばらくお待ちください …</string>
<string name="send">送信</string>
<string name="homepage_url">https://davdroid.bitfire.at/?pk_campaign=davdroid-app</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">バッテリー最適化</string>
<string name="startup_battery_optimization_message">Android は数日後に DAVdroid の同期を無効にする/減らすことがあります。これを防止するには、バッテリー最適化をオフにしてください。</string>
<string name="startup_battery_optimization_disable">DAVdroid 用にオフにする</string>
<string name="startup_dont_show_again">次回から表示しない</string>
<string name="startup_development_version">DAVdroid プレビュー リリース</string>
<string name="startup_development_version_message">これは DAVdroid の開発版です。期待した通りに動作しない可能性があることにご注意ください。私たちが DAVdroid を改善するために、建設的なフィードバックをお願いします。</string>
@@ -19,7 +25,8 @@
<string name="startup_google_play_accounts_removed_message">特定の条件下で、DAVdroid を再起動後またはアップグレードした後、Play ストア DRM によりすべての DAVdroid アカウントがなくなる可能性があります。この問題の影響を受けている場合 (のみ)、Play ストアから「DAVdroid JB 回避策」をインストールしてください。</string>
<string name="startup_google_play_accounts_removed_more_info">追加情報</string>
<string name="startup_opentasks_not_installed">OpenTasks がインストールされていません</string>
<string name="startup_opentasks_not_installed_message">OpenTasks アプリがインストールされていないか、アクセスできないため、DAVdroid はタスクリストを同期することができません。 OpenTasks をインストールした後、DAVdroidを再インストールして、再度アカウントを追加してください。</string>
<string name="startup_opentasks_not_installed_message">OpenTasks アプリが利用できないため、DAVdroid はタスクリストを同期することができません。</string>
<string name="startup_opentasks_reinstall_davdroid">OpenTasks をインストールした後で、DAVdroidを再インストールして、再度アカウントを追加してください (Android のバグ)。</string>
<string name="startup_opentasks_not_installed_install">OpenTasks をインストール</string>
<!--AboutActivity-->
<string name="about_license_terms">ライセンス規約</string>
@@ -40,9 +47,12 @@
<string name="navigation_drawer_external_links">外部リンク</string>
<string name="navigation_drawer_website">Web サイト</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_faq_url">https://davdroid.bitfire.at/faq/?pk_campaign=davdroid-app</string>
<string name="navigation_drawer_forums">コミュニティ</string>
<string name="navigation_drawer_donate">寄付</string>
<string name="account_list_empty">DAVdroid にようこそ!\n\nCalDAV/CardDAV アカウントを追加できるようになりました。</string>
<string name="accounts_global_sync_disabled">システム全体の自動同期が無効です</string>
<string name="accounts_global_sync_enable">有効</string>
<!--DavService-->
<string name="dav_service_refresh_failed">サービスの検出に失敗しました</string>
<string name="dav_service_refresh_couldnt_refresh">コレクション リストを更新できません</string>
@@ -52,12 +62,19 @@
<string name="app_settings_reset_hints">ヒントをリセット</string>
<string name="app_settings_reset_hints_summary">以前非表示にしたヒントを、再度有効にします</string>
<string name="app_settings_reset_hints_success">すべてのヒントが再度表示されます</string>
<string name="app_settings_connection">接続</string>
<string name="app_settings_override_proxy">プロキシ設定を上書き</string>
<string name="app_settings_override_proxy_on">カスタムのプロキシ設定を使用する</string>
<string name="app_settings_override_proxy_off">システムデフォルトのプロキシ設定を使用する</string>
<string name="app_settings_override_proxy_host">HTTP プロキシのホスト名</string>
<string name="app_settings_override_proxy_port">HTTP プロキシのポート</string>
<string name="app_settings_security">セキュリティ</string>
<string name="app_settings_reset_trusted_certificates">信頼済証明書をリセット</string>
<string name="app_settings_reset_trusted_certificates_summary">以前承認されたすべての証明書を忘れます</string>
<plurals name="app_settings_reset_trusted_certificates_success">
<item quantity="other">%d 証明書の信頼を解除しました</item>
</plurals>
<string name="app_settings_distrust_system_certs">システム証明書の信頼を解除</string>
<string name="app_settings_distrust_system_certs_on">システムとユーザーが追加したCAを信頼しない</string>
<string name="app_settings_distrust_system_certs_off">システムとユーザーが追加したCAを信頼する (推奨)</string>
<string name="app_settings_reset_certificates">(未)信頼済証明書をリセット</string>
<string name="app_settings_reset_certificates_summary">すべてのカスタム証明書の信頼をリセット</string>
<string name="app_settings_reset_certificates_success">すべてのカスタム証明書をクリアしました</string>
<string name="app_settings_debug">デバッグ</string>
<string name="app_settings_log_to_external_storage">外部ファイルにログ出力</string>
<string name="app_settings_log_to_external_storage_on">(可能な場合) 外部ストレージにログ出力します</string>
@@ -68,6 +85,9 @@
<string name="account_synchronize_now">今すぐ同期</string>
<string name="account_synchronizing_now">同期中</string>
<string name="account_settings">アカウント設定</string>
<string name="account_rename">アカウントの名前を変更</string>
<string name="account_rename_new_name">未保存のローカルデータが破棄されることがあります。 名前の変更後に再同期が必要です。 新しいアカウント名:</string>
<string name="account_rename_rename">名前を変更</string>
<string name="account_delete">アカウントを削除</string>
<string name="account_delete_confirmation_title">アカウントを削除してもよろしいですか?</string>
<string name="account_delete_confirmation_text">アドレス帳、カレンダー、タスクリストのローカルコピーがすべて削除されます。</string>
@@ -75,7 +95,19 @@
<string name="account_create_new_address_book">新しいアドレス帳を作成</string>
<string name="account_refresh_calendar_list">カレンダーリストを更新</string>
<string name="account_create_new_calendar">新しいカレンダーを作成</string>
<!--PermissionsActivity-->
<string name="permissions_title">DAVdroid アクセス許可</string>
<string name="permissions_calendar">カレンダー アクセス許可</string>
<string name="permissions_calendar_details">ローカルのカレンダーと CalDAV イベントを同期するため、DAVdroid がカレンダーにアクセスする必要があります。</string>
<string name="permissions_calendar_request">カレンダー アクセス許可の要求</string>
<string name="permissions_contacts">連絡先アクセス許可</string>
<string name="permissions_contacts_details">ローカルの連絡先と CalDAV アドレス帳を同期するため、DAVdroid が連絡先にアクセスする必要があります。</string>
<string name="permissions_contacts_request">連絡先アクセス許可の要求</string>
<string name="permissions_opentasks">OpenTasks アクセス許可</string>
<string name="permissions_opentasks_details">ローカルのタスクリストと CalDAV タスクを同期するため、DAVdroid が OpenTasks にアクセスする必要があります。</string>
<string name="permissions_opentasks_request">OpenTasks アクセス許可の要求</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://davdroid.bitfire.at/configuration/?pk_campaign=davdroid-app</string>
<string name="login_title">アカウントを追加</string>
<string name="login_type_email">メールアドレスでログイン</string>
<string name="login_email_address">メールアドレス</string>
@@ -88,12 +120,12 @@
<string name="login_user_name">ユーザー名</string>
<string name="login_user_name_required">ユーザー名が必要です</string>
<string name="login_base_url">ベース URL</string>
<string name="login_auth_preemptive">プリエンプティブ認証 (推奨ですが、ダイジェスト認証と互換性がありません)</string>
<string name="login_login">ログイン</string>
<string name="login_back">戻る</string>
<string name="login_create_account">アカウントを作成</string>
<string name="login_account_name">アカウント名</string>
<string name="login_account_name_info">Android はあなたが作成した予定の ORGANIZER フィールドとしてアカウント名を使用するので、アカウント名としてメールアドレスを使用してください。同じ名前のアカウントを 2 つ持つことはできません。</string>
<string name="login_account_contact_group_method">連絡先グループ方法:</string>
<string name="login_account_name_required">アカウント名が必要です</string>
<string name="login_account_not_created">アカウントを作成できません</string>
<string name="login_configuration_detection">設定の検出</string>
@@ -108,9 +140,6 @@
<string name="settings_password">パスワード</string>
<string name="settings_password_summary">ご利用のサーバーに従ってパスワードを更新します。</string>
<string name="settings_enter_password">パスワードを入力:</string>
<string name="settings_preemptive">プリエンプティブ認証</string>
<string name="settings_preemptive_on">すべてのリクエストで資格情報を送信します (推奨)</string>
<string name="settings_preemptive_off">サーバーが要求した後、資格情報を送信します</string>
<string name="settings_sync">同期</string>
<string name="settings_sync_interval_contacts">連絡先同期間隔</string>
<string name="settings_sync_summary_manually">手動のみ</string>
@@ -118,16 +147,6 @@
<string name="settings_sync_summary_not_available">利用不可</string>
<string name="settings_sync_interval_calendars">カレンダー同期間隔</string>
<string name="settings_sync_interval_tasks">タスク同期間隔</string>
<string-array name="settings_sync_interval_seconds">
<item>-1</item>
<item>300</item>
<item>600</item>
<item>900</item>
<item>3600</item>
<item>7200</item>
<item>14400</item>
<item>86400</item>
</string-array>
<string-array name="settings_sync_interval_names">
<item>手動のみ</item>
<item>5 分ごと</item>
@@ -138,15 +157,29 @@
<item>4 時間ごと</item>
<item>1 日 1 回</item>
</string-array>
<string name="settings_sync_wifi_only">WiFi でのみ同期</string>
<string name="settings_sync_wifi_only_on">同期は WiFi 接続に制限されます</string>
<string name="settings_sync_wifi_only_off">接続の種類は考慮されません</string>
<string name="settings_sync_wifi_only_ssid">WiFi SSID 制限</string>
<string name="settings_sync_wifi_only_ssid_on">%s でのみ同期します</string>
<string name="settings_sync_wifi_only_ssid_off">すべての WiFi 接続を使用することができます</string>
<string name="settings_sync_wifi_only_ssid_message">このネットワークで同期を制限する WiFi ネットワーク (SSID) の名前を入力してください。すべての WiFi 接続は空白のままにします。</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">連絡先グループ方法</string>
<string-array name="settings_contact_group_method_entries">
<item>グループは個別の VCards</item>
<item>グループは連絡先カテゴリーごと</item>
</string-array>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">過去イベントの時間限度</string>
<string name="settings_sync_time_range_past_none">すべてのイベントが同期されます</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="other">%d 日より前のイベントは無視されます</item>
</plurals>
<string name="settings_sync_time_range_past_message">この日数より過去のイベントは無視されます (0 も可)。すべてのイベントを同期させるには、空白のままにしてください。</string>
<string name="settings_version_update">DAVdroid バージョンの更新</string>
<string name="settings_version_update_settings_updated">内部設定が更新されました。</string>
<string name="settings_version_update_install_hint">問題がありますか? DAVdroid をアンインストールして、再度インストールしてください。</string>
<string name="settings_manage_calendar_colors">カレンダーの色を管理</string>
<string name="settings_manage_calendar_colors_on">カレンダーの色は DAVdroid が管理します</string>
<string name="settings_manage_calendar_colors_off">カレンダーの色を DAVdroid が設定しません</string>
<!--collection management-->
<string name="create_addressbook">アドレス帳を作成</string>
<string name="create_addressbook_display_name_hint">マイ アドレス帳</string>
@@ -175,6 +208,8 @@
<string name="exception_show_details">詳細を表示</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">デバッグ情報</string>
<string name="sync_error_permissions">DAVdroid アクセス許可</string>
<string name="sync_error_permissions_text">追加のアクセス許可が必要です</string>
<string name="sync_error_calendar">カレンダーの同期に失敗しました (%s)</string>
<string name="sync_error_contacts">アドレス帳の同期に失敗しました (%s)</string>
<string name="sync_error_tasks">タスクの同期に失敗しました (%s)</string>
@@ -192,7 +227,11 @@
<item>リモートのエントリーをリスト中</item>
<item>ローカル/リモートのエントリーを比較中</item>
<item>リモートのエントリーをダウンロード中</item>
<item>後処理中</item>
<item>同期の状態を保存中</item>
</string-array>
<string name="sync_error_unauthorized">ユーザー名/パスワードが間違っています</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: 接続セキュリティ</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroidは、未知の証明書を検出しました。それを信頼しますか?</string>
</resources>

View File

@@ -0,0 +1,252 @@
<?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>
<string name="homepage_url">https://davdroid.bitfire.at/?pk_campaign=davdroid-app</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Batterij optimalisatie</string>
<string name="startup_battery_optimization_message">Android kan mogelijk de DAVdroid synchronisatie stoppen na een paar dagen. Om dit te voorkomen zet u de batterij optimalisatie uit.</string>
<string name="startup_battery_optimization_disable">DAVdroid afsluiten</string>
<string name="startup_dont_show_again">Niet opnieuw weergeven</string>
<string name="startup_development_version">DAVdroid voorlopige versie</string>
<string name="startup_development_version_message">Dit is een ontwikkelversie van DAVdroid. Het kan zijn dat dingen niet werken zoals verwacht. Geef ons constructieve feedback om DAVdroid te verbeteren.</string>
<string name="startup_development_version_give_feedback">Feedback geven</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_google_play_accounts_removed_more_info">Meer informatie</string>
<string name="startup_opentasks_not_installed">OpenTasks niet geinstalleerd</string>
<string name="startup_opentasks_not_installed_message">De OpenTasks app is niet beschikbaar, Hierdoor is het voor DAVdroid niet mogelijk om uw taken te synchroniseren.</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_terms">Licentie voorwaarden</string>
<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_to_external_storage_warning">Verwijder logs zo snel mogelijk</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_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_faq_url">https://davdroid.bitfire.at/faq/?pk_campaign=davdroid-app</string>
<string name="navigation_drawer_forums">Community</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_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>
<!--PermissionsActivity-->
<string name="permissions_title">DAVdroid rechten</string>
<string name="permissions_calendar">Agenda rechten</string>
<string name="permissions_calendar_details">Om CalDAV afspraken te synchroniseren met u agenda dient DAVdroid toegang te verkrijgen. </string>
<string name="permissions_calendar_request">Agenda rechten verkrijgen</string>
<string name="permissions_contacts">Contact rechten</string>
<string name="permissions_contacts_details">Om CalDAV afspraken te synchroniseren met u contacten dient DAVdroid toegang te verkrijgen. </string>
<string name="permissions_contacts_request">Contacten rechten verkrijgen</string>
<string name="permissions_opentasks">OpenTasks rechten</string>
<string name="permissions_opentasks_details">Om CalDAV taken te synchroniseren met uw local takenlijst dient DAVdroid toegang te hebben tot OpenTasks</string>
<string name="permissions_opentasks_request">OpenTasks rechten verkrijgen</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://davdroid.bitfire.at/configuration/?pk_campaign=davdroid-app</string>
<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>
<string name="login_view_logs">Bekijk logs</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_summary_not_available">Niet beschikbaar</string>
<string name="settings_sync_interval_calendars">Agenda\'s verversen</string>
<string name="settings_sync_interval_tasks">Taak sync. tussentijd</string>
<string-array name="settings_sync_interval_seconds">
<item>-1</item>
<item>300</item>
<item>600</item>
<item>900</item>
<item>3600</item>
<item>7200</item>
<item>14400</item>
<item>86400</item>
</string-array>
<string-array name="settings_sync_interval_names">
<item>Alleen handmatig</item>
<item>Elke 5 minuten</item>
<item>Elke 10 minuten</item>
<item>Elke 15 minuten</item>
<item>Elk uur</item>
<item>Elke 2 uur</item>
<item>Elke 4 uur</item>
<item>Dagelijks</item>
</string-array>
<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_ssid">WiFi SSID beperking</string>
<string name="settings_sync_wifi_only_ssid_on">Zal alleen synchroniseren over %s</string>
<string name="settings_sync_wifi_only_ssid_off">Alle WiFI verbindingen mogen worden gebruikt</string>
<string name="settings_sync_wifi_only_ssid_message">Type de naam van het WiFi netwerk (SSID) om synchronisatie tot dit netwerk te beperken. Leeg laten voor sync over alle netwerken.</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>
<!--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 errors and DebugInfoActivity-->
<string name="debug_info_title">Debug informatie</string>
<string name="sync_error_permissions">DAVdroid rechten</string>
<string name="sync_error_permissions_text">Aanvullende rechten vereist</string>
<string name="sync_error_calendar">Agenda synchronisatie is mislukt (%s)</string>
<string name="sync_error_contacts">Adresboek synchronisatie is mislukt (%s)</string>
<string name="sync_error_tasks">Taak synchronisatie is mislukt (%s)</string>
<string name="sync_error">Fout tijdens %s</string>
<string name="sync_error_http_dav">Serverfout tijdens %s</string>
<string name="sync_error_local_storage">Database fout tijdens %s</string>
<string-array name="sync_error_phases">
<item>synchronisatie voorbereiden</item>
<item>querying mogelijkheden</item>
<item>verwerken van lokaal verwijderde data</item>
<item>voorberteiding maken/wijzigen data</item>
<item>uploaden maken/bewerken data</item>
<item>controleren syngronisatie voortgang</item>
<item>lijst lokale data</item>
<item>lijst remote data</item>
<item>vergelijken lokale/remote data</item>
<item>downloaden remote data</item>
<item>nabewerking</item>
<item>opslaan sync voortgang</item>
</string-array>
<string name="sync_error_unauthorized">Gebruikersnaam/wachtwoord onjuist</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

@@ -0,0 +1,247 @@
<?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>
<!--startup dialogs-->
<string name="startup_battery_optimization">Optymalizacja baterii</string>
<string name="startup_battery_optimization_message">Android może wyłączyć/zmniejszyć synchronizacje DAVdroid po kilku dniach. Aby temu zapobiec należy wyłączyć optymalizację baterii.</string>
<string name="startup_battery_optimization_disable">Wyłącz dla DAVdroid</string>
<string name="startup_dont_show_again">Nie pokazuj ponownie</string>
<string name="startup_development_version">DAVdroid Preview Release</string>
<string name="startup_development_version_message">Jest to wersja rozwojowa DAVdroid. Należy pamiętać, że rzeczy mogą nie działać zgodnie z oczekiwaniami. Prosimy o konstruktywny informacje zwrotne, aby ulepszyć DAVdroid.</string>
<string name="startup_development_version_give_feedback">Przekaż opinię</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_google_play_accounts_removed_more_info">Więcej informacji</string>
<string name="startup_opentasks_not_installed">OpenTasks nie jest zainstalowany</string>
<string name="startup_opentasks_not_installed_message">Aplikacja Open Tasks nie jest dostępna, więc DAVdroid nie będzie mógł synchronizować listy zadań. </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_terms">Warunki licencji</string>
<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_to_external_storage_warning">Usuń logi tak szybko jak to możliwe!</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_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_faq">FQA</string>
<string name="navigation_drawer_forums">Społeczność</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 HTTP proxy</string>
<string name="app_settings_override_proxy_port">Port HTTP proxy</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 to 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 niedostępne</string>
<string name="app_settings_show_debug_info">Pokaż informacje do debug\'owania</string>
<string name="app_settings_show_debug_info_details">Przeglądaj/udostępnij oprogramowanie i szczegóły 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_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ż liste kalendarzy</string>
<string name="account_create_new_calendar">Stwórz nowy kalendarz</string>
<!--PermissionsActivity-->
<string name="permissions_title">Uprawnienia DAVdroid</string>
<string name="permissions_calendar">Uprawnienia kalendarza</string>
<string name="permissions_calendar_details">Aby synchronizować wydarzenia CalDav z lokalnymi kalendarzami, DAVdroid potrzebuje dostępu do twoich kalendarzy.</string>
<string name="permissions_calendar_request">Zezwól na uprawnienia kalendarza</string>
<string name="permissions_contacts">Uprawnienia kontaktów</string>
<string name="permissions_contacts_details">Aby synchronizować książki adresowe CardDAV z lokalnymi kontaktami, DAVdroid potrzebuje dostępu do twoich kontaktów.</string>
<string name="permissions_contacts_request">Zezwól na uprawnienia kontaktów</string>
<string name="permissions_opentasks">Uprawnienia OpenTasks</string>
<string name="permissions_opentasks_details">Aby synchronizować zadania CalDav z lokalnymi listami zadań, DAVdroid potrzebuje dostępu do OpenTasks.</string>
<string name="permissions_opentasks_request">Zezwól na uprawnienia OpenTasks</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">Wymanagny 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ę z http(s)://</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żtkonika</string>
<string name="login_base_url">Podstawowy URL</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>
<string name="login_view_logs">Pokaż logi</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 nazwe 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_sync">Synchronizacja</string>
<string name="settings_sync_interval_contacts">Okres synchronizacji kontktó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_summary_not_available">Niedostępne</string>
<string name="settings_sync_interval_calendars">Okres synchronizacji kalendarzy</string>
<string name="settings_sync_interval_tasks">Okres synchronizacji list zadań</string>
<string-array name="settings_sync_interval_seconds">
<item>-1</item>
<item>300</item>
<item>600</item>
<item>900</item>
<item>3600</item>
<item>7200</item>
<item>14400</item>
<item>86400</item>
</string-array>
<string-array name="settings_sync_interval_names">
<item>Tylko ręcznie</item>
<item>Co 5 minut</item>
<item>Co 10 minut</item>
<item>Co 15 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 ograniczony 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_ssid">Organicznie WiFi SSID</string>
<string name="settings_sync_wifi_only_ssid_on">Będzie synchronizować tylko przez %s.</string>
<string name="settings_sync_wifi_only_ssid_off">Wszystkie połączenia WiFi mogą zostać wykorzystane</string>
<string name="settings_sync_wifi_only_ssid_message">Wprowadź nazwę sieci WiFi (SSID), aby ograniczyć synchronizację do tej sieci lub pozostaw puste dla wszystkich połączeń WiFi.</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_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ą synchronizowane</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>
<!--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 kolekcje 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 (opcjoalnie)</string>
<string name="create_collection_home_set">Ustaw początek:</string>
<string name="create_collection_create">Stwórz</string>
<string name="delete_collection">Usuń kolekcje</string>
<string name="delete_collection_confirm_title">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>
<!--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 errors and DebugInfoActivity-->
<string name="debug_info_title">Informacje debugowe</string>
<string name="sync_error_permissions">Uprawnienia DAVdroid</string>
<string name="sync_error_permissions_text">Wymagane dodatkowe uprawnienia</string>
<string name="sync_error_calendar">Synchronizacja kalendarza nie powiodała się (%s)</string>
<string name="sync_error_contacts">Synchronizacja książki adresowej nie powiodała się (%s)</string>
<string name="sync_error_tasks">Synchronizacja zadań nie powiodała się (%s)</string>
<string name="sync_error">Błąd podczas %s</string>
<string name="sync_error_http_dav">Błąd servera podczas %s</string>
<string name="sync_error_local_storage">Bład bazy danych podczas %s</string>
<string-array name="sync_error_phases">
<item>przygotowanie synchronizacji</item>
<item>odpytywanie możliwości</item>
<item>przetwarzanie lokalnie usuniętych wpisów</item>
<item>przygotowanie stworzonych/zmodyfikowanych wpisów</item>
<item>wysyłanie stworzonych/zmodyfikowanych wpisów</item>
<item>sprawdzanie stanu synchronizacji</item>
<item>listowanie lokalnych wpisów</item>
<item>listowanie zdalnych wpisów</item>
<item>porównywanie lokalnych/zdalnych wpisów</item>
<item>pobieranie zdalnych wpisów</item>
<item>przetwarzanie końcowe</item>
<item>zapisywanie stanu synchronizacji</item>
</string-array>
<string name="sync_error_unauthorized">Błędna nazwa użytkownika lub hasło</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

@@ -2,24 +2,30 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">Livro de endereços DAVdroid</string>
<string name="address_books_authority_title">Livros de endereços</string>
<string name="help">Ajuda</string>
<string name="manage_accounts">Gerenciar contas</string>
<string name="please_wait">Por favor, aguarde...</string>
<string name="send">Enviar</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Otimização da bateria</string>
<string name="startup_battery_optimization_message">O Android pode desativar/reduzir a sincronização do DAVdroid depois de alguns dias. Para evitar isso, desligue a otimização da bateria.</string>
<string name="startup_battery_optimization_disable">Desligar para o DAVdroid</string>
<string name="startup_dont_show_again">Não mostrar novamente</string>
<string name="startup_development_version">Versão prévia do DAVdroid</string>
<string name="startup_development_version_message">Esta é uma versão de desenvolvimento do DAVdroid. Lembre-se de que as coisas podem não funcionar como esperado. Por favor, envie sugestões e comentários construtivos para melhorar o DAVdroid.</string>
<string name="startup_development_version_give_feedback">Enviar sugestões</string>
<string name="startup_donate">Informação sobre Código Aberto</string>
<string name="startup_donate_message">Estamos felizes que você usa o DAVdroid, que é um software de código aberto (GPLv3). O desenvolvimento do DAVdroid é trabalhoso e consome muitas horas de trabalho. Por esse motivo, considere fazer uma doação.</string>
<string name="startup_donate_message">Estamos felizes que você usa o DAVdroid, um software de código aberto (GPLv3). O desenvolvimento do DAVdroid é trabalhoso e consome muitas horas de trabalho. Por esse motivo, considere fazer uma doação.</string>
<string name="startup_donate_now">Mostrar a página de doações</string>
<string name="startup_donate_later">Talvez depois</string>
<string name="startup_google_play_accounts_removed">Informação sobre o erro de DRM da Play Store</string>
<string name="startup_google_play_accounts_removed_message">Sob certas condições, o DRM da Play Store pode fazer com que todas as contas DAVdroid sejam perdidas depois de uma reinicialização ou atualização do DAVdroid. Se você for afetado por esse problema, instale o \"DAVdroid JB Workaround\" a partir da Play Store.</string>
<string name="startup_google_play_accounts_removed_more_info">Mais informações</string>
<string name="startup_opentasks_not_installed">O OpenTasks não está instalado</string>
<string name="startup_opentasks_not_installed_message">O aplicativo OpenTasks não está instalado ou está inacessível, não sendo possível sincronizar suas listas de tarefas com o DAVdroid. Reinstale o DAVdroid e então adicione suas contas novamente depois da instalação do OpenTasks.</string>
<string name="startup_opentasks_not_installed_message">O aplicativo OpenTasks não está disponível, não sendo possível sincronizar as listas de tarefas pelo DAVdroid.</string>
<string name="startup_opentasks_reinstall_davdroid">Depois da instalação do OpenTasks, torna-se necessário REINSTALAR o DAVdroid e adicionar suas contas novamente (erro do Android).</string>
<string name="startup_opentasks_not_installed_install">Instalar o OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Termos da Licença</string>
@@ -43,6 +49,8 @@
<string name="navigation_drawer_forums">Comunidade</string>
<string name="navigation_drawer_donate">Doações</string>
<string name="account_list_empty">Bem-vindo ao DAVdroid!\n\nVocê pode adicionar uma conta CalDAV/CardDAV agora.</string>
<string name="accounts_global_sync_disabled">A sincronização automática pelo sistema está desativada</string>
<string name="accounts_global_sync_enable">Ativar</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Falha na detecção do serviço</string>
<string name="dav_service_refresh_couldnt_refresh">Não foi possível atualizar a lista da coleção</string>
@@ -52,23 +60,32 @@
<string name="app_settings_reset_hints">Restaurar sugestões</string>
<string name="app_settings_reset_hints_summary">Restaura as sugestões que foram descartadas anteriormente</string>
<string name="app_settings_reset_hints_success">Todas as sugestões serão exibidas novamente</string>
<string name="app_settings_connection">Conexão</string>
<string name="app_settings_override_proxy">Substituir as configurações de proxy</string>
<string name="app_settings_override_proxy_on">Usar configurações de proxy personalizadas</string>
<string name="app_settings_override_proxy_off">Usar configurações de proxy padrão do sistema</string>
<string name="app_settings_override_proxy_host">Nome do servidor proxy HTTP</string>
<string name="app_settings_override_proxy_port">Porta do proxy HTTP</string>
<string name="app_settings_security">Segurança</string>
<string name="app_settings_reset_trusted_certificates">Restaurar certificados confiáveis</string>
<string name="app_settings_reset_trusted_certificates_summary">Esquece de todos os certificados que foram aceitos anteriormente</string>
<plurals name="app_settings_reset_trusted_certificates_success">
<item quantity="one">Retirada a confiança de um certificado</item>
<item quantity="other">Retirada a confiança de %d certificados</item>
</plurals>
<string name="app_settings_distrust_system_certs">Desconfiar dos certificados de sistema</string>
<string name="app_settings_distrust_system_certs_on">ACs adicionadas pelo usuário e pelo sistema não serão confiáveis</string>
<string name="app_settings_distrust_system_certs_off">ACs adicionadas pelo usuário e pelo sistema serão confiáveis (recomendado)</string>
<string name="app_settings_reset_certificates">Restaurar certificados não-confiáveis</string>
<string name="app_settings_reset_certificates_summary">Restaura a confiança de todos os certificados personalizados</string>
<string name="app_settings_reset_certificates_success">Todos os certificados personalizados foram restaurados</string>
<string name="app_settings_debug">Depuração</string>
<string name="app_settings_log_to_external_storage">Registrar em arquivo externo</string>
<string name="app_settings_log_to_external_storage_on">Registrando no armazenamento externo (se disponível)</string>
<string name="app_settings_log_to_external_storage_off">O registro em arquivo externo está desativado</string>
<string name="app_settings_show_debug_info">Mostrar informações de depuração</string>
<string name="app_settings_show_debug_info_details">Exibir/compartilhar o software e os detalhes da configuração</string>
<string name="app_settings_show_debug_info_details">Exibe/compartilha o software e os detalhes da configuração</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Sincronizar agora</string>
<string name="account_synchronizing_now">Sincronizando</string>
<string name="account_settings">Configurações da conta</string>
<string name="account_rename">Renomear conta</string>
<string name="account_rename_new_name">Dados locais que não foram salvos podem ser descartados. É necessário efetuar uma nova sincronização após renomear. Novo nome da conta:</string>
<string name="account_rename_rename">Renomear</string>
<string name="account_delete">Excluir conta</string>
<string name="account_delete_confirmation_title">Deseja excluir a conta?</string>
<string name="account_delete_confirmation_text">Todas as cópias locais dos livros de endereços, calendários e listas de tarefas serão excluídas.</string>
@@ -76,6 +93,17 @@
<string name="account_create_new_address_book">Criar novo livro de endereços</string>
<string name="account_refresh_calendar_list">Atualizar lista de calendários</string>
<string name="account_create_new_calendar">Criar novo calendário</string>
<!--PermissionsActivity-->
<string name="permissions_title">Permissões do DAVdroid</string>
<string name="permissions_calendar">Permissões do calendário</string>
<string name="permissions_calendar_details">Para sincronizar os eventos CalDAV com seus calendários locais, o DAVdroid precisa acessar seus calendários.</string>
<string name="permissions_calendar_request">Solicitar permissão do calendário</string>
<string name="permissions_contacts">Permissões dos contados</string>
<string name="permissions_contacts_details">Para sincronizar livros de endereços CardDAV com seus contatos locais, o DAVdroid precisa acessar seus contatos.</string>
<string name="permissions_contacts_request">Solicitar permissão dos contatos</string>
<string name="permissions_opentasks">Permissões do OpenTasks</string>
<string name="permissions_opentasks_details">Para sincronizar tarefas CalDAV com suas listas de tarefas locais, o DAVdroid precisa acessar o OpenTasks.</string>
<string name="permissions_opentasks_request">Solicitar permissão do OpenTasks</string>
<!--AddAccountActivity-->
<string name="login_title">Adicionar conta</string>
<string name="login_type_email">Autenticação com endereço de e-mail</string>
@@ -89,12 +117,12 @@
<string name="login_user_name">Usuário</string>
<string name="login_user_name_required">É necessário um nome de usuário</string>
<string name="login_base_url">URL base</string>
<string name="login_auth_preemptive">Autenticação preferencial (recomendado, mas incompatível com autenticação Digest)</string>
<string name="login_login">Autenticar</string>
<string name="login_back">Voltar</string>
<string name="login_create_account">Criar conta</string>
<string name="login_account_name">Nome da conta</string>
<string name="login_account_name_info">Use seu endereço de e-mail como nome da conta porque o Android irá usar esse nome como campo AGENDA nos eventos que você criar. Não é possível ter duas contas com o mesmo nome.</string>
<string name="login_account_contact_group_method">Método do grupo Contato:</string>
<string name="login_account_name_required">É necessário um nome de conta</string>
<string name="login_account_not_created">A conta não pôde ser criada</string>
<string name="login_configuration_detection">Detecção de configuração</string>
@@ -109,9 +137,6 @@
<string name="settings_password">Senha</string>
<string name="settings_password_summary">Atualize a senha de acordo com seu servidor</string>
<string name="settings_enter_password">Digite sua senha:</string>
<string name="settings_preemptive">Autenticação preferencial</string>
<string name="settings_preemptive_on">Credenciais enviadas a cada requisição (recomendado)</string>
<string name="settings_preemptive_off">Credenciais enviadas após a requisição do servidor</string>
<string name="settings_sync">Sincronização</string>
<string name="settings_sync_interval_contacts">Intervalo sinc. de contatos</string>
<string name="settings_sync_summary_manually">Apenas manualmente</string>
@@ -139,16 +164,34 @@
<item>A cada 4 horas</item>
<item>Uma vez por dia</item>
</string-array>
<string name="settings_sync_time_range_past">Limite para eventos passados</string>
<string name="settings_sync_wifi_only">Sincronizar apenas por Wi-Fi</string>
<string name="settings_sync_wifi_only_on">Sincronização restrita a conexões Wi-Fi</string>
<string name="settings_sync_wifi_only_off">O tipo de conexão não é considerado</string>
<string name="settings_sync_wifi_only_ssid">Restrição de SSID Wi-Fi</string>
<string name="settings_sync_wifi_only_ssid_on">Sincronizará apenas com %s</string>
<string name="settings_sync_wifi_only_ssid_off">Qualquer conexão Wi-Fi pode ser utilizada</string>
<string name="settings_sync_wifi_only_ssid_message">Informe o nome da rede Wi-Fi (SSID) que será usada para sincronização ou deixe em branco para usar qualquer conexão Wi-Fi.</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Método do grupo Contato</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>Grupos são VCards separados</item>
<item>Grupos são categorias por contato</item>
</string-array>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Limite de tempo para eventos passados</string>
<string name="settings_sync_time_range_past_none">Todos os eventos serão sincronizados</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Os eventos que ocorreram a mais de um dia serão ignorados</item>
<item quantity="other">Eventos que ocorreram a mais de %d dias serão ignorados</item>
</plurals>
<string name="settings_sync_time_range_past_message">Os eventos que ocorreram antes desse número de dias serão ignorados (pode ser 0). Deixe em branco para sincronizar todos os eventos.</string>
<string name="settings_version_update">Atualização da versão do DAVdroid</string>
<string name="settings_version_update_settings_updated">As configurações internas foram atualizadas.</string>
<string name="settings_version_update_install_hint">Problemas? Desinstale o DAVdroid e instale novamente.</string>
<string name="settings_manage_calendar_colors">Gerenciar cores dos calendários</string>
<string name="settings_manage_calendar_colors_on">Cores dos calendários definidas pelo DAVdroid</string>
<string name="settings_manage_calendar_colors_off">Cores dos calendários não definidas pelo DAVdroid</string>
<!--collection management-->
<string name="create_addressbook">Criar livro de endereços</string>
<string name="create_addressbook_display_name_hint">Meu livro de endereços</string>
@@ -177,6 +220,8 @@
<string name="exception_show_details">Mostrar detalhes</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Informações de depuração</string>
<string name="sync_error_permissions">Permissões do DAVdroid</string>
<string name="sync_error_permissions_text">É necessário permissões adicionais</string>
<string name="sync_error_calendar">Falha na sincronização do calendário (%s)</string>
<string name="sync_error_contacts">Falha na sincronização do livro de endereços (%s)</string>
<string name="sync_error_tasks">Falha na sincronização das tarefas (%s)</string>
@@ -185,7 +230,7 @@
<string name="sync_error_local_storage">Erro do banco de dados ao %s</string>
<string-array name="sync_error_phases">
<item>preparando sincronização</item>
<item>procurando capacidades</item>
<item>procurando habilidades</item>
<item>processando os itens excluídos localmente</item>
<item>preparando os itens criados/modificados</item>
<item>enviando os itens criados/modificados</item>
@@ -194,7 +239,11 @@
<item>listando os itens remotos</item>
<item>comparando os itens locais/remotos</item>
<item>baixando os itens remotos</item>
<item>pós-processamento</item>
<item>salvando o estado da sincronização</item>
</string-array>
<string name="sync_error_unauthorized">Usuário/senha incorreto</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: Segurança da conexão</string>
<string name="trust_certificate_unknown_certificate_found">O DAVdroid encontrou um certificado desconhecido. Deseja torná-lo confiável?</string>
</resources>

View File

@@ -10,10 +10,10 @@
<!--DavService-->
<!--AppSettingsActivity-->
<!--AccountActivity-->
<!--PermissionsActivity-->
<!--AddAccountActivity-->
<string name="login_type_email">Login com seu enderço de email</string>
<string name="login_type_url">Login com URL e nome do usuário</string>
<string name="login_auth_preemptive">Autenticação preferida (recomendado, mas incompatível com Digest)</string>
<!--AccountSettingsActivity-->
<string name="settings_authentication">Autenticação</string>
<string name="settings_username">Nome de usuário</string>
@@ -21,9 +21,6 @@
<string name="settings_password">Senha</string>
<string name="settings_password_summary">Atualize sua denha de acordo com a do seu servidor.</string>
<string name="settings_enter_password">Informe sua senha:</string>
<string name="settings_preemptive">Pre-emptive authentication</string>
<string name="settings_preemptive_on">Credenciais são enviadas com cadas requisição (recomendado)</string>
<string name="settings_preemptive_off">Credenciais são enviadas apos o servidor requisitar</string>
<string name="settings_sync">Sincronização</string>
<string name="settings_sync_interval_contacts">Intervalo de sincronização dos contatos</string>
<string name="settings_sync_summary_manually">Manualmente apenas</string>
@@ -53,4 +50,5 @@
<!--collection management-->
<!--ExceptionInfoFragment-->
<!--sync errors and DebugInfoActivity-->
<!--cert4android-->
</resources>

View File

@@ -0,0 +1,243 @@
<?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">Помощь</string>
<string name="manage_accounts">Управление аккаунтами</string>
<string name="please_wait">Пожалуйста подождите...</string>
<string name="send">Отправить</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Оптимизация батареи</string>
<string name="startup_battery_optimization_message">Андроид может отключить/уменьшить синхронизацию DAVdroid через несколько дней. Чтобы этого не произошло, выключите оптимизацию батареи.</string>
<string name="startup_battery_optimization_disable">Отключить для DAVdroid</string>
<string name="startup_dont_show_again">Больше не показывать</string>
<string name="startup_development_version">DAVdroid Предварительный Релиз</string>
<string name="startup_development_version_message">Это разрабатываемая версия DAVdroid. Она может не работать как Вы ожидаете. Пожалуйста, присылайте нам конструктивные отзывы, чтобы улучшить DAVdroid.</string>
<string name="startup_development_version_give_feedback">Оставить отзыв</string>
<string name="startup_donate">Open-Source информация</string>
<string name="startup_donate_message">Мы рады, что вы используете DAVdroid, который является программным обеспечением с открытым исходным кодом (GPLv3). Разработка DAVdroid является сложной задачей, потребовавшей от нас тысяч рабочих часов. Пожалуйста, рассмотрите возможность поддержать проект.</string>
<string name="startup_donate_now">Поддержать проект</string>
<string name="startup_donate_later">Возможно, позже</string>
<string name="startup_google_play_accounts_removed">Информация об ошибке в Play Store DRM</string>
<string name="startup_google_play_accounts_removed_message">При определённых условиях Play Store DRM может стать причиной потери всех DAVdroid аккаунтов после перезагрузки устройства или после обновления DAVdroid. Если Вы столкнулись с этой проблемой (и только в этом случае), установите \"DAVdroid JB Workaround\" из Play Store.</string>
<string name="startup_google_play_accounts_removed_more_info">Дополнительно</string>
<string name="startup_opentasks_not_installed">OpenTasks не установлен</string>
<string name="startup_opentasks_not_installed_message">OpenTasks недоступен, DAVdroid не сможет синхронизировать список задач</string>
<string name="startup_opentasks_reinstall_davdroid">После установки OpenTasks необходимо ПЕРЕУСТАНОВИТЬ DAVdroid и добавить Ваши аккаунты ещё раз (ошибка Android).</string>
<string name="startup_opentasks_not_installed_install">Установить OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Лицензионное соглашение</string>
<string name="about_license_info_no_warranty">Эта программа поставляется АБСОЛЮТНО БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ. Это свободная программа, и вы приглашаетесь повторно распространять ее при определенных условиях.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">Файл логов DAVdroid</string>
<string name="logging_to_external_storage">Сохранение логов во внешнем хранилище: %s</string>
<string name="logging_to_external_storage_warning">Удалять логи насколько возможно быстро!</string>
<string name="logging_couldnt_create_file">Не удалось создать внешний лог файл: %s</string>
<string name="logging_no_external_storage">Внешнее хранилище не найдено</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">Открыть панель навигации</string>
<string name="navigation_drawer_close">Закрыть панель навигации</string>
<string name="navigation_drawer_subtitle">Адаптер синхронизации CalDAV/CardDAV</string>
<string name="navigation_drawer_about">О программе / Лицензия</string>
<string name="navigation_drawer_settings">Настройки</string>
<string name="navigation_drawer_news_updates">Новости и обновления</string>
<string name="navigation_drawer_external_links">Внешние ссылки</string>
<string name="navigation_drawer_website">Веб сайт</string>
<string name="navigation_drawer_faq">ЧАВО</string>
<string name="navigation_drawer_forums">Сообщество</string>
<string name="navigation_drawer_donate">Пожертвовать</string>
<string name="account_list_empty">Вас приветствует DAVdroid\n\nМожете добавить CalDAV/CardDAV аккаунт сейчас.</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Не удалось обнаружить сервисы</string>
<string name="dav_service_refresh_couldnt_refresh">Невозможно обновить список коллекций</string>
<!--AppSettingsActivity-->
<string name="app_settings">Настройки</string>
<string name="app_settings_user_interface">Интерфейс пользователя</string>
<string name="app_settings_reset_hints">Включить подсказки</string>
<string name="app_settings_reset_hints_summary">Включить подсказки, которые были отключены ранее</string>
<string name="app_settings_reset_hints_success">Все подсказки будут показаны снова</string>
<string name="app_settings_connection">Соединение</string>
<string name="app_settings_override_proxy">Переопределить настройки прокси-сервера</string>
<string name="app_settings_override_proxy_on">Использовать пользовательские настройки прокси-сервера</string>
<string name="app_settings_override_proxy_off">Использовать системные настройки прокси-сервера</string>
<string name="app_settings_override_proxy_host">Имя хоста HTTP прокси-сервера</string>
<string name="app_settings_override_proxy_port">Порт HTTP прокси-сервера</string>
<string name="app_settings_security">Безопасность</string>
<string name="app_settings_distrust_system_certs">Не доверять системным сертификатам</string>
<string name="app_settings_distrust_system_certs_on">Не доверять системным и добавленным пользователем CA</string>
<string name="app_settings_distrust_system_certs_off">Доверять системным и добавленным пользователем CA (рекомендуется)</string>
<string name="app_settings_reset_certificates">Сброс (не)доверенных сертификатов</string>
<string name="app_settings_reset_certificates_summary">Отменить доверие ко всем пользовательским сертификатам</string>
<string name="app_settings_reset_certificates_success">Все пользовательские сертификаты были очищены</string>
<string name="app_settings_debug">Отладка</string>
<string name="app_settings_log_to_external_storage">Сохранять лог во внешний файл</string>
<string name="app_settings_log_to_external_storage_on">Сохранение логов во внешнем хранилище (если доступно)</string>
<string name="app_settings_log_to_external_storage_off">Сохранение логов во внешний файл отключено</string>
<string name="app_settings_show_debug_info">Отладочная информация</string>
<string name="app_settings_show_debug_info_details">Просмотреть/поделиться программой и настройками</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Синхронизировать</string>
<string name="account_synchronizing_now">Синхронизация</string>
<string name="account_settings">Настройки аккаунта</string>
<string name="account_rename">Переименовать аккаунт</string>
<string name="account_rename_new_name">Несохранённые локальные данные могут быть потеряны. Требуется выполнить синхронизацию после переименования. Новое имя аккаунта:</string>
<string name="account_rename_rename">Переименовать</string>
<string name="account_delete">Удалить аккаунт</string>
<string name="account_delete_confirmation_title">Вы действительно хотите удалить аккаунт?</string>
<string name="account_delete_confirmation_text">Все локальные копии контактов, календарей и задач будут удалены.</string>
<string name="account_refresh_address_book_list">Обновить список адресных книг</string>
<string name="account_create_new_address_book">Создать новую адресную книгу</string>
<string name="account_refresh_calendar_list">Обновить календарь</string>
<string name="account_create_new_calendar">Создать новый календарь</string>
<!--PermissionsActivity-->
<string name="permissions_title">Разрешения DAVdroid</string>
<string name="permissions_calendar">Разрешения для календаря</string>
<string name="permissions_calendar_details">Для синхронизации CalDAV событий с Вашими локальными календарями DAVdroid должен иметь доступ к Вашим календарям.</string>
<string name="permissions_calendar_request">Запрос разрешений для календаря</string>
<string name="permissions_contacts">Разрешения для контактов</string>
<string name="permissions_contacts_details">Для синхронизации CardDAV адресных книг с Вашими локальными контактами DAVdroid должен иметь доступ к Вашим контактам.</string>
<string name="permissions_contacts_request">Запрос разрешений для контактов</string>
<string name="permissions_opentasks">Разрешения OpenTasks</string>
<string name="permissions_opentasks_details">Для синхронизации CalDAV задач с Вашими локальными списками задач DAVdroid должен иметь доступ к OpenTasks.</string>
<string name="permissions_opentasks_request">Запрос разрешений для OpenTasks</string>
<!--AddAccountActivity-->
<string name="login_title">Добавить аккаунт</string>
<string name="login_type_email">Вход по адресу электронной почты</string>
<string name="login_email_address">Адрес электронной почты</string>
<string name="login_email_address_error">Требуется действующий адрес электронной почты</string>
<string name="login_password">Пароль</string>
<string name="login_password_required">Требуется пароль</string>
<string name="login_type_url">Вход через URL и имя пользователя</string>
<string name="login_url_must_be_http_or_https">URL должен начинаться с http(s)://</string>
<string name="login_url_host_name_required">Требуется имя хоста</string>
<string name="login_user_name">Имя</string>
<string name="login_user_name_required">Требуется имя</string>
<string name="login_base_url">Базовый URL</string>
<string name="login_login">Логин</string>
<string name="login_back">Назад</string>
<string name="login_create_account">Создать аккаунт</string>
<string name="login_account_name">Имя аккаунта</string>
<string name="login_account_name_info">Используйте Ваш адрес адрес электронной почты в качестве имени аккаунта, так как Android будет использовать имя аккаунта в поле ORGANIZER для событий, которые Вы создаёте. Вы не можете иметь два аккаунта с одинаковыми именами.</string>
<string name="login_account_contact_group_method">Метод группировки контактов:</string>
<string name="login_account_name_required">Требуется имя аккаунта</string>
<string name="login_account_not_created">Аккаунт не может быть создан</string>
<string name="login_configuration_detection">Обнаружение конфигурации</string>
<string name="login_querying_server">Пожалуйста, подождите, выполняется запрос к серверу...</string>
<string name="login_no_caldav_carddav">Не найдены CalDAV или CardDAV сервисы.</string>
<string name="login_view_logs">Просмотр логов</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Настройки: %s</string>
<string name="settings_authentication">Аутентификация</string>
<string name="settings_username">Имя</string>
<string name="settings_enter_username">Введите имя пользователя:</string>
<string name="settings_password">Пароль</string>
<string name="settings_password_summary">Обновить пароль в соответствии с вашим сервером.</string>
<string name="settings_enter_password">Введите свой пароль:</string>
<string name="settings_sync">Синхронизация</string>
<string name="settings_sync_interval_contacts">Интервал синхронизации контактов</string>
<string name="settings_sync_summary_manually">Вручную</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Каждые %d минут и немедленно при локальных изменениях</string>
<string name="settings_sync_summary_not_available">Недоступно</string>
<string name="settings_sync_interval_calendars">Интервал синхронизации календарей</string>
<string name="settings_sync_interval_tasks">Интервал синхронизации задач</string>
<string-array name="settings_sync_interval_seconds">
<item>-1</item>
<item>300</item>
<item>600</item>
<item>900</item>
<item>3600</item>
<item>7200</item>
<item>14400</item>
<item>86400</item>
</string-array>
<string-array name="settings_sync_interval_names">
<item>Вручную</item>
<item>Каждые 5 минут</item>
<item>Каждые 10 минут</item>
<item>Каждые 15 минут</item>
<item>Каждый час</item>
<item>Каждые 2 часа</item>
<item>Каждые 4 часа</item>
<item>Раз в сутки</item>
</string-array>
<string name="settings_sync_wifi_only">Синхронизировать только через WiFi</string>
<string name="settings_sync_wifi_only_on">Разрешить синхронизацию только через WiFi</string>
<string name="settings_sync_wifi_only_off">Не учитывать тип соединения</string>
<string name="settings_sync_wifi_only_ssid">Разрешённые WiFi SSID</string>
<string name="settings_sync_wifi_only_ssid_on">Синхронизировать только через %s</string>
<string name="settings_sync_wifi_only_ssid_off">Могут использоваться все WiFi подключения</string>
<string name="settings_sync_wifi_only_ssid_message">Укажите имя WiFi сети (SSID), чтобы разрешить синхронизацию только через эту сеть. Или оставьте пустым для синхронизации через любые WiFi соединения.</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Метод группировки контактов</string>
<string-array name="settings_contact_group_method_entries">
<item>Группы как отдельные vCards</item>
<item>Группы как категории внутри контакта</item>
</string-array>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Интервал синхронизации</string>
<string name="settings_sync_time_range_past_none">Все события будут синхронизированы</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">События старше одного дня будут игнорироваться</item>
<item quantity="few">События старше %d дней будут игнорироваться</item>
<item quantity="many">События старше %d дней будут игнорироваться</item>
<item quantity="other">События старше %d дней будут игнорироваться</item>
</plurals>
<string name="settings_sync_time_range_past_message">События старше указанного количества дней будут игнорироваться (может быть 0). Оставьте пустым для синхронизации всех событий.</string>
<string name="settings_manage_calendar_colors">Управление цветами календаря</string>
<string name="settings_manage_calendar_colors_on">Цвета календаря управляются DAVdroid</string>
<string name="settings_manage_calendar_colors_off">Цвета календаря не управляются DAVdroid</string>
<!--collection management-->
<string name="create_addressbook">Создать адресную книгу</string>
<string name="create_addressbook_display_name_hint">Моя Адресная книга</string>
<string name="create_calendar">Создать CalDAV коллекцию</string>
<string name="create_calendar_display_name_hint">Мой Календарь</string>
<string name="create_calendar_time_zone">Часовой пояс:</string>
<string name="create_calendar_type">Тип коллекции:</string>
<string name="create_calendar_type_only_events">Календарь (только события)</string>
<string name="create_calendar_type_only_tasks">Список задач (только задачи)</string>
<string name="create_calendar_type_events_and_tasks">Совмещённый (события и задачи)</string>
<string name="create_collection_color">Установить цвет коллекции</string>
<string name="create_collection_creating">Создание коллекции</string>
<string name="create_collection_display_name">Отображаемое имя (название) этой коллекции:</string>
<string name="create_collection_display_name_required">Требуется название</string>
<string name="create_collection_description">Описание (необязательно):</string>
<string name="create_collection_home_set">Главная папка:</string>
<string name="create_collection_create">Создать</string>
<string name="delete_collection">Удалить коллекцию</string>
<string name="delete_collection_confirm_title">Вы уверены?</string>
<string name="delete_collection_confirm_warning">Коллекция (%s) и все её данные будут удалены с сервера.</string>
<string name="delete_collection_deleting_collection">Удаление коллекции</string>
<!--ExceptionInfoFragment-->
<string name="exception">Произошла ошибка.</string>
<string name="exception_httpexception">Произошла ошибка HTTP</string>
<string name="exception_ioexception">Произошла ошибка ввода/вывода.</string>
<string name="exception_show_details">Подробнее</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Отладочная информация</string>
<string name="sync_error_permissions">Разрешения DAVdroid</string>
<string name="sync_error_permissions_text">Требуются дополнительные разрешения</string>
<string name="sync_error_calendar">Ошибка при синхронизации Календаря (%s)</string>
<string name="sync_error_contacts">Ошибка при синхронизации Контактов (%s)</string>
<string name="sync_error_tasks">Ошибка при синхронизации Задачи (%s)</string>
<string name="sync_error">Ошибка %s</string>
<string name="sync_error_http_dav">Ошибка сервера %s</string>
<string name="sync_error_local_storage">Ошибка базы данных %s</string>
<string-array name="sync_error_phases">
<item>подготовка синхронизации</item>
<item>запрос поддерживаемых возможностей</item>
<item>обработка локально удалённых записей</item>
<item>подготовка созданных/изменённых записей</item>
<item>загрузка созданных/изменённых записей</item>
<item>проверка статуса синхронизации</item>
<item>просмотр локальных записей</item>
<item>просмотр серверных записей</item>
<item>сравнение локальных/серверных записей</item>
<item>загрузка серверных записей</item>
<item>пост-обработка</item>
<item>сохранение статуса синхронизации</item>
</string-array>
<string name="sync_error_unauthorized">Имя пользователя/пароль неверные</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: Безопасность подключения</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid обнаружил неизвестный сертификат. Вы хотите доверять ему?</string>
</resources>

View File

@@ -2,11 +2,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="app_name">ДАВдроид</string>
<string name="account_title_address_book">ДАВдроид адресар</string>
<string name="address_books_authority_title">Адресари</string>
<string name="help">Помоћ</string>
<string name="manage_accounts">Управљај налозима</string>
<string name="please_wait">Сачекајте…</string>
<string name="send">Пошаљи</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Оптимизација батерије</string>
<string name="startup_battery_optimization_message">Андроид може да искључи/умањи синхронизацију ДАВдроида након неколико дана. Да бисте спречили ово, искључите оптимизацију батерије.</string>
<string name="startup_battery_optimization_disable">Искључи за ДАВдроид</string>
<string name="startup_dont_show_again">Не приказуј поново</string>
<string name="startup_development_version">ДАВдроид прелиминарно издање</string>
<string name="startup_development_version_message">Ово је развојно издање ДАВдроида. Имајте на уму да можда неће радити очекивано. Замољавамо вас за конструктивне повратне информације како бисмо га побољшали.</string>
@@ -19,9 +24,12 @@
<string name="startup_google_play_accounts_removed_message">Под одређеним околностима ДРМ Плеј продавнице може да узрокује да сви налози ДАВдроида нестане након рестарта или ажурирања ДАВдроида. Ако и само ако имате овај проблем, инсталирајте „DAVdroid JB Workaround“ са Плеј продавнице.</string>
<string name="startup_google_play_accounts_removed_more_info">Још информација</string>
<string name="startup_opentasks_not_installed">Отворени задаци нису инсталирани</string>
<string name="startup_opentasks_not_installed_message">Отворени задаци нису инсталирани или доступни, па ДАВдроид неће моћи да синхронизује листе задатака. Поново инсталирајте ДАВдроид и поново додајте ваше налоге након инсталације Отворених задатака.</string>
<string name="startup_opentasks_not_installed_message">Отворени задаци нису доступни па ДАВдроид неће моћи да синхронизује листе задатака. </string>
<string name="startup_opentasks_reinstall_davdroid">Након инсталирања Отворених задатака, морате поново да инсталирате ДАВдроид и поново додате ваше налоге (због грешке у Андроиду).</string>
<string name="startup_opentasks_not_installed_install">Инсталирај Отворене задатке</string>
<!--AboutActivity-->
<string name="about_license_terms">Услови лиценце</string>
<string name="about_license_info_no_warranty">Овај програм НЕМА НИКАКВЕ ГАРАНЦИЈЕ. Бесплатан је софтвер којег можете слободно да делите под одређеним условима.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">ДАВдроид евиденција</string>
<string name="logging_to_external_storage">Уписивање евиденције у спољашње складиште: %s</string>
@@ -41,6 +49,8 @@
<string name="navigation_drawer_forums">Заједница</string>
<string name="navigation_drawer_donate">Донирај</string>
<string name="account_list_empty">Добро дошли у ДАВдроид!\n\nМожете сада да додате КалДАВ/КардДАВ налог.</string>
<string name="accounts_global_sync_disabled">Синхронизација је системски искључена</string>
<string name="accounts_global_sync_enable">Укључи</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Откривање услуге није успело</string>
<string name="dav_service_refresh_couldnt_refresh">Не могох да освежим списак збирки</string>
@@ -48,18 +58,34 @@
<string name="app_settings">Поставке</string>
<string name="app_settings_user_interface">Корисничко сучеље</string>
<string name="app_settings_reset_hints">Ресетуј савете</string>
<string name="app_settings_reset_hints_summary">Поновно приказивање претходно одбачених савета</string>
<string name="app_settings_reset_hints_success">Сви савети ће поново бити приказани</string>
<string name="app_settings_connection">Повезивање</string>
<string name="app_settings_override_proxy">Надјачај поставке проксија</string>
<string name="app_settings_override_proxy_on">Користи прилагођене поставке проксија</string>
<string name="app_settings_override_proxy_off">Користи системски подразумеване поставке проксија</string>
<string name="app_settings_override_proxy_host">ХТТП прокси домаћин</string>
<string name="app_settings_override_proxy_port">ХТТП прокси порт</string>
<string name="app_settings_security">Безбедност</string>
<string name="app_settings_reset_trusted_certificates">Ресетуј поуздане сертификате</string>
<string name="app_settings_distrust_system_certs">Посумњај у системске сертификате</string>
<string name="app_settings_distrust_system_certs_on">Системски и кориснички додати сертификати неће бити поуздани</string>
<string name="app_settings_distrust_system_certs_off">Системски и кориснички додати сертификати ће бити поуздани (препоручљиво)</string>
<string name="app_settings_reset_certificates">Ресетуј (не)поуздане сертификате</string>
<string name="app_settings_reset_certificates_summary">Ресетуј поуздање свих прилагођених сертификата</string>
<string name="app_settings_reset_certificates_success">Сви прилагођени сертификати су уклоњени</string>
<string name="app_settings_debug">Тражење грешака</string>
<string name="app_settings_log_to_external_storage">Уписуј у спољашњи фајл</string>
<string name="app_settings_log_to_external_storage_on">Уписивање евиденције у спољашње складиште (ако је доступно)</string>
<string name="app_settings_log_to_external_storage_off">Уписивање евиденције у спољашње складиште је искључено</string>
<string name="app_settings_show_debug_info">Прикажи податке за исправљање грешака</string>
<string name="app_settings_show_debug_info_details">Приказ/дељење детаља софтвера и поставки</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Синхронизуј одмах</string>
<string name="account_synchronizing_now">Синхронизујем</string>
<string name="account_settings">Поставке налога</string>
<string name="account_rename">Преименуј налог</string>
<string name="account_rename_new_name">Несачувани локални подаци могу бити изгубљни. Потребна је ресинхронизација након преименовања. Нови назив налога:</string>
<string name="account_rename_rename">Преименуј</string>
<string name="account_delete">Обриши налог</string>
<string name="account_delete_confirmation_title">Заиста обрисати налог?</string>
<string name="account_delete_confirmation_text">Све локалне копије адресара, календара и листи задатака ће бити обрисане.</string>
@@ -67,6 +93,17 @@
<string name="account_create_new_address_book">Направи нови адресар</string>
<string name="account_refresh_calendar_list">Освежи списак календара</string>
<string name="account_create_new_calendar">Направи нови календар</string>
<!--PermissionsActivity-->
<string name="permissions_title">ДАВдроид дозволе</string>
<string name="permissions_calendar">Дозволе за календар</string>
<string name="permissions_calendar_details">Да би синхронизовао КалДАВ догађаје са вашим локалним календарима, ДАВдроиду треба приступ вашим календарима.</string>
<string name="permissions_calendar_request">Захтевај дозволе за календар</string>
<string name="permissions_contacts">Дозволе за контакте</string>
<string name="permissions_contacts_details">Да би синхронизовао КардДАВ адресаре са вашим локалним контактима, ДАВдроиду треба приступ вашим контактима.</string>
<string name="permissions_contacts_request">Захтевај дозволе за контакте</string>
<string name="permissions_opentasks">Дозволе за задатке</string>
<string name="permissions_opentasks_details">Да би синхронизовао КалДАВ задатке са вашим локалним листама задатака, ДАВдроиду треба приступ Задацима.</string>
<string name="permissions_opentasks_request">Захтевај дозволе за задатке</string>
<!--AddAccountActivity-->
<string name="login_title">Додај налог</string>
<string name="login_type_email">Пријавите се адресом е-поште</string>
@@ -80,12 +117,12 @@
<string name="login_user_name">Корисничко име</string>
<string name="login_user_name_required">Корисничко име је обавезно</string>
<string name="login_base_url">Корени УРЛ</string>
<string name="login_auth_preemptive">Превентивна аутентификација (препоручено, али некомпатибилно са Дигест аутентификацијом)</string>
<string name="login_login">Пријава</string>
<string name="login_back">Назад</string>
<string name="login_create_account">Направи налог</string>
<string name="login_account_name">Назив налога</string>
<string name="login_account_name_info">Користите вашу е-адресу за назив налога јер Андроид користи назив налога за поље ОРГАНИЗАТОР за догађаје које направите. Не можете имати два налога истог назива.</string>
<string name="login_account_contact_group_method">Режим група контаката:</string>
<string name="login_account_name_required">Назив налога је обавезан</string>
<string name="login_account_not_created">Не могох направити налог</string>
<string name="login_configuration_detection">Откривање конфигурације</string>
@@ -100,9 +137,6 @@
<string name="settings_password">Лозинка</string>
<string name="settings_password_summary">Ажурирајте лозинку за ваш сервер.</string>
<string name="settings_enter_password">Унесите лозинку:</string>
<string name="settings_preemptive">Превентивна аутентификација</string>
<string name="settings_preemptive_on">Слање акредитива са сваким захтевом (препоручено)</string>
<string name="settings_preemptive_off">Слање акредитива по захтеву сервера</string>
<string name="settings_sync">Синхронизација</string>
<string name="settings_sync_interval_contacts">Интервал синх. контаката</string>
<string name="settings_sync_summary_manually">Само ручно</string>
@@ -130,9 +164,35 @@
<item>Свака 4 сата</item>
<item>Једном дневно</item>
</string-array>
<string name="settings_version_update">Надоградња ДАВдроид издања</string>
<string name="settings_version_update_settings_updated">Унутрашње поставке су ажуриране.</string>
<string name="settings_version_update_install_hint">Проблеми? Уклоните ДАВдроид па га поново инсталирајте.</string>
<string name="settings_sync_wifi_only">Само преко бежичног</string>
<string name="settings_sync_wifi_only_on">Синхронизовање само преко бежичних мрежа</string>
<string name="settings_sync_wifi_only_off">Тип везе није узет у обзир</string>
<string name="settings_sync_wifi_only_ssid">Ограничења ССИД-а бежичних</string>
<string name="settings_sync_wifi_only_ssid_on">Синхронизовање само преко %s</string>
<string name="settings_sync_wifi_only_ssid_off">Коришћење свих бежичних мрежа</string>
<string name="settings_sync_wifi_only_ssid_message">Унесите назив бежичне мреже (њен ССИД) да бисте ограничили синхронизацију само на ту мрежу, или оставите празно за синхронизовање преко било које бежичне мреже.</string>
<string name="settings_carddav">КардДАВ</string>
<string name="settings_contact_group_method">Режим група контаката</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>Групе су одвојене В-карте</item>
<item>Групе су категорије по контакту</item>
</string-array>
<string name="settings_caldav">КалДАВ</string>
<string name="settings_sync_time_range_past">Ограничење догађаја у прошлости</string>
<string name="settings_sync_time_range_past_none">Сви догађаји се синхронизују</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Догађаји старији од једног дана ће бити занемарени</item>
<item quantity="few">Догађаји старији од %d дана ће бити занемарени</item>
<item quantity="other">Догађаји старији од %d дана ће бити занемарени</item>
</plurals>
<string name="settings_sync_time_range_past_message">Догађаји старији од овог броја дана ће бити занемарени (може бити 0). Оставите празно за синхронизацију свих догађаја.</string>
<string name="settings_manage_calendar_colors">Управљај бојама календара</string>
<string name="settings_manage_calendar_colors_on">Бојама календара управља ДАВдроид</string>
<string name="settings_manage_calendar_colors_off">Боје календара није поставио ДАВдроид</string>
<!--collection management-->
<string name="create_addressbook">Направи адресар</string>
<string name="create_addressbook_display_name_hint">Мој адресар</string>
@@ -148,6 +208,7 @@
<string name="create_collection_display_name">Назив (наслов) ове збирке:</string>
<string name="create_collection_display_name_required">Наслов је обавезан</string>
<string name="create_collection_description">Опис (необавезан):</string>
<string name="create_collection_home_set">Домаћа фасцикла:</string>
<string name="create_collection_create">Направи</string>
<string name="delete_collection">Обриши збирку</string>
<string name="delete_collection_confirm_title">Да ли сте сигурни?</string>
@@ -160,6 +221,8 @@
<string name="exception_show_details">Прикажи детаље</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Подаци за исправљање грешака</string>
<string name="sync_error_permissions">ДАВдроид дозволе</string>
<string name="sync_error_permissions_text">Потребне су додатне доволе</string>
<string name="sync_error_calendar">Синхронизација календара није успела (%s)</string>
<string name="sync_error_contacts">Синхронизација адресара није успела (%s)</string>
<string name="sync_error_tasks">Синхронизација задатака није успела (%s)</string>
@@ -167,17 +230,21 @@
<string name="sync_error_http_dav">Грешка сервера током %s</string>
<string name="sync_error_local_storage">Грешка базе података током %s</string>
<string-array name="sync_error_phases">
<item>припреме синхронизације</item>
<item>упита за могућности</item>
<item>обраде локално обрисаних уноса</item>
<item>припреме направљених/измењених уноса</item>
<item>отпремања направљених/измењених уноса</item>
<item>провере стања синхронизације</item>
<item>излиставања локалних уноса</item>
<item>излиставања удаљених уноса</item>
<item>упоређивања локалних/удаљених уноса</item>
<item>преузимања удаљених уноса</item>
<item>уписа стања синхронизације</item>
<item>припремам синхронизацију</item>
<item>проверавам могућности</item>
<item>обрађујем локално обрисане уносе</item>
<item>припремам направљене/измењене уносе</item>
<item>отпремам направљене/измењене уносе</item>
<item>проверавам стање синхронизације</item>
<item>излиставам локалне уносе</item>
<item>излиставам удаљене уносе</item>
<item>упоређујем локалне/удаљене уносе</item>
<item>преузимам удаљене уносе</item>
<item>додатна обрада</item>
<item>уписујем стање синхронизације</item>
</string-array>
<string name="sync_error_unauthorized">Корисничко име или лозинка погрешни</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">ДАВдроид: Безбедност везе</string>
<string name="trust_certificate_unknown_certificate_found">ДАВдроид је наишао на непознат сертификат. Желите ли да се поуздате у њега?</string>
</resources>

View File

@@ -0,0 +1,201 @@
<?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">Yardım</string>
<string name="manage_accounts">Hesapları yönet</string>
<string name="please_wait">Lütfen bekle ...</string>
<string name="send">Gönder</string>
<!--startup dialogs-->
<string name="startup_dont_show_again">Bir daha gösterme</string>
<string name="startup_development_version">DAVDroid Önizlenim Dağıtımı</string>
<string name="startup_development_version_message">Bu DAVdroid\'in bir geliştirme sürümüdür. Bazı şeyler beklendiği gibi çalışmayabilir. DAVdroid\'i iyileştirmek için bize lütfen yapıcı eleştirilerini ilet.</string>
<string name="startup_development_version_give_feedback">Geribildirim ver</string>
<string name="startup_donate">ık-Kaynak Bilgisi</string>
<string name="startup_donate_message">ık kaynaklı yazılım (GPLv3) olan DAVdroid\'i kullandığına çok mutluyuz. DAVdroid\'i geliştirmek zor bir iş ve üzerinde binlerce saat çalıştığımızdan, lütfen bir bağışta bulunmayı düşün.</string>
<string name="startup_donate_now">Bağış sayfasını göster</string>
<string name="startup_donate_later">Belki sonra</string>
<string name="startup_google_play_accounts_removed">Play Store DRM hata bilgisi</string>
<string name="startup_google_play_accounts_removed_message">Bazı durumlarda Play Store DRM\'i, cihazı yeniden başlatınca veya DAVdroid\'i yükseltince DAVdroid hesaplarının yokolmasına sebep olabiliyor. Bu sorundan etkileniyorsan (ve sadece bu durumda), lütfen Play Store\'daki \"DAVdroid JB Workaround\" uygulamasını kur.</string>
<string name="startup_google_play_accounts_removed_more_info">Daha fazla bilgi</string>
<string name="startup_opentasks_not_installed">OpenTasks kurulu değil</string>
<string name="startup_opentasks_not_installed_message">OpenTasks uygulaması yok, dolayısıyla DAVdroid iş listelerini senkronize edemeyecektir.</string>
<string name="startup_opentasks_reinstall_davdroid">OpenTasks\'i kurduktan sonra, DAVdroid\'i YENİDEN KURMAN ve hesaplarını yeniden eklemen gerek. (Android hatası).</string>
<string name="startup_opentasks_not_installed_install">OpenTasks kur</string>
<!--AboutActivity-->
<string name="about_license_terms">Lisans şartları</string>
<string name="about_license_info_no_warranty">Bu uygulama HİÇ BİR GARANTİ ile gelmemektedir. Bedava bir yazılımdır ve belli koşullar altında dağıtabilirsiniz.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid dosya jurnallemesi</string>
<string name="logging_to_external_storage">Harici depolamaya jurnalleniyor: %s</string>
<string name="logging_to_external_storage_warning">Jurnalleri bir an önce silin!</string>
<string name="logging_couldnt_create_file">Harici jurnal dosyası yaratılamadı: %s </string>
<string name="logging_no_external_storage">Harici depolama alanı bulunamadı</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">Navigasyon çekmecesini aç</string>
<string name="navigation_drawer_close">Navigasyon çekmecesini kapat</string>
<string name="navigation_drawer_subtitle">CalDAV/CardDAV Senkronizasyon Adaptörü</string>
<string name="navigation_drawer_about">Hakkında / Lisans</string>
<string name="navigation_drawer_settings">Ayarlar</string>
<string name="navigation_drawer_news_updates">Haberler &amp; güncellemeler </string>
<string name="navigation_drawer_external_links">Harici bağlantılar</string>
<string name="navigation_drawer_website">Web sitesi</string>
<string name="navigation_drawer_faq">SSS</string>
<string name="navigation_drawer_forums">Camia</string>
<string name="navigation_drawer_donate">Bağış yap</string>
<string name="account_list_empty">DAVdroid\'e hoşgeldin!\n\nŞimdi bir CalDAV/CardDAV hesabı ekleyebilirsin.</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Servis keşfi başarısız</string>
<string name="dav_service_refresh_couldnt_refresh">Kolleksiyon listesi yenilenemedi</string>
<!--AppSettingsActivity-->
<string name="app_settings">Ayarlar</string>
<string name="app_settings_user_interface">Kullanıcı arayüzü</string>
<string name="app_settings_reset_hints">İpuçlarını sıfırla</string>
<string name="app_settings_reset_hints_summary">Daha önceden azat edilen ipuçlarını yeniden etkinleştirir</string>
<string name="app_settings_reset_hints_success">Tüm ipuçları artık gösterilecek</string>
<string name="app_settings_security">Güvenlik</string>
<string name="app_settings_debug">Hata ayıklama</string>
<string name="app_settings_log_to_external_storage">Harici dosyaya jurnalle</string>
<string name="app_settings_log_to_external_storage_on">Harici depolamaya jurnalleniyor (eğer uygunsa)</string>
<string name="app_settings_log_to_external_storage_off">Harici depolama mevcut değil</string>
<string name="app_settings_show_debug_info">Hata ayıklama bilgilerini göster</string>
<string name="app_settings_show_debug_info_details">Yazılım ve konfigürasyon detaylarına bak/paylaş</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Şimdi senkronize et</string>
<string name="account_synchronizing_now">Senkronize ediyor</string>
<string name="account_settings">Hesap ayarları</string>
<string name="account_delete">Hesabı sil</string>
<string name="account_delete_confirmation_title">Hesap gerçekten silinsin mi?</string>
<string name="account_delete_confirmation_text">Rehber, takvim ve iş listelerinin tüm yerel kopyaları silinecektir.</string>
<string name="account_refresh_address_book_list">Rehber listesini yenile</string>
<string name="account_create_new_address_book">Yeni rehber oluştur</string>
<string name="account_refresh_calendar_list">Takvim listesini yenile</string>
<string name="account_create_new_calendar">Yeni takvim oluştur</string>
<!--PermissionsActivity-->
<string name="permissions_title">DAVdroid izinleri</string>
<string name="permissions_calendar">Takvim izinleri</string>
<string name="permissions_calendar_details">Takvim (sadece olaylar)
</string>
<string name="permissions_calendar_request">Takvim izinleri iste</string>
<string name="permissions_contacts">Kişiler izinleri</string>
<string name="permissions_contacts_details">CardDAV rehberlerinin cihazınızdaki kişilerinizle senkronize edebilmek için, DAVdroid cihazınızdaki kişilerinize erişime ihtiyacı vardır.</string>
<string name="permissions_contacts_request">Kişiler izinleri iste</string>
<string name="permissions_opentasks">OpenTasks izinleri</string>
<string name="permissions_opentasks_details">CalDav iş listelerinizi yerel iş listelerinizle senkronize edebilmek için DAVdroid\'in OpenTasks\'e erişime ihtiyacı vardır.</string>
<string name="permissions_opentasks_request">OpenTasks izinleri iste</string>
<!--AddAccountActivity-->
<string name="login_title">Hesap ekle</string>
<string name="login_type_email">Eposta adresi ile giriş yap</string>
<string name="login_email_address">Eposta adresi</string>
<string name="login_email_address_error">Geçerli eposta adresi zorunludur</string>
<string name="login_password">Parola</string>
<string name="login_password_required">Parola zorunludur</string>
<string name="login_type_url">URL ve kullanıcı adı ile giriş yap</string>
<string name="login_url_must_be_http_or_https">URL http(s):// ile başlamalıdır</string>
<string name="login_url_host_name_required">Sunucu adı zorunludur</string>
<string name="login_user_name">Kullanıcı adı</string>
<string name="login_user_name_required">Kullanıcı adı zorunludur</string>
<string name="login_base_url">Baz URL</string>
<string name="login_login">Giriş</string>
<string name="login_back">Geri</string>
<string name="login_create_account">Hesap yarat</string>
<string name="login_account_name">Hesap adı</string>
<string name="login_account_name_info">Hesap ismi olarak e-posta adresini kullan çünkü Android hesap ismini yarattığın olaylarda DÜZENLEYEN alanında kullanacaktır. Aynı isimde iki faklı hesabın olamaz.</string>
<string name="login_account_name_required">Hesap adı zorunludur</string>
<string name="login_account_not_created">Hesap yaratılamadı</string>
<string name="login_configuration_detection">Konfigürasyon keşfi</string>
<string name="login_querying_server">Lütfen bekle, sunucu sorgulanıyor...</string>
<string name="login_no_caldav_carddav">CalDAV veya CardDAV servisi bulunamadı.</string>
<string name="login_view_logs">Jurnallere bak</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Ayarlar: %s</string>
<string name="settings_authentication">Doğrulama</string>
<string name="settings_username">Kullanıcı adı</string>
<string name="settings_enter_username">Kullanıcı adı girin:</string>
<string name="settings_password">Parola</string>
<string name="settings_password_summary">Parolayı sunucunuza göre güncelleyin.</string>
<string name="settings_enter_password">Parola girin:</string>
<string name="settings_sync">Senkronizasyon</string>
<string name="settings_sync_interval_contacts">Kişiler senk. aralığı</string>
<string name="settings_sync_summary_manually">Sadece elle</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Her %d dakika + yerel değişikliklerde hemen</string>
<string name="settings_sync_summary_not_available">Mevcut değil</string>
<string name="settings_sync_interval_calendars">Takvimler senk. aralığı</string>
<string name="settings_sync_interval_tasks">İşler senk. aralığı</string>
<string-array name="settings_sync_interval_seconds">
<item>-1</item>
<item>300</item>
<item>600</item>
<item>900</item>
<item>3600</item>
<item>7200</item>
<item>14400</item>
<item>86400</item>
</string-array>
<string-array name="settings_sync_interval_names">
<item>Sadece elle</item>
<item>Her 5 dakikada bir</item>
<item>Her 10 dakikada bir</item>
<item>Her 15 dakikada bir</item>
<item>Her saatte bir</item>
<item>Her 2 saatte bir</item>
<item>Her 4 saatte bir</item>
<item>Günde bir</item>
</string-array>
<string name="settings_sync_wifi_only">Sadece WiFi üzerinden senkronize et</string>
<string name="settings_sync_wifi_only_on">Senkronizasyon WiFi bağlantıları ile kısıtlıdır</string>
<string name="settings_sync_wifi_only_off">Bağlantı tipi göz önünde bulundurulmaz</string>
<string name="settings_sync_wifi_only_ssid">WiFi SSID kısıtlaması</string>
<string name="settings_sync_wifi_only_ssid_on">Sadece %s üzerinden senkronize olur</string>
<string name="settings_sync_wifi_only_ssid_off">Tüm WiFi bağlantıları kullanılabilir</string>
<string name="settings_sync_wifi_only_ssid_message">Senkronizasyonu sadece bir WiFi ağına kısıtlamak için bu ağın adını (SSID) gir, veya tüm WiFi bağlantıları için boş bırak.</string>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Geçmiş olay zaman sınırı</string>
<string name="settings_sync_time_range_past_none">Tüm olaylar senkronize edilecek</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="other">%d günden daha eski olaylar göz ardı edilecektir</item>
</plurals>
<string name="settings_sync_time_range_past_message">Bu sayıdan daha eski olan olaylar yok sayılacaktır (0 olabilir). Tüm olayları senkronize etmek için boş bırak.</string>
<string name="settings_manage_calendar_colors">Takvim renklerini yönet</string>
<string name="settings_manage_calendar_colors_on">Takvim renkleri DAVdroid tarafından yönetilmekte</string>
<string name="settings_manage_calendar_colors_off">Takvim renkleri DAVdroid tarafından ayarlanmadı</string>
<!--collection management-->
<string name="create_addressbook">Rehber yarat</string>
<string name="create_addressbook_display_name_hint">Benim Rehberim</string>
<string name="create_calendar">CalDav koleksiyonu yarat</string>
<string name="create_calendar_display_name_hint">Benim Takvimim</string>
<string name="create_calendar_time_zone">Saat dilimi:</string>
<string name="create_calendar_type">Koleksiyon Tipi:</string>
<string name="create_calendar_type_only_events">Takvim (sadece olaylar)</string>
<string name="create_calendar_type_only_tasks">İşler listesi (sadece işler)
</string>
<string name="create_calendar_type_events_and_tasks">Birleşik (olaylar ve işler)</string>
<string name="create_collection_color">Bir koleksiyon rengi tanımla</string>
<string name="create_collection_creating">Koleksiyon yaratılıyor</string>
<string name="create_collection_display_name">Bu koleksiyonun gösterilen adı (başlığı):</string>
<string name="create_collection_display_name_required">Başlık zorunlu</string>
<string name="create_collection_description">ıklama (isteğe bağlı):</string>
<string name="create_collection_home_set">Ev seti:</string>
<string name="create_collection_create">Yarat</string>
<string name="delete_collection">Koleksiyonu sil</string>
<string name="delete_collection_confirm_title">Emin misin?</string>
<string name="delete_collection_confirm_warning">Bu koleksiyon (%s) ve tüm verisi sunucudan silinecek.</string>
<string name="delete_collection_deleting_collection">Koleksiyon siliniyor</string>
<!--ExceptionInfoFragment-->
<string name="exception">Bir hata oluştu.</string>
<string name="exception_httpexception">Bir HTTP hatası oluştu.</string>
<string name="exception_ioexception">Bir I/O hatası oluştu.</string>
<string name="exception_show_details">Detayları göster</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Hata ayıklama bilgisi</string>
<string name="sync_error_permissions">DAVdroid izinleri</string>
<string name="sync_error_permissions_text">Ek izinler zorunludur</string>
<string name="sync_error_calendar">Takvim senkronizasyonu başarısız (%s)</string>
<string name="sync_error_contacts">Rehber senkronizasyonu başarısız (%s)</string>
<string name="sync_error_tasks">İş senkronizasyonu başarısız (%s)</string>
<string name="sync_error">%s yaparken hata</string>
<string name="sync_error_http_dav">%s yaparken sunucu hatası</string>
<string name="sync_error_local_storage">%s yaparken veritabanı hatası</string>
<string name="sync_error_unauthorized">Kullanıcı adı/parola yanlış</string>
<!--cert4android-->
</resources>

View File

@@ -0,0 +1,246 @@
<?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</string>
<string name="address_books_authority_title">Адресні книги</string>
<string name="help">Допомога</string>
<string name="manage_accounts">Керування обліковими записами</string>
<string name="please_wait">Будь ласка, зачекайте...</string>
<string name="send">Відправити</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Оптимізація енергоспоживання</string>
<string name="startup_battery_optimization_message">Android може вимкнути, чи призупинити синхронізацію DAVdroid через деякий час. Аби запобігти цьому, вимкніть оптимізацію енергоспоживання для додатку.</string>
<string name="startup_battery_optimization_disable">Вимкнути для DAVdroid</string>
<string name="startup_dont_show_again">Не показувати знову</string>
<string name="startup_development_version">DAVdroid попередній випуск</string>
<string name="startup_development_version_message">Це версія DAVdroid для розробників. Майте на увазі, дещо може працювати не так, як заплановано. Будь ласка, залиште конструктивний відгук для поліпшення DAVdroid.</string>
<string name="startup_development_version_give_feedback">Залишити відгук</string>
<string name="startup_donate">Інформація Open-Source</string>
<string name="startup_donate_message">Ми раді, що Ви використовуєте DAVdroid, який є програмним засобом з відкритим джерельним кодом (GPLv3). Розробка DAVdroid є досить складним завданням і потребує від нас тисячі годин роботи. Будь ласка, розгляньте можливість підтримати проект.</string>
<string name="startup_donate_now">Показати сторінку пожертви</string>
<string name="startup_donate_later">Можливо пізніше</string>
<string name="startup_google_play_accounts_removed">Інформація про ваду в Play Store DRM</string>
<string name="startup_google_play_accounts_removed_message">При деяких обставинах Play Store DRM може стати причиною втрати всіх облікових записів DAVdroid після перезавантаження пристрою чи оновлення DAVdroid. Якщо ви зіткнулися із цим (і лише у цьому випадку), будь ласка, встановіть \"DAVdroid JB Workaround\" з Play Store.</string>
<string name="startup_google_play_accounts_removed_more_info">Детальніше</string>
<string name="startup_opentasks_not_installed">OpenTasks не встановлено</string>
<string name="startup_opentasks_not_installed_message">Додаток OpenTasks не встановлено, ото ж DAVdroid не зможе синхронізувати завдання.</string>
<string name="startup_opentasks_reinstall_davdroid">Після встановлення OpenTasks, необхідно перевстановити DAVdroid та додати облікові записи знову (Вада системи Android).</string>
<string name="startup_opentasks_not_installed_install">Встановити OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Умови ліцензії</string>
<string name="about_license_info_no_warranty">Цей програмний засіб постачається АБСОЛЮТНО БЕЗ БУДЬ-ЯКИХ ГАРАНТІЙ. Це вільне програмне забезпечення, і ви можете поширювати її, за деякими умовами.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">Файл звітування DAVdroid</string>
<string name="logging_to_external_storage">Звітування до зовнішнього сховища: %s</string>
<string name="logging_to_external_storage_warning">Вилучіть звіт якомога швидше!</string>
<string name="logging_couldnt_create_file">Не вдалося створити файл зовнішнього звіту: %s</string>
<string name="logging_no_external_storage">Не знайдено зовнішнього сховища</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">Відкрити панель навігації</string>
<string name="navigation_drawer_close">Закрити панель навігації</string>
<string name="navigation_drawer_subtitle">Адаптер синхронізації CalDAV/CardDAV</string>
<string name="navigation_drawer_about">Про / Ліцензія</string>
<string name="navigation_drawer_settings">Налаштування</string>
<string name="navigation_drawer_news_updates">Новини та оновлення</string>
<string name="navigation_drawer_external_links">Зовнішні посилання</string>
<string name="navigation_drawer_website">Веб сайт</string>
<string name="navigation_drawer_faq">Питання/Відповіді</string>
<string name="navigation_drawer_forums">Спільнота</string>
<string name="navigation_drawer_donate">Підтримка</string>
<string name="account_list_empty">Вітаємо у DAVdroid!\n\nТепер можете додавати облікові записи CalDAV/CardDAV.</string>
<string name="accounts_global_sync_disabled">Автоматичну синхронізацію вимкнено зі сторони системи</string>
<string name="accounts_global_sync_enable">Увімкнути</string>
<!--DavService-->
<string name="dav_service_refresh_failed">Не вдалося виявити сервіси</string>
<string name="dav_service_refresh_couldnt_refresh">Не вдалося оновити перелік колекції</string>
<!--AppSettingsActivity-->
<string name="app_settings">Налаштування</string>
<string name="app_settings_user_interface">Інтерфейс користувача</string>
<string name="app_settings_reset_hints">Скинути підказки</string>
<string name="app_settings_reset_hints_summary">Включення підказок, які раніше були вимкнуті</string>
<string name="app_settings_reset_hints_success">Всі підказки будуть показані знову</string>
<string name="app_settings_connection">З\'єднання</string>
<string name="app_settings_override_proxy">Перевизначити налаштування проксі</string>
<string name="app_settings_override_proxy_on">Власні налаштування проксі</string>
<string name="app_settings_override_proxy_off">Типові системні налаштування проксі</string>
<string name="app_settings_override_proxy_host">Ім\'я хосту HTTP проксі</string>
<string name="app_settings_override_proxy_port">Порт HTTP проксі</string>
<string name="app_settings_security">Безпека</string>
<string name="app_settings_distrust_system_certs">Не довіряти системним сертифікатам</string>
<string name="app_settings_distrust_system_certs_on">Не довіряти системним та доданим користувачем сертифікатам</string>
<string name="app_settings_distrust_system_certs_off">Довіряти системним та доданим користувачем сертифікатам (рекомендується)</string>
<string name="app_settings_reset_certificates">Скидання (не)довірених сертифікатів</string>
<string name="app_settings_reset_certificates_summary">Скинути довіру до всіх призначених користувачу сертифікатів</string>
<string name="app_settings_reset_certificates_success">Всі сертифікати, що призначені користувачу очищено</string>
<string name="app_settings_debug">Зневадження</string>
<string name="app_settings_log_to_external_storage">Звіт до зовнішнього файлу</string>
<string name="app_settings_log_to_external_storage_on">Звітування до зовнішнього файлу (якщо доступно)</string>
<string name="app_settings_log_to_external_storage_off">Звітування у зовнішній файл вимкнено</string>
<string name="app_settings_show_debug_info">Показати інформацію зневадження</string>
<string name="app_settings_show_debug_info_details">Переглянути/поділитися програмним засобом та деталями конфігурації</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Синхронізувати зараз</string>
<string name="account_synchronizing_now">Синхронізація</string>
<string name="account_settings">Налаштування облікового запису</string>
<string name="account_rename">Перейменувати обліковий запис</string>
<string name="account_rename_new_name">Незбережені локальні дані можуть бути втрачені. Необхідно виконати синхронізацію після перейменування. Нова назва облікового запису:</string>
<string name="account_rename_rename">Перейменувати</string>
<string name="account_delete">Видалити запис</string>
<string name="account_delete_confirmation_title">Дійсно видалити обліковий запис?</string>
<string name="account_delete_confirmation_text">Всі локальні копії адресних книг, календарів та завдань будуть вилучені.</string>
<string name="account_refresh_address_book_list">Оновити перелік адресних книг</string>
<string name="account_create_new_address_book">Створити нову адресну книгу</string>
<string name="account_refresh_calendar_list">Оновити перелік каленарів</string>
<string name="account_create_new_calendar">Створити новий календар</string>
<!--PermissionsActivity-->
<string name="permissions_title">Дозволи DAVdroid</string>
<string name="permissions_calendar">Дозволи календаря</string>
<string name="permissions_calendar_details">Для синхронізації CalDAV подій з локальним календарем необхідний дозвіл до Ваших календарів.</string>
<string name="permissions_calendar_request">Запит дозволів календаря</string>
<string name="permissions_contacts">Дозволи контактів</string>
<string name="permissions_contacts_details">Для синхронізації адресної книги CalDAV з локальними контактами необхідний дозвіл до Ваших контактів.</string>
<string name="permissions_contacts_request">Запит дозволів контактів</string>
<string name="permissions_opentasks">Дозволи OpenTasks</string>
<string name="permissions_opentasks_details">Для синхронізації CalDAV завдань з локальним списком завдань необхідний дозвіл до OpenTasks.</string>
<string name="permissions_opentasks_request">Запит дозволів OpenTasks</string>
<!--AddAccountActivity-->
<string name="login_title">Додати запис</string>
<string name="login_type_email">Увійти за допомогою електронної пошти</string>
<string name="login_email_address">Адреса пошти</string>
<string name="login_email_address_error">Потребує валідну електронну адресу</string>
<string name="login_password">Пароль</string>
<string name="login_password_required">Потребує пароль</string>
<string name="login_type_url">Увійти за допомогою URL та імені користувача</string>
<string name="login_url_must_be_http_or_https">URL адреса повинна починатися з http(s)://</string>
<string name="login_url_host_name_required">Потребує назву хосту</string>
<string name="login_user_name">Ім\'я користувача</string>
<string name="login_user_name_required">Потребує ім\'я користувача</string>
<string name="login_base_url">Базовий URL</string>
<string name="login_login">Увійти</string>
<string name="login_back">Назад</string>
<string name="login_create_account">Створити запис</string>
<string name="login_account_name">Назва запису</string>
<string name="login_account_name_info">Використовуйте вашу електронну адресу як ім\'я облікового запису, так як Android буде використовувати ім\'я облікового запису в полі ORGANIZER для подій, які ви створюватимете. Ви не можете мати два облікових записи з однаковими іменами.</string>
<string name="login_account_contact_group_method">Метод групування контактів:</string>
<string name="login_account_name_required">Потребує назви облікового запису</string>
<string name="login_account_not_created">Обліковий запис не може бути створений</string>
<string name="login_configuration_detection">Виявлення конфігурації</string>
<string name="login_querying_server">Будь ласка, зачекайте, запит до серверу...</string>
<string name="login_no_caldav_carddav">Не вдалося знайти CalDAV чи CardDAV сервіс.</string>
<string name="login_view_logs">Переглянути звіти</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Налаштування: %s</string>
<string name="settings_authentication">Автентифікація</string>
<string name="settings_username">Ім\'я користувача</string>
<string name="settings_enter_username">Введіть ім\'я користувача:</string>
<string name="settings_password">Пароль</string>
<string name="settings_password_summary">Оновити пароль, згідно налаштувань Вашого сервера.</string>
<string name="settings_enter_password">Введіть Ваш пароль:</string>
<string name="settings_sync">Синхронізація</string>
<string name="settings_sync_interval_contacts">Інтервал синхронізації контактів</string>
<string name="settings_sync_summary_manually">Лише вручну</string>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Кожних %d хвилин, а також негайно при внесенні локальних змін</string>
<string name="settings_sync_summary_not_available">Не доступно</string>
<string name="settings_sync_interval_calendars">Інтервал синхронізації календарів</string>
<string name="settings_sync_interval_tasks">Інтервал синхронізації завдань</string>
<string-array name="settings_sync_interval_seconds">
<item>-1</item>
<item>300</item>
<item>600</item>
<item>900</item>
<item>3600</item>
<item>7200</item>
<item>14400</item>
<item>86400</item>
</string-array>
<string-array name="settings_sync_interval_names">
<item>Лише вручну</item>
<item>Кожних 5 хвилин</item>
<item>Кожних 10 хвилин</item>
<item>Кожних 15 хвилин</item>
<item>Щогодини</item>
<item>Кожних 2 години</item>
<item>Кожних 4 години</item>
<item>Раз на добу</item>
</string-array>
<string name="settings_sync_wifi_only">Синхронізувати лише через Wi-Fi</string>
<string name="settings_sync_wifi_only_on">Виконувати синхронізацію лише через Wi-Fi</string>
<string name="settings_sync_wifi_only_off">Не враховувати тип з\'єднання</string>
<string name="settings_sync_wifi_only_ssid">Обмеження WiFi SSID</string>
<string name="settings_sync_wifi_only_ssid_on">Синхронізувати лише через %s</string>
<string name="settings_sync_wifi_only_ssid_off">Може використовуватись всі Wi-Fi з\'єднання</string>
<string name="settings_sync_wifi_only_ssid_message">Вкажіть назву Wi-Fi мережі (SSID), аби дозволити синхронізацію лише через неї. Або залиште пустим, для синхронізації через будь-які Wi-Fi мережі.</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Метод групування контактів</string>
<string-array name="settings_contact_group_method_entries">
<item>Групи як окремі VCard</item>
<item>Групи як категорії в середині контактів</item>
</string-array>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Інтервал синхронізації</string>
<string name="settings_sync_time_range_past_none">Всі події будуть синхронізовані</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Події старші одного дня будуть проігноровані</item>
<item quantity="few">Події старші %d днів будуть проігноровані</item>
<item quantity="other">Події старші %d днів будуть проігноровані</item>
</plurals>
<string name="settings_sync_time_range_past_message">Події старші вказаного часу будуть проігноровані (може бути 0). Залиште порожнім, аби синхронізувати всі події.</string>
<string name="settings_manage_calendar_colors">Керування кольорами календаря</string>
<string name="settings_manage_calendar_colors_on">Кольори календаря керуються DAVdroid</string>
<string name="settings_manage_calendar_colors_off">Кольори календаря не керуються DAVdroid</string>
<!--collection management-->
<string name="create_addressbook">Створити адресну книгу</string>
<string name="create_addressbook_display_name_hint">Моя адресна книга</string>
<string name="create_calendar">Створити CalDAV колекцію</string>
<string name="create_calendar_display_name_hint">Мій календар</string>
<string name="create_calendar_time_zone">Часова зона:</string>
<string name="create_calendar_type">Тип колекції:</string>
<string name="create_calendar_type_only_events">Календар (лише події)</string>
<string name="create_calendar_type_only_tasks">Список завдань (лише завдання)</string>
<string name="create_calendar_type_events_and_tasks">Об\'єднаний (події та завдання)</string>
<string name="create_collection_color">Встановити колір колекції</string>
<string name="create_collection_creating">Створення колекції</string>
<string name="create_collection_display_name">Ім\'я, що показуватиметься (назва ) для колекції</string>
<string name="create_collection_display_name_required">Потребує назву</string>
<string name="create_collection_description">Опис (за бажанням):</string>
<string name="create_collection_home_set">Головна тека:</string>
<string name="create_collection_create">Створити</string>
<string name="delete_collection">Видалити колекцію</string>
<string name="delete_collection_confirm_title">Ви впевнені?</string>
<string name="delete_collection_confirm_warning">Колекція (%s) та всі пов\'язані данні будуть вилучені з даного серверу.</string>
<string name="delete_collection_deleting_collection">Видалення колекції</string>
<!--ExceptionInfoFragment-->
<string name="exception">Трапилась помилка.</string>
<string name="exception_httpexception">Трапилась помилка HTTP.</string>
<string name="exception_ioexception">Трапилась помилка I/O.</string>
<string name="exception_show_details">Показати подробиці</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">Інформація зневадження</string>
<string name="sync_error_permissions">Дозволи DAVdroid</string>
<string name="sync_error_permissions_text">Потребує додаткові дозволи</string>
<string name="sync_error_calendar">Помилка синхронізації календаря (%s)</string>
<string name="sync_error_contacts">Помилка синхронізації адресної книги (%s)</string>
<string name="sync_error_tasks">Помилка синхронізації завдань (%s)</string>
<string name="sync_error">Помилка під час %s</string>
<string name="sync_error_http_dav">Помилка серверу під час %s</string>
<string name="sync_error_local_storage">Помилка серверу під час %s</string>
<string-array name="sync_error_phases">
<item>підготовка синхронізації</item>
<item>опитування можливостей</item>
<item>обробка локально вилучених записів</item>
<item>підготовка створених/змінених записів</item>
<item>вивантаження створених/змінених записів</item>
<item>перевірка стану синхронізації</item>
<item>перелік локальних записів</item>
<item>перелік віддалених записів</item>
<item>порівняння локальних/віддалених записів</item>
<item>завантаження віддалених записів</item>
<item>після обробка</item>
<item>збереження стану синхронізації</item>
</string-array>
<string name="sync_error_unauthorized">Невірне ім\'я користувача чи пароль</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: Безпека з\'єднання</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid зіткнувся з невідомим сертифікатом. Чи довіряти йому?</string>
</resources>

View File

@@ -2,11 +2,17 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">DAVdroid 通讯录</string>
<string name="address_books_authority_title">通讯录</string>
<string name="help">帮助</string>
<string name="manage_accounts">管理账户</string>
<string name="please_wait">请稍等...</string>
<string name="send">发送</string>
<string name="homepage_url">https://davdroid.bitfire.at/?pk_campaign=davdroid-app</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">电池优化</string>
<string name="startup_battery_optimization_message">系统可能会在几天后减少或停用 DAVdroid 同步。为了避免这一情况,请禁用对 DAVdroid 的电池优化。</string>
<string name="startup_battery_optimization_disable">禁用电池优化</string>
<string name="startup_dont_show_again">不再显示</string>
<string name="startup_development_version">DAVdroid 预览版</string>
<string name="startup_development_version_message">这是 DAVdroid 的开发版本,部分功能可能无法正常工作。请您提出建设性反馈,帮助我们完善 DAVdroid。</string>
@@ -16,10 +22,11 @@
<string name="startup_donate_now">显示捐助页面</string>
<string name="startup_donate_later">稍后提示</string>
<string name="startup_google_play_accounts_removed">Play 商店 DRM 问题提醒</string>
<string name="startup_google_play_accounts_removed_message">部分情况下Play 商店的 DRM 可能会导致所有 DAVdroid 账户在设备重启或升级 DAVdroid 后消失。如果你遇到了该问题(并且只有这一问题),请从 Play 商店安装“DAVdroid JB Workaround”。</string>
<string name="startup_google_play_accounts_removed_message">某些情况下Play 商店的 DRM 可能会导致所有 DAVdroid 账户在设备重启或升级 DAVdroid 后消失。如果你遇到了该问题,请从 Play 商店安装“DAVdroid JB Workaround”,否则请不要安装修复程序</string>
<string name="startup_google_play_accounts_removed_more_info">更多信息</string>
<string name="startup_opentasks_not_installed">OpenTasks 未安装</string>
<string name="startup_opentasks_not_installed_message">OpenTasks 应用未安装或不可见,因此 DAVdroid 无法同步任务列表。请在安装 OpenTasks 后,重新安装 DAVdroid再增加账户。</string>
<string name="startup_opentasks_not_installed_message">未安装 OpenTasks 应用,故 DAVdroid 无法同步任务列表。</string>
<string name="startup_opentasks_reinstall_davdroid">安装 OpenTasks 后,由于 Android 的限制,请重新安装 DAVdroid 并重新创建账户。</string>
<string name="startup_opentasks_not_installed_install">安装 OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">许可协议</string>
@@ -37,12 +44,15 @@
<string name="navigation_drawer_about">关于 / 许可</string>
<string name="navigation_drawer_settings">设置</string>
<string name="navigation_drawer_news_updates">最新消息</string>
<string name="navigation_drawer_external_links">外部网站</string>
<string name="navigation_drawer_external_links">外部链接</string>
<string name="navigation_drawer_website">应用网站</string>
<string name="navigation_drawer_faq">常见问题</string>
<string name="navigation_drawer_faq_url">https://davdroid.bitfire.at/faq/?pk_campaign=davdroid-app</string>
<string name="navigation_drawer_forums">社区</string>
<string name="navigation_drawer_donate">捐助</string>
<string name="account_list_empty">欢迎使用 DAVdroid\n\n现在你可以增加 CalDAV/CardDAV 账户。</string>
<string name="accounts_global_sync_disabled">系统全局自动同步已禁用</string>
<string name="accounts_global_sync_enable">启用</string>
<!--DavService-->
<string name="dav_service_refresh_failed">服务配置检测失败</string>
<string name="dav_service_refresh_couldnt_refresh">无法刷新集合列表</string>
@@ -52,12 +62,19 @@
<string name="app_settings_reset_hints">重设提示</string>
<string name="app_settings_reset_hints_summary">重新显示之前忽略过的提示</string>
<string name="app_settings_reset_hints_success">所有提示将会再次显示</string>
<string name="app_settings_connection">连接</string>
<string name="app_settings_override_proxy">覆盖代理设置</string>
<string name="app_settings_override_proxy_on">使用自定义代理设置</string>
<string name="app_settings_override_proxy_off">使用系统默认代理设置</string>
<string name="app_settings_override_proxy_host">HTTP 代理主机名</string>
<string name="app_settings_override_proxy_port">HTTP 代理端口</string>
<string name="app_settings_security">安全</string>
<string name="app_settings_reset_trusted_certificates">重设受信任证书</string>
<string name="app_settings_reset_trusted_certificates_summary">取消信任所有之前接受的证书</string>
<plurals name="app_settings_reset_trusted_certificates_success">
<item quantity="other">已删除 %d 个证书</item>
</plurals>
<string name="app_settings_distrust_system_certs">不信任系统证书</string>
<string name="app_settings_distrust_system_certs_on">系统和用户增加的发布者不会被信任</string>
<string name="app_settings_distrust_system_certs_off">系统和用户增加的发布者会被信任(推荐)</string>
<string name="app_settings_reset_certificates">重设证书信任状态</string>
<string name="app_settings_reset_certificates_summary">重设所有自定义证书的信任状态</string>
<string name="app_settings_reset_certificates_success">所有自定义证书已清除</string>
<string name="app_settings_debug">调试</string>
<string name="app_settings_log_to_external_storage">外部文件日志</string>
<string name="app_settings_log_to_external_storage_on">记录日志到外部存储(如果可用)</string>
@@ -68,6 +85,9 @@
<string name="account_synchronize_now"> 立即同步</string>
<string name="account_synchronizing_now">正在同步</string>
<string name="account_settings">账户设置</string>
<string name="account_rename">重命名账户</string>
<string name="account_rename_new_name">重命名后,未上传的本地修改会被撤销,您需要重新执行同步。新账户名:</string>
<string name="account_rename_rename">重命名</string>
<string name="account_delete">删除账户</string>
<string name="account_delete_confirmation_title">真的要删除账户吗?</string>
<string name="account_delete_confirmation_text">所有通讯录、日历和任务列表的本机存储将被删除。</string>
@@ -75,7 +95,19 @@
<string name="account_create_new_address_book">创建通讯录</string>
<string name="account_refresh_calendar_list">刷新日历列表</string>
<string name="account_create_new_calendar">创建日历</string>
<!--PermissionsActivity-->
<string name="permissions_title">DAVdroid 权限</string>
<string name="permissions_calendar">日历权限</string>
<string name="permissions_calendar_details">要把 CalDAV 事件与本地日历同步DAVdroid 需要日历权限。</string>
<string name="permissions_calendar_request">请求日历权限</string>
<string name="permissions_contacts">通讯录权限</string>
<string name="permissions_contacts_details">要把 CardDAV 通讯录与本地通讯录同步DAVdroid 需要通讯录权限。</string>
<string name="permissions_contacts_request">请求通讯录权限</string>
<string name="permissions_opentasks">OpenTasks 权限</string>
<string name="permissions_opentasks_details">要把 CalDAV 任务与本地任务列表同步DAVdroid 需要访问 OpenTasks。</string>
<string name="permissions_opentasks_request">请求 OpenTasks 权限</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://davdroid.bitfire.at/configuration/?pk_campaign=davdroid-app</string>
<string name="login_title">增加账户</string>
<string name="login_type_email">使用邮箱地址登录</string>
<string name="login_email_address">Email 地址</string>
@@ -88,12 +120,12 @@
<string name="login_user_name">用户名</string>
<string name="login_user_name_required">请输入用户名</string>
<string name="login_base_url">根地址</string>
<string name="login_auth_preemptive">强制认证模式(推荐使用,但不兼容 Digest 认证方式)</string>
<string name="login_login">登录</string>
<string name="login_back">返回</string>
<string name="login_create_account">创建账户</string>
<string name="login_account_name">账户显示名</string>
<string name="login_account_name_info">请使用你的邮箱地址作为帐户名,因为 Android 会将你创建的日历事件的创建者项设置为帐户名。你不能拥有多个帐户名相同的账户。</string>
<string name="login_account_contact_group_method">联系人分组方式</string>
<string name="login_account_name_required">请输入账户名</string>
<string name="login_account_not_created">账户无法创建</string>
<string name="login_configuration_detection">正在配置</string>
@@ -108,9 +140,6 @@
<string name="settings_password">密码</string>
<string name="settings_password_summary">修改服务器密码</string>
<string name="settings_enter_password">输入密码</string>
<string name="settings_preemptive">强制认证模式</string>
<string name="settings_preemptive_on">认证信息在每次请求中发送(推荐)</string>
<string name="settings_preemptive_off">认证信息在服务器要求后才发送</string>
<string name="settings_sync">同步</string>
<string name="settings_sync_interval_contacts">通讯录自动同步间隔</string>
<string name="settings_sync_summary_manually">手动同步</string>
@@ -138,15 +167,33 @@
<item>每 4 小时</item>
<item>每 24 小时</item>
</string-array>
<string name="settings_sync_wifi_only">只在 WiFi 下同步</string>
<string name="settings_sync_wifi_only_on">同步只在 WiFi 连接下进行</string>
<string name="settings_sync_wifi_only_off">同步不受数据连接类型限制</string>
<string name="settings_sync_wifi_only_ssid">WiFi SSID 限制</string>
<string name="settings_sync_wifi_only_ssid_on">同步只在 %s 网络下进行</string>
<string name="settings_sync_wifi_only_ssid_off">任何 WiFi 网络下均会同步</string>
<string name="settings_sync_wifi_only_ssid_message">输入 WiFi 网络的名称 (SSID) ,即可限制同步只在此网络下进行。留空则不限制。</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">联系人分组方式</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>分为不同的 VCards</item>
<item>每个联系人的分类</item>
</string-array>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">旧日程时间限制</string>
<string name="settings_sync_time_range_past_none">同步所有日程</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="other">%d 天前的日程不会被同步</item>
</plurals>
<string name="settings_sync_time_range_past_message">超过这个数字的天数的旧日程将会被忽略(可以为 0。留空则同步所有日程。</string>
<string name="settings_version_update">DAVdroid 版本升级</string>
<string name="settings_version_update_settings_updated">应用设置已更新。</string>
<string name="settings_version_update_install_hint">出现问题了?请卸载 DAVdroid,再重新安装。</string>
<string name="settings_manage_calendar_colors">管理日历颜色</string>
<string name="settings_manage_calendar_colors_on">日历颜色由 DAVdroid 设置</string>
<string name="settings_manage_calendar_colors_off">日历颜色不由 DAVdroid 设置</string>
<!--collection management-->
<string name="create_addressbook">创建通讯录</string>
<string name="create_addressbook_display_name_hint">我的通讯录</string>
@@ -175,6 +222,8 @@
<string name="exception_show_details">显示详情</string>
<!--sync errors and DebugInfoActivity-->
<string name="debug_info_title">调试信息</string>
<string name="sync_error_permissions">DAVdroid 权限</string>
<string name="sync_error_permissions_text">需要额外权限</string>
<string name="sync_error_calendar">日历同步失败(%s</string>
<string name="sync_error_contacts">通讯录同步失败(%s</string>
<string name="sync_error_tasks">任务同步失败(%s</string>
@@ -192,7 +241,11 @@
<item>检查远程数据</item>
<item>比较本地和远程数据</item>
<item>下载远程数据</item>
<item>预处理</item>
<item>保存同步状态</item>
</string-array>
<string name="sync_error_unauthorized">用户名或密码错误</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: 连接安全性</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid 遇到了未知证书。你是否要信任该证书?</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="at.bitfire.davdroid"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="com.android.vending.CHECK_LICENSE" />
</manifest>

View File

@@ -0,0 +1,118 @@
package at.bitfire.davdroid.syncadapter;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.net.Uri;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v7.app.NotificationCompat;
import com.google.android.vending.licensing.AESObfuscator;
import com.google.android.vending.licensing.LicenseChecker;
import com.google.android.vending.licensing.LicenseCheckerCallback;
import com.google.android.vending.licensing.ServerManagedPolicy;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
public class LicenseCheckSyncPlugin implements ISyncPlugin, LicenseCheckerCallback {
private static final String LICENSE_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA046s5AzGwaRpSYu4FJ5bTkRvIc93P8dhVXDhgAD7M946R8PE1zKCLPSep4eupw8nxQThXOK1OOJUeh8UbtBBlK+V51UNCjFXQcrLT9XmVmd2OpqhUHszhp3ESg6XpqRwKM7cX4uccpFWKRnn+epTzWoUMTlzaqGQWfF+YSCGMnkJzttdXWzYswhHebQPKYwnaTlGGc0sGl+K6XV/ewe/G47K5sKpX1qdFSBmloH79jFlOumpGooHn51YQ0mTTNu1ErNHi0iugYTDN9QNpLVEtWmxFLwDgwzgXOgd2X2UQDMPCgWapatQWfzUWcIjGLqprcPn29VW3ckQyIx0YMaEzQIDAQAB";
private static final byte OBFUSCATOR_SALT[] = { 72, 52, 61, -90, -65, 114, 99, 77, -54, 56, 44, 124, 57, 45, -91, 67, -88, 35, -102, 21 };
private LicenseChecker licenseChecker;
private static Boolean licenseOk;
private static final Object licenseLock = new Object();
private static final int TIMEOUT = 30000;
@Override
public synchronized boolean beforeSync(@NonNull Context context, @NonNull SyncResult syncResult) {
synchronized(licenseLock) {
if (licenseOk == null || !licenseOk) {
App.log.info("Checking Google Play license");
licenseOk = null;
@SuppressLint("HardwareIds") final String deviceId = Settings.Secure.getString(context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
licenseChecker = new LicenseChecker(context, new ServerManagedPolicy(context, new AESObfuscator(OBFUSCATOR_SALT, BuildConfig.APPLICATION_ID, deviceId)), LICENSE_PUBLIC_KEY);
licenseChecker.checkAccess(this);
if (licenseOk == null)
try {
licenseLock.wait(TIMEOUT);
} catch(InterruptedException ignored) {
}
}
if (licenseOk == null)
licenseOk = false;
}
if (licenseChecker != null) {
licenseChecker.onDestroy();
licenseChecker = null;
}
NotificationManagerCompat nm = NotificationManagerCompat.from(context);
if (!licenseOk) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + BuildConfig.APPLICATION_ID));
intent.setPackage("com.android.vending"); // open only with Play Store
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
builder .setSmallIcon(R.drawable.ic_error_light)
.setLargeIcon(App.getLauncherBitmap(context))
.setContentTitle("License check failed")
.setContentText("Couldn't verify Google Play purchase")
.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setPriority(NotificationCompat.PRIORITY_HIGH);
nm.notify(Constants.NOTIFICATION_SUBSCRIPTION, builder.build());
App.log.warning("No valid license, skipping sync");
syncResult.stats.numIoExceptions++;
syncResult.delayUntil = 15; // wait 15 seconds before trying again
} else
nm.cancel(Constants.NOTIFICATION_SUBSCRIPTION);
return licenseOk;
}
@Override
public void afterSync(@NonNull Context context, @NonNull SyncResult syncResult) {
}
@Override
public void allow(int reason) {
App.log.info("Google Play license valid: " + reason);
synchronized(licenseLock) {
licenseOk = true;
licenseLock.notify();
}
}
@Override
public void dontAllow(int reason) {
App.log.warning("Google Play license invalid: " + reason);
synchronized(licenseLock) {
licenseOk = false;
licenseLock.notify();
}
}
@Override
public void applicationError(int errorCode) {
App.log.severe("Application error " + errorCode + " when checking Google Play license");
synchronized(licenseLock) {
licenseOk = false;
licenseLock.notify();
}
}
}

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2013 2016 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_app_settings"
android:icon="@drawable/ic_settings_action"
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_faq"
android:icon="@drawable/ic_help_action"
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

@@ -0,0 +1 @@
at.bitfire.davdroid.syncadapter.LicenseCheckSyncPlugin

View File

@@ -12,33 +12,32 @@
android:installLocation="internalOnly">
<!-- normal permissions -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.READ_SYNC_STATS"/>
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<!-- account management permissions not required for own accounts since API level 22 -->
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" android:maxSdkVersion="22"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS" android:maxSdkVersion="22"/>
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" android:maxSdkVersion="22"/>
<!-- legacy permissions -->
<!--
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"
tools:ignore="UnusedAttribute"/>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18"
tools:ignore="UnusedAttribute"/>
<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 -->
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<!-- android.permission-group.CONTACTS -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<!-- android.permission-group.CALENDAR -->
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
<!-- ical4android declares task access permissions -->
@@ -47,20 +46,30 @@
android:name=".App"
android:allowBackup="true"
android:fullBackupContent="false"
android:icon="@drawable/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
tools:ignore="UnusedAttribute">
<receiver
android:name=".App$ReinitLoggingReceiver"
android:name=".App$ReinitSettingsReceiver"
android:exported="false"
android:process=":sync">
<intent-filter>
<action android:name="at.bitfire.davdroid.REINIT_LOGGER"/>
<action android:name="at.bitfire.davdroid.REINIT_SETTINGS"/>
</intent-filter>
</receiver>
<receiver
android:name=".AccountSettings$AppUpdatedReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package" android:path="at.bitfire.davdroid" />
</intent-filter>
</receiver>
<!-- account type "DAVdroid" -->
<service
android:name=".syncadapter.AccountAuthenticatorService"
android:exported="false">
@@ -72,6 +81,65 @@
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/account_authenticator"/>
</service>
<service
android:name=".syncadapter.CalendarsSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_calendars"/>
</service>
<service
android:name=".syncadapter.TasksSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_tasks"/>
</service>
<!-- account type "DAVdroid Address book" -->
<service
android:name=".syncadapter.NullAuthenticatorService"
android:exported="false">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/account_authenticator_address_book"/>
</service>
<provider
android:authorities="@string/address_books_authority"
android:exported="false"
android:label="@string/address_books_authority_title"
android:name=".syncadapter.AddressBookProvider"
android:multiprocess="false"/>
<service
android:name=".syncadapter.AddressBooksSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_address_books"/>
</service>
<service
android:name=".syncadapter.ContactsSyncAdapterService"
android:exported="true"
@@ -88,46 +156,26 @@
android:name="android.provider.CONTACTS_STRUCTURE"
android:resource="@xml/contacts"/>
</service>
<service
android:name=".syncadapter.CalendarsSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_calendars"/>
</service>
<service
android:name=".syncadapter.TasksSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_tasks"/>
</service>
<service
android:name=".DavService"
android:enabled="true">
</service>
<receiver
android:name=".AccountsChangedReceiver"
android:enabled="true"
android:exported="true">
<receiver android:name=".AccountsChangedReceiver">
<intent-filter>
<action android:name="android.accounts.LOGIN_ACCOUNTS_CHANGED"/>
</intent-filter>
</receiver>
<receiver android:name=".PackageChangedReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED"/>
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED"/>
<data android:scheme="package"/>
</intent-filter>
</receiver>
<activity
android:name=".ui.AccountsActivity"
android:label="@string/app_name"
@@ -149,6 +197,10 @@
android:label="@string/app_settings"
android:parentActivityName=".ui.AccountsActivity"/>
<activity android:name=".ui.PermissionsActivity"
android:label="@string/permissions_title"
android:parentActivityName=".ui.AccountsActivity"/>
<activity
android:name=".ui.setup.LoginActivity"
android:label="@string/login_title"
@@ -173,11 +225,16 @@
android:exported="true"
android:label="@string/debug_info_title">
</activity>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="@string/authority_log_provider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/log_paths" />
</provider>
<!-- MemorizingTrustManager -->
<activity
android:name="de.duenndns.ssl.MemorizingActivity"
android:theme="@android:style/Theme.Holo.Light.Dialog.NoActionBar"/>
</application>

View File

@@ -9,35 +9,33 @@ package at.bitfire.davdroid;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.PeriodicSync;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Calendars;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.v7.app.NotificationCompat;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.model.ServiceDB.Collections;
import at.bitfire.davdroid.model.ServiceDB.HomeSets;
@@ -48,25 +46,39 @@ import at.bitfire.davdroid.resource.LocalTaskList;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.TaskProvider;
import at.bitfire.vcard4android.ContactsStorageException;
import at.bitfire.vcard4android.GroupMethod;
import lombok.Cleanup;
import okhttp3.HttpUrl;
public class AccountSettings {
private final static int CURRENT_VERSION = 3;
private final static int CURRENT_VERSION = 6;
private final static String
KEY_SETTINGS_VERSION = "version",
KEY_SETTINGS_VERSION = "version",
KEY_USERNAME = "user_name",
KEY_AUTH_PREEMPTIVE = "auth_preemptive";
KEY_USERNAME = "user_name",
KEY_WIFI_ONLY = "wifi_only", // sync on WiFi only (default: false)
KEY_WIFI_ONLY_SSID = "wifi_only_ssid"; // restrict sync to specific WiFi SSID
/** 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
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
*/
private final static String KEY_TIME_RANGE_PAST_DAYS = "time_range_past_days";
private final static int DEFAULT_TIME_RANGE_PAST_DAYS = 90;
/* Whether DAVdroid sets the local calendar color to the value from service DB at every sync
value = null (not existing) true (default)
"0" false */
private final static String KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors";
/** Contact group method:
value = null (not existing) groups as separate VCards (default)
"CATEGORIES" groups are per-contact CATEGORIES
*/
private final static String KEY_CONTACT_GROUP_METHOD = "contact_group_method";
public final static long SYNC_INTERVAL_MANUALLY = -1;
final Context context;
@@ -74,6 +86,7 @@ public class AccountSettings {
final Account account;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public AccountSettings(@NonNull Context context, @NonNull Account account) throws InvalidAccountException {
this.context = context;
this.account = account;
@@ -90,34 +103,17 @@ public class AccountSettings {
version = Integer.parseInt(versionStr);
} catch (NumberFormatException ignored) {
}
App.log.info("Account " + account.name + " has version " + version + ", current version: " + CURRENT_VERSION);
if (version < CURRENT_VERSION) {
Notification notify = new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_new_releases_light)
.setLargeIcon(((BitmapDrawable)context.getResources().getDrawable(R.drawable.ic_launcher)).getBitmap())
.setContentTitle(context.getString(R.string.settings_version_update))
.setContentText(context.getString(R.string.settings_version_update_settings_updated))
.setSubText(context.getString(R.string.settings_version_update_install_hint))
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(context.getString(R.string.settings_version_update_settings_updated)))
.setCategory(NotificationCompat.CATEGORY_SYSTEM)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setLocalOnly(true)
.build();
NotificationManager nm = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(Constants.NOTIFICATION_ACCOUNT_SETTINGS_UPDATED, notify);
App.log.fine("Account " + account.name + " has version " + version + ", current version: " + CURRENT_VERSION);
if (version < CURRENT_VERSION)
update(version);
}
}
}
public static Bundle initialUserData(String userName, boolean preemptive) {
public static Bundle initialUserData(String userName) {
Bundle bundle = new Bundle();
bundle.putString(KEY_SETTINGS_VERSION, String.valueOf(CURRENT_VERSION));
bundle.putString(KEY_USERNAME, userName);
bundle.putString(KEY_AUTH_PREEMPTIVE, Boolean.toString(preemptive));
return bundle;
}
@@ -130,9 +126,6 @@ public class AccountSettings {
public String password() { return accountManager.getPassword(account); }
public void password(@NonNull String password) { accountManager.setPassword(account, password); }
public boolean preemptiveAuth() { return Boolean.parseBoolean(accountManager.getUserData(account, KEY_AUTH_PREEMPTIVE)); }
public void preemptiveAuth(boolean preemptive) { accountManager.setUserData(account, KEY_AUTH_PREEMPTIVE, Boolean.toString(preemptive)); }
// sync. settings
@@ -159,6 +152,27 @@ public class AccountSettings {
}
}
public boolean getSyncWifiOnly() {
return accountManager.getUserData(account, KEY_WIFI_ONLY) != null;
}
public void setSyncWiFiOnly(boolean wiFiOnly) {
accountManager.setUserData(account, KEY_WIFI_ONLY, wiFiOnly ? "1" : null);
}
@Nullable
public String getSyncWifiOnlySSID() {
return accountManager.getUserData(account, KEY_WIFI_ONLY_SSID);
}
public void setSyncWifiOnlySSID(String ssid) {
accountManager.setUserData(account, KEY_WIFI_ONLY_SSID, ssid);
}
// CalDAV settings
@Nullable
public Integer getTimeRangePastDays() {
String strDays = accountManager.getUserData(account, KEY_TIME_RANGE_PAST_DAYS);
if (strDays != null) {
@@ -168,10 +182,40 @@ public class AccountSettings {
return DEFAULT_TIME_RANGE_PAST_DAYS;
}
public void setTimeRangePastDays(Integer days) {
public void setTimeRangePastDays(@Nullable Integer days) {
accountManager.setUserData(account, KEY_TIME_RANGE_PAST_DAYS, String.valueOf(days == null ? -1 : days));
}
public boolean getManageCalendarColors() {
return accountManager.getUserData(account, KEY_MANAGE_CALENDAR_COLORS) == null;
}
public void setManageCalendarColors(boolean manage) {
accountManager.setUserData(account, KEY_MANAGE_CALENDAR_COLORS, manage ? null : "0");
}
// CardDAV settings
@NonNull
public GroupMethod getGroupMethod() {
if (BuildConfig.settingContactGroupMethod != null)
return BuildConfig.settingContactGroupMethod;
final String name = accountManager.getUserData(account, KEY_CONTACT_GROUP_METHOD);
return name != null ?
GroupMethod.valueOf(name) :
GroupMethod.GROUP_VCARDS;
}
public void setGroupMethod(@NonNull GroupMethod method) {
if (BuildConfig.settingContactGroupMethod == null) {
final String name = method == GroupMethod.GROUP_VCARDS ? null : method.name();
accountManager.setUserData(account, KEY_CONTACT_GROUP_METHOD, name);
} else if (BuildConfig.settingContactGroupMethod != method)
throw new UnsupportedOperationException("Setting is read-only");
}
// update from previous account settings
@@ -181,6 +225,7 @@ public class AccountSettings {
try {
Method updateProc = getClass().getDeclaredMethod("update_" + fromVersion + "_" + toVersion);
updateProc.invoke(this);
accountManager.setUserData(account, KEY_SETTINGS_VERSION, String.valueOf(toVersion));
} catch (Exception e) {
App.log.log(Level.SEVERE, "Couldn't update account settings", e);
}
@@ -201,7 +246,7 @@ public class AccountSettings {
if (provider == null)
throw new ContactsStorageException("Couldn't access Contacts provider");
LocalAddressBook addr = new LocalAddressBook(account, provider);
LocalAddressBook addr = new LocalAddressBook(context, account, provider);
// until now, ContactsContract.Settings.UNGROUPED_VISIBLE was not set explicitly
ContentValues values = new ContentValues();
@@ -217,8 +262,6 @@ public class AccountSettings {
if (!TextUtils.isEmpty(cTag))
addr.setCTag(cTag);
accountManager.setUserData(account, "addressbook_ctag", null);
accountManager.setUserData(account, KEY_SETTINGS_VERSION, "2");
}
@SuppressWarnings({ "Recycle", "unused" })
@@ -237,7 +280,7 @@ public class AccountSettings {
ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY);
if (client != null)
try {
LocalAddressBook addrBook = new LocalAddressBook(account, client);
LocalAddressBook addrBook = new LocalAddressBook(context, account, client);
String url = addrBook.getURL();
if (url != null) {
App.log.fine("Migrating address book " + url);
@@ -276,7 +319,7 @@ public class AccountSettings {
client = context.getContentResolver().acquireContentProviderClient(CalendarContract.AUTHORITY);
if (client != null)
try {
LocalCalendar calendars[] = (LocalCalendar[])LocalCalendar.find(account, client, LocalCalendar.Factory.INSTANCE, null, null);
List<LocalCalendar> calendars = LocalCalendar.find(account, client, LocalCalendar.Factory.INSTANCE, null, null);
for (LocalCalendar calendar : calendars) {
String url = calendar.getName();
App.log.fine("Migrating calendar " + url);
@@ -292,7 +335,7 @@ public class AccountSettings {
TaskProvider provider = LocalTaskList.acquireTaskProvider(context.getContentResolver());
if (provider != null)
try {
LocalTaskList[] taskLists = (LocalTaskList[])LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, null, null);
List<LocalTaskList> taskLists = LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, null, null);
for (LocalTaskList taskList : taskLists) {
String url = taskList.getSyncId();
App.log.fine("Migrating task list " + url);
@@ -344,8 +387,105 @@ public class AccountSettings {
refresh.putExtra(DavService.EXTRA_DAV_SERVICE_ID, serviceCalDAV);
context.startService(refresh);
}
}
@SuppressWarnings({ "Recycle", "unused" })
private void update_3_4() {
setGroupMethod(GroupMethod.CATEGORIES);
}
/* Android 7.1.1 OpenTasks fix */
@SuppressWarnings({ "Recycle", "unused" })
private void update_4_5() {
// call PackageChangedReceiver which then enables/disables OpenTasks sync when it's (not) available
PackageChangedReceiver.updateTaskSync(context);
}
@SuppressWarnings({ "Recycle", "unused" })
private void update_5_6() throws ContactsStorageException {
@Cleanup("release") ContentProviderClient provider = context.getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY);
if (provider == null)
// no access to contacts provider
return;
// don't run syncs during the migration
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0);
ContentResolver.setIsSyncable(account, App.getAddressBooksAuthority(), 0);
ContentResolver.cancelSync(account, null);
try {
// get previous address book settings (including URL)
@Cleanup("recycle") Parcel parcel = Parcel.obtain();
byte[] raw = ContactsContract.SyncState.get(provider, account);
if (raw == null)
App.log.info("No contacts sync state, ignoring account");
else {
parcel.unmarshall(raw, 0, raw.length);
parcel.setDataPosition(0);
Bundle params = parcel.readBundle();
String url = params.getString("url");
if (url == null)
App.log.info("No address book URL, ignoring account");
else {
// create new address book
CollectionInfo info = new CollectionInfo();
info.type = CollectionInfo.Type.ADDRESS_BOOK;
info.url = url;
info.displayName = account.name;
App.log.log(Level.INFO, "Creating new address book account", url);
Account addressBookAccount = new Account(LocalAddressBook.accountName(account, info), App.getAddressBookAccountType());
if (!accountManager.addAccountExplicitly(addressBookAccount, null, LocalAddressBook.initialUserData(account, info.url)))
throw new ContactsStorageException("Couldn't create address book account");
LocalAddressBook addressBook = new LocalAddressBook(context, addressBookAccount, provider);
// move contacts to new address book
App.log.info("Moving contacts from " + account + " to " + addressBookAccount);
ContentValues newAccount = new ContentValues(2);
newAccount.put(ContactsContract.RawContacts.ACCOUNT_NAME, addressBookAccount.name);
newAccount.put(ContactsContract.RawContacts.ACCOUNT_TYPE, addressBookAccount.type);
int affected = provider.update(ContactsContract.RawContacts.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type)
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build(),
newAccount,
ContactsContract.RawContacts.ACCOUNT_NAME + "=? AND " + ContactsContract.RawContacts.ACCOUNT_TYPE + "=?",
new String[]{account.name, account.type});
App.log.info(affected + " contacts moved to new address book");
}
ContactsContract.SyncState.set(provider, account, null);
}
} catch(RemoteException e) {
throw new ContactsStorageException("Couldn't migrate contacts to new address book", e);
}
// update version number so that further syncs don't repeat the migration
accountManager.setUserData(account, KEY_SETTINGS_VERSION, "6");
// request sync of new address book account
ContentResolver.setIsSyncable(account, App.getAddressBooksAuthority(), 1);
setSyncInterval(App.getAddressBooksAuthority(), Constants.DEFAULT_SYNC_INTERVAL);
}
public static class AppUpdatedReceiver extends BroadcastReceiver {
@Override
@SuppressLint("UnsafeProtectedBroadcastReceiver,MissingPermission")
public void onReceive(Context context, Intent intent) {
App.log.info("DAVdroid was updated, checking for AccountSettings version");
// peek into AccountSettings to initiate a possible migration
AccountManager accountManager = AccountManager.get(context);
for (Account account : accountManager.getAccountsByType(context.getString(R.string.account_type)))
try {
App.log.info("Checking account " + account.name);
new AccountSettings(context, account);
} catch (InvalidAccountException e) {
App.log.log(Level.SEVERE, "Couldn't check for updated account settings", e);
}
}
accountManager.setUserData(account, KEY_SETTINGS_VERSION, "3");
}
}

View File

@@ -8,6 +8,7 @@
package at.bitfire.davdroid;
import android.accounts.AccountManager;
import android.accounts.OnAccountsUpdateListener;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -22,12 +23,14 @@ public class AccountsChangedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Intent serviceIntent = new Intent(context, DavService.class);
serviceIntent.setAction(DavService.ACTION_ACCOUNTS_UPDATED);
context.startService(serviceIntent);
if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(intent.getAction())) {
Intent serviceIntent = new Intent(context, DavService.class);
serviceIntent.setAction(DavService.ACTION_ACCOUNTS_UPDATED);
context.startService(serviceIntent);
for (OnAccountsUpdateListener listener : listeners)
listener.onAccountsUpdated(null);
for (OnAccountsUpdateListener listener : listeners)
listener.onAccountsUpdated(null);
}
}
public static void registerListener(OnAccountsUpdateListener listener, boolean callImmediately) {

View File

@@ -8,12 +8,20 @@
package at.bitfire.davdroid;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Application;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Process;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v7.app.NotificationCompat;
import android.util.Log;
@@ -28,20 +36,34 @@ import java.util.logging.Logger;
import javax.net.ssl.HostnameVerifier;
import at.bitfire.cert4android.CustomCertManager;
import at.bitfire.davdroid.log.LogcatHandler;
import at.bitfire.davdroid.log.PlainTextFormatter;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.model.Settings;
import de.duenndns.ssl.MemorizingTrustManager;
import lombok.Cleanup;
import lombok.Getter;
import okhttp3.internal.tls.OkHostnameVerifier;
public class App extends Application {
public static final String LOG_TO_EXTERNAL_STORAGE = "logToExternalStorage";
public static final String
FLAVOR_GOOGLE_PLAY = "gplay",
FLAVOR_ICLOUD = "icloud",
FLAVOR_SOLDUPE = "soldupe",
FLAVOR_STANDARD = "standard";
public static final String
DISTRUST_SYSTEM_CERTIFICATES = "distrustSystemCerts",
LOG_TO_EXTERNAL_STORAGE = "logToExternalStorage",
OVERRIDE_PROXY = "overrideProxy",
OVERRIDE_PROXY_HOST = "overrideProxyHost",
OVERRIDE_PROXY_PORT = "overrideProxyPort";
public static final String OVERRIDE_PROXY_HOST_DEFAULT = "localhost";
public static final int OVERRIDE_PROXY_PORT_DEFAULT = 8118;
@Getter
private static MemorizingTrustManager memorizingTrustManager;
private CustomCertManager certManager;
@Getter
private static SSLSocketFactoryCompat sslSocketFactoryCompat;
@@ -50,53 +72,74 @@ public class App extends Application {
private static HostnameVerifier hostnameVerifier;
public final static Logger log = Logger.getLogger("davdroid");
static {
at.bitfire.dav4android.Constants.log = Logger.getLogger("davdroid.dav4android");
at.bitfire.cert4android.Constants.log = Logger.getLogger("davdroid.cert4android");
}
@Getter
private static String addressBookAccountType;
@Getter
private static String addressBooksAuthority;
@Override
@SuppressLint("HardwareIds")
public void onCreate() {
super.onCreate();
// initialize MemorizingTrustManager
memorizingTrustManager = new MemorizingTrustManager(this);
sslSocketFactoryCompat = new SSLSocketFactoryCompat(memorizingTrustManager);
hostnameVerifier = memorizingTrustManager.wrapHostnameVerifier(OkHostnameVerifier.INSTANCE);
// initializer logger
reinitCertManager();
reinitLogger();
addressBookAccountType = getString(R.string.account_type_address_book);
addressBooksAuthority = getString(R.string.address_books_authority);
}
public void reinitCertManager() {
if (BuildConfig.customCerts) {
if (certManager != null)
certManager.close();
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(this);
Settings settings = new Settings(dbHelper.getReadableDatabase());
certManager = new CustomCertManager(this, !settings.getBoolean(DISTRUST_SYSTEM_CERTIFICATES, false));
sslSocketFactoryCompat = new SSLSocketFactoryCompat(certManager);
hostnameVerifier = certManager.hostnameVerifier(OkHostnameVerifier.INSTANCE);
}
}
public void reinitLogger() {
// don't use Android default logging, we have our own handlers
log.setUseParentHandlers(false);
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(this);
Settings settings = new Settings(dbHelper.getReadableDatabase());
boolean logToFile = settings.getBoolean(LOG_TO_EXTERNAL_STORAGE, false),
logVerbose = logToFile || Log.isLoggable(log.getName(), Log.DEBUG);
App.log.info("Verbose logging: " + logVerbose);
// set logging level according to preferences
log.setLevel(logVerbose ? Level.ALL : Level.INFO);
final Logger rootLogger = Logger.getLogger("");
rootLogger.setLevel(logVerbose ? Level.ALL : Level.INFO);
// remove all handlers
for (Handler handler : log.getHandlers())
log.removeHandler(handler);
// remove all handlers and add our own logcat handler
rootLogger.setUseParentHandlers(false);
for (Handler handler : rootLogger.getHandlers())
rootLogger.removeHandler(handler);
rootLogger.addHandler(LogcatHandler.INSTANCE);
// add logcat handler
log.addHandler(LogcatHandler.INSTANCE);
NotificationManager nm = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
// log to external file according to preferences
if (logToFile) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder .setSmallIcon(R.drawable.ic_sd_storage_light)
.setLargeIcon(((BitmapDrawable)getResources().getDrawable(R.drawable.ic_launcher)).getBitmap())
.setLargeIcon(getLauncherBitmap(this))
.setContentTitle(getString(R.string.logging_davdroid_file_logging))
.setLocalOnly(true);
File dir = getExternalFilesDir(null);
if (dir != null)
try {
String fileName = new File(dir, "davdroid-" + android.os.Process.myPid() + "-" +
String fileName = new File(dir, "davdroid-" + Process.myPid() + "-" +
DateFormatUtils.format(System.currentTimeMillis(), "yyyyMMdd-HHmmss") + ".txt").toString();
log.info("Logging to " + fileName);
@@ -125,15 +168,30 @@ public class App extends Application {
nm.cancel(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING);
}
@Nullable
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static Bitmap getLauncherBitmap(@NonNull Context context) {
Bitmap bitmapLogo = null;
Drawable drawableLogo = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP ?
context.getDrawable(R.mipmap.ic_launcher) :
context.getResources().getDrawable(R.mipmap.ic_launcher);
if (drawableLogo instanceof BitmapDrawable)
bitmapLogo = ((BitmapDrawable)drawableLogo).getBitmap();
return bitmapLogo;
}
public static class ReinitLoggingReceiver extends BroadcastReceiver {
public static class ReinitSettingsReceiver extends BroadcastReceiver {
public static final String ACTION_REINIT_SETTINGS = "at.bitfire.davdroid.REINIT_SETTINGS";
@Override
public void onReceive(Context context, Intent intent) {
log.info("Received broadcast: re-initializing logger");
log.info("Received broadcast: re-initializing settings (logger/cert manager)");
App app = (App)context.getApplicationContext();
app.reinitLogger();
app.reinitCertManager();
}
}

View File

@@ -11,18 +11,16 @@ import android.net.Uri;
public class Constants {
public static final String
ACCOUNT_TYPE = "bitfire.at.davdroid";
// notification IDs
public final static int
NOTIFICATION_ACCOUNT_SETTINGS_UPDATED = 0,
NOTIFICATION_EXTERNAL_FILE_LOGGING = 1,
NOTIFICATION_REFRESH_COLLECTIONS = 2,
NOTIFICATION_CONTACTS_SYNC = 10,
NOTIFICATION_CALENDAR_SYNC = 11,
NOTIFICATION_TASK_SYNC = 12;
NOTIFICATION_TASK_SYNC = 12,
NOTIFICATION_PERMISSIONS = 20,
NOTIFICATION_SUBSCRIPTION = 21;
public static final Uri webUri = Uri.parse("https://davdroid.bitfire.at/?pk_campaign=davdroid-app");
public static final int DEFAULT_SYNC_INTERVAL = 4 * 3600; // 4 hours
}

View File

@@ -10,8 +10,8 @@ package at.bitfire.davdroid;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentValues;
@@ -19,13 +19,19 @@ import android.content.Intent;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.drawable.BitmapDrawable;
import android.os.Binder;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v7.app.NotificationCompat;
import android.text.TextUtils;
import org.apache.commons.collections4.iterators.IteratorChain;
import org.apache.commons.collections4.iterators.SingletonIterator;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
@@ -37,6 +43,7 @@ import java.util.Set;
import java.util.logging.Level;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.Property;
import at.bitfire.dav4android.UrlUtils;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
@@ -50,7 +57,10 @@ import at.bitfire.davdroid.model.ServiceDB.Collections;
import at.bitfire.davdroid.model.ServiceDB.HomeSets;
import at.bitfire.davdroid.model.ServiceDB.OpenHelper;
import at.bitfire.davdroid.model.ServiceDB.Services;
import at.bitfire.davdroid.resource.LocalAddressBook;
import at.bitfire.davdroid.ui.DebugInfoActivity;
import at.bitfire.vcard4android.ContactsStorageException;
import kotlin.Pair;
import lombok.Cleanup;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
@@ -65,25 +75,30 @@ public class DavService extends Service {
private final IBinder binder = new InfoBinder();
private final Set<Long> runningRefresh = new HashSet<>();
private final List<RefreshingStatusListener> refreshingStatusListeners = new LinkedList<>();
private final List<WeakReference<RefreshingStatusListener>> refreshingStatusListeners = new LinkedList<>();
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();
long id = intent.getLongExtra(EXTRA_DAV_SERVICE_ID, -1);
if (intent != null) {
String action = intent.getAction();
long id = intent.getLongExtra(EXTRA_DAV_SERVICE_ID, -1);
switch (action) {
case ACTION_ACCOUNTS_UPDATED:
cleanupAccounts();
break;
case ACTION_REFRESH_COLLECTIONS:
if (runningRefresh.add(id)) {
new Thread(new RefreshCollections(id)).start();
for (RefreshingStatusListener listener : refreshingStatusListeners)
listener.onDavRefreshStatusChanged(id, true);
}
break;
switch (action) {
case ACTION_ACCOUNTS_UPDATED:
cleanupAccounts();
break;
case ACTION_REFRESH_COLLECTIONS:
if (runningRefresh.add(id)) {
new Thread(new RefreshCollections(id)).start();
for (WeakReference<RefreshingStatusListener> ref : refreshingStatusListeners) {
RefreshingStatusListener listener = ref.get();
if (listener != null)
listener.onDavRefreshStatusChanged(id, true);
}
}
break;
}
}
return START_NOT_STICKY;
@@ -108,15 +123,19 @@ public class DavService extends Service {
return runningRefresh.contains(id);
}
public void addRefreshingStatusListener(RefreshingStatusListener listener, boolean callImmediate) {
refreshingStatusListeners.add(listener);
public void addRefreshingStatusListener(@NonNull RefreshingStatusListener listener, boolean callImmediate) {
refreshingStatusListeners.add(new WeakReference<>(listener));
if (callImmediate)
for (long id : runningRefresh)
listener.onDavRefreshStatusChanged(id, true);
}
public void removeRefreshingStatusListener(RefreshingStatusListener listener) {
refreshingStatusListeners.remove(listener);
public void removeRefreshingStatusListener(@NonNull RefreshingStatusListener listener) {
for (Iterator<WeakReference<RefreshingStatusListener>> iterator = refreshingStatusListeners.iterator(); iterator.hasNext(); ) {
RefreshingStatusListener item = iterator.next().get();
if (listener.equals(item))
iterator.remove();
}
}
}
@@ -125,6 +144,7 @@ public class DavService extends Service {
which actually do the work
*/
@SuppressLint("MissingPermission")
void cleanupAccounts() {
App.log.info("Cleaning up orphaned accounts");
@@ -133,10 +153,25 @@ public class DavService extends Service {
SQLiteDatabase db = dbHelper.getWritableDatabase();
List<String> sqlAccountNames = new LinkedList<>();
Set<String> accountNames = new HashSet<>();
AccountManager am = AccountManager.get(this);
for (Account account : am.getAccountsByType(Constants.ACCOUNT_TYPE))
for (Account account : am.getAccountsByType(getString(R.string.account_type))) {
sqlAccountNames.add(DatabaseUtils.sqlEscapeString(account.name));
accountNames.add(account.name);
}
// delete orphaned address book accounts
for (Account addrBookAccount : am.getAccountsByType(App.getAddressBookAccountType())) {
LocalAddressBook addressBook = new LocalAddressBook(this, addrBookAccount, null);
try {
if (!accountNames.contains(addressBook.getMainAccount().name))
addressBook.delete();
} catch(ContactsStorageException e) {
App.log.log(Level.SEVERE, "Couldn't get address book main account", e);
}
}
// delete orphaned services in DB
if (sqlAccountNames.isEmpty())
db.delete(Services._TABLE, null, null);
else
@@ -181,25 +216,27 @@ public class DavService extends Service {
queryHomeSets(serviceType, dav, homeSets);
// refresh home sets: calendar-proxy-read/write-for
CalendarProxyReadFor proxyRead = (CalendarProxyReadFor)dav.properties.get(CalendarProxyReadFor.NAME);
if (proxyRead != null)
for (String href : proxyRead.principals) {
for (Pair<DavResource, Property> result : dav.findProperties(CalendarProxyReadFor.NAME)) {
CalendarProxyReadFor proxyRead = (CalendarProxyReadFor)result.getSecond();
for (String href : proxyRead.getHrefs()) {
App.log.fine("Principal is a read-only proxy for " + href + ", checking for home sets");
queryHomeSets(serviceType, new DavResource(httpClient, dav.location.resolve(href)), homeSets);
queryHomeSets(serviceType, new DavResource(httpClient, result.getFirst().getLocation().resolve(href)), homeSets);
}
CalendarProxyWriteFor proxyWrite = (CalendarProxyWriteFor)dav.properties.get(CalendarProxyWriteFor.NAME);
if (proxyWrite != null)
for (String href : proxyWrite.principals) {
App.log.fine("Principal is a read-write proxy for " + href + ", checking for home sets");
queryHomeSets(serviceType, new DavResource(httpClient, dav.location.resolve(href)), homeSets);
}
for (Pair<DavResource, Property> result : dav.findProperties(CalendarProxyWriteFor.NAME)) {
CalendarProxyWriteFor proxyWrite = (CalendarProxyWriteFor)result.getSecond();
for (String href : proxyWrite.getHrefs()) {
App.log.fine("Principal is a read/write proxy for " + href + ", checking for home sets");
queryHomeSets(serviceType, new DavResource(httpClient, result.getFirst().getLocation().resolve(href)), homeSets);
}
}
// refresh home sets: direct group memberships
GroupMembership groupMembership = (GroupMembership)dav.properties.get(GroupMembership.NAME);
GroupMembership groupMembership = (GroupMembership)dav.getProperties().get(GroupMembership.NAME);
if (groupMembership != null)
for (String href : groupMembership.hrefs) {
for (String href : groupMembership.getHrefs()) {
App.log.fine("Principal is member of group " + href + ", checking for home sets");
DavResource group = new DavResource(httpClient, dav.location.resolve(href));
DavResource group = new DavResource(httpClient, dav.getLocation().resolve(href));
try {
queryHomeSets(serviceType, group, homeSets);
} catch(HttpException|DavException e) {
@@ -217,26 +254,28 @@ public class DavService extends Service {
if (info.selected)
selectedCollections.add(HttpUrl.parse(info.url));
for (Iterator<HttpUrl> iterator = homeSets.iterator(); iterator.hasNext(); ) {
HttpUrl homeSet = iterator.next();
for (Iterator<HttpUrl> itHomeSets = homeSets.iterator(); itHomeSets.hasNext(); ) {
HttpUrl homeSet = itHomeSets.next();
App.log.fine("Listing home set " + homeSet);
DavResource dav = new DavResource(httpClient, homeSet);
try {
dav.propfind(1, CollectionInfo.DAV_PROPERTIES);
for (DavResource member : dav.members) {
IteratorChain<DavResource> itCollections = new IteratorChain<>(dav.getMembers().iterator(), new SingletonIterator(dav));
while (itCollections.hasNext()) {
DavResource member = itCollections.next();
CollectionInfo info = CollectionInfo.fromDavResource(member);
info.confirmed = true;
App.log.log(Level.FINE, "Found collection", info);
if ((serviceType.equals(Services.SERVICE_CARDDAV) && info.type == CollectionInfo.Type.ADDRESS_BOOK) ||
(serviceType.equals(Services.SERVICE_CALDAV) && info.type == CollectionInfo.Type.CALENDAR))
collections.put(member.location, info);
collections.put(member.getLocation(), info);
}
} catch(HttpException e) {
if (e.status == 403 || e.status == 404 || e.status == 410)
if (e.getStatus() == 403 || e.getStatus() == 404 || e.getStatus() == 410)
// delete home set only if it was not accessible (40x)
iterator.remove();
itHomeSets.remove();
}
}
@@ -258,7 +297,7 @@ public class DavService extends Service {
(serviceType.equals(Services.SERVICE_CALDAV) && info.type != CollectionInfo.Type.CALENDAR))
iterator.remove();
} catch(HttpException e) {
if (e.status == 403 || e.status == 404 || e.status == 410)
if (e.getStatus()== 403 || e.getStatus()== 404 || e.getStatus()== 410)
// delete collection only if it was not accessible (40x)
iterator.remove();
else
@@ -273,8 +312,8 @@ public class DavService extends Service {
info.selected = true;
}
db.beginTransactionNonExclusive();
try {
db.beginTransaction();
saveHomeSets(homeSets);
saveCollections(collections.values());
db.setTransactionSuccessful();
@@ -288,25 +327,28 @@ public class DavService extends Service {
App.log.log(Level.SEVERE, "Couldn't refresh collection list", e);
Intent debugIntent = new Intent(DavService.this, DebugInfoActivity.class);
debugIntent.putExtra(DebugInfoActivity.KEY_EXCEPTION, e);
debugIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e);
if (account != null)
debugIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account);
NotificationManager nm = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
NotificationManagerCompat nm = NotificationManagerCompat.from(DavService.this);
Notification notify = new NotificationCompat.Builder(DavService.this)
.setSmallIcon(R.drawable.ic_error_light)
.setLargeIcon(((BitmapDrawable)getResources().getDrawable(R.drawable.ic_launcher)).getBitmap())
.setLargeIcon(App.getLauncherBitmap(DavService.this))
.setContentTitle(getString(R.string.dav_service_refresh_failed))
.setContentText(getString(R.string.dav_service_refresh_couldnt_refresh))
.setContentIntent(PendingIntent.getActivity(DavService.this, 0, debugIntent, 0))
.setContentIntent(PendingIntent.getActivity(DavService.this, 0, debugIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.build();
nm.notify(Constants.NOTIFICATION_REFRESH_COLLECTIONS, notify);
} finally {
dbHelper.close();
runningRefresh.remove(service);
for (RefreshingStatusListener listener : refreshingStatusListeners)
listener.onDavRefreshStatusChanged(service, false);
for (WeakReference<RefreshingStatusListener> ref : refreshingStatusListeners) {
RefreshingStatusListener listener = ref.get();
if (listener != null)
listener.onDavRefreshStatusChanged(service, false);
}
}
}
@@ -319,28 +361,32 @@ public class DavService extends Service {
private void queryHomeSets(String serviceType, DavResource dav, Set<HttpUrl> homeSets) throws IOException, HttpException, DavException {
if (Services.SERVICE_CARDDAV.equals(serviceType)) {
dav.propfind(0, AddressbookHomeSet.NAME, GroupMembership.NAME);
AddressbookHomeSet addressbookHomeSet = (AddressbookHomeSet)dav.properties.get(AddressbookHomeSet.NAME);
if (addressbookHomeSet != null)
for (String href : addressbookHomeSet.hrefs)
homeSets.add(UrlUtils.withTrailingSlash(dav.location.resolve(href)));
for (Pair<DavResource, Property> result : dav.findProperties(AddressbookHomeSet.NAME)) {
AddressbookHomeSet addressbookHomeSet = (AddressbookHomeSet)result.getSecond();
for (String href : addressbookHomeSet.getHrefs())
homeSets.add(UrlUtils.withTrailingSlash(result.getFirst().getLocation().resolve(href)));
}
} else if (Services.SERVICE_CALDAV.equals(serviceType)) {
dav.propfind(0, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME);
CalendarHomeSet calendarHomeSet = (CalendarHomeSet)dav.properties.get(CalendarHomeSet.NAME);
if (calendarHomeSet != null)
for (String href : calendarHomeSet.hrefs)
homeSets.add(UrlUtils.withTrailingSlash(dav.location.resolve(href)));
for (Pair<DavResource, Property> result : dav.findProperties(CalendarHomeSet.NAME)) {
CalendarHomeSet calendarHomeSet = (CalendarHomeSet)result.getSecond();
for (String href : calendarHomeSet.getHrefs())
homeSets.add(UrlUtils.withTrailingSlash(result.getFirst().getLocation().resolve(href)));
}
}
}
@NonNull
private Account account() {
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[] { Services.ACCOUNT_NAME }, Services.ID + "=?", new String[] { String.valueOf(service) }, null, null, null);
if (cursor.moveToNext()) {
return new Account(cursor.getString(0), Constants.ACCOUNT_TYPE);
return new Account(cursor.getString(0), getString(R.string.account_type));
} else
throw new IllegalArgumentException("Service not found");
}
@NonNull
private String serviceType() {
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[] { Services.SERVICE }, Services.ID + "=?", new String[] { String.valueOf(service) }, null, null, null);
if (cursor.moveToNext())
@@ -349,6 +395,7 @@ public class DavService extends Service {
throw new IllegalArgumentException("Service not found");
}
@Nullable
private HttpUrl readPrincipal() {
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[] { Services.PRINCIPAL }, Services.ID + "=?", new String[] { String.valueOf(service) }, null, null, null);
if (cursor.moveToNext()) {
@@ -359,6 +406,7 @@ public class DavService extends Service {
return null;
}
@NonNull
private Set<HttpUrl> readHomeSets() {
Set<HttpUrl> homeSets = new LinkedHashSet<>();
@Cleanup Cursor cursor = db.query(HomeSets._TABLE, new String[] { HomeSets.URL }, HomeSets.SERVICE_ID + "=?", new String[] { String.valueOf(service) }, null, null, null);
@@ -377,6 +425,7 @@ public class DavService extends Service {
}
}
@NonNull
private Map<HttpUrl, CollectionInfo> readCollections() {
Map<HttpUrl, CollectionInfo> collections = new LinkedHashMap<>();
@Cleanup Cursor cursor = db.query(Collections._TABLE, null, Collections.SERVICE_ID + "=?", new String[]{String.valueOf(service)}, null, null, null);

View File

@@ -8,8 +8,15 @@
package at.bitfire.davdroid;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.support.annotation.NonNull;
import org.apache.commons.lang3.StringUtils;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import okhttp3.HttpUrl;
public class DavUtils {
@@ -19,4 +26,16 @@ public class DavUtils {
return String.format("#%06X%02X", color, alpha);
}
public static String lastSegmentOfUrl(@NonNull String url) {
// the list returned by HttpUrl.pathSegments() is unmodifiable, so we have to create a copy
List<String> segments = new LinkedList<>(HttpUrl.parse(url).pathSegments());
Collections.reverse(segments);
for (String segment : segments)
if (!StringUtils.isEmpty(segment))
return segment;
return "/";
}
}

View File

@@ -10,10 +10,14 @@ package at.bitfire.davdroid;
import android.accounts.Account;
import android.content.Context;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
@@ -21,9 +25,10 @@ import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import at.bitfire.dav4android.BasicDigestAuthenticator;
import lombok.RequiredArgsConstructor;
import okhttp3.Credentials;
import at.bitfire.dav4android.BasicDigestAuthHandler;
import at.bitfire.dav4android.UrlUtils;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.model.Settings;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@@ -34,49 +39,60 @@ public class HttpClient {
private static final OkHttpClient client = new OkHttpClient();
private static final UserAgentInterceptor userAgentInterceptor = new UserAgentInterceptor();
private static final String userAgent;
private static final String productName, userAgent;
static {
if (BuildConfig.FLAVOR == App.FLAVOR_ICLOUD)
productName = "MultiSync for Cloud";
else if (BuildConfig.FLAVOR == App.FLAVOR_SOLDUPE)
productName = "Soldupe Sync";
else
productName = "DAVdroid";
String date = new SimpleDateFormat("yyyy/MM/dd", Locale.US).format(new Date(BuildConfig.buildTime));
userAgent = "DAVdroid/" + BuildConfig.VERSION_NAME + " (" + date + "; dav4android; okhttp3) Android/" + Build.VERSION.RELEASE;
userAgent = productName + "/" + BuildConfig.VERSION_NAME + " (" + date + "; dav4android; okhttp3) Android/" + Build.VERSION.RELEASE;
}
private HttpClient() {
}
public static OkHttpClient create(@NonNull Context context, @NonNull Account account, @NonNull final Logger logger) throws InvalidAccountException {
OkHttpClient.Builder builder = defaultBuilder(logger);
public static OkHttpClient create(@Nullable Context context, @NonNull AccountSettings settings, @NonNull final Logger logger) {
OkHttpClient.Builder builder = defaultBuilder(context, logger);
// use account settings for authentication and logging
AccountSettings settings = new AccountSettings(context, account);
if (settings.preemptiveAuth())
builder.addNetworkInterceptor(new PreemptiveAuthenticationInterceptor(settings.username(), settings.password()));
else
builder.authenticator(new BasicDigestAuthenticator(null, settings.username(), settings.password()));
// use account settings for authentication
builder = addAuthentication(builder, null, settings.username(), settings.password());
return builder.build();
}
public static OkHttpClient create(@NonNull Logger logger) {
return defaultBuilder(logger).build();
public static OkHttpClient create(@NonNull Context context, @NonNull Logger logger) {
return defaultBuilder(context, logger).build();
}
public static OkHttpClient create(@NonNull Context context, @NonNull AccountSettings settings) {
return create(context, settings, App.log);
}
public static OkHttpClient create(@NonNull Context context, @NonNull Account account) throws InvalidAccountException {
return create(context, account, App.log);
AccountSettings settings = new AccountSettings(context, account);
return create(context, settings, App.log);
}
public static OkHttpClient create() {
return create(App.log);
public static OkHttpClient create(@Nullable Context context) {
return create(context, App.log);
}
private static OkHttpClient.Builder defaultBuilder(@NonNull final Logger logger) {
private static OkHttpClient.Builder defaultBuilder(@Nullable Context context, @NonNull final Logger logger) {
OkHttpClient.Builder builder = client.newBuilder();
// use MemorizingTrustManager to manage self-signed certificates
if (App.getSslSocketFactoryCompat() != null)
builder.sslSocketFactory(App.getSslSocketFactoryCompat());
if (App.getHostnameVerifier() != null)
builder.hostnameVerifier(App.getHostnameVerifier());
if (context != null) {
App app = (App)context.getApplicationContext();
if (App.getSslSocketFactoryCompat() != null && app.getCertManager() != null)
builder.sslSocketFactory(App.getSslSocketFactoryCompat(), app.getCertManager());
if (App.getHostnameVerifier() != null)
builder.hostnameVerifier(App.getHostnameVerifier());
}
// set timeouts
builder.connectTimeout(30, TimeUnit.SECONDS);
@@ -86,11 +102,33 @@ public class HttpClient {
// don't allow redirects, because it would break PROPFIND handling
builder.followRedirects(false);
// custom proxy support
if (context != null) {
SQLiteOpenHelper dbHelper = new ServiceDB.OpenHelper(context);
try {
Settings settings = new Settings(dbHelper.getReadableDatabase());
if (settings.getBoolean(App.OVERRIDE_PROXY, false)) {
InetSocketAddress address = new InetSocketAddress(
settings.getString(App.OVERRIDE_PROXY_HOST, App.OVERRIDE_PROXY_HOST_DEFAULT),
settings.getInt(App.OVERRIDE_PROXY_PORT, App.OVERRIDE_PROXY_PORT_DEFAULT)
);
Proxy proxy = new Proxy(Proxy.Type.HTTP, address);
builder.proxy(proxy);
App.log.log(Level.INFO, "Using proxy", proxy);
}
} catch(IllegalArgumentException|NullPointerException e) {
App.log.log(Level.SEVERE, "Can't set proxy, ignoring", e);
} finally {
dbHelper.close();
}
}
// add User-Agent to every request
builder.addNetworkInterceptor(userAgentInterceptor);
// add cookie store for non-persistent cookies (some services like Horde use cookies for session tracking)
builder.cookieJar(MemoryCookieStore.INSTANCE);
builder.cookieJar(new MemoryCookieStore());
// add network logging, if requested
if (logger.isLoggable(Level.FINEST)) {
@@ -107,24 +145,23 @@ public class HttpClient {
return builder;
}
private static OkHttpClient.Builder addAuthentication(@NonNull OkHttpClient.Builder builder, @NonNull String username, @NonNull String password, boolean preemptive) {
if (preemptive)
builder.addNetworkInterceptor(new PreemptiveAuthenticationInterceptor(username, password));
else
builder.authenticator(new BasicDigestAuthenticator(null, username, password));
return builder;
private static OkHttpClient.Builder addAuthentication(@NonNull OkHttpClient.Builder builder, @Nullable String host, @NonNull String username, @NonNull String password) {
BasicDigestAuthHandler authHandler = new BasicDigestAuthHandler(UrlUtils.hostToDomain(host), username, password);
return builder
.addNetworkInterceptor(authHandler)
.authenticator(authHandler);
}
public static OkHttpClient addAuthentication(@NonNull OkHttpClient client, @NonNull String username, @NonNull String password, boolean preemptive) {
public static OkHttpClient addAuthentication(@NonNull OkHttpClient client, @NonNull String username, @NonNull String password) {
OkHttpClient.Builder builder = client.newBuilder();
addAuthentication(builder, username, password, preemptive);
addAuthentication(builder, null, username, password);
return builder.build();
}
public static OkHttpClient addAuthentication(@NonNull OkHttpClient client, @NonNull String host, @NonNull String username, @NonNull String password) {
return client.newBuilder()
.authenticator(new BasicDigestAuthenticator(host, username, password))
.build();
OkHttpClient.Builder builder = client.newBuilder();
addAuthentication(builder, host, username, password);
return builder.build();
}
@@ -140,18 +177,4 @@ public class HttpClient {
}
}
@RequiredArgsConstructor
static class PreemptiveAuthenticationInterceptor implements Interceptor {
final String username, password;
@Override
public Response intercept(Chain chain) throws IOException {
App.log.fine("Adding basic authorization header for user " + username);
Request request = chain.request().newBuilder()
.header("Authorization", Credentials.basic(username, password))
.build();
return chain.proceed(request);
}
}
}

View File

@@ -8,10 +8,13 @@
package at.bitfire.davdroid;
import java.util.Collections;
import org.apache.commons.collections4.MapIterator;
import org.apache.commons.collections4.keyvalue.MultiKey;
import org.apache.commons.collections4.map.HashedMap;
import org.apache.commons.collections4.map.MultiKeyMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import okhttp3.Cookie;
import okhttp3.CookieJar;
@@ -23,22 +26,42 @@ import okhttp3.HttpUrl;
*/
public class MemoryCookieStore implements CookieJar {
public static final MemoryCookieStore INSTANCE = new MemoryCookieStore();
protected final Map<HttpUrl, List<Cookie>> store = new ConcurrentHashMap<>();
/**
* Stored cookies. The multi-key consists of three parts: name, domain, and path.
* This ensures that cookies can be overwritten. [RFC 6265 5.3 Storage Model]
* Not thread-safe!
*/
protected final MultiKeyMap<String, Cookie> storage = MultiKeyMap.multiKeyMap(new HashedMap<MultiKey<? extends String>, Cookie>());
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
store.put(url, cookies);
synchronized(storage) {
for (Cookie cookie : cookies)
storage.put(cookie.name(), cookie.domain(), cookie.path(), cookie);
}
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
List<Cookie> cookies = store.get(url);
List<Cookie> cookies = new LinkedList<>();
if (cookies == null)
cookies = Collections.emptyList();
synchronized(storage) {
MapIterator<MultiKey<? extends String>, Cookie> iter = storage.mapIterator();
while (iter.hasNext()) {
iter.next();
Cookie cookie = iter.getValue();
// remove expired cookies
if (cookie.expiresAt() <= System.currentTimeMillis()) {
iter.remove();
continue;
}
// add applicable cookies
if (cookie.matches(url))
cookies.add(cookie);
}
}
return cookies;
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid;
import android.accounts.Account;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.support.annotation.NonNull;
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 lombok.Cleanup;
public class PackageChangedReceiver extends BroadcastReceiver {
@Override
@SuppressLint("MissingPermission")
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction()) ||
Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(intent.getAction()))
updateTaskSync(context);
}
static void updateTaskSync(@NonNull Context context) {
boolean tasksInstalled = LocalTaskList.tasksProviderAvailable(context);
App.log.info("Package (un)installed; OpenTasks provider now available = " + tasksInstalled);
// check all accounts and (de)activate OpenTasks if a CalDAV service is defined
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(context);
SQLiteDatabase db = dbHelper.getReadableDatabase();
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[] { Services.ACCOUNT_NAME },
Services.SERVICE + "=?", new String[] { Services.SERVICE_CALDAV }, null, null, null);
while (cursor.moveToNext()) {
Account account = new Account(cursor.getString(0), context.getString(R.string.account_type));
if (tasksInstalled) {
if (ContentResolver.getIsSyncable(account, TaskProvider.ProviderName.OpenTasks.getAuthority()) <= 0) {
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.getAuthority(), 1);
ContentResolver.setSyncAutomatically(account, TaskProvider.ProviderName.OpenTasks.getAuthority(), true);
ContentResolver.addPeriodicSync(account, TaskProvider.ProviderName.OpenTasks.getAuthority(), new Bundle(), Constants.DEFAULT_SYNC_INTERVAL);
}
} else
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.getAuthority(), 0);
}
}
}

View File

@@ -15,7 +15,6 @@ import android.text.TextUtils;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.HashSet;
@@ -28,7 +27,6 @@ import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
import de.duenndns.ssl.MemorizingTrustManager;
import lombok.Cleanup;
public class SSLSocketFactoryCompat extends SSLSocketFactory {
@@ -54,56 +52,55 @@ public class SSLSocketFactoryCompat extends SSLSocketFactory {
SSLSocketFactoryCompat.protocols = protocols.toArray(new String[protocols.size()]);
/* set up reasonable cipher suites */
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// choose known secure cipher suites
List<String> allowedCiphers = Arrays.asList(
// 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",
"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");
List<String> availableCiphers = Arrays.asList(socket.getSupportedCipherSuites());
App.log.fine("Available cipher suites: " + TextUtils.join(", ", availableCiphers));
App.log.fine("Cipher suites enabled by default: " + TextUtils.join(", ", socket.getEnabledCipherSuites()));
// choose known secure cipher suites
List<String> allowedCiphers = Arrays.asList(
// 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"
);
List<String> availableCiphers = Arrays.asList(socket.getSupportedCipherSuites());
App.log.info("Available cipher suites: " + TextUtils.join(", ", availableCiphers));
// take all allowed ciphers that are available and put them into preferredCiphers
HashSet<String> preferredCiphers = new HashSet<>(allowedCiphers);
preferredCiphers.retainAll(availableCiphers);
/* 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 maximum security, preferredCiphers should *replace* enabled ciphers (thus disabling
* ciphers which are enabled by default, but have become unsecure), but I guess 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, ...
HashSet<String> enabledCiphers = new HashSet<>(Arrays.asList(socket.getEnabledCipherSuites()));
App.log.info("Cipher suites enabled by default: " + TextUtils.join(", ", enabledCiphers));
// ... add explicitly allowed ciphers ...
enabledCiphers.addAll(allowedCiphers);
// ... and keep only those which are actually available
enabledCiphers.retainAll(availableCiphers);
// add preferred ciphers to enabled ciphers
HashSet<String> enabledCiphers = preferredCiphers;
enabledCiphers.addAll(new HashSet<>(Arrays.asList(socket.getEnabledCipherSuites())));
App.log.info("Enabling (only) those TLS ciphers: " + TextUtils.join(", ", enabledCiphers));
SSLSocketFactoryCompat.cipherSuites = enabledCiphers.toArray(new String[enabledCiphers.size()]);
}
App.log.info("Enabling (only) those TLS ciphers: " + TextUtils.join(", ", enabledCiphers));
SSLSocketFactoryCompat.cipherSuites = enabledCiphers.toArray(new String[enabledCiphers.size()]);
}
} catch (IOException e) {
App.log.severe("Couldn't determine default TLS settings");
}
}
public SSLSocketFactoryCompat(@NonNull MemorizingTrustManager mtm) {
public SSLSocketFactoryCompat(@NonNull X509TrustManager trustManager) {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new X509TrustManager[] { mtm }, null);
sslContext.init(null, new X509TrustManager[] { trustManager }, null);
delegate = sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new AssertionError(); // The system has no TLS. Just give up.

View File

@@ -8,8 +8,6 @@
package at.bitfire.davdroid.log;
import android.util.Log;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
@@ -33,12 +31,14 @@ public class PlainTextFormatter extends Formatter {
public String format(LogRecord r) {
StringBuilder builder = new StringBuilder();
if (!logcat) {
builder.append(DateFormatUtils.format(r.getMillis(), "yyyy-MM-dd HH:mm:ss"));
builder.append(String.format(" %d ", r.getThreadID()));
}
if (!logcat)
builder .append(DateFormatUtils.format(r.getMillis(), "yyyy-MM-dd HH:mm:ss"))
.append(" ").append(r.getThreadID()).append(" ");
builder.append(String.format("[%s] %s", shortClassName(r.getSourceClassName()), r.getMessage()));
if (!r.getSourceClassName().replaceFirst("\\$.*", "").equals(r.getLoggerName()))
builder.append("[").append(shortClassName(r.getSourceClassName())).append("] ");
builder.append(r.getMessage());
if (r.getThrown() != null)
builder .append("\nEXCEPTION ")

View File

@@ -65,48 +65,48 @@ public class CollectionInfo implements Serializable {
public static CollectionInfo fromDavResource(DavResource dav) {
CollectionInfo info = new CollectionInfo();
info.url = dav.location.toString();
info.url = dav.getLocation().toString();
ResourceType type = (ResourceType)dav.properties.get(ResourceType.NAME);
ResourceType type = (ResourceType)dav.getProperties().get(ResourceType.NAME);
if (type != null) {
if (type.types.contains(ResourceType.ADDRESSBOOK))
if (type.getTypes().contains(ResourceType.ADDRESSBOOK))
info.type = Type.ADDRESS_BOOK;
else if (type.types.contains(ResourceType.CALENDAR))
else if (type.getTypes().contains(ResourceType.CALENDAR))
info.type = Type.CALENDAR;
}
info.readOnly = false;
CurrentUserPrivilegeSet privilegeSet = (CurrentUserPrivilegeSet)dav.properties.get(CurrentUserPrivilegeSet.NAME);
CurrentUserPrivilegeSet privilegeSet = (CurrentUserPrivilegeSet)dav.getProperties().get(CurrentUserPrivilegeSet.NAME);
if (privilegeSet != null)
info.readOnly = !privilegeSet.mayWriteContent;
info.readOnly = !privilegeSet.getMayWriteContent();
DisplayName displayName = (DisplayName)dav.properties.get(DisplayName.NAME);
if (displayName != null && !StringUtils.isEmpty(displayName.displayName))
info.displayName = displayName.displayName;
DisplayName displayName = (DisplayName)dav.getProperties().get(DisplayName.NAME);
if (displayName != null && !StringUtils.isEmpty(displayName.getDisplayName()))
info.displayName = displayName.getDisplayName();
if (info.type == Type.ADDRESS_BOOK) {
AddressbookDescription addressbookDescription = (AddressbookDescription)dav.properties.get(AddressbookDescription.NAME);
AddressbookDescription addressbookDescription = (AddressbookDescription)dav.getProperties().get(AddressbookDescription.NAME);
if (addressbookDescription != null)
info.description = addressbookDescription.description;
info.description = addressbookDescription.getDescription();
} else if (info.type == Type.CALENDAR) {
CalendarDescription calendarDescription = (CalendarDescription)dav.properties.get(CalendarDescription.NAME);
CalendarDescription calendarDescription = (CalendarDescription)dav.getProperties().get(CalendarDescription.NAME);
if (calendarDescription != null)
info.description = calendarDescription.description;
info.description = calendarDescription.getDescription();
CalendarColor calendarColor = (CalendarColor)dav.properties.get(CalendarColor.NAME);
CalendarColor calendarColor = (CalendarColor)dav.getProperties().get(CalendarColor.NAME);
if (calendarColor != null)
info.color = calendarColor.color;
info.color = calendarColor.getColor();
CalendarTimezone timeZone = (CalendarTimezone)dav.properties.get(CalendarTimezone.NAME);
CalendarTimezone timeZone = (CalendarTimezone)dav.getProperties().get(CalendarTimezone.NAME);
if (timeZone != null)
info.timeZone = timeZone.vTimeZone;
info.timeZone = timeZone.getVTimeZone();
info.supportsVEVENT = info.supportsVTODO = true;
SupportedCalendarComponentSet supportedCalendarComponentSet = (SupportedCalendarComponentSet)dav.properties.get(SupportedCalendarComponentSet.NAME);
SupportedCalendarComponentSet supportedCalendarComponentSet = (SupportedCalendarComponentSet)dav.getProperties().get(SupportedCalendarComponentSet.NAME);
if (supportedCalendarComponentSet != null) {
info.supportsVEVENT = supportedCalendarComponentSet.supportsEvents;
info.supportsVTODO = supportedCalendarComponentSet.supportsTasks;
info.supportsVEVENT = supportedCalendarComponentSet.getSupportsEvents();
info.supportsVTODO = supportedCalendarComponentSet.getSupportsTasks();
}
}

View File

@@ -1,29 +0,0 @@
/*
* Copyright © 2013 2016 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 lombok.ToString;
@ToString
public class HomeSet {
public long id, serviceID;
public String URL;
public static HomeSet fromDB(ContentValues values) {
HomeSet homeSet = new HomeSet();
homeSet.id = values.getAsLong(ServiceDB.HomeSets.ID);
homeSet.serviceID = values.getAsLong(ServiceDB.HomeSets.SERVICE_ID);
homeSet.URL = values.getAsString(ServiceDB.HomeSets.URL);
return homeSet;
}
}

View File

@@ -1,29 +0,0 @@
/*
* Copyright © 2013 2016 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;
public class Service {
public long id;
public String accountName, service, principal;
public long lastRefresh;
public static Service fromDB(ContentValues values) {
Service service = new Service();
service.id = values.getAsLong(ServiceDB.Services.ID);
service.accountName = values.getAsString(ServiceDB.Services.ACCOUNT_NAME);
service.service = values.getAsString(ServiceDB.Services.SERVICE);
service.principal = values.getAsString(ServiceDB.Services.PRINCIPAL);
return service;
}
}

View File

@@ -8,12 +8,15 @@
package at.bitfire.davdroid.model;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import at.bitfire.davdroid.App;
import lombok.Cleanup;
@@ -76,16 +79,20 @@ public class ServiceDB {
@Override
public void onOpen(SQLiteDatabase db) {
db.enableWriteAheadLogging();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
db.setForeignKeyConstraintsEnabled(true);
else
if (Build.VERSION.SDK_INT < 16)
db.execSQL("PRAGMA foreign_keys=ON;");
}
@Override
@RequiresApi(16)
public void onConfigure(SQLiteDatabase db) {
setWriteAheadLoggingEnabled(true);
db.setForeignKeyConstraintsEnabled(true);
}
@Override
public void onCreate(SQLiteDatabase db) {
App.log.info("Creating services database");
App.log.info("Creating database " + db.getPath());
db.execSQL("CREATE TABLE " + Settings._TABLE + "(" +
Settings.NAME + " TEXT NOT NULL," +
@@ -116,7 +123,7 @@ public class ServiceDB {
Collections.DISPLAY_NAME + " TEXT NULL," +
Collections.DESCRIPTION + " TEXT NULL," +
Collections.COLOR + " INTEGER NULL," +
Collections.TIME_ZONE + " TEXt NULL," +
Collections.TIME_ZONE + " TEXT NULL," +
Collections.SUPPORTS_VEVENT + " INTEGER NULL," +
Collections.SUPPORTS_VTODO + " INTEGER NULL," +
Collections.SYNC + " INTEGER DEFAULT 0 NOT NULL" +
@@ -126,6 +133,7 @@ public class ServiceDB {
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// no different versions yet
}
@@ -177,4 +185,11 @@ public class ServiceDB {
}
}
public static void onRenameAccount(@NonNull SQLiteDatabase db, @NonNull String oldName, @NonNull String newName) {
ContentValues values = new ContentValues(1);
values.put(Services.ACCOUNT_NAME, newName);
db.update(Services._TABLE, values, Services.ACCOUNT_NAME + "=?", new String[] { oldName });
}
}

View File

@@ -11,6 +11,7 @@ package at.bitfire.davdroid.model;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.Nullable;
import lombok.Cleanup;
@@ -22,10 +23,11 @@ public class Settings {
this.db = db;
}
public boolean getBoolean(String name, boolean defaultValue) {
@Cleanup Cursor cursor = db.query(ServiceDB.Settings._TABLE, new String[] { ServiceDB.Settings.VALUE },
ServiceDB.Settings.NAME + "=?", new String[] { name }, null, null, null);
if (cursor.moveToNext())
if (cursor.moveToNext() && !cursor.isNull(0))
return cursor.getInt(0) != 0;
else
return defaultValue;
@@ -38,6 +40,42 @@ public class Settings {
db.insertWithOnConflict(ServiceDB.Settings._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
public int getInt(String name, int defaultValue) {
@Cleanup Cursor cursor = db.query(ServiceDB.Settings._TABLE, new String[] { ServiceDB.Settings.VALUE },
ServiceDB.Settings.NAME + "=?", new String[] { name }, null, null, null);
if (cursor.moveToNext() && !cursor.isNull(0))
return cursor.isNull(0) ? defaultValue : cursor.getInt(0);
else
return defaultValue;
}
public void putInt(String name, int value) {
ContentValues values = new ContentValues(2);
values.put(ServiceDB.Settings.NAME, name);
values.put(ServiceDB.Settings.VALUE, value);
db.insertWithOnConflict(ServiceDB.Settings._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
@Nullable
public String getString(String name, @Nullable String defaultValue) {
@Cleanup Cursor cursor = db.query(ServiceDB.Settings._TABLE, new String[] { ServiceDB.Settings.VALUE },
ServiceDB.Settings.NAME + "=?", new String[] { name }, null, null, null);
if (cursor.moveToNext())
return cursor.getString(0);
else
return defaultValue;
}
public void putString(String name, @Nullable String value) {
ContentValues values = new ContentValues(2);
values.put(ServiceDB.Settings.NAME, name);
values.put(ServiceDB.Settings.VALUE, value);
db.insertWithOnConflict(ServiceDB.Settings._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
public void remove(String name) {
db.delete(ServiceDB.Settings._TABLE, ServiceDB.Settings.NAME + "=?", new String[] { name });
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright © 2013 2016 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.provider.ContactsContract.RawContacts;
public class UnknownProperties {
public static final String CONTENT_ITEM_TYPE = "x.davdroid/unknown-properties";
public static final String
MIMETYPE = RawContacts.Data.MIMETYPE,
RAW_CONTACT_ID = RawContacts.Data.RAW_CONTACT_ID,
UNKNOWN_PROPERTIES = RawContacts.Data.DATA1;
}

View File

@@ -8,208 +8,385 @@
package at.bitfire.davdroid.resource;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Base64;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.DavUtils;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.vcard4android.AndroidAddressBook;
import at.bitfire.vcard4android.AndroidContact;
import at.bitfire.vcard4android.AndroidGroupFactory;
import at.bitfire.vcard4android.AndroidGroup;
import at.bitfire.vcard4android.CachedGroupMembership;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
public class LocalAddressBook extends AndroidAddressBook implements LocalCollection {
public class LocalAddressBook extends AndroidAddressBook implements LocalCollection<LocalResource> {
protected static final String
SYNC_STATE_CTAG = "ctag",
SYNC_STATE_URL = "url";
USER_DATA_MAIN_ACCOUNT_TYPE = "real_account_type",
USER_DATA_MAIN_ACCOUNT_NAME = "real_account_name",
USER_DATA_URL = "url",
USER_DATA_CTAG = "ctag";
protected final Context context;
private final Bundle syncState = new Bundle();
/**
* Whether contact groups (LocalGroup resources) are included in query results for
* {@link #getAll()}, {@link #getDeleted()}, {@link #getDirty()} and
* {@link #getWithoutFileName()}.
*/
public boolean includeGroups = true;
public LocalAddressBook(Account account, ContentProviderClient provider) {
super(account, provider, AndroidGroupFactory.INSTANCE, LocalContact.Factory.INSTANCE);
public static LocalAddressBook[] find(@NonNull Context context, @NonNull ContentProviderClient provider, @Nullable Account mainAccount) throws ContactsStorageException {
AccountManager accountManager = AccountManager.get(context);
List<LocalAddressBook> result = new LinkedList<>();
for (Account account : accountManager.getAccountsByType(App.getAddressBookAccountType())) {
LocalAddressBook addressBook = new LocalAddressBook(context, account, provider);
if (mainAccount == null || addressBook.getMainAccount().equals(mainAccount))
result.add(addressBook);
}
return result.toArray(new LocalAddressBook[result.size()]);
}
public static LocalAddressBook create(@NonNull Context context, @NonNull ContentProviderClient provider, @NonNull Account mainAccount, @NonNull CollectionInfo info) throws ContactsStorageException {
AccountManager accountManager = AccountManager.get(context);
Account account = new Account(accountName(mainAccount, info), App.getAddressBookAccountType());
if (!accountManager.addAccountExplicitly(account, null, initialUserData(mainAccount, info.url)))
throw new ContactsStorageException("Couldn't create address book account");
LocalAddressBook addressBook = new LocalAddressBook(context, account, provider);
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
return addressBook;
}
public void update(@NonNull CollectionInfo info) throws AuthenticatorException, OperationCanceledException, IOException, ContactsStorageException {
final String newAccountName = accountName(getMainAccount(), info);
if (!account.name.equals(newAccountName) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
final AccountManager accountManager = AccountManager.get(context);
AccountManagerFuture<Account> future = accountManager.renameAccount(account, newAccountName, new AccountManagerCallback<Account>() {
@Override
public void run(AccountManagerFuture<Account> future) {
try {
// update raw contacts to new account name
if (provider != null) {
ContentValues values = new ContentValues(1);
values.put(RawContacts.ACCOUNT_NAME, newAccountName);
provider.update(syncAdapterURI(RawContacts.CONTENT_URI), values, RawContacts.ACCOUNT_NAME + "=?", new String[] { account.name });
}
} catch(RemoteException e) {
App.log.log(Level.WARNING, "Couldn't re-assign contacts to new account name", e);
}
}
}, null);
account = future.getResult();
}
// make sure it will still be synchronized when contacts are updated
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
}
public void delete() {
AccountManager accountManager = AccountManager.get(context);
accountManager.removeAccount(account, null, null);
}
/**
* Returns an array of local contacts, excluding those which have been modified locally (and not uploaded yet).
*/
public LocalAddressBook(Context context, Account account, ContentProviderClient provider) {
super(account, provider, LocalGroup.Factory.INSTANCE, LocalContact.Factory.INSTANCE);
this.context = context;
}
@NonNull
public LocalContact findContactByUID(String uid) throws ContactsStorageException, FileNotFoundException {
LocalContact[] contacts = (LocalContact[])queryContacts(LocalContact.COLUMN_UID + "=?", new String[] { uid });
if (contacts.length == 0)
throw new FileNotFoundException();
return contacts[0];
}
@Override
public LocalContact[] getAll() throws ContactsStorageException {
return (LocalContact[])queryContacts(null, null);
@NonNull
public List<LocalResource> getAll() throws ContactsStorageException {
List<LocalResource> all = new LinkedList<>();
Collections.addAll(all, (LocalContact[])queryContacts(null, null));
if (includeGroups)
Collections.addAll(all, (LocalGroup[])queryGroups(null, null));
return all;
}
/**
* Returns an array of local contacts which have been deleted locally. (DELETED != 0).
* Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0).
*/
@Override
public LocalContact[] getDeleted() throws ContactsStorageException {
return (LocalContact[])queryContacts(RawContacts.DELETED + "!=0", null);
@NonNull
public List<LocalResource> getDeleted() throws ContactsStorageException {
List<LocalResource> deleted = new LinkedList<>();
Collections.addAll(deleted, getDeletedContacts());
if (includeGroups)
Collections.addAll(deleted, getDeletedGroups());
return deleted;
}
/**
* Returns an array of local contacts which have been changed locally (DIRTY != 0).
* Queries all contacts with DIRTY flag and checks whether their data checksum has changed, i.e.
* if they're "really dirty" (= data has changed, not only metadata, which is not hashed).
* The DIRTY flag is removed from contacts which are not "really dirty", i.e. from contacts
* whose contact data checksum has not changed.
* @return number of "really dirty" contacts
*/
public int verifyDirty() throws ContactsStorageException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
App.log.severe("verifyDirty() should not be called on Android <7");
int reallyDirty = 0;
for (LocalContact contact : getDirtyContacts()) {
try {
int lastHash = contact.getLastHashCode(),
currentHash = contact.dataHashCode();
if (lastHash == currentHash) {
// hash is code still the same, contact is not "really dirty" (only metadata been have changed)
App.log.log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact);
contact.resetDirty();
} else {
App.log.log(Level.FINE, "Contact data has changed from hash " + lastHash + " to " + currentHash, contact);
reallyDirty++;
}
} catch(FileNotFoundException e) {
throw new ContactsStorageException("Couldn't calculate hash code", e);
}
}
if (includeGroups)
reallyDirty += getDirtyGroups().length;
return reallyDirty;
}
/**
* Returns an array of local contacts/groups which have been changed locally (DIRTY != 0).
*/
@Override
public LocalContact[] getDirty() throws ContactsStorageException {
return (LocalContact[])queryContacts(RawContacts.DIRTY + "!=0", null);
@NonNull
public List<LocalResource> getDirty() throws ContactsStorageException {
List<LocalResource> dirty = new LinkedList<>();
Collections.addAll(dirty, getDirtyContacts());
if (includeGroups)
Collections.addAll(dirty, getDirtyGroups());
return dirty;
}
/**
* Returns an array of local contacts which don't have a file name yet.
*/
@Override
public LocalContact[] getWithoutFileName() throws ContactsStorageException {
return (LocalContact[])queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null);
@NonNull
public List<LocalResource> getWithoutFileName() throws ContactsStorageException {
List<LocalResource> nameless = new LinkedList<>();
Collections.addAll(nameless, (LocalContact[])queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null));
if (includeGroups)
Collections.addAll(nameless, (LocalGroup[])queryGroups(AndroidGroup.COLUMN_FILENAME + " IS NULL", null));
return nameless;
}
public void deleteAll() throws ContactsStorageException {
@NonNull
public LocalContact[] getDeletedContacts() throws ContactsStorageException {
return (LocalContact[])queryContacts(RawContacts.DELETED + "!= 0", null);
}
@NonNull
public LocalContact[] getDirtyContacts() throws ContactsStorageException {
return (LocalContact[])queryContacts(RawContacts.DIRTY + "!= 0", null);
}
@NonNull
public LocalGroup[] getDeletedGroups() throws ContactsStorageException {
return (LocalGroup[])queryGroups(Groups.DELETED + "!= 0", null);
}
@NonNull
public LocalGroup[] getDirtyGroups() throws ContactsStorageException {
return (LocalGroup[])queryGroups(Groups.DIRTY + "!= 0", null);
}
@NonNull LocalContact[] getByGroupMembership(long groupID) throws ContactsStorageException {
try {
@Cleanup Cursor cursor = provider.query(syncAdapterURI(ContactsContract.Data.CONTENT_URI),
new String[] { RawContacts.Data.RAW_CONTACT_ID },
"(" + GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?) OR (" + CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?)",
new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupID), CachedGroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupID) },
null);
Set<Long> ids = new HashSet<>();
while (cursor != null && cursor.moveToNext())
ids.add(cursor.getLong(0));
LocalContact[] contacts = new LocalContact[ids.size()];
int i = 0;
for (Long id : ids)
contacts[i++] = new LocalContact(this, id, null, null);
return contacts;
} catch (RemoteException e) {
throw new ContactsStorageException("Couldn't query contacts", e);
}
}
public void deleteAll() throws ContactsStorageException {
try {
provider.delete(syncAdapterURI(RawContacts.CONTENT_URI), null, null);
} catch (RemoteException e) {
throw new ContactsStorageException("Couldn't delete all local contacts", e);
provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null);
} catch(RemoteException e) {
throw new ContactsStorageException("Couldn't delete all local contacts and groups", e);
}
}
// GROUPS
/**
* Finds the first group with the given title.
* @param displayName title of the group to look for
* @return group with given title, or null if none
* Finds the first group with the given title. If there is no group with this
* title, a new group is created.
* @param title title of the group to look for
* @return id of the group with given title
* @throws ContactsStorageException on contact provider errors
*/
@SuppressWarnings("Recycle")
public LocalGroup findGroupByTitle(String displayName) throws ContactsStorageException {
public long findOrCreateGroup(@NonNull String title) throws ContactsStorageException {
try {
@Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
new String[] { Groups._ID },
ContactsContract.Groups.TITLE + "=?", new String[] { displayName }, null);
new String[] { Groups._ID },
Groups.TITLE + "=?", new String[] { title },
null);
if (cursor != null && cursor.moveToNext())
return new LocalGroup(this, cursor.getLong(0));
} catch (RemoteException e) {
return cursor.getLong(0);
ContentValues values = new ContentValues();
values.put(Groups.TITLE, title);
Uri uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values);
return ContentUris.parseId(uri);
} catch(RemoteException e) {
throw new ContactsStorageException("Couldn't find local contact group", e);
}
return null;
}
@SuppressWarnings("Recycle")
public LocalGroup[] getDeletedGroups() throws ContactsStorageException {
List<LocalGroup> groups = new LinkedList<>();
try {
@Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
new String[] { Groups._ID },
Groups.DELETED + "!=0", null, null);
while (cursor != null && cursor.moveToNext())
groups.add(new LocalGroup(this, cursor.getLong(0)));
} catch (RemoteException e) {
throw new ContactsStorageException("Couldn't query deleted groups", e);
}
return groups.toArray(new LocalGroup[groups.size()]);
}
@SuppressWarnings("Recycle")
public LocalGroup[] getDirtyGroups() throws ContactsStorageException {
List<LocalGroup> groups = new LinkedList<>();
try {
@Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
new String[] { Groups._ID },
Groups.DIRTY + "!=0", null, null);
while (cursor != null && cursor.moveToNext())
groups.add(new LocalGroup(this, cursor.getLong(0)));
} catch (RemoteException e) {
throw new ContactsStorageException("Couldn't query dirty groups", e);
}
return groups.toArray(new LocalGroup[groups.size()]);
}
@SuppressWarnings("Recycle")
public void markMembersDirty(long groupId) throws ContactsStorageException {
ContentValues dirty = new ContentValues(1);
dirty.put(RawContacts.DIRTY, 1);
try {
// query all GroupMemberships of this groupId, mark every corresponding raw contact as DIRTY
@Cleanup Cursor cursor = provider.query(syncAdapterURI(Data.CONTENT_URI),
new String[] { GroupMembership.RAW_CONTACT_ID },
Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
while (cursor != null && cursor.moveToNext()) {
long id = cursor.getLong(0);
App.log.fine("Marking raw contact #" + id + " as dirty");
provider.update(syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)), dirty, null, null);
public void removeEmptyGroups() throws ContactsStorageException {
// find groups without members
/** should be done using {@link Groups.SUMMARY_COUNT}, but it's not implemented in Android yet */
for (LocalGroup group : (LocalGroup[])queryGroups(null, null))
if (group.getMembers().length == 0) {
App.log.log(Level.FINE, "Deleting group", group);
group.delete();
}
} catch (RemoteException e) {
throw new ContactsStorageException("Couldn't query dirty groups", e);
}
public void removeGroups() throws ContactsStorageException {
try {
provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null);
} catch(RemoteException e) {
throw new ContactsStorageException("Couldn't remove all groups", e);
}
}
// SYNC STATE
// SETTINGS
@SuppressWarnings("Recycle")
protected void readSyncState() throws ContactsStorageException {
@Cleanup("recycle") Parcel parcel = Parcel.obtain();
byte[] raw = getSyncState();
syncState.clear();
if (raw != null) {
parcel.unmarshall(raw, 0, raw.length);
parcel.setDataPosition(0);
syncState.putAll(parcel.readBundle());
}
public static Bundle initialUserData(@NonNull Account mainAccount, @NonNull String url) {
Bundle bundle = new Bundle(3);
bundle.putString(USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name);
bundle.putString(USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type);
bundle.putString(USER_DATA_URL, url);
return bundle;
}
@SuppressWarnings("Recycle")
protected void writeSyncState() throws ContactsStorageException {
@Cleanup("recycle") Parcel parcel = Parcel.obtain();
parcel.writeBundle(syncState);
setSyncState(parcel.marshall());
public Account getMainAccount() throws ContactsStorageException {
AccountManager accountManager = AccountManager.get(context);
String name = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME),
type = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE);
if (name != null && type != null)
return new Account(name, type);
else
throw new ContactsStorageException("Address book doesn't exist anymore");
}
public void setMainAccount(@NonNull Account mainAccount) throws ContactsStorageException {
AccountManager accountManager = AccountManager.get(context);
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name);
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type);
}
public String getURL() throws ContactsStorageException {
synchronized (syncState) {
readSyncState();
return syncState.getString(SYNC_STATE_URL);
}
AccountManager accountManager = AccountManager.get(context);
return accountManager.getUserData(account, USER_DATA_URL);
}
public void setURL(String url) throws ContactsStorageException {
synchronized (syncState) {
readSyncState();
syncState.putString(SYNC_STATE_URL, url);
writeSyncState();
}
AccountManager accountManager = AccountManager.get(context);
accountManager.setUserData(account, USER_DATA_URL, url);
}
@Override
public String getCTag() throws ContactsStorageException {
synchronized (syncState) {
readSyncState();
return syncState.getString(SYNC_STATE_CTAG);
}
AccountManager accountManager = AccountManager.get(context);
return accountManager.getUserData(account, USER_DATA_CTAG);
}
@Override
public void setCTag(String cTag) throws ContactsStorageException {
synchronized (syncState) {
readSyncState();
syncState.putString(SYNC_STATE_CTAG, cTag);
writeSyncState();
}
AccountManager accountManager = AccountManager.get(context);
accountManager.setUserData(account, USER_DATA_CTAG, cTag);
}
// HELPERS
public static String accountName(@NonNull Account mainAccount, @NonNull CollectionInfo info) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(info.url.hashCode());
String hash = Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP | Base64.NO_PADDING);
StringBuilder sb = new StringBuilder(!TextUtils.isEmpty(info.displayName) ? info.displayName : DavUtils.lastSegmentOfUrl(info.url));
sb .append(" (")
.append(mainAccount.name)
.append(" ")
.append(hash)
.append(")");
return sb.toString();
}
}

View File

@@ -9,14 +9,12 @@
package at.bitfire.davdroid.resource;
import android.accounts.Account;
import android.annotation.TargetApi;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Calendars;
@@ -30,36 +28,35 @@ import net.fortuna.ical4j.model.component.VTimeZone;
import org.apache.commons.lang3.StringUtils;
import java.io.FileNotFoundException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.DavUtils;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.ical4android.AndroidCalendar;
import at.bitfire.ical4android.AndroidCalendarFactory;
import at.bitfire.ical4android.BatchOperation;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.Constants;
import at.bitfire.ical4android.DateUtils;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
public class LocalCalendar extends AndroidCalendar implements LocalCollection {
public class LocalCalendar extends AndroidCalendar<LocalEvent> implements LocalCollection<LocalEvent> {
public static final int defaultColor = 0xFF8bc34a; // light green 500
public static final String COLUMN_CTAG = Calendars.CAL_SYNC1;
protected static final int
DIRTY_INCREASE_SEQUENCE = 1,
DIRTY_DONT_INCREASE_SEQUENCE = 2;
static String[] BASE_INFO_COLUMNS = new String[] {
Events._ID,
Events._SYNC_ID,
LocalEvent.COLUMN_ETAG
};
@NonNull
@Override
protected String[] eventBaseInfoColumns() {
return BASE_INFO_COLUMNS;
@@ -71,26 +68,31 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
}
public static Uri create(@NonNull Account account, @NonNull ContentProviderClient provider, @NonNull CollectionInfo info) throws CalendarStorageException {
ContentValues values = valuesFromCollectionInfo(info);
ContentValues 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);
values.put(Calendars.OWNER_ACCOUNT, account.name);
// flag as visible & synchronizable at creation, might be changed by user at any time
values.put(Calendars.VISIBLE, 1);
values.put(Calendars.SYNC_EVENTS, 1);
return create(account, provider, values);
}
public void update(CollectionInfo info) throws CalendarStorageException {
update(valuesFromCollectionInfo(info));
public void update(CollectionInfo info, boolean updateColor) throws CalendarStorageException {
update(valuesFromCollectionInfo(info, updateColor));
}
@TargetApi(15)
private static ContentValues valuesFromCollectionInfo(CollectionInfo info) {
private static ContentValues valuesFromCollectionInfo(CollectionInfo info, boolean withColor) {
ContentValues values = new ContentValues();
values.put(Calendars.NAME, info.url);
values.put(Calendars.CALENDAR_DISPLAY_NAME, info.displayName);
values.put(Calendars.CALENDAR_COLOR, info.color != null ? info.color : defaultColor);
values.put(Calendars.CALENDAR_DISPLAY_NAME, !TextUtils.isEmpty(info.displayName) ? info.displayName : DavUtils.lastSegmentOfUrl(info.url));
if (withColor)
values.put(Calendars.CALENDAR_COLOR, info.color != null ? info.color : defaultColor);
if (info.readOnly)
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ);
@@ -100,54 +102,50 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1);
}
values.put(Calendars.SYNC_EVENTS, 1);
values.put(Calendars.VISIBLE, 1);
if (!TextUtils.isEmpty(info.timeZone)) {
VTimeZone timeZone = DateUtils.parseVTimeZone(info.timeZone);
if (timeZone != null && timeZone.getTimeZoneId() != null)
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(timeZone.getTimeZoneId().getValue()));
}
if (!TextUtils.isEmpty(info.timeZone))
try {
VTimeZone timeZone = DateUtils.parseVTimeZone(info.timeZone);
if (timeZone.getTimeZoneId() != null)
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(timeZone.getTimeZoneId().getValue()));
} catch(IllegalArgumentException e) {
App.log.log(Level.WARNING, "Couldn't parse calendar default time zone", e);
}
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT);
if (Build.VERSION.SDK_INT >= 15) {
values.put(Calendars.ALLOWED_AVAILABILITY, StringUtils.join(new int[] { Reminders.AVAILABILITY_TENTATIVE, Reminders.AVAILABILITY_FREE, Reminders.AVAILABILITY_BUSY }, ","));
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(new int[] { CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE }, ", "));
}
values.put(Calendars.ALLOWED_AVAILABILITY, StringUtils.join(new int[] { Reminders.AVAILABILITY_TENTATIVE, Reminders.AVAILABILITY_FREE, Reminders.AVAILABILITY_BUSY }, ","));
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(new int[] { CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE }, ", "));
return values;
}
@Override
public LocalResource[] getAll() throws CalendarStorageException, ContactsStorageException {
return (LocalEvent[])queryEvents(Events.ORIGINAL_ID + " IS NULL", null);
public List<LocalEvent> getAll() throws CalendarStorageException, ContactsStorageException {
return queryEvents(Events.ORIGINAL_ID + " IS NULL", null);
}
@Override
public LocalEvent[] getDeleted() throws CalendarStorageException {
return (LocalEvent[])queryEvents(Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NULL", null);
public List<LocalEvent> getDeleted() throws CalendarStorageException {
return queryEvents(Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NULL", null);
}
@Override
public LocalEvent[] getWithoutFileName() throws CalendarStorageException {
return (LocalEvent[])queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null);
public List<LocalEvent> getWithoutFileName() throws CalendarStorageException {
return queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null);
}
@Override
public LocalResource[] getDirty() throws CalendarStorageException, FileNotFoundException {
List<LocalResource> dirty = new LinkedList<>();
// get dirty events which are not required to have an increased SEQUENCE value
Collections.addAll(dirty, (LocalEvent[])queryEvents(Events.DIRTY + "=" + DIRTY_DONT_INCREASE_SEQUENCE + " AND " + Events.ORIGINAL_ID + " IS NULL", null));
public List<LocalEvent> getDirty() throws CalendarStorageException, FileNotFoundException {
List<LocalEvent> dirty = new LinkedList<>();
// get dirty events which are required to have an increased SEQUENCE value
for (LocalEvent event : (LocalEvent[])queryEvents(Events.DIRTY + "=" + DIRTY_INCREASE_SEQUENCE + " AND " + Events.ORIGINAL_ID + " IS NULL", null)) {
if (event.getEvent().sequence == null) // sequence has not been assigned yet (i.e. this event was just locally created)
event.getEvent().sequence = 0;
else
event.getEvent().sequence++;
for (LocalEvent event : queryEvents(Events.DIRTY + "!=0 AND " + Events.ORIGINAL_ID + " IS NULL", null)) {
if (event.getEvent().getSequence() == null) // sequence has not been assigned yet (i.e. this event was just locally created)
event.getEvent().setSequence(0);
else if (event.weAreOrganizer)
event.getEvent().setSequence(event.getEvent().getSequence() + 1);
dirty.add(event);
}
return dirty.toArray(new LocalResource[dirty.size()]);
return dirty;
}
@@ -155,7 +153,7 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
@SuppressWarnings("Recycle")
public String getCTag() throws CalendarStorageException {
try {
@Cleanup Cursor cursor = provider.query(calendarSyncURI(), new String[] { COLUMN_CTAG }, null, null, null);
@Cleanup Cursor cursor = getProvider().query(calendarSyncURI(), new String[] { COLUMN_CTAG }, null, null, null);
if (cursor != null && cursor.moveToNext())
return cursor.getString(0);
} catch (RemoteException e) {
@@ -169,7 +167,7 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
try {
ContentValues values = new ContentValues(1);
values.put(COLUMN_CTAG, cTag);
provider.update(calendarSyncURI(), values, null, null);
getProvider().update(calendarSyncURI(), values, null, null);
} catch (RemoteException e) {
throw new CalendarStorageException("Couldn't write local (last known) CTag", e);
}
@@ -180,7 +178,7 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
// process deleted exceptions
App.log.info("Processing deleted exceptions");
try {
@Cleanup Cursor cursor = provider.query(
@Cleanup Cursor cursor = getProvider().query(
syncAdapterURI(Events.CONTENT_URI),
new String[] { Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE },
Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null);
@@ -190,22 +188,23 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
originalID = cursor.getLong(1); // can't be null (by query)
// get original event's SEQUENCE
@Cleanup Cursor cursor2 = provider.query(
@Cleanup Cursor cursor2 = getProvider().query(
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)),
new String[] { LocalEvent.COLUMN_SEQUENCE },
null, null, null);
int originalSequence = (cursor2 == null || cursor2.isNull(0)) ? 0 : cursor2.getInt(0);
BatchOperation batch = new BatchOperation(provider);
BatchOperation batch = new BatchOperation(getProvider());
// re-schedule original event and set it to DIRTY
batch.enqueue(ContentProviderOperation.newUpdate(
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
.withValue(LocalEvent.COLUMN_SEQUENCE, originalSequence + 1)
.withValue(Events.DIRTY, DIRTY_INCREASE_SEQUENCE)
.build());
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
.withValue(LocalEvent.COLUMN_SEQUENCE, originalSequence + 1)
.withValue(Events.DIRTY, 1)
));
// remove exception
batch.enqueue(ContentProviderOperation.newDelete(
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id))).build());
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newDelete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
));
batch.commit();
}
} catch (RemoteException e) {
@@ -215,7 +214,7 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
// process dirty exceptions
App.log.info("Processing dirty exceptions");
try {
@Cleanup Cursor cursor = provider.query(
@Cleanup Cursor cursor = getProvider().query(
syncAdapterURI(Events.CONTENT_URI),
new String[] { Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE },
Events.DIRTY + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null);
@@ -225,18 +224,18 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
originalID = cursor.getLong(1); // can't be null (by query)
int sequence = cursor.isNull(2) ? 0 : cursor.getInt(2);
BatchOperation batch = new BatchOperation(provider);
BatchOperation batch = new BatchOperation(getProvider());
// original event to DIRTY
batch.enqueue(ContentProviderOperation.newUpdate(
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
.withValue(Events.DIRTY, DIRTY_DONT_INCREASE_SEQUENCE)
.build());
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
.withValue(Events.DIRTY, 1)
));
// increase SEQUENCE and set DIRTY to 0
batch.enqueue(ContentProviderOperation.newUpdate(
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
.withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1)
.withValue(Events.DIRTY, 0)
.build());
));
batch.commit();
}
} catch (RemoteException e) {
@@ -245,17 +244,14 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
}
public static class Factory implements AndroidCalendarFactory {
public static class Factory implements AndroidCalendarFactory<LocalCalendar> {
public static final Factory INSTANCE = new Factory();
@Override
public AndroidCalendar newInstance(Account account, ContentProviderClient provider, long id) {
public LocalCalendar newInstance(Account account, ContentProviderClient provider, long id) {
return new LocalCalendar(account, provider, id);
}
@Override
public AndroidCalendar[] newArray(int size) {
return new LocalCalendar[size];
}
}
}

View File

@@ -9,17 +9,18 @@
package at.bitfire.davdroid.resource;
import java.io.FileNotFoundException;
import java.util.List;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.ContactsStorageException;
public interface LocalCollection {
public interface LocalCollection<T extends LocalResource> {
LocalResource[] getDeleted() throws CalendarStorageException, ContactsStorageException;
LocalResource[] getWithoutFileName() throws CalendarStorageException, ContactsStorageException;
LocalResource[] getDirty() throws CalendarStorageException, ContactsStorageException, FileNotFoundException;
List<T> getDeleted() throws CalendarStorageException, ContactsStorageException;
List<T> getWithoutFileName() throws CalendarStorageException, ContactsStorageException;
List<T> getDirty() throws CalendarStorageException, ContactsStorageException, FileNotFoundException;
LocalResource[] getAll() throws CalendarStorageException, ContactsStorageException;
List<T> getAll() throws CalendarStorageException, ContactsStorageException;
String getCTag() throws CalendarStorageException, ContactsStorageException;
void setCTag(String cTag) throws CalendarStorageException, ContactsStorageException;

View File

@@ -10,27 +10,43 @@ package at.bitfire.davdroid.resource;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Build;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.RawContacts.Data;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.FileNotFoundException;
import java.util.logging.Level;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.davdroid.model.UnknownProperties;
import at.bitfire.vcard4android.AndroidAddressBook;
import at.bitfire.vcard4android.AndroidContact;
import at.bitfire.vcard4android.AndroidContactFactory;
import at.bitfire.vcard4android.BatchOperation;
import at.bitfire.vcard4android.CachedGroupMembership;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
import ezvcard.Ezvcard;
import lombok.Cleanup;
public class LocalContact extends AndroidContact implements LocalResource {
static {
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " vcard4android ez-vcard/" + Ezvcard.VERSION;
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ez-vcard/" + Ezvcard.VERSION;
}
public static final String COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3;
protected final Set<Long>
cachedGroupMemberships = new HashSet<>(),
groupMemberships = new HashSet<>();
protected LocalContact(AndroidAddressBook addressBook, long id, String fileName, String eTag) {
super(addressBook, id, fileName, eTag);
@@ -40,22 +56,41 @@ public class LocalContact extends AndroidContact implements LocalResource {
super(addressBook, contact, fileName, eTag);
}
public void clearDirty(String eTag) throws ContactsStorageException {
public void resetDirty() throws ContactsStorageException {
ContentValues values = new ContentValues(1);
values.put(ContactsContract.RawContacts.DIRTY, 0);
try {
ContentValues values = new ContentValues(1);
values.put(ContactsContract.RawContacts.DIRTY, 0);
values.put(COLUMN_ETAG, eTag);
addressBook.provider.update(rawContactSyncURI(), values, null, null);
this.eTag = eTag;
} catch (RemoteException e) {
} catch(RemoteException e) {
throw new ContactsStorageException("Couldn't clear dirty flag", e);
}
}
public void updateFileNameAndUID(String uid) throws ContactsStorageException {
public void clearDirty(String eTag) throws ContactsStorageException {
try {
String newFileName = uid + ".vcf";
ContentValues values = new ContentValues(3);
values.put(COLUMN_ETAG, eTag);
values.put(ContactsContract.RawContacts.DIRTY, 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
int hashCode = dataHashCode();
values.put(COLUMN_HASHCODE, hashCode);
App.log.finer("Clearing dirty flag with eTag = " + eTag + ", contact hash = " + hashCode);
}
addressBook.provider.update(rawContactSyncURI(), values, null, null);
this.eTag = eTag;
} catch (FileNotFoundException|RemoteException e) {
throw new ContactsStorageException("Couldn't clear dirty flag", e);
}
}
public void prepareForUpload() throws ContactsStorageException {
try {
final String uid = UUID.randomUUID().toString();
final String newFileName = uid + ".vcf";
ContentValues values = new ContentValues(2);
values.put(COLUMN_FILENAME, newFileName);
@@ -69,53 +104,158 @@ public class LocalContact extends AndroidContact implements LocalResource {
}
// group support
@Override
protected void populateGroupMembership(ContentValues row) {
if (row.containsKey(GroupMembership.GROUP_ROW_ID)) {
long groupId = row.getAsLong(GroupMembership.GROUP_ROW_ID);
// fetch group
LocalGroup group = new LocalGroup(addressBook, groupId);
try {
Contact groupInfo = group.getContact();
// add to CATEGORIES
contact.getCategories().add(groupInfo.displayName);
} catch (FileNotFoundException|ContactsStorageException e) {
App.log.log(Level.WARNING, "Couldn't find assigned group #" + groupId + ", ignoring membership", e);
}
protected void populateData(String mimeType, ContentValues row) {
switch (mimeType) {
case CachedGroupMembership.CONTENT_ITEM_TYPE:
cachedGroupMemberships.add(row.getAsLong(CachedGroupMembership.GROUP_ID));
break;
case GroupMembership.CONTENT_ITEM_TYPE:
groupMemberships.add(row.getAsLong(GroupMembership.GROUP_ROW_ID));
break;
case UnknownProperties.CONTENT_ITEM_TYPE:
contact.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES);
break;
}
}
@Override
protected void insertGroupMemberships(BatchOperation batch) throws ContactsStorageException {
for (String category : contact.getCategories()) {
// Is there already a category with this display name?
LocalGroup group = ((LocalAddressBook)addressBook).findGroupByTitle(category);
protected void insertDataRows(BatchOperation batch) throws ContactsStorageException {
super.insertDataRows(batch);
if (group == null) {
// no, we have to create the group before inserting the membership
Contact groupInfo = new Contact();
groupInfo.displayName = category;
group = new LocalGroup(addressBook, groupInfo);
group.create();
}
Long groupId = group.getId();
if (groupId != null) {
ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(dataSyncURI());
if (id == null)
builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0);
else
builder.withValue(GroupMembership.RAW_CONTACT_ID, id);
builder .withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
.withValue(GroupMembership.GROUP_ROW_ID, groupId);
batch.enqueue(builder.build());
if (contact.unknownProperties != null) {
final BatchOperation.Operation op;
final ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(dataSyncURI());
if (id == null) {
op = new BatchOperation.Operation(builder, UnknownProperties.RAW_CONTACT_ID, 0);
} else {
op = new BatchOperation.Operation(builder);
builder.withValue(UnknownProperties.RAW_CONTACT_ID, id);
}
builder .withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE)
.withValue(UnknownProperties.UNKNOWN_PROPERTIES, contact.unknownProperties);
batch.enqueue(op);
}
}
/**
* Calculates a hash code from the contact's data (VCard) and group memberships.
* Attention: re-reads {@link #contact} from the database, discarding all changes in memory
* @return hash code of contact data (including group memberships)
*/
protected int dataHashCode() throws FileNotFoundException, ContactsStorageException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
App.log.severe("dataHashCode() should not be called on Android <7");
// reset contact so that getContact() reads from database
contact = null;
// groupMemberships is filled by getContact()
int dataHash = getContact().hashCode(),
groupHash = groupMemberships.hashCode();
App.log.finest("Calculated data hash = " + dataHash + ", group memberships hash = " + groupHash);
return dataHash ^ groupHash;
}
public void updateHashCode(@Nullable BatchOperation batch) throws ContactsStorageException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
App.log.severe("updateHashCode() should not be called on Android <7");
ContentValues values = new ContentValues(1);
try {
int hashCode = dataHashCode();
App.log.fine("Storing contact hash = " + hashCode);
values.put(COLUMN_HASHCODE, hashCode);
if (batch == null)
addressBook.provider.update(rawContactSyncURI(), values, null, null);
else {
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(rawContactSyncURI())
.withValues(values);
batch.enqueue(new BatchOperation.Operation(builder));
}
} catch(FileNotFoundException|RemoteException e) {
throw new ContactsStorageException("Couldn't store contact checksum", e);
}
}
public int getLastHashCode() throws ContactsStorageException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
App.log.severe("getLastHashCode() should not be called on Android <7");
try {
@Cleanup Cursor c = addressBook.provider.query(rawContactSyncURI(), new String[] { COLUMN_HASHCODE }, null, null, null);
if (c == null || !c.moveToNext() || c.isNull(0))
return 0;
return c.getInt(0);
} catch(RemoteException e) {
throw new ContactsStorageException("Could't read last hash code", e);
}
}
public void addToGroup(BatchOperation batch, long groupID) {
assertID();
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newInsert(dataSyncURI())
.withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
.withValue(GroupMembership.RAW_CONTACT_ID, id)
.withValue(GroupMembership.GROUP_ROW_ID, groupID)
));
groupMemberships.add(groupID);
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newInsert(dataSyncURI())
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
.withValue(CachedGroupMembership.RAW_CONTACT_ID, id)
.withValue(CachedGroupMembership.GROUP_ID, groupID)
.withYieldAllowed(true)
));
cachedGroupMemberships.add(groupID);
}
public void removeGroupMemberships(BatchOperation batch) {
assertID();
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newDelete(dataSyncURI())
.withSelection(
Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + " IN (?,?)",
new String[] { String.valueOf(id), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembership.CONTENT_ITEM_TYPE }
)
.withYieldAllowed(true)
));
groupMemberships.clear();
cachedGroupMemberships.clear();
}
/**
* 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
* 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 ContactsStorageException on contact provider errors
* @throws FileNotFoundException if the current contact can't be found
*/
@NonNull
public Set<Long> getCachedGroupMemberships() throws ContactsStorageException, FileNotFoundException {
getContact();
return cachedGroupMemberships;
}
/**
* Returns the IDs of all groups the contact is member of.
* @return set of {@link GroupMembership#GROUP_ROW_ID}s (may be empty)
* @throws ContactsStorageException on contact provider errors
* @throws FileNotFoundException if the current contact can't be found
*/
@NonNull
public Set<Long> getGroupMemberships() throws ContactsStorageException, FileNotFoundException {
getContact();
return groupMemberships;
}
@@ -134,6 +274,7 @@ public class LocalContact extends AndroidContact implements LocalResource {
return new LocalContact(addressBook, contact, fileName, eTag);
}
@Override
public LocalContact[] newArray(int size) {
return new LocalContact[size];
}

View File

@@ -11,6 +11,7 @@ package at.bitfire.davdroid.resource;
import android.annotation.TargetApi;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Build;
import android.os.RemoteException;
import android.provider.CalendarContract;
@@ -19,19 +20,26 @@ import android.support.annotation.NonNull;
import net.fortuna.ical4j.model.property.ProdId;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.UUID;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.ical4android.AndroidCalendar;
import at.bitfire.ical4android.AndroidEvent;
import at.bitfire.ical4android.AndroidEventFactory;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.Event;
import lombok.Cleanup;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@TargetApi(17)
@ToString(of={ "fileName","eTag" }, callSuper=true)
public class LocalEvent extends AndroidEvent implements LocalResource {
static {
Event.prodId = new ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4android ical4j/2.x");
Event.setProdId(new ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4j/2.x"));
}
static final String COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1,
@@ -41,6 +49,8 @@ public class LocalEvent extends AndroidEvent implements LocalResource {
@Getter protected String fileName;
@Getter @Setter protected String eTag;
public boolean weAreOrganizer = true;
public LocalEvent(@NonNull AndroidCalendar calendar, Event event, String fileName, String eTag) {
super(calendar, event);
this.fileName = fileName;
@@ -59,24 +69,31 @@ public class LocalEvent extends AndroidEvent implements LocalResource {
/* process LocalEvent-specific fields */
@Override
protected void populateEvent(ContentValues values) {
protected void populateEvent(ContentValues values) throws FileNotFoundException, CalendarStorageException {
super.populateEvent(values);
fileName = values.getAsString(Events._SYNC_ID);
eTag = values.getAsString(COLUMN_ETAG);
event.uid = values.getAsString(COLUMN_UID);
getEvent().setUid(values.getAsString(COLUMN_UID));
event.sequence = values.getAsInteger(COLUMN_SEQUENCE);
getEvent().setSequence(values.getAsInteger(COLUMN_SEQUENCE));
if (Build.VERSION.SDK_INT >= 17) {
Integer isOrganizer = values.getAsInteger(Events.IS_ORGANIZER);
weAreOrganizer = isOrganizer != null && isOrganizer != 0;
} else {
String organizer = values.getAsString(Events.ORGANIZER);
weAreOrganizer = organizer == null || organizer.equals(getCalendar().getAccount().name);
}
}
@Override
protected void buildEvent(Event recurrence, ContentProviderOperation.Builder builder) {
protected void buildEvent(Event recurrence, ContentProviderOperation.Builder builder) throws FileNotFoundException, CalendarStorageException {
super.buildEvent(recurrence, builder);
boolean buildException = recurrence != null;
Event eventToBuild = buildException ? recurrence : event;
Event eventToBuild = buildException ? recurrence : getEvent();
builder .withValue(COLUMN_UID, event.uid)
.withValue(COLUMN_SEQUENCE, eventToBuild.sequence)
builder .withValue(COLUMN_UID, getEvent().getUid())
.withValue(COLUMN_SEQUENCE, eventToBuild.getSequence())
.withValue(CalendarContract.Events.DIRTY, 0)
.withValue(CalendarContract.Events.DELETED, 0);
@@ -90,20 +107,27 @@ public class LocalEvent extends AndroidEvent implements LocalResource {
/* custom queries */
public void updateFileNameAndUID(String uid) throws CalendarStorageException {
public void prepareForUpload() throws CalendarStorageException {
try {
String newFileName = uid + ".ics";
String uid = null;
@Cleanup Cursor c = getCalendar().getProvider().query(eventSyncURI(), new String[] { COLUMN_UID }, null, null, null);
if (c.moveToNext())
uid = c.getString(0);
if (uid == null)
uid = UUID.randomUUID().toString();
final String newFileName = uid + ".ics";
ContentValues values = new ContentValues(2);
values.put(Events._SYNC_ID, newFileName);
values.put(COLUMN_UID, uid);
calendar.provider.update(eventSyncURI(), values, null, null);
getCalendar().getProvider().update(eventSyncURI(), values, null, null);
fileName = newFileName;
if (event != null)
event.uid = uid;
if (getEvent() != null)
getEvent().setUid(uid);
} catch (RemoteException e) {
} catch (FileNotFoundException|RemoteException e) {
throw new CalendarStorageException("Couldn't update UID", e);
}
}
@@ -114,33 +138,29 @@ public class LocalEvent extends AndroidEvent implements LocalResource {
ContentValues values = new ContentValues(2);
values.put(CalendarContract.Events.DIRTY, 0);
values.put(COLUMN_ETAG, eTag);
if (event != null)
values.put(COLUMN_SEQUENCE, event.sequence);
calendar.provider.update(eventSyncURI(), values, null, null);
if (getEvent() != null)
values.put(COLUMN_SEQUENCE, getEvent().getSequence());
getCalendar().getProvider().update(eventSyncURI(), values, null, null);
this.eTag = eTag;
} catch (RemoteException e) {
} catch (IOException|RemoteException e) {
throw new CalendarStorageException("Couldn't update UID", e);
}
}
static class Factory implements AndroidEventFactory {
static class Factory implements AndroidEventFactory<LocalEvent> {
static final Factory INSTANCE = new Factory();
@Override
public AndroidEvent newInstance(AndroidCalendar calendar, long id, ContentValues baseInfo) {
public LocalEvent newInstance(AndroidCalendar calendar, long id, ContentValues baseInfo) {
return new LocalEvent(calendar, id, baseInfo);
}
@Override
public AndroidEvent newInstance(AndroidCalendar calendar, Event event) {
public LocalEvent newInstance(AndroidCalendar calendar, Event event) {
return new LocalEvent(calendar, event, null, null);
}
@Override
public AndroidEvent[] newArray(int size) {
return new LocalEvent[size];
}
}
}

View File

@@ -8,28 +8,253 @@
package at.bitfire.davdroid.resource;
import android.content.ContentProviderOperation;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Build;
import android.os.Parcel;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.RawContacts.Data;
import org.apache.commons.lang3.ArrayUtils;
import java.io.FileNotFoundException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import at.bitfire.dav4android.Constants;
import at.bitfire.vcard4android.AndroidAddressBook;
import at.bitfire.vcard4android.AndroidGroup;
import at.bitfire.vcard4android.AndroidGroupFactory;
import at.bitfire.vcard4android.BatchOperation;
import at.bitfire.vcard4android.CachedGroupMembership;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
import lombok.ToString;
public class LocalGroup extends AndroidGroup {
@ToString(callSuper=true)
public class LocalGroup extends AndroidGroup implements LocalResource {
/** marshalled list of member UIDs, as sent by server */
public static final String COLUMN_PENDING_MEMBERS = Groups.SYNC3;
public LocalGroup(AndroidAddressBook addressBook, long id) {
super(addressBook, id);
public LocalGroup(AndroidAddressBook addressBook, long id, String fileName, String eTag) {
super(addressBook, id, fileName, eTag);
}
public LocalGroup(AndroidAddressBook addressBook, Contact contact) {
super(addressBook, contact);
public LocalGroup(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) {
super(addressBook, contact, fileName, eTag);
}
public void clearDirty() throws ContactsStorageException {
ContentValues values = new ContentValues(1);
values.put(ContactsContract.Groups.DIRTY, 0);
@Override
public void clearDirty(String eTag) throws ContactsStorageException {
assertID();
ContentValues values = new ContentValues(2);
values.put(Groups.DIRTY, 0);
values.put(COLUMN_ETAG, this.eTag = eTag);
update(values);
// update cached group memberships
BatchOperation batch = new BatchOperation(addressBook.provider);
// delete cached group memberships
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
.withSelection(
CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?",
new String[] { CachedGroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id) }
)
));
// insert updated cached group memberships
for (long member : getMembers())
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
.withValue(CachedGroupMembership.RAW_CONTACT_ID, member)
.withValue(CachedGroupMembership.GROUP_ID, id)
.withYieldAllowed(true)
));
batch.commit();
}
@Override
public void prepareForUpload() throws ContactsStorageException {
final String uid = UUID.randomUUID().toString();
final String newFileName = uid + ".vcf";
ContentValues values = new ContentValues(2);
values.put(COLUMN_FILENAME, newFileName);
values.put(COLUMN_UID, uid);
update(values);
fileName = newFileName;
}
@Override
protected ContentValues contentValues() {
ContentValues values = super.contentValues();
@Cleanup("recycle") Parcel members = Parcel.obtain();
members.writeStringList(contact.members);
values.put(COLUMN_PENDING_MEMBERS, members.marshall());
return values;
}
/**
* Marks all members of the current group as dirty.
*/
public void markMembersDirty() throws ContactsStorageException {
assertID();
BatchOperation batch = new BatchOperation(addressBook.provider);
for (long member : getMembers())
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, member)))
.withValue(RawContacts.DIRTY, 1)
.withYieldAllowed(true)
));
batch.commit();
}
/**
* Processes all groups with non-null {@link #COLUMN_PENDING_MEMBERS}: the pending memberships
* are (if possible) applied, keeping cached memberships in sync.
* @param addressBook address book to take groups from
* @throws ContactsStorageException on contact provider errors
*/
public static void applyPendingMemberships(LocalAddressBook addressBook) throws ContactsStorageException {
try {
@Cleanup Cursor cursor = addressBook.provider.query(
addressBook.syncAdapterURI(Groups.CONTENT_URI),
new String[] { Groups._ID, COLUMN_PENDING_MEMBERS },
COLUMN_PENDING_MEMBERS + " IS NOT NULL", new String[] {},
null
);
BatchOperation batch = new BatchOperation(addressBook.provider);
while (cursor != null && cursor.moveToNext()) {
long id = cursor.getLong(0);
Constants.log.fine("Assigning members to group " + id);
// required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed
Set<Long> changeContactIDs = new HashSet<>();
// delete all memberships and cached memberships for this group
for (LocalContact contact : addressBook.getByGroupMembership(id)) {
contact.removeGroupMemberships(batch);
changeContactIDs.add(contact.getId());
}
// extract list of member UIDs
List<String> members = new LinkedList<>();
byte[] raw = cursor.getBlob(1);
@Cleanup("recycle") Parcel parcel = Parcel.obtain();
parcel.unmarshall(raw, 0, raw.length);
parcel.setDataPosition(0);
parcel.readStringList(members);
// insert memberships
for (String uid : members) {
Constants.log.fine("Assigning member: " + uid);
try {
LocalContact member = addressBook.findContactByUID(uid);
member.addToGroup(batch, id);
changeContactIDs.add(member.getId());
} catch(FileNotFoundException e) {
Constants.log.log(Level.WARNING, "Group member not found: " + uid, e);
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
for (Long contactID : changeContactIDs) {
LocalContact contact = new LocalContact(addressBook, contactID, null, null);
contact.updateHashCode(batch);
}
// remove pending memberships
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
.withValue(COLUMN_PENDING_MEMBERS, null)
.withYieldAllowed(true)
));
batch.commit();
}
} catch(RemoteException e) {
throw new ContactsStorageException("Couldn't get pending memberships", e);
}
}
// helpers
private void assertID() {
if (id == null)
throw new IllegalStateException("Group has not been saved yet");
}
/**
* Lists all members of this group.
* @return list of all members' raw contact IDs
* @throws ContactsStorageException on contact provider errors
*/
protected long[] getMembers() throws ContactsStorageException {
assertID();
List<Long> members = new LinkedList<>();
try {
@Cleanup Cursor cursor = addressBook.provider.query(
addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI),
new String[] { Data.RAW_CONTACT_ID },
GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id) },
null
);
while (cursor != null && cursor.moveToNext())
members.add(cursor.getLong(0));
} catch(RemoteException e) {
throw new ContactsStorageException("Couldn't list group members", e);
}
return ArrayUtils.toPrimitive(members.toArray(new Long[members.size()]));
}
// factory
static class Factory extends AndroidGroupFactory {
static final Factory INSTANCE = new Factory();
@Override
public LocalGroup newInstance(AndroidAddressBook addressBook, long id, String fileName, String eTag) {
return new LocalGroup(addressBook, id, fileName, eTag);
}
@Override
public LocalGroup newInstance(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) {
return new LocalGroup(addressBook, contact, fileName, eTag);
}
@Override
public LocalGroup[] newArray(int size) {
return new LocalGroup[size];
}
}
}

View File

@@ -20,7 +20,7 @@ public interface LocalResource {
int delete() throws CalendarStorageException, ContactsStorageException;
void updateFileNameAndUID(String uuid) throws CalendarStorageException, ContactsStorageException;
void prepareForUpload() throws CalendarStorageException, ContactsStorageException;
void clearDirty(String eTag) throws CalendarStorageException, ContactsStorageException;
}

View File

@@ -20,6 +20,7 @@ import org.dmfs.provider.tasks.TaskContract.Tasks;
import java.io.FileNotFoundException;
import java.text.ParseException;
import java.util.UUID;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.ical4android.AndroidTask;
@@ -29,10 +30,12 @@ import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.Task;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ToString(of={ "fileName","eTag" }, callSuper=true)
public class LocalTask extends AndroidTask implements LocalResource {
static {
Task.prodId = new ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4android ical4j/2.x");
Task.setProdId(new ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4j/2.x"));
}
static final String COLUMN_ETAG = Tasks.SYNC1,
@@ -65,37 +68,54 @@ public class LocalTask extends AndroidTask implements LocalResource {
fileName = values.getAsString(Events._SYNC_ID);
eTag = values.getAsString(COLUMN_ETAG);
task.uid = values.getAsString(COLUMN_UID);
task.sequence = values.getAsInteger(COLUMN_SEQUENCE);
final Task task;
try {
task = getTask();
} catch(Exception e) {
throw new IllegalStateException(e);
}
task.setUid(values.getAsString(COLUMN_UID));
task.setSequence(values.getAsInteger(COLUMN_SEQUENCE));
}
@Override
protected void buildTask(ContentProviderOperation.Builder builder, boolean update) {
super.buildTask(builder, update);
Task task = null;
try {
task = getTask();
} catch(Exception e) {
throw new IllegalStateException(e);
}
builder .withValue(Tasks._SYNC_ID, fileName)
.withValue(COLUMN_UID, task.uid)
.withValue(COLUMN_SEQUENCE, task.sequence)
.withValue(COLUMN_UID, task.getUid())
.withValue(COLUMN_SEQUENCE, task.getSequence())
.withValue(COLUMN_ETAG, eTag);
}
/* custom queries */
public void updateFileNameAndUID(String uid) throws CalendarStorageException {
public void prepareForUpload() throws CalendarStorageException {
try {
String newFileName = uid + ".ics";
final String uid = UUID.randomUUID().toString();
final String newFileName = uid + ".ics";
ContentValues values = new ContentValues(2);
values.put(Tasks._SYNC_ID, newFileName);
values.put(COLUMN_UID, uid);
taskList.provider.client.update(taskSyncURI(), values, null, null);
getTaskList().getProvider().getClient().update(taskSyncURI(), values, null, null);
fileName = newFileName;
if (task != null)
task.uid = uid;
} catch (RemoteException e) {
Task task = getTask();
task.setUid(uid);
} catch (FileNotFoundException|RemoteException e) {
throw new CalendarStorageException("Couldn't update UID", e);
}
}
@@ -106,18 +126,19 @@ public class LocalTask extends AndroidTask implements LocalResource {
ContentValues values = new ContentValues(2);
values.put(Tasks._DIRTY, 0);
values.put(COLUMN_ETAG, eTag);
if (task != null)
values.put(COLUMN_SEQUENCE, task.sequence);
taskList.provider.client.update(taskSyncURI(), values, null, null);
Task task = getTask();
values.put(COLUMN_SEQUENCE, task.getSequence());
getTaskList().getProvider().getClient().update(taskSyncURI(), values, null, null);
this.eTag = eTag;
} catch (RemoteException e) {
} catch (FileNotFoundException|RemoteException e) {
throw new CalendarStorageException("Couldn't update _DIRTY/ETag/SEQUENCE", e);
}
}
static class Factory implements AndroidTaskFactory {
static class Factory implements AndroidTaskFactory<LocalTask> {
static final Factory INSTANCE = new Factory();
@Override
@@ -130,9 +151,5 @@ public class LocalTask extends AndroidTask implements LocalResource {
return new LocalTask(taskList, task, null, null);
}
@Override
public LocalTask[] newArray(int size) {
return new LocalTask[size];
}
}
}

View File

@@ -9,18 +9,24 @@
package at.bitfire.davdroid.resource;
import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.dmfs.provider.tasks.TaskContract.TaskLists;
import org.dmfs.provider.tasks.TaskContract.Tasks;
import java.io.FileNotFoundException;
import java.util.List;
import at.bitfire.davdroid.DavUtils;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.ical4android.AndroidTaskList;
import at.bitfire.ical4android.AndroidTaskListFactory;
@@ -28,7 +34,7 @@ import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.TaskProvider;
import lombok.Cleanup;
public class LocalTaskList extends AndroidTaskList implements LocalCollection {
public class LocalTaskList extends AndroidTaskList<LocalTask> implements LocalCollection<LocalTask> {
public static final int defaultColor = 0xFFC3EA6E; // "DAVdroid green"
@@ -40,10 +46,8 @@ public class LocalTaskList extends AndroidTaskList implements LocalCollection {
LocalTask.COLUMN_ETAG
};
// can be cached, because after installing OpenTasks, you have to re-install DAVdroid anyway
private static Boolean tasksProviderAvailable;
@NonNull
@Override
protected String[] taskBaseInfoColumns() {
return BASE_INFO_COLUMNS;
@@ -55,50 +59,52 @@ public class LocalTaskList extends AndroidTaskList implements LocalCollection {
}
public static Uri create(Account account, TaskProvider provider, CollectionInfo info) throws CalendarStorageException {
ContentValues values = valuesFromCollectionInfo(info);
ContentValues values = valuesFromCollectionInfo(info, true);
values.put(TaskLists.OWNER, account.name);
values.put(TaskLists.SYNC_ENABLED, 1);
values.put(TaskLists.VISIBLE, 1);
return create(account, provider, values);
}
public void update(CollectionInfo info) throws CalendarStorageException {
update(valuesFromCollectionInfo(info));
public void update(CollectionInfo info, boolean updateColor) throws CalendarStorageException {
update(valuesFromCollectionInfo(info, updateColor));
}
private static ContentValues valuesFromCollectionInfo(CollectionInfo info) {
private static ContentValues valuesFromCollectionInfo(CollectionInfo info, boolean withColor) {
ContentValues values = new ContentValues();
values.put(TaskLists._SYNC_ID, info.url);
values.put(TaskLists.LIST_NAME, info.displayName);
values.put(TaskLists.LIST_COLOR, info.color != null ? info.color : defaultColor);
values.put(TaskLists.SYNC_ENABLED, 1);
values.put(TaskLists.VISIBLE, 1);
values.put(TaskLists.LIST_NAME, !TextUtils.isEmpty(info.displayName) ? info.displayName : DavUtils.lastSegmentOfUrl(info.url));
if (withColor)
values.put(TaskLists.LIST_COLOR, info.color != null ? info.color : defaultColor);
return values;
}
@Override
public LocalTask[] getAll() throws CalendarStorageException {
return (LocalTask[])queryTasks(null, null);
public List<LocalTask> getAll() throws CalendarStorageException {
return queryTasks(null, null);
}
@Override
public LocalTask[] getDeleted() throws CalendarStorageException {
return (LocalTask[])queryTasks(Tasks._DELETED + "!=0", null);
public List<LocalTask> getDeleted() throws CalendarStorageException {
return queryTasks(Tasks._DELETED + "!=0", null);
}
@Override
public LocalTask[] getWithoutFileName() throws CalendarStorageException {
return (LocalTask[])queryTasks(Tasks._SYNC_ID + " IS NULL", null);
public List<LocalTask> getWithoutFileName() throws CalendarStorageException {
return queryTasks(Tasks._SYNC_ID + " IS NULL", null);
}
@Override
public LocalResource[] getDirty() throws CalendarStorageException, FileNotFoundException {
LocalTask[] tasks = (LocalTask[])queryTasks(Tasks._DIRTY + "!=0", null);
if (tasks != null)
public List<LocalTask> getDirty() throws CalendarStorageException, FileNotFoundException {
List<LocalTask> tasks = queryTasks(Tasks._DIRTY + "!=0", null);
for (LocalTask task : tasks) {
if (task.getTask().sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created)
task.getTask().sequence = 0;
if (task.getTask().getSequence() == null) // sequence has not been assigned yet (i.e. this task was just locally created)
task.getTask().setSequence(0);
else
task.getTask().sequence++;
task.getTask().setSequence(task.getTask().getSequence() + 1);
}
return tasks;
}
@@ -108,7 +114,7 @@ public class LocalTaskList extends AndroidTaskList implements LocalCollection {
@SuppressWarnings("Recycle")
public String getCTag() throws CalendarStorageException {
try {
@Cleanup Cursor cursor = provider.client.query(taskListSyncUri(), new String[] { COLUMN_CTAG }, null, null, null);
@Cleanup Cursor cursor = getProvider().getClient().query(taskListSyncUri(), new String[] { COLUMN_CTAG }, null, null, null);
if (cursor != null && cursor.moveToNext())
return cursor.getString(0);
} catch (RemoteException e) {
@@ -122,7 +128,7 @@ public class LocalTaskList extends AndroidTaskList implements LocalCollection {
try {
ContentValues values = new ContentValues(1);
values.put(COLUMN_CTAG, cTag);
provider.client.update(taskListSyncUri(), values, null, null);
getProvider().getClient().update(taskListSyncUri(), values, null, null);
} catch (RemoteException e) {
throw new CalendarStorageException("Couldn't write local (last known) CTag", e);
}
@@ -131,27 +137,36 @@ public class LocalTaskList extends AndroidTaskList implements LocalCollection {
// helpers
public static boolean tasksProviderAvailable(@NonNull ContentResolver resolver) {
if (tasksProviderAvailable != null)
return tasksProviderAvailable;
public static boolean tasksProviderAvailable(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
return context.getPackageManager().resolveContentProvider(TaskProvider.ProviderName.OpenTasks.getAuthority(), 0) != null;
else {
@Cleanup TaskProvider provider = TaskProvider.acquire(resolver, TaskProvider.ProviderName.OpenTasks);
return tasksProviderAvailable = (provider != null);
@Cleanup TaskProvider provider = TaskProvider.acquire(context.getContentResolver(), TaskProvider.ProviderName.OpenTasks);
return provider != null;
}
}
public static class Factory implements AndroidTaskListFactory {
public static class Factory implements AndroidTaskListFactory<LocalTaskList> {
public static final Factory INSTANCE = new Factory();
@Override
public AndroidTaskList newInstance(Account account, TaskProvider provider, long id) {
public LocalTaskList newInstance(Account account, TaskProvider provider, long id) {
return new LocalTaskList(account, provider, id);
}
@Override
public AndroidTaskList[] newArray(int size) {
return new LocalTaskList[size];
}
// HELPERS
public static void onRenameAccount(@NonNull ContentResolver resolver, @NonNull String oldName, @NonNull String newName) throws RemoteException {
@Cleanup("release") ContentProviderClient client = resolver.acquireContentProviderClient(TaskProvider.ProviderName.OpenTasks.getAuthority());
if (client != null) {
ContentValues values = new ContentValues(1);
values.put(Tasks.ACCOUNT_NAME, newName);
client.update(Tasks.getContentUri(TaskProvider.ProviderName.OpenTasks.getAuthority()), values, Tasks.ACCOUNT_NAME + "=?", new String[]{oldName});
}
}
}

View File

@@ -21,68 +21,69 @@ import android.os.IBinder;
import at.bitfire.davdroid.ui.setup.LoginActivity;
public class AccountAuthenticatorService extends Service {
private static AccountAuthenticator accountAuthenticator;
private AccountAuthenticator getAuthenticator() {
if (accountAuthenticator != null)
return accountAuthenticator;
return accountAuthenticator = new AccountAuthenticator(this);
}
@Override
public IBinder onBind(Intent intent) {
if (intent.getAction().equals(android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT))
return getAuthenticator().getIBinder();
return null;
}
private static class AccountAuthenticator extends AbstractAccountAuthenticator {
final Context context;
public AccountAuthenticator(Context context) {
super(context);
this.context = context;
}
private AccountAuthenticator accountAuthenticator;
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType,
String[] requiredFeatures, Bundle options) throws NetworkErrorException {
Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
@Override
public void onCreate() {
accountAuthenticator = new AccountAuthenticator(this);
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public IBinder onBind(Intent intent) {
if (intent.getAction().equals(android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT))
return accountAuthenticator.getIBinder();
return null;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
return null;
}
private static class AccountAuthenticator extends AbstractAccountAuthenticator {
final Context context;
public AccountAuthenticator(Context context) {
super(context);
this.context = context;
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType,
String[] requiredFeatures, Bundle options) throws NetworkErrorException {
Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
@Override
public String getAuthTokenLabel(String authTokenType) {
return null;
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
return null;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
return null;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public String getAuthTokenLabel(String authTokenType) {
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
return null;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.syncadapter;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public class AddressBookProvider extends ContentProvider {
@Override
public boolean onCreate() {
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright © 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
*/
package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.SyncResult;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.logging.Level;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.model.ServiceDB.Collections;
import at.bitfire.davdroid.resource.LocalAddressBook;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
public class AddressBooksSyncAdapterService extends SyncAdapterService {
@Override
protected AbstractThreadedSyncAdapter syncAdapter() {
return new AddressBooksSyncAdapter(this);
}
private static class AddressBooksSyncAdapter extends SyncAdapter {
public AddressBooksSyncAdapter(Context context) {
super(context);
}
@Override
public void sync(Account account, Bundle extras, String authority, ContentProviderClient addressBooksProvider, SyncResult syncResult) {
@Cleanup("release") ContentProviderClient contactsProvider = getContext().getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY);
if (contactsProvider == null) {
App.log.severe("Couldn't access contacts provider");
syncResult.databaseError = true;
return;
}
SQLiteOpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
try {
AccountSettings settings = new AccountSettings(getContext(), account);
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
return;
updateLocalAddressBooks(contactsProvider, account);
AccountManager accountManager = AccountManager.get(getContext());
for (Account addressBookAccount : accountManager.getAccountsByType(getContext().getString(R.string.account_type_address_book))) {
App.log.log(Level.INFO, "Running sync for address book", addressBookAccount);
Bundle syncExtras = new 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(InvalidAccountException e) {
App.log.log(Level.SEVERE, "Couldn't get account settings", e);
} catch(ContactsStorageException e) {
App.log.log(Level.SEVERE, "Couldn't prepare local address books", e);
} finally {
dbHelper.close();
}
App.log.info("Address book sync complete");
}
private void updateLocalAddressBooks(ContentProviderClient provider, Account account) throws ContactsStorageException {
final Context context = getContext();
@Cleanup SQLiteOpenHelper dbHelper = new ServiceDB.OpenHelper(context);
// enumerate remote and local address books
SQLiteDatabase db = dbHelper.getReadableDatabase();
Long service = getService(db, account);
Map<String, CollectionInfo> remote = remoteAddressBooks(db, service);
LocalAddressBook[] local = LocalAddressBook.find(context, provider, account);
// delete obsolete local address books
for (LocalAddressBook addressBook : local) {
String url = addressBook.getURL();
if (!remote.containsKey(url)) {
App.log.log(Level.FINE, "Deleting obsolete local address book", url);
addressBook.delete();
} else {
// we already have a local address book for this remote collection, don't take into consideration anymore
try {
addressBook.update(remote.get(url));
} catch(AuthenticatorException|OperationCanceledException|IOException e) {
App.log.log(Level.WARNING, "Couldn't rename address book account", e);
}
remote.remove(url);
}
}
// create new local address books
for (String url : remote.keySet()) {
CollectionInfo info = remote.get(url);
App.log.info("Adding local address book " + info);
LocalAddressBook.create(context, provider, account, info);
}
}
@Nullable
private Long getService(@NonNull SQLiteDatabase db, @NonNull Account account) {
@Cleanup Cursor c = db.query(ServiceDB.Services._TABLE, new String[] { ServiceDB.Services.ID },
ServiceDB.Services.ACCOUNT_NAME + "=? AND " + ServiceDB.Services.SERVICE + "=?", new String[] { account.name, ServiceDB.Services.SERVICE_CARDDAV }, null, null, null);
if (c.moveToNext())
return c.getLong(0);
else
return null;
}
@NonNull
private Map<String, CollectionInfo> remoteAddressBooks(@NonNull SQLiteDatabase db, Long service) {
Map<String, CollectionInfo> collections = new LinkedHashMap<>();
if (service != null) {
@Cleanup Cursor c = db.query(Collections._TABLE, null,
Collections.SERVICE_ID + "=? AND " + Collections.SYNC, new String[] { String.valueOf(service) }, null, null, null);
while (c.moveToNext()) {
ContentValues values = new ContentValues();
DatabaseUtils.cursorRowToContentValues(c, values);
CollectionInfo info = CollectionInfo.fromDB(values);
collections.put(info.url, info);
}
}
return collections;
}
}
}

View File

@@ -9,17 +9,15 @@
package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.content.ContentValues;
import android.content.Context;
import android.content.SyncResult;
import android.os.Bundle;
import android.provider.CalendarContract.Calendars;
import android.text.TextUtils;
import org.apache.commons.codec.Charsets;
import org.apache.commons.lang3.StringUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
@@ -34,16 +32,14 @@ import at.bitfire.dav4android.DavCalendar;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.property.CalendarColor;
import at.bitfire.dav4android.property.CalendarData;
import at.bitfire.dav4android.property.DisplayName;
import at.bitfire.dav4android.property.GetCTag;
import at.bitfire.dav4android.property.GetContentType;
import at.bitfire.dav4android.property.GetETag;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.ArrayUtils;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.davdroid.resource.LocalEvent;
@@ -58,25 +54,35 @@ import okhttp3.MediaType;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
/**
* Synchronization manager for CalDAV collections; handles events ({@code VEVENT}).
*/
public class CalendarSyncManager extends SyncManager {
protected static final int MAX_MULTIGET = 20;
public CalendarSyncManager(Context context, Account account, Bundle extras, String authority, SyncResult result, LocalCalendar calendar) throws InvalidAccountException {
super(Constants.NOTIFICATION_CALENDAR_SYNC, context, account, extras, authority, result);
public CalendarSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult result, LocalCalendar calendar) {
super(context, account, settings, extras, authority, result, "calendar/" + calendar.getId());
localCollection = calendar;
}
@Override
protected int notificationId() {
return Constants.NOTIFICATION_CALENDAR_SYNC;
}
@Override
protected String getSyncErrorTitle() {
return context.getString(R.string.sync_error_calendar, account.name);
}
@Override
protected void prepare() {
protected boolean prepare() {
collectionURL = HttpUrl.parse(localCalendar().getName());
davCollection = new DavCalendar(httpClient, collectionURL);
return true;
}
@Override
@@ -94,9 +100,14 @@ public class CalendarSyncManager extends SyncManager {
@Override
protected RequestBody prepareUpload(LocalResource resource) throws IOException, CalendarStorageException {
LocalEvent local = (LocalEvent)resource;
App.log.log(Level.FINE, "Preparing upload of event " + local.getFileName(), local.getEvent());
ByteArrayOutputStream os = new ByteArrayOutputStream();
local.getEvent().write(os);
return RequestBody.create(
DavCalendar.MIME_ICALENDAR,
local.getEvent().toStream().toByteArray()
os.toByteArray()
);
}
@@ -112,14 +123,18 @@ public class CalendarSyncManager extends SyncManager {
}
// fetch list of remote VEVENTs and build hash table to index file name
davCalendar().calendarQuery("VEVENT", limitStart, null);
final DavCalendar calendar = davCalendar();
currentDavResource = calendar;
calendar.calendarQuery("VEVENT", limitStart, null);
remoteResources = new HashMap<>(davCollection.members.size());
for (DavResource iCal : davCollection.members) {
remoteResources = new HashMap<>(davCollection.getMembers().size());
for (DavResource iCal : davCollection.getMembers()) {
String fileName = iCal.fileName();
App.log.fine("Found remote VEVENT: " + fileName);
remoteResources.put(fileName, iCal);
}
currentDavResource = null;
}
@Override
@@ -134,10 +149,15 @@ public class CalendarSyncManager extends SyncManager {
if (bunch.length == 1) {
// only one contact, use GET
DavResource remote = bunch[0];
final DavResource remote = bunch[0];
currentDavResource = remote;
ResponseBody body = remote.get("text/calendar");
String eTag = ((GetETag)remote.properties.get(GetETag.NAME)).eTag;
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
GetETag eTag = (GetETag)remote.getProperties().get(GetETag.NAME);
if (eTag == null || StringUtils.isEmpty(eTag.getETag()))
throw new DavException("Received CalDAV GET response without ETag for " + remote.getLocation());
Charset charset = Charsets.UTF_8;
MediaType contentType = body.contentType();
@@ -145,40 +165,47 @@ public class CalendarSyncManager extends SyncManager {
charset = contentType.charset(Charsets.UTF_8);
@Cleanup InputStream stream = body.byteStream();
processVEvent(remote.fileName(), eTag, stream, charset);
processVEvent(remote.fileName(), eTag.getETag(), stream, charset);
} else {
// multiple contacts, use multi-get
List<HttpUrl> urls = new LinkedList<>();
for (DavResource remote : bunch)
urls.add(remote.location);
davCalendar().multiget(urls.toArray(new HttpUrl[urls.size()]));
urls.add(remote.getLocation());
final DavCalendar calendar = davCalendar();
currentDavResource = calendar;
calendar.multiget(urls.toArray(new HttpUrl[urls.size()]));
// process multiget results
for (DavResource remote : davCollection.members) {
for (final DavResource remote : davCollection.getMembers()) {
currentDavResource = remote;
String eTag;
GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME);
GetETag getETag = (GetETag)remote.getProperties().get(GetETag.NAME);
if (getETag != null)
eTag = getETag.eTag;
eTag = getETag.getETag();
else
throw new DavException("Received multi-get response without ETag");
Charset charset = Charsets.UTF_8;
GetContentType getContentType = (GetContentType)remote.properties.get(GetContentType.NAME);
if (getContentType != null && getContentType.type != null) {
MediaType type = MediaType.parse(getContentType.type);
GetContentType getContentType = (GetContentType)remote.getProperties().get(GetContentType.NAME);
if (getContentType != null && getContentType.getType() != null) {
MediaType type = MediaType.parse(getContentType.getType());
if (type != null)
charset = type.charset(Charsets.UTF_8);
}
CalendarData calendarData = (CalendarData)remote.properties.get(CalendarData.NAME);
if (calendarData == null || calendarData.iCalendar == null)
CalendarData calendarData = (CalendarData)remote.getProperties().get(CalendarData.NAME);
if (calendarData == null || calendarData.getICalendar() == null)
throw new DavException("Received multi-get response without address data");
@Cleanup InputStream stream = new ByteArrayInputStream(calendarData.iCalendar.getBytes());
@Cleanup InputStream stream = new ByteArrayInputStream(calendarData.getICalendar().getBytes());
processVEvent(remote.fileName(), eTag, stream, charset);
}
}
currentDavResource = null;
}
}
@@ -202,6 +229,7 @@ public class CalendarSyncManager extends SyncManager {
// delete local event, if it exists
LocalEvent localEvent = (LocalEvent)localResources.get(fileName);
currentLocalResource = localEvent;
if (localEvent != null) {
App.log.info("Updating " + fileName + " in local calendar");
localEvent.setETag(eTag);
@@ -210,11 +238,14 @@ public class CalendarSyncManager extends SyncManager {
} else {
App.log.info("Adding " + fileName + " to local calendar");
localEvent = new LocalEvent(localCalendar(), newData, fileName, eTag);
currentLocalResource = localEvent;
localEvent.add();
syncResult.stats.numInserts++;
}
} else
App.log.severe("Received VCALENDAR with not exactly one VEVENT with UID, but without RECURRENCE-ID; ignoring " + fileName);
currentLocalResource = null;
}
}

View File

@@ -8,31 +8,33 @@
package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.app.Service;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.CalendarContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.model.ServiceDB.Collections;
import at.bitfire.davdroid.model.ServiceDB.OpenHelper;
import at.bitfire.davdroid.model.ServiceDB.Services;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.ical4android.CalendarStorageException;
@@ -41,47 +43,54 @@ import lombok.Cleanup;
public class CalendarsSyncAdapterService extends SyncAdapterService {
@Override
public void onCreate() {
super.onCreate();
syncAdapter = new SyncAdapter(this, dbHelper);
protected AbstractThreadedSyncAdapter syncAdapter() {
return new SyncAdapter(this);
}
private static class SyncAdapter extends SyncAdapterService.SyncAdapter {
public SyncAdapter(Context context, OpenHelper dbHelper) {
super(context, dbHelper);
public SyncAdapter(Context context) {
super(context);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
super.onPerformSync(account, extras, authority, provider, syncResult);
public void sync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
try {
updateLocalCalendars(provider, account);
AccountSettings settings = new AccountSettings(getContext(), account);
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
return;
for (LocalCalendar calendar : (LocalCalendar[])LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, CalendarContract.Calendars.SYNC_EVENTS + "!=0", null)) {
updateLocalCalendars(provider, account, settings);
for (LocalCalendar calendar : LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, CalendarContract.Calendars.SYNC_EVENTS + "!=0", null)) {
App.log.info("Synchronizing calendar #" + calendar.getId() + ", URL: " + calendar.getName());
CalendarSyncManager syncManager = new CalendarSyncManager(getContext(), account, extras, authority, syncResult, calendar);
CalendarSyncManager syncManager = new CalendarSyncManager(getContext(), account, settings, extras, authority, syncResult, calendar);
syncManager.performSync();
}
} catch (CalendarStorageException e) {
App.log.log(Level.SEVERE, "Couldn't enumerate local calendars", e);
syncResult.databaseError = true;
} catch (InvalidAccountException e) {
} catch(InvalidAccountException e) {
App.log.log(Level.SEVERE, "Couldn't get account settings", e);
} catch(CalendarStorageException|SQLiteException e) {
App.log.log(Level.SEVERE, "Couldn't prepare local calendars", e);
syncResult.databaseError = true;
}
App.log.info("Calendar sync complete");
}
private void updateLocalCalendars(ContentProviderClient provider, Account account) throws CalendarStorageException {
// enumerate remote and local calendars
Long service = getService(account);
Map<String, CollectionInfo> remote = remoteCalendars(service);
LocalCalendar[] local = (LocalCalendar[])LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, null, null);
private void updateLocalCalendars(ContentProviderClient provider, Account account, AccountSettings settings) throws CalendarStorageException {
@Cleanup SQLiteOpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
// delete obsolete local calendar
// enumerate remote and local calendars
SQLiteDatabase db = dbHelper.getReadableDatabase();
Long service = getService(db, account);
Map<String, CollectionInfo> remote = remoteCalendars(db, service);
List<LocalCalendar> local = LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, null, null);
boolean updateColors = settings.getManageCalendarColors();
// delete obsolete local calendars
for (LocalCalendar calendar : local) {
String url = calendar.getName();
if (!remote.containsKey(url)) {
@@ -91,7 +100,7 @@ public class CalendarsSyncAdapterService extends SyncAdapterService {
// remote CollectionInfo found for this local collection, update data
CollectionInfo info = remote.get(url);
App.log.fine("Updating local calendar " + url + " with " + info);
calendar.update(info);
calendar.update(info, updateColors);
// we already have a local calendar for this remote collection, don't take into consideration anymore
remote.remove(url);
}
@@ -106,7 +115,7 @@ public class CalendarsSyncAdapterService extends SyncAdapterService {
}
@Nullable
Long getService(Account account) {
Long getService(@NonNull SQLiteDatabase db, @NonNull Account account) {
@Cleanup Cursor c = db.query(Services._TABLE, new String[] { Services.ID },
Services.ACCOUNT_NAME + "=? AND " + Services.SERVICE + "=?", new String[] { account.name, Services.SERVICE_CALDAV }, null, null, null);
if (c.moveToNext())
@@ -116,7 +125,7 @@ public class CalendarsSyncAdapterService extends SyncAdapterService {
}
@NonNull
private Map<String, CollectionInfo> remoteCalendars(Long service) {
private Map<String, CollectionInfo> remoteCalendars(@NonNull SQLiteDatabase db, Long service) {
Map<String, CollectionInfo> collections = new LinkedHashMap<>();
if (service != null) {
@Cleanup Cursor cursor = db.query(Collections._TABLE, null,

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* 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
@@ -8,90 +8,73 @@
package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.app.Service;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.logging.Level;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.model.ServiceDB.Collections;
import at.bitfire.davdroid.model.ServiceDB.OpenHelper;
import at.bitfire.davdroid.resource.LocalAddressBook;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
public class ContactsSyncAdapterService extends SyncAdapterService {
@Override
public void onCreate() {
super.onCreate();
syncAdapter = new ContactsSyncAdapter(this, dbHelper);
}
@Override
protected AbstractThreadedSyncAdapter syncAdapter() {
return new ContactsSyncAdapter(this);
}
private static class ContactsSyncAdapter extends SyncAdapter {
public ContactsSyncAdapter(Context context, OpenHelper dbHelper) {
super(context, dbHelper);
public ContactsSyncAdapter(Context context) {
super(context);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
super.onPerformSync(account, extras, authority, provider, syncResult);
public void sync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
SQLiteOpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
try {
LocalAddressBook addressBook = new LocalAddressBook(getContext(), account, provider);
Long service = getService(account);
if (service != null) {
CollectionInfo remote = remoteAddressBook(service);
if (remote != null)
try {
ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, extras, authority, provider, syncResult, remote);
syncManager.performSync();
} catch (InvalidAccountException e) {
App.log.log(Level.SEVERE, "Couldn't get account settings", e);
}
else
App.log.info("No address book collection selected for synchronization");
} else
App.log.info("No CardDAV service found in DB");
AccountSettings settings = new AccountSettings(getContext(), addressBook.getMainAccount());
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
return;
App.log.info("Address book sync complete");
}
App.log.info("Synchronizing address book: " + addressBook.getURL());
App.log.info("Taking settings from: " + addressBook.getMainAccount());
@Nullable
private Long getService(@NonNull Account account) {
@Cleanup Cursor c = db.query(ServiceDB.Services._TABLE, new String[] { ServiceDB.Services.ID },
ServiceDB.Services.ACCOUNT_NAME + "=? AND " + ServiceDB.Services.SERVICE + "=?", new String[] { account.name, ServiceDB.Services.SERVICE_CARDDAV }, null, null, null);
if (c.moveToNext())
return c.getLong(0);
else
return null;
}
ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, settings, extras, authority, provider, syncResult, addressBook);
syncManager.performSync();
} catch(InvalidAccountException e) {
App.log.log(Level.SEVERE, "Couldn't get account settings", e);
} catch(ContactsStorageException e) {
App.log.log(Level.SEVERE, "Couldn't prepare local address books", e);
} finally {
dbHelper.close();
}
@Nullable
private CollectionInfo remoteAddressBook(long service) {
@Cleanup Cursor c = db.query(Collections._TABLE, null,
Collections.SERVICE_ID + "=? AND " + Collections.SYNC, new String[] { String.valueOf(service) }, null, null, null);
if (c.moveToNext()) {
ContentValues values = new ContentValues();
DatabaseUtils.cursorRowToContentValues(c, values);
return CollectionInfo.fromDB(values);
} else
return null;
App.log.info("Contacts sync complete");
}
}

View File

@@ -10,20 +10,36 @@ package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.SyncResult;
import android.database.Cursor;
import android.os.Build;
import android.os.Bundle;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Groups;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.apache.commons.codec.Charsets;
import org.apache.commons.collections4.SetUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import at.bitfire.dav4android.DavAddressBook;
@@ -34,23 +50,24 @@ import at.bitfire.dav4android.property.AddressData;
import at.bitfire.dav4android.property.GetCTag;
import at.bitfire.dav4android.property.GetContentType;
import at.bitfire.dav4android.property.GetETag;
import at.bitfire.dav4android.property.ResourceType;
import at.bitfire.dav4android.property.SupportedAddressData;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.ArrayUtils;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.davdroid.resource.LocalAddressBook;
import at.bitfire.davdroid.resource.LocalContact;
import at.bitfire.davdroid.resource.LocalGroup;
import at.bitfire.davdroid.resource.LocalResource;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.BatchOperation;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
import at.bitfire.vcard4android.GroupMethod;
import ezvcard.VCardVersion;
import ezvcard.util.IOUtils;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import okhttp3.HttpUrl;
@@ -61,18 +78,60 @@ import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* <p>Synchronization manager for CardDAV collections; handles contacts and groups.</p>
*
* <p></p>Group handling differs according to the {@link #groupMethod}. There are two basic methods to
* handle/manage groups:</p>
* <ul>
* <li>{@code CATEGORIES}: groups memberships are attached to each contact and represented as
* "category". When a group is dirty or has been deleted, all its members have to be set to
* dirty, too (because they have to be uploaded without the respective category). This
* is done in {@link #prepareDirty()}. Empty groups can be deleted without further processing,
* which is done in {@link #postProcess()} because groups may become empty after downloading
* updated remoted contacts.</li>
* <li>Groups as separate VCards: individual and group contacts (with a list of member UIDs) are
* distinguished. When a local group is dirty, its members don't need to be set to dirty.
* <ol>
* <li>However, when a contact is dirty, it has
* 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
* {@link android.provider.ContactsContract.CommonDataKinds.GroupMembership}. DAVdroid 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 {@link android.provider.ContactsContract.CommonDataKinds.GroupMembership}
* data rows in respective {@link 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
* actual ones) is marked as dirty. This is done in {@link #prepareDirty()}.</li>
* <li>When downloading remote contacts, groups (+ member information) may be received
* by the actual members. Thus, the member lists have to be cached until all VCards
* are received. This is done by caching the member UIDs of each group in
* {@link LocalGroup#COLUMN_PENDING_MEMBERS}. In {@link #postProcess()},
* these "pending memberships" are assigned to the actual contacs and then cleaned up.</li>
* </ol>
* </ul>
*/
public class ContactsSyncManager extends SyncManager {
protected static final int MAX_MULTIGET = 10;
final private ContentProviderClient provider;
final private CollectionInfo remote;
private boolean hasVCard4;
private GroupMethod groupMethod;
public ContactsSyncManager(Context context, Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult result, CollectionInfo remote) throws InvalidAccountException {
super(Constants.NOTIFICATION_CONTACTS_SYNC, context, account, extras, authority, result);
public ContactsSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, ContentProviderClient provider, SyncResult result, LocalAddressBook localAddressBook) {
super(context, account, settings, extras, authority, result, "addressBook");
this.provider = provider;
this.remote = remote;
localCollection = localAddressBook;
}
@Override
protected int notificationId() {
return Constants.NOTIFICATION_CONTACTS_SYNC;
}
@Override
@@ -82,72 +141,156 @@ public class ContactsSyncManager extends SyncManager {
@Override
protected void prepare() throws ContactsStorageException {
// prepare local address book
localCollection = new LocalAddressBook(account, provider);
protected boolean prepare() throws ContactsStorageException {
LocalAddressBook localAddressBook = localAddressBook();
String url = remote.url;
String lastUrl = localAddressBook().getURL();
if (!url.equals(lastUrl)) {
App.log.info("Selected address book has changed from " + lastUrl + " to " + url + ", deleting all local contacts");
((LocalAddressBook)localCollection).deleteAll();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
int reallyDirty = localAddressBook.verifyDirty(),
deleted = localAddressBook.getDeleted().size();
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_UPLOAD) && reallyDirty == 0 && deleted == 0) {
App.log.info("This sync was called to up-sync dirty/deleted contacts, but no contacts have been changed");
return false;
}
}
collectionURL = HttpUrl.parse(url);
// set up Contacts Provider Settings
ContentValues values = new ContentValues(2);
values.put(ContactsContract.Settings.SHOULD_SYNC, 1);
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1);
localAddressBook.updateSettings(values);
collectionURL = HttpUrl.parse(localAddressBook.getURL());
davCollection = new DavAddressBook(httpClient, collectionURL);
processChangedGroups();
return true;
}
@Override
protected void queryCapabilities() throws DavException, IOException, HttpException {
// prepare remote address book
hasVCard4 = false;
davCollection.propfind(0, SupportedAddressData.NAME, GetCTag.NAME);
SupportedAddressData supportedAddressData = (SupportedAddressData) davCollection.properties.get(SupportedAddressData.NAME);
if (supportedAddressData != null)
for (MediaType type : supportedAddressData.types)
if ("text/vcard; version=4.0".equalsIgnoreCase(type.toString()))
hasVCard4 = true;
SupportedAddressData supportedAddressData = (SupportedAddressData)davCollection.getProperties().get(SupportedAddressData.NAME);
hasVCard4 = supportedAddressData != null && supportedAddressData.hasVCard4();
App.log.info("Server advertises VCard/4 support: " + hasVCard4);
groupMethod = settings.getGroupMethod();
App.log.info("Contact group method: " + groupMethod);
localAddressBook().includeGroups = groupMethod == GroupMethod.GROUP_VCARDS;
}
@Override
protected RequestBody prepareUpload(LocalResource resource) throws IOException, ContactsStorageException {
LocalContact local = (LocalContact)resource;
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
super.prepareDirty();
LocalAddressBook addressBook = localAddressBook();
if (groupMethod == GroupMethod.CATEGORIES) {
/* groups memberships are represented as contact CATEGORIES */
// groups with DELETED=1: set all members to dirty, then remove group
for (LocalGroup group : addressBook.getDeletedGroups()) {
App.log.fine("Finally removing group " + group);
// useless because Android deletes group memberships as soon as a group is set to DELETED:
// group.markMembersDirty();
group.delete();
}
// groups with DIRTY=1: mark all memberships as dirty, then clean DIRTY flag of group
for (LocalGroup group : addressBook.getDirtyGroups()) {
App.log.fine("Marking members of modified group " + group + " as dirty");
group.markMembersDirty();
group.clearDirty(null);
}
} else {
/* groups as separate VCards: there are group contacts and individual contacts */
// mark groups with changed members as dirty
BatchOperation batch = new BatchOperation(addressBook.provider);
for (LocalContact contact : addressBook.getDirtyContacts())
try {
App.log.fine("Looking for changed group memberships of contact " + contact.getFileName());
Set<Long> cachedGroups = contact.getCachedGroupMemberships(),
currentGroups = contact.getGroupMemberships();
for (Long groupID : SetUtils.disjunction(cachedGroups, currentGroups)) {
App.log.fine("Marking group as dirty: " + groupID);
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)))
.withValue(Groups.DIRTY, 1)
.withYieldAllowed(true)
));
}
} catch(FileNotFoundException ignored) {
}
batch.commit();
}
}
@Override
protected RequestBody prepareUpload(@NonNull LocalResource resource) throws IOException, ContactsStorageException {
final Contact contact;
if (resource instanceof LocalContact) {
LocalContact local = ((LocalContact)resource);
contact = local.getContact();
if (groupMethod == GroupMethod.CATEGORIES) {
// add groups as CATEGORIES
for (long groupID : local.getGroupMemberships()) {
try {
@Cleanup Cursor c = provider.query(
localAddressBook().syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)),
new String[] { Groups.TITLE },
null, null,
null
);
if (c != null && c.moveToNext()) {
String title = c.getString(0);
if (!TextUtils.isEmpty(title))
contact.categories.add(title);
}
} catch(RemoteException e) {
throw new ContactsStorageException("Couldn't find group for adding CATEGORIES", e);
}
}
}
} else if (resource instanceof LocalGroup)
contact = ((LocalGroup)resource).getContact();
else
throw new IllegalArgumentException("Argument must be LocalContact or LocalGroup");
App.log.log(Level.FINE, "Preparing upload of VCard " + resource.getFileName(), contact);
ByteArrayOutputStream os = new ByteArrayOutputStream();
contact.write(hasVCard4 ? VCardVersion.V4_0 : VCardVersion.V3_0, groupMethod, os);
return RequestBody.create(
hasVCard4 ? DavAddressBook.MIME_VCARD4 : DavAddressBook.MIME_VCARD3_UTF8,
local.getContact().toStream(hasVCard4 ? VCardVersion.V4_0 : VCardVersion.V3_0).toByteArray()
os.toByteArray()
);
}
@Override
protected void listRemote() throws IOException, HttpException, DavException {
final DavAddressBook addressBook = davAddressBook();
currentDavResource = addressBook;
// fetch list of remote VCards and build hash table to index file name
addressBook.propfind(1, ResourceType.NAME, GetETag.NAME);
try {
davAddressBook().addressbookQuery();
} catch(HttpException e) {
/* non-successful responses to CARDDAV:addressbook-query with empty filter, tested on 2015/10/21
* fastmail.com 403 Forbidden (DAV:error CARDDAV:supported-filter)
* mailbox.org (OpenXchange) 400 Bad Request
* SOGo 207 Multi-status, but without entries http://www.sogo.nu/bugs/view.php?id=3370
* Zimbra ZCS 500 Server Error https://bugzilla.zimbra.com/show_bug.cgi?id=101902
*/
if (e.status == 400 || e.status == 403 || e.status == 500 || e.status == 501) {
App.log.log(Level.WARNING, "Server error on REPORT addressbook-query, falling back to PROPFIND", e);
davAddressBook().propfind(1, GetETag.NAME);
} else
// no defined fallback, pass through exception
throw e;
}
remoteResources = new HashMap<>(davCollection.getMembers().size());
for (DavResource vCard : davCollection.getMembers()) {
// ignore member collections
ResourceType type = (ResourceType)vCard.getProperties().get(ResourceType.NAME);
if (type != null && type.getTypes().contains(ResourceType.COLLECTION))
continue;
remoteResources = new HashMap<>(davCollection.members.size());
for (DavResource vCard : davCollection.members) {
String fileName = vCard.fileName();
App.log.fine("Found remote VCard: " + fileName);
remoteResources.put(fileName, vCard);
}
currentDavResource = null;
}
@Override
@@ -166,10 +309,15 @@ public class ContactsSyncManager extends SyncManager {
if (bunch.length == 1) {
// only one contact, use GET
DavResource remote = bunch[0];
final DavResource remote = bunch[0];
currentDavResource = remote;
ResponseBody body = remote.get("text/vcard;version=4.0, text/vcard;charset=utf-8;q=0.8, text/vcard;q=0.5");
String eTag = ((GetETag) remote.properties.get(GetETag.NAME)).eTag;
// CardDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc6352#section-6.3.2.3]
GetETag eTag = (GetETag)remote.getProperties().get(GetETag.NAME);
if (eTag == null || StringUtils.isEmpty(eTag.getETag()))
throw new DavException("Received CardDAV GET response without ETag for " + remote.getLocation());
Charset charset = Charsets.UTF_8;
MediaType contentType = body.contentType();
@@ -177,47 +325,64 @@ public class ContactsSyncManager extends SyncManager {
charset = contentType.charset(Charsets.UTF_8);
@Cleanup InputStream stream = body.byteStream();
processVCard(remote.fileName(), eTag, stream, charset, downloader);
processVCard(remote.fileName(), eTag.getETag(), stream, charset, downloader);
} else {
// multiple contacts, use multi-get
List<HttpUrl> urls = new LinkedList<>();
for (DavResource remote : bunch)
urls.add(remote.location);
davAddressBook().multiget(urls.toArray(new HttpUrl[urls.size()]), hasVCard4);
urls.add(remote.getLocation());
final DavAddressBook addressBook = davAddressBook();
currentDavResource = addressBook;
addressBook.multiget(urls.toArray(new HttpUrl[urls.size()]), hasVCard4);
// process multi-get results
for (final DavResource remote : davCollection.getMembers()) {
currentDavResource = remote;
// process multiget results
for (DavResource remote : davCollection.members) {
String eTag;
GetETag getETag = (GetETag) remote.properties.get(GetETag.NAME);
GetETag getETag = (GetETag)remote.getProperties().get(GetETag.NAME);
if (getETag != null)
eTag = getETag.eTag;
eTag = getETag.getETag();
else
throw new DavException("Received multi-get response without ETag");
Charset charset = Charsets.UTF_8;
GetContentType getContentType = (GetContentType) remote.properties.get(GetContentType.NAME);
if (getContentType != null && getContentType.type != null) {
MediaType type = MediaType.parse(getContentType.type);
GetContentType getContentType = (GetContentType)remote.getProperties().get(GetContentType.NAME);
if (getContentType != null && getContentType.getType() != null) {
MediaType type = MediaType.parse(getContentType.getType());
if (type != null)
charset = type.charset(Charsets.UTF_8);
}
AddressData addressData = (AddressData) remote.properties.get(AddressData.NAME);
if (addressData == null || addressData.vCard == null)
AddressData addressData = (AddressData)remote.getProperties().get(AddressData.NAME);
if (addressData == null || addressData.getVCard() == null)
throw new DavException("Received multi-get response without address data");
@Cleanup InputStream stream = new ByteArrayInputStream(addressData.vCard.getBytes());
@Cleanup InputStream stream = new ByteArrayInputStream(addressData.getVCard().getBytes());
processVCard(remote.fileName(), eTag, stream, charset, downloader);
}
}
currentDavResource = null;
}
}
@Override
protected void saveSyncState() throws CalendarStorageException, ContactsStorageException {
super.saveSyncState();
((LocalAddressBook)localCollection).setURL(remote.url);
protected void postProcess() throws CalendarStorageException, ContactsStorageException {
if (groupMethod == GroupMethod.CATEGORIES) {
/* VCard3 group handling: groups memberships are represented as contact CATEGORIES */
// remove empty groups
App.log.info("Removing empty groups");
localAddressBook().removeEmptyGroups();
} else {
/* VCard4 group handling: there are group contacts and individual contacts */
App.log.info("Assigning memberships of downloaded contact groups");
LocalGroup.applyPendingMemberships(localAddressBook());
}
}
@@ -226,48 +391,97 @@ public class ContactsSyncManager extends SyncManager {
private DavAddressBook davAddressBook() { return (DavAddressBook)davCollection; }
private LocalAddressBook localAddressBook() { return (LocalAddressBook)localCollection; }
private void processChangedGroups() throws ContactsStorageException {
LocalAddressBook addressBook = localAddressBook();
// groups with DELETED=1: remove group finally
for (LocalGroup group : addressBook.getDeletedGroups()) {
long groupId = group.getId();
App.log.fine("Finally removing group #" + groupId);
// remove group memberships, but not as sync adapter (should marks contacts as DIRTY)
// NOTE: doesn't work that way because Contact Provider removes the group memberships even for DELETED groups
// addressBook.removeGroupMemberships(groupId, false);
group.delete();
}
// groups with DIRTY=1: mark all memberships as dirty, then clean DIRTY flag of group
for (LocalGroup group : addressBook.getDirtyGroups()) {
long groupId = group.getId();
App.log.fine("Marking members of modified group #" + groupId + " as dirty");
addressBook.markMembersDirty(groupId);
group.clearDirty();
}
}
private void processVCard(String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException {
App.log.info("Processing CardDAV resource " + fileName);
Contact[] contacts = Contact.fromStream(stream, charset, downloader);
if (contacts.length == 1) {
Contact newData = contacts[0];
if (contacts.length == 0) {
App.log.warning("Received VCard without data, ignoring");
return;
} else if (contacts.length > 1)
App.log.warning("Received multiple VCards, using first one");
// update local contact, if it exists
LocalContact localContact = (LocalContact)localResources.get(fileName);
if (localContact != null) {
App.log.info("Updating " + fileName + " in local address book");
localContact.eTag = eTag;
localContact.update(newData);
final Contact newData = contacts[0];
if (groupMethod == GroupMethod.CATEGORIES && newData.group) {
groupMethod = GroupMethod.GROUP_VCARDS;
App.log.warning("Received group VCard although group method is CATEGORIES. Deleting all groups; new group method: " + groupMethod);
localAddressBook().removeGroups();
settings.setGroupMethod(groupMethod);
}
// update local contact, if it exists
LocalResource local = localResources.get(fileName);
currentLocalResource = local;
if (local != null) {
App.log.log(Level.INFO, "Updating " + fileName + " in local address book", newData);
if (local instanceof LocalGroup && newData.group) {
// update group
LocalGroup group = (LocalGroup)local;
group.eTag = eTag;
group.updateFromServer(newData);
syncResult.stats.numUpdates++;
} else if (local instanceof LocalContact && !newData.group) {
// update contact
LocalContact contact = (LocalContact)local;
contact.eTag = eTag;
contact.update(newData);
syncResult.stats.numUpdates++;
} else {
App.log.info("Adding " + fileName + " to local address book");
localContact = new LocalContact(localAddressBook(), newData, fileName, eTag);
localContact.add();
syncResult.stats.numInserts++;
// group has become an individual contact or vice versa
try {
local.delete();
local = null;
} catch(CalendarStorageException e) {
// CalendarStorageException is not used by LocalGroup and LocalContact
}
}
} else
App.log.severe("Received VCard with not exactly one VCARD, ignoring " + fileName);
}
if (local == null) {
if (newData.group) {
App.log.log(Level.INFO, "Creating local group", newData);
LocalGroup group = new LocalGroup(localAddressBook(), newData, fileName, eTag);
currentLocalResource = group;
group.create();
local = group;
} else {
App.log.log(Level.INFO, "Creating local contact", newData);
LocalContact contact = new LocalContact(localAddressBook(), newData, fileName, eTag);
currentLocalResource = contact;
contact.create();
local = contact;
}
syncResult.stats.numInserts++;
}
if (groupMethod == GroupMethod.CATEGORIES && local instanceof LocalContact) {
// VCard3: update group memberships from CATEGORIES
LocalContact contact = (LocalContact)local;
currentLocalResource = contact;
BatchOperation batch = new BatchOperation(provider);
App.log.log(Level.FINE, "Removing contact group memberships");
contact.removeGroupMemberships(batch);
for (String category : contact.getContact().categories) {
long groupID = localAddressBook().findOrCreateGroup(category);
App.log.log(Level.FINE, "Adding membership in group " + category + " (" + groupID + ")");
contact.addToGroup(batch, groupID);
}
batch.commit();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && local instanceof LocalContact)
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
((LocalContact)local).updateHashCode(null);
currentLocalResource = null;
}
@@ -292,7 +506,7 @@ public class ContactsSyncManager extends SyncManager {
return null;
}
OkHttpClient resourceClient = HttpClient.create();
OkHttpClient resourceClient = HttpClient.create(context);
// authenticate only against a certain host, and only upon request
resourceClient = HttpClient.addAuthentication(resourceClient, baseUrl.host(), settings.username(), settings.password());

View File

@@ -0,0 +1,12 @@
package at.bitfire.davdroid.syncadapter;
import android.content.Context;
import android.content.SyncResult;
import android.support.annotation.NonNull;
interface ISyncPlugin {
boolean beforeSync(@NonNull Context context, @NonNull SyncResult syncResult);
void afterSync(@NonNull Context context, @NonNull SyncResult syncResult);
}

View File

@@ -0,0 +1,90 @@
/*
* 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.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.NetworkErrorException;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import at.bitfire.davdroid.ui.AccountsActivity;
public class NullAuthenticatorService extends Service {
private AccountAuthenticator accountAuthenticator;
@Override
public void onCreate() {
accountAuthenticator = new NullAuthenticatorService.AccountAuthenticator(this);
}
@Override
public IBinder onBind(Intent intent) {
if (intent.getAction().equals(android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT))
return accountAuthenticator.getIBinder();
return null;
}
private static class AccountAuthenticator extends AbstractAccountAuthenticator {
final Context context;
public AccountAuthenticator(Context context) {
super(context);
this.context = context;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
return null;
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
Intent intent = new Intent(context, AccountsActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public String getAuthTokenLabel(String authTokenType) {
return null;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
return null;
}
}
}

View File

@@ -9,68 +9,134 @@
package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.database.sqlite.SQLiteDatabase;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import org.apache.commons.collections4.IteratorUtils;
import java.util.List;
import java.util.ServiceLoader;
import java.util.logging.Level;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.ui.PermissionsActivity;
//import com.android.vending.billing.IInAppBillingService;
public abstract class SyncAdapterService extends Service {
ServiceDB.OpenHelper dbHelper;
AbstractThreadedSyncAdapter syncAdapter;
@Override
public void onCreate() {
dbHelper = new ServiceDB.OpenHelper(this);
}
@Override
public void onDestroy() {
dbHelper.close();
}
abstract protected AbstractThreadedSyncAdapter syncAdapter();
@Nullable
@Override
public IBinder onBind(Intent intent) {
return syncAdapter.getSyncAdapterBinder();
return syncAdapter().getSyncAdapterBinder();
}
public static abstract class SyncAdapter extends AbstractThreadedSyncAdapter {
protected final SQLiteDatabase db;
public SyncAdapter(Context context, ServiceDB.OpenHelper dbHelper) {
private static final ServiceLoader<ISyncPlugin> syncPluginLoader = ServiceLoader.load(ISyncPlugin.class);
private final List<ISyncPlugin> syncPlugins = IteratorUtils.toList(syncPluginLoader.iterator());
public SyncAdapter(Context context) {
super(context, false);
db = dbHelper.getReadableDatabase();
for (ISyncPlugin plugin : syncPlugins)
App.log.info("Registered sync plugin: " + plugin.getClass().getName());
}
abstract void sync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult);
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
App.log.info("Starting " + authority + " sync");
App.log.log(Level.INFO, authority + " sync of " + account + " has been initiated.", extras.keySet().toArray());
// required for dav4android (ServiceLoader)
Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
final Context context = getContext();
Thread.currentThread().setContextClassLoader(context.getClassLoader());
// peek into AccountSettings to cause possible migration (v0.9 -> v1.0)
try {
new AccountSettings(getContext(), account);
} catch (InvalidAccountException e) {
App.log.log(Level.SEVERE, "Couldn't check for updated account settings", e);
}
boolean runSync = true;
for (ISyncPlugin plugin : syncPlugins)
if (!plugin.beforeSync(context, syncResult))
runSync = false;
if (runSync)
sync(account, extras, authority, provider, syncResult);
for (ISyncPlugin plugin : syncPlugins)
plugin.afterSync(context, syncResult);
App.log.info("Sync for " + authority + " complete");
}
@Override
public void onSecurityException(Account account, Bundle extras, String authority, SyncResult syncResult) {
App.log.log(Level.WARNING, "Security exception when opening content provider for " + authority);
syncResult.databaseError = true;
Intent intent = new Intent(getContext(), PermissionsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Notification notify = new NotificationCompat.Builder(getContext())
.setSmallIcon(R.drawable.ic_error_light)
.setLargeIcon(App.getLauncherBitmap(getContext()))
.setContentTitle(getContext().getString(R.string.sync_error_permissions))
.setContentText(getContext().getString(R.string.sync_error_permissions_text))
.setContentIntent(PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.build();
NotificationManagerCompat nm = NotificationManagerCompat.from(getContext());
nm.notify(Constants.NOTIFICATION_PERMISSIONS, notify);
}
protected boolean checkSyncConditions(@NonNull AccountSettings settings) {
if (settings.getSyncWifiOnly()) {
ConnectivityManager cm = (ConnectivityManager)getContext().getSystemService(CONNECTIVITY_SERVICE);
NetworkInfo network = cm.getActiveNetworkInfo();
if (network == null) {
App.log.info("No network available, stopping");
return false;
}
if (network.getType() != ConnectivityManager.TYPE_WIFI || !network.isConnected()) {
App.log.info("Not on connected WiFi, stopping");
return false;
}
String onlySSID = settings.getSyncWifiOnlySSID();
if (onlySSID != null) {
onlySSID = "\"" + onlySSID + "\"";
WifiManager wifi = (WifiManager)getContext().getApplicationContext().getSystemService(WIFI_SERVICE);
WifiInfo info = wifi.getConnectionInfo();
if (info == null || !onlySSID.equals(info.getSSID())) {
App.log.info("Connected to wrong WiFi network (" + info.getSSID() + ", required: " + onlySSID + "), ignoring");
return false;
}
}
}
return true;
}
}
}

View File

@@ -1,4 +1,4 @@
/*
/*
* Copyright © 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
@@ -9,15 +9,14 @@ package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.os.Build;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v7.app.NotificationCompat;
import android.text.TextUtils;
@@ -25,9 +24,9 @@ import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import at.bitfire.dav4android.DavResource;
@@ -42,11 +41,9 @@ import at.bitfire.dav4android.property.GetETag;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.LocalResource;
import at.bitfire.davdroid.ui.AccountActivity;
import at.bitfire.davdroid.ui.AccountSettingsActivity;
import at.bitfire.davdroid.ui.DebugInfoActivity;
import at.bitfire.ical4android.CalendarStorageException;
@@ -67,10 +64,11 @@ abstract public class SyncManager {
SYNC_PHASE_LIST_REMOTE = 7,
SYNC_PHASE_COMPARE_LOCAL_REMOTE = 8,
SYNC_PHASE_DOWNLOAD_REMOTE = 9,
SYNC_PHASE_SAVE_SYNC_STATE = 10;
SYNC_PHASE_POST_PROCESSING = 10,
SYNC_PHASE_SAVE_SYNC_STATE = 11;
protected final NotificationManager notificationManager;
protected final int notificationId;
protected final NotificationManagerCompat notificationManager;
protected final String uniqueCollectionId;
protected final Context context;
protected final Account account;
@@ -86,6 +84,13 @@ abstract public class SyncManager {
protected DavResource davCollection;
/** state information for debug info (local resource) */
protected LocalResource currentLocalResource;
/** state information for debug info (remote resource) */
protected DavResource currentDavResource;
/** remote CTag at the time of {@link #listRemote()} */
protected String remoteCTag = null;
@@ -100,24 +105,24 @@ abstract public class SyncManager {
public SyncManager(int notificationId, Context context, Account account, Bundle extras, String authority, SyncResult syncResult) throws InvalidAccountException {
public SyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult syncResult, String uniqueCollectionId) {
this.context = context;
this.account = account;
this.settings = settings;
this.extras = extras;
this.authority = authority;
this.syncResult = syncResult;
// get account settings (for sync interval etc.)
settings = new AccountSettings(context, account);
// create HttpClient with given logger
httpClient = HttpClient.create(context, account);
httpClient = HttpClient.create(context, settings);
// dismiss previous error notifications
notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(account.name, this.notificationId = notificationId);
this.uniqueCollectionId = uniqueCollectionId;
notificationManager = NotificationManagerCompat.from(context);
notificationManager.cancel(uniqueCollectionId, notificationId());
}
protected abstract int notificationId();
protected abstract String getSyncErrorTitle();
@TargetApi(21)
@@ -125,7 +130,10 @@ abstract public class SyncManager {
int syncPhase = SYNC_PHASE_PREPARE;
try {
App.log.info("Preparing synchronization");
prepare();
if (!prepare()) {
App.log.info("No reason to synchronize, aborting");
return;
}
if (Thread.interrupted())
return;
@@ -170,18 +178,22 @@ abstract public class SyncManager {
App.log.info("Downloading remote entries");
downloadRemote();
syncPhase = SYNC_PHASE_POST_PROCESSING;
App.log.info("Post-processing");
postProcess();
syncPhase = SYNC_PHASE_SAVE_SYNC_STATE;
App.log.info("Saving sync state");
saveSyncState();
} else
App.log.info("Remote collection didn't change, skipping remote sync");
} catch (IOException|ServiceUnavailableException e) {
} catch(IOException|ServiceUnavailableException e) {
App.log.log(Level.WARNING, "I/O exception during sync, trying again later", e);
syncResult.stats.numIoExceptions++;
if (e instanceof ServiceUnavailableException) {
Date retryAfter = ((ServiceUnavailableException) e).retryAfter;
Date retryAfter = ((ServiceUnavailableException) e).getRetryAfter();
if (retryAfter != null) {
// how many seconds to wait? getTime() returns ms, so divide by 1000
syncResult.delayUntil = (retryAfter.getTime() - new Date().getTime()) / 1000;
@@ -215,18 +227,25 @@ abstract public class SyncManager {
detailsIntent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account);
} else {
detailsIntent = new Intent(context, DebugInfoActivity.class);
detailsIntent.putExtra(DebugInfoActivity.KEY_EXCEPTION, e);
detailsIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e);
detailsIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account);
detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority);
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase);
if (currentLocalResource != null)
detailsIntent.putExtra(DebugInfoActivity.KEY_LOCAL_RESOURCE, currentLocalResource.toString());
if (currentDavResource != null)
detailsIntent.putExtra(DebugInfoActivity.KEY_REMOTE_RESOURCE, currentDavResource.toString());
}
// to make the PendingIntent unique
detailsIntent.setData(Uri.parse("uri://" + getClass().getName() + "/" + uniqueCollectionId));
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
builder .setSmallIcon(R.drawable.ic_launcher)
builder .setSmallIcon(R.drawable.ic_error_light)
.setLargeIcon(App.getLauncherBitmap(context))
.setContentTitle(getSyncErrorTitle())
.setContentIntent(PendingIntent.getActivity(context, notificationId, detailsIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setLocalOnly(true);
.setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR);
try {
String[] phases = context.getResources().getStringArray(R.array.sync_error_phases);
@@ -236,12 +255,15 @@ abstract public class SyncManager {
// should never happen
}
notificationManager.notify(account.name, notificationId, builder.build());
notificationManager.notify(uniqueCollectionId, notificationId(), builder.build());
}
}
abstract protected void prepare() throws ContactsStorageException;
/** Prepares synchronization (for instance, allocates necessary resources).
* @return whether actual synchronization is required / can be made. true = synchronization
* shall be continued, false = synchronization can be skipped */
abstract protected boolean prepare() throws ContactsStorageException;
abstract protected void queryCapabilities() throws IOException, HttpException, DavException, CalendarStorageException, ContactsStorageException;
@@ -252,17 +274,21 @@ abstract public class SyncManager {
protected void processLocallyDeleted() throws CalendarStorageException, ContactsStorageException {
// Remove locally deleted entries from server (if they have a name, i.e. if they were uploaded before),
// but only if they don't have changed on the server. Then finally remove them from the local address book.
LocalResource[] localList = localCollection.getDeleted();
for (LocalResource local : localList) {
List<LocalResource> localList = localCollection.getDeleted();
for (final LocalResource local : localList) {
if (Thread.interrupted())
return;
currentLocalResource = local;
final String fileName = local.getFileName();
if (!TextUtils.isEmpty(fileName)) {
App.log.info(fileName + " has been deleted locally -> deleting from server");
final DavResource remote = new DavResource(httpClient, collectionURL.newBuilder().addPathSegment(fileName).build());
currentDavResource = remote;
try {
new DavResource(httpClient, collectionURL.newBuilder().addPathSegment(fileName).build())
.delete(local.getETag());
remote.delete(local.getETag());
} catch (IOException|HttpException e) {
App.log.warning("Couldn't delete " + fileName + " from server; ignoring (may be downloaded again)");
}
@@ -270,15 +296,22 @@ abstract public class SyncManager {
App.log.info("Removing local record #" + local.getId() + " which has been deleted locally and was never uploaded");
local.delete();
syncResult.stats.numDeletes++;
currentLocalResource = null;
currentDavResource = null;
}
}
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
// assign file names and UIDs to new contacts so that we can use the file name as an index
for (LocalResource local : localCollection.getWithoutFileName()) {
String uuid = UUID.randomUUID().toString();
App.log.info("Found local record #" + local.getId() + " without file name; assigning file name/UID based on " + uuid);
local.updateFileNameAndUID(uuid);
App.log.info("Looking for contacts/groups without file name");
for (final LocalResource local : (Iterable<LocalResource>)localCollection.getWithoutFileName()) {
currentLocalResource = local;
App.log.fine("Found local record #" + local.getId() + " without file name; generating file name/UID if necessary");
local.prepareForUpload();
currentLocalResource = null;
}
}
@@ -290,19 +323,20 @@ abstract public class SyncManager {
*/
protected void uploadDirty() throws IOException, HttpException, CalendarStorageException, ContactsStorageException {
// upload dirty contacts
for (LocalResource local : localCollection.getDirty()) {
for (final LocalResource local : (Iterable<LocalResource>)localCollection.getDirty()) {
if (Thread.interrupted())
return;
currentLocalResource = local;
final String fileName = local.getFileName();
DavResource remote = new DavResource(httpClient, collectionURL.newBuilder().addPathSegment(fileName).build());
final DavResource remote = new DavResource(httpClient, collectionURL.newBuilder().addPathSegment(fileName).build());
currentDavResource = remote;
// generate entity to upload (VCard, iCal, whatever)
RequestBody body = prepareUpload(local);
try {
if (local.getETag() == null) {
App.log.info("Uploading new record " + fileName);
remote.put(body, null, true);
@@ -310,21 +344,23 @@ abstract public class SyncManager {
App.log.info("Uploading locally modified record " + fileName);
remote.put(body, local.getETag(), false);
}
} catch (ConflictException|PreconditionFailedException e) {
// we can't interact with the user to resolve the conflict, so we treat 409 like 412
App.log.log(Level.INFO, "Resource has been modified on the server before upload, ignoring", e);
}
String eTag = null;
GetETag newETag = (GetETag) remote.properties.get(GetETag.NAME);
GetETag newETag = (GetETag) remote.getProperties().get(GetETag.NAME);
if (newETag != null) {
eTag = newETag.eTag;
eTag = newETag.getETag();
App.log.fine("Received new ETag=" + eTag + " after uploading");
} else
App.log.fine("Didn't receive new ETag after uploading, setting to null");
local.clearDirty(eTag);
currentLocalResource = null;
currentDavResource = null;
}
}
@@ -337,9 +373,9 @@ abstract public class SyncManager {
*/
protected boolean checkSyncState() throws CalendarStorageException, ContactsStorageException {
// check CTag (ignore on manual sync)
GetCTag getCTag = (GetCTag)davCollection.properties.get(GetCTag.NAME);
GetCTag getCTag = (GetCTag)davCollection.getProperties().get(GetCTag.NAME);
if (getCTag != null)
remoteCTag = getCTag.cTag;
remoteCTag = getCTag.getCTag();
String localCTag = null;
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL))
@@ -359,8 +395,8 @@ abstract public class SyncManager {
*/
protected void listLocal() throws CalendarStorageException, ContactsStorageException {
// fetch list of local contacts and build hash table to index file name
LocalResource[] localList = localCollection.getAll();
localResources = new HashMap<>(localList.length);
List<LocalResource> localList = localCollection.getAll();
localResources = new HashMap<>(localList.size());
for (LocalResource resource : localList) {
App.log.fine("Found local resource: " + resource.getFileName());
localResources.put(resource.getFileName(), resource);
@@ -387,18 +423,22 @@ abstract public class SyncManager {
*/
toDownload = new HashSet<>();
for (String localName : localResources.keySet()) {
DavResource remote = remoteResources.get(localName);
final DavResource remote = remoteResources.get(localName);
currentDavResource = remote;
if (remote == null) {
App.log.info(localName + " is not on server anymore, deleting");
localResources.get(localName).delete();
final LocalResource local = localResources.get(localName);
currentLocalResource = local;
local.delete();
syncResult.stats.numDeletes++;
} else {
// contact is still on server, check whether it has been updated remotely
GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME);
if (getETag == null || getETag.eTag == null)
GetETag getETag = (GetETag)remote.getProperties().get(GetETag.NAME);
if (getETag == null || getETag.getETag() == null)
throw new DavException("Server didn't provide ETag");
String localETag = localResources.get(localName).getETag(),
remoteETag = getETag.eTag;
remoteETag = getETag.getETag();
if (remoteETag.equals(localETag))
syncResult.stats.numSkippedEntries++;
else {
@@ -408,6 +448,9 @@ abstract public class SyncManager {
// remote entry has been seen, remove from list
remoteResources.remove(localName);
currentDavResource = null;
currentLocalResource = null;
}
}
@@ -424,6 +467,12 @@ abstract public class SyncManager {
*/
abstract protected void downloadRemote() throws IOException, HttpException, DavException, ContactsStorageException, CalendarStorageException;
/**
* For post-processing of entries, for instance assigning groups.
*/
protected void postProcess() throws CalendarStorageException, ContactsStorageException {
}
protected void saveSyncState() throws CalendarStorageException, ContactsStorageException {
/* Save sync state (CTag). It doesn't matter if it has changed during the sync process
(for instance, because another client has uploaded changes), because this will simply

View File

@@ -8,110 +8,123 @@
package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.app.Service;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.dmfs.provider.tasks.TaskContract;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.model.ServiceDB.Collections;
import at.bitfire.davdroid.model.ServiceDB.OpenHelper;
import at.bitfire.davdroid.model.ServiceDB.Services;
import at.bitfire.davdroid.resource.LocalTaskList;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.TaskProvider;
import lombok.Cleanup;
/**
* Synchronization manager for CalDAV collections; handles tasks ({@code VTODO}).
*/
public class TasksSyncAdapterService extends SyncAdapterService {
@Override
public void onCreate() {
super.onCreate();
syncAdapter = new SyncAdapter(this, dbHelper);
}
@Override
protected AbstractThreadedSyncAdapter syncAdapter() {
return new SyncAdapter(this);
}
private static class SyncAdapter extends SyncAdapterService.SyncAdapter {
public SyncAdapter(Context context, OpenHelper dbHelper) {
super(context, dbHelper);
public SyncAdapter(Context context) {
super(context);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient providerClient, SyncResult syncResult) {
super.onPerformSync(account, extras, authority, providerClient, syncResult);
public void sync(Account account, Bundle extras, String authority, ContentProviderClient providerClient, SyncResult syncResult) {
try {
@Cleanup TaskProvider provider = TaskProvider.acquire(getContext().getContentResolver(), TaskProvider.ProviderName.OpenTasks);
if (provider == null)
throw new CalendarStorageException("Couldn't access OpenTasks provider");
updateLocalTaskLists(provider, account);
AccountSettings settings = new AccountSettings(getContext(), account);
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
return;
for (LocalTaskList taskList : (LocalTaskList[])LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, null, null)) {
App.log.info("Synchronizing task list #" + taskList.getId() + ", URL: " + taskList.getSyncId());
TasksSyncManager syncManager = new TasksSyncManager(getContext(), account, extras, authority, provider, syncResult, taskList);
updateLocalTaskLists(provider, account, settings);
for (LocalTaskList taskList : LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, TaskContract.TaskLists.SYNC_ENABLED + "!=0", null)) {
App.log.info("Synchronizing task list #" + taskList.getId() + " [" + taskList.getSyncId() + "]");
TasksSyncManager syncManager = new TasksSyncManager(getContext(), account, settings, extras, authority, provider, syncResult, taskList);
syncManager.performSync();
}
} catch (CalendarStorageException e) {
App.log.log(Level.SEVERE, "Couldn't enumerate local task lists", e);
} catch (InvalidAccountException e) {
App.log.log(Level.SEVERE, "Couldn't get account settings", e);
} finally {
db.close();
}
App.log.info("Task sync complete");
}
private void updateLocalTaskLists(TaskProvider provider, Account account) throws CalendarStorageException {
// enumerate remote and local task lists
Long service = getService(account);
Map<String, CollectionInfo> remote = remoteTaskLists(service);
LocalTaskList[] local = (LocalTaskList[])LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, null, null);
private void updateLocalTaskLists(TaskProvider provider, Account account, AccountSettings settings) throws CalendarStorageException {
SQLiteOpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
try {
// enumerate remote and local task lists
SQLiteDatabase db = dbHelper.getReadableDatabase();
Long service = getService(db, account);
Map<String, CollectionInfo> remote = remoteTaskLists(db, service);
List<LocalTaskList> local = LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, null, null);
// delete obsolete local task lists
for (LocalTaskList list : local) {
String url = list.getSyncId();
if (!remote.containsKey(url)) {
App.log.fine("Deleting obsolete local task list" + url);
list.delete();
} else {
// remote CollectionInfo found for this local collection, update data
CollectionInfo info = remote.get(url);
App.log.fine("Updating local task list " + url + " with " + info);
list.update(info);
// we already have a local task list for this remote collection, don't take into consideration anymore
remote.remove(url);
boolean updateColors = settings.getManageCalendarColors();
// delete obsolete local task lists
for (LocalTaskList list : local) {
String url = list.getSyncId();
if (!remote.containsKey(url)) {
App.log.fine("Deleting obsolete local task list " + url);
list.delete();
} else {
// remote CollectionInfo found for this local collection, update data
CollectionInfo info = remote.get(url);
App.log.fine("Updating local task list " + url + " with " + info);
list.update(info, updateColors);
// we already have a local task list for this remote collection, don't take into consideration anymore
remote.remove(url);
}
}
}
// create new local task lists
for (String url : remote.keySet()) {
CollectionInfo info = remote.get(url);
App.log.info("Adding local task list " + info);
LocalTaskList.create(account, provider, info);
// create new local task lists
for (String url : remote.keySet()) {
CollectionInfo info = remote.get(url);
App.log.info("Adding local task list " + info);
LocalTaskList.create(account, provider, info);
}
} finally {
dbHelper.close();
}
}
@Nullable
Long getService(Account account) {
Long getService(@NonNull SQLiteDatabase db, @NonNull Account account) {
@Cleanup Cursor c = db.query(Services._TABLE, new String[] { Services.ID },
Services.ACCOUNT_NAME + "=? AND " + Services.SERVICE + "=?", new String[] { account.name, Services.SERVICE_CALDAV }, null, null, null);
if (c.moveToNext())
@@ -121,7 +134,7 @@ public class TasksSyncAdapterService extends SyncAdapterService {
}
@NonNull
private Map<String, CollectionInfo> remoteTaskLists(Long service) {
private Map<String, CollectionInfo> remoteTaskLists(@NonNull SQLiteDatabase db, Long service) {
Map<String, CollectionInfo> collections = new LinkedHashMap<>();
if (service != null) {
@Cleanup Cursor cursor = db.query(Collections._TABLE, null,

View File

@@ -9,22 +9,18 @@
package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.content.ContentValues;
import android.content.Context;
import android.content.SyncResult;
import android.os.Bundle;
import android.text.TextUtils;
import org.apache.commons.codec.Charsets;
import org.apache.commons.lang3.StringUtils;
import org.dmfs.provider.tasks.TaskContract.TaskLists;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -34,16 +30,14 @@ import at.bitfire.dav4android.DavCalendar;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.property.CalendarColor;
import at.bitfire.dav4android.property.CalendarData;
import at.bitfire.dav4android.property.DisplayName;
import at.bitfire.dav4android.property.GetCTag;
import at.bitfire.dav4android.property.GetContentType;
import at.bitfire.dav4android.property.GetETag;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.ArrayUtils;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.LocalResource;
import at.bitfire.davdroid.resource.LocalTask;
@@ -65,12 +59,17 @@ public class TasksSyncManager extends SyncManager {
final protected TaskProvider provider;
public TasksSyncManager(Context context, Account account, Bundle extras, String authority, TaskProvider provider, SyncResult result, LocalTaskList taskList) throws InvalidAccountException {
super(Constants.NOTIFICATION_TASK_SYNC, context, account, extras, authority, result);
public TasksSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, TaskProvider provider, SyncResult result, LocalTaskList taskList) {
super(context, account, settings, extras, authority, result, "taskList/" + taskList.getId());
this.provider = provider;
localCollection = taskList;
}
@Override
protected int notificationId() {
return Constants.NOTIFICATION_TASK_SYNC;
}
@Override
protected String getSyncErrorTitle() {
return context.getString(R.string.sync_error_tasks, account.name);
@@ -78,9 +77,10 @@ public class TasksSyncManager extends SyncManager {
@Override
protected void prepare() {
protected boolean prepare() {
collectionURL = HttpUrl.parse(localTaskList().getSyncId());
davCollection = new DavCalendar(httpClient, collectionURL);
return true;
}
@Override
@@ -91,22 +91,32 @@ public class TasksSyncManager extends SyncManager {
@Override
protected RequestBody prepareUpload(LocalResource resource) throws IOException, CalendarStorageException {
LocalTask local = (LocalTask)resource;
App.log.log(Level.FINE, "Preparing upload of task " + local.getFileName(), local.getTask() );
ByteArrayOutputStream os = new ByteArrayOutputStream();
local.getTask().write(os);
return RequestBody.create(
DavCalendar.MIME_ICALENDAR,
local.getTask().toStream().toByteArray()
os.toByteArray()
);
}
@Override
protected void listRemote() throws IOException, HttpException, DavException {
// fetch list of remote VTODOs and build hash table to index file name
davCalendar().calendarQuery("VTODO", null, null);
remoteResources = new HashMap<>(davCollection.members.size());
for (DavResource vCard : davCollection.members) {
final DavCalendar calendar = davCalendar();
currentDavResource = calendar;
calendar.calendarQuery("VTODO", null, null);
remoteResources = new HashMap<>(davCollection.getMembers().size());
for (DavResource vCard : davCollection.getMembers()) {
String fileName = vCard.fileName();
App.log.fine("Found remote VTODO: " + fileName);
remoteResources.put(fileName, vCard);
}
currentDavResource = null;
}
@Override
@@ -122,10 +132,15 @@ public class TasksSyncManager extends SyncManager {
if (bunch.length == 1) {
// only one contact, use GET
DavResource remote = bunch[0];
final DavResource remote = bunch[0];
currentDavResource = remote;
ResponseBody body = remote.get("text/calendar");
String eTag = ((GetETag)remote.properties.get(GetETag.NAME)).eTag;
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
GetETag eTag = (GetETag)remote.getProperties().get(GetETag.NAME);
if (eTag == null || StringUtils.isEmpty(eTag.getETag()))
throw new DavException("Received CalDAV GET response without ETag for " + remote.getLocation());
Charset charset = Charsets.UTF_8;
MediaType contentType = body.contentType();
@@ -133,40 +148,47 @@ public class TasksSyncManager extends SyncManager {
charset = contentType.charset(Charsets.UTF_8);
@Cleanup InputStream stream = body.byteStream();
processVTodo(remote.fileName(), eTag, stream, charset);
processVTodo(remote.fileName(), eTag.getETag(), stream, charset);
} else {
// multiple contacts, use multi-get
List<HttpUrl> urls = new LinkedList<>();
for (DavResource remote : bunch)
urls.add(remote.location);
davCalendar().multiget(urls.toArray(new HttpUrl[urls.size()]));
urls.add(remote.getLocation());
final DavCalendar calendar = davCalendar();
currentDavResource = calendar;
calendar.multiget(urls.toArray(new HttpUrl[urls.size()]));
// process multiget results
for (DavResource remote : davCollection.members) {
for (final DavResource remote : davCollection.getMembers()) {
currentDavResource = remote;
String eTag;
GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME);
GetETag getETag = (GetETag)remote.getProperties().get(GetETag.NAME);
if (getETag != null)
eTag = getETag.eTag;
eTag = getETag.getETag();
else
throw new DavException("Received multi-get response without ETag");
Charset charset = Charsets.UTF_8;
GetContentType getContentType = (GetContentType)remote.properties.get(GetContentType.NAME);
if (getContentType != null && getContentType.type != null) {
MediaType type = MediaType.parse(getContentType.type);
GetContentType getContentType = (GetContentType)remote.getProperties().get(GetContentType.NAME);
if (getContentType != null && getContentType.getType() != null) {
MediaType type = MediaType.parse(getContentType.getType());
if (type != null)
charset = type.charset(Charsets.UTF_8);
}
CalendarData calendarData = (CalendarData)remote.properties.get(CalendarData.NAME);
if (calendarData == null || calendarData.iCalendar == null)
CalendarData calendarData = (CalendarData)remote.getProperties().get(CalendarData.NAME);
if (calendarData == null || calendarData.getICalendar() == null)
throw new DavException("Received multi-get response without address data");
@Cleanup InputStream stream = new ByteArrayInputStream(calendarData.iCalendar.getBytes());
@Cleanup InputStream stream = new ByteArrayInputStream(calendarData.getICalendar().getBytes());
processVTodo(remote.fileName(), eTag, stream, charset);
}
}
currentDavResource = null;
}
}
@@ -177,7 +199,7 @@ public class TasksSyncManager extends SyncManager {
private DavCalendar davCalendar() { return (DavCalendar)davCollection; }
private void processVTodo(String fileName, String eTag, InputStream stream, Charset charset) throws IOException, CalendarStorageException {
Task[] tasks;
List<Task> tasks;
try {
tasks = Task.fromStream(stream, charset);
} catch (InvalidCalendarException e) {
@@ -185,11 +207,12 @@ public class TasksSyncManager extends SyncManager {
return;
}
if (tasks.length == 1) {
Task newData = tasks[0];
if (tasks.size() == 1) {
Task newData = tasks.get(0);
// update local task, if it exists
LocalTask localTask = (LocalTask)localResources.get(fileName);
currentLocalResource = localTask;
if (localTask != null) {
App.log.info("Updating " + fileName + " in local tasklist");
localTask.setETag(eTag);
@@ -198,6 +221,7 @@ public class TasksSyncManager extends SyncManager {
} else {
App.log.info("Adding " + fileName + " to local task list");
localTask = new LocalTask(localTaskList(), newData, fileName, eTag);
currentLocalResource = localTask;
localTask.add();
syncResult.stats.numInserts++;
}

View File

@@ -30,7 +30,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
@@ -40,15 +40,52 @@ import at.bitfire.davdroid.App;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.davdroid.R;
import ezvcard.Ezvcard;
import ezvcard.util.IOUtils;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
public class AboutActivity extends AppCompatActivity {
@RequiredArgsConstructor
private static class ComponentInfo {
final String title, version, website, copyright;
final Integer licenseInfo;
final String licenseTextFile;
}
private ComponentInfo components[];
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
components = new ComponentInfo[] {
new ComponentInfo(
null, BuildConfig.VERSION_NAME, getString(R.string.homepage_url),
"Ricki Hirner, Bernhard Stockmann (bitfire web engineering)",
null, null
), new ComponentInfo(
"AmbilWarna", null, "https://github.com/yukuku/ambilwarna",
"Yuku", R.string.about_license_info_no_warranty, "apache2.html"
), new ComponentInfo(
"Apache Commons", null, "http://commons.apache.org/",
"Apache Software Foundation", R.string.about_license_info_no_warranty, "apache2.html"
), new ComponentInfo(
"dnsjava", null, "http://dnsjava.org/",
"Brian Wellington", R.string.about_license_info_no_warranty, "bsd.html"
), new ComponentInfo(
"ez-vcard", Ezvcard.VERSION, "https://github.com/mangstadt/ez-vcard",
"Michael Angstadt", R.string.about_license_info_no_warranty, "bsd.html"
), new ComponentInfo(
"ical4j", "2.x", "https://ical4j.github.io/",
"Ben Fortuna", R.string.about_license_info_no_warranty, "bsd-3clause.html"
), new ComponentInfo(
"OkHttp", null, "https://square.github.io/okhttp/",
"Square, Inc.", R.string.about_license_info_no_warranty, "apache2.html"
), new ComponentInfo(
"Project Lombok", null, "https://projectlombok.org/",
"The Project Lombok Authors", R.string.about_license_info_no_warranty, "mit.html"
)
};
setContentView(R.layout.activity_about);
setSupportActionBar((Toolbar)findViewById(R.id.toolbar));
@@ -62,47 +99,7 @@ public class AboutActivity extends AppCompatActivity {
}
@RequiredArgsConstructor
private static class ComponentInfo {
final String title, version, website, copyright;
final int licenseInfo;
final String licenseTextFile;
}
private final static ComponentInfo components[] = {
new ComponentInfo(
"DAVdroid", BuildConfig.VERSION_NAME, "https://davdroid.bitfire.at",
"© " + DateFormatUtils.format(BuildConfig.buildTime, "yyyy") + " Ricki Hirner, Bernhard Stockmann (bitfire web engineering)",
R.string.about_license_info_no_warranty, "gpl-3.0-standalone.html"
), new ComponentInfo(
"AmbilWarna", null, "https://github.com/yukuku/ambilwarna",
"© Yuku", R.string.about_license_info_no_warranty, "apache2.html"
), new ComponentInfo(
"Apache Commons", null, "http://commons.apache.org/",
"Apache Software Foundation", R.string.about_license_info_no_warranty, "apache2.html"
), new ComponentInfo(
"dnsjava", null, "http://dnsjava.org/",
"© Brian Wellington", R.string.about_license_info_no_warranty, "bsd.html"
), new ComponentInfo(
"ez-vcard", Ezvcard.VERSION, "https://github.com/mangstadt/ez-vcard",
"© Michael Angstadt", R.string.about_license_info_no_warranty, "bsd.html"
), new ComponentInfo(
"ical4j", "2.x", "http://ical4j.github.io/",
"© Ben Fortuna", R.string.about_license_info_no_warranty, "bsd-3clause.html"
), new ComponentInfo(
"MemorizingTrustManager", null, "https://github.com/ge0rg/MemorizingTrustManager",
"© Georg Lukas", R.string.about_license_info_no_warranty, "mit.html"
), new ComponentInfo(
"OkHttp", null, "https://square.github.io/okhttp/",
"© Square, Inc.", R.string.about_license_info_no_warranty, "apache2.html"
), new ComponentInfo(
"Project Lombok", null, "https://projectlombok.org/",
"© The Project Lombok Authors", R.string.about_license_info_no_warranty, "mit.html"
)
};
private static class TabsAdapter extends FragmentPagerAdapter {
private class TabsAdapter extends FragmentPagerAdapter {
public TabsAdapter(FragmentManager fm) {
super(fm);
}
@@ -114,7 +111,7 @@ public class AboutActivity extends AppCompatActivity {
@Override
public CharSequence getPageTitle(int position) {
return components[position].title;
return components[position].title != null ? components[position].title : getString(R.string.app_name);
}
@Override
@@ -140,27 +137,36 @@ public class AboutActivity extends AppCompatActivity {
@Override
@SuppressLint("SetTextI18n")
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
ComponentInfo info = components[getArguments().getInt(KEY_POSITION)];
ComponentInfo info = ((AboutActivity)getActivity()).components[getArguments().getInt(KEY_POSITION)];
View v = inflater.inflate(R.layout.about_component, container, false);
TextView tv = (TextView)v.findViewById(R.id.title);
tv.setText(info.title + (info.version != null ? (" " + info.version) : ""));
tv.setText((info.title == null ? getString(R.string.app_name) : info.title) +
(info.version != null ? (" " + info.version) : ""));
tv = (TextView)v.findViewById(R.id.website);
tv.setAutoLinkMask(Linkify.WEB_URLS);
tv.setText(info.website);
tv = (TextView)v.findViewById(R.id.copyright);
tv.setText(info.copyright);
tv.setText("© " + info.copyright);
tv = (TextView)v.findViewById(R.id.license_info);
tv.setText(info.licenseInfo);
if (info.licenseInfo == null)
tv.setVisibility(View.GONE);
else
tv.setText(info.licenseInfo);
// load and format license text
Bundle args = new Bundle(1);
args.putString(KEY_FILE_NAME, info.licenseTextFile);
getLoaderManager().initLoader(0, args, this);
if (info.licenseTextFile == null) {
v.findViewById(R.id.license_header).setVisibility(View.GONE);
v.findViewById(R.id.license_text).setVisibility(View.GONE);
} else {
Bundle args = new Bundle(1);
args.putString(KEY_FILE_NAME, info.licenseTextFile);
getLoaderManager().initLoader(0, args, this);
}
return v;
}

View File

@@ -14,6 +14,8 @@ import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.annotation.TargetApi;
import android.app.Dialog;
import android.app.LoaderManager;
import android.content.AsyncTaskLoader;
import android.content.ComponentName;
@@ -24,6 +26,7 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.Loader;
import android.content.ServiceConnection;
import android.content.SyncStatusObserver;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
@@ -31,10 +34,12 @@ import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.CardView;
@@ -45,14 +50,13 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.TextView;
import org.apache.commons.lang3.BooleanUtils;
@@ -62,20 +66,23 @@ import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.cert4android.CustomCertManager;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.DavService;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.model.ServiceDB.Collections;
import at.bitfire.davdroid.model.ServiceDB.OpenHelper;
import at.bitfire.davdroid.model.ServiceDB.Services;
import at.bitfire.davdroid.resource.LocalAddressBook;
import at.bitfire.davdroid.resource.LocalTaskList;
import at.bitfire.ical4android.TaskProvider;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
import static android.content.ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE;
public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener, LoaderManager.LoaderCallbacks<AccountActivity.AccountInfo> {
public static final String EXTRA_ACCOUNT = "account";
@@ -86,6 +93,7 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
Toolbar tbCardDAV, tbCalDAV;
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -113,12 +121,36 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
getLoaderManager().initLoader(0, getIntent().getExtras(), this);
}
@Override
protected void onPause() {
super.onPause();
CustomCertManager certManager = ((App)getApplicationContext()).getCertManager();
if (certManager != null)
certManager.appInForeground = false;
}
@Override
protected void onResume() {
super.onResume();
CustomCertManager certManager = ((App)getApplicationContext()).getCertManager();
if (certManager != null)
certManager.appInForeground = true;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_account, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem itemRename = menu.findItem(R.id.rename_account);
// renameAccount is available for API level 21+
itemRename.setVisible(Build.VERSION.SDK_INT >= 21);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
@@ -130,6 +162,9 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
intent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account);
startActivity(intent);
break;
case R.id.rename_account:
RenameAccountFragment.newInstance(account).show(getSupportFragmentManager(), null);
break;
case R.id.delete_account:
new AlertDialog.Builder(AccountActivity.this)
.setIcon(R.drawable.ic_error_dark)
@@ -152,12 +187,15 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
@Override
public boolean onMenuItemClick(MenuItem item) {
Intent intent;
switch (item.getItemId()) {
case R.id.refresh_address_books:
Intent intent = new Intent(this, DavService.class);
intent.setAction(DavService.ACTION_REFRESH_COLLECTIONS);
intent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, accountInfo.carddav.id);
startService(intent);
if (accountInfo != null && accountInfo.carddav != null) {
intent = new Intent(this, DavService.class);
intent.setAction(DavService.ACTION_REFRESH_COLLECTIONS);
intent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, accountInfo.carddav.id);
startService(intent);
}
break;
case R.id.create_address_book:
intent = new Intent(this, CreateAddressBookActivity.class);
@@ -165,10 +203,12 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
startActivity(intent);
break;
case R.id.refresh_calendars:
intent = new Intent(this, DavService.class);
intent.setAction(DavService.ACTION_REFRESH_COLLECTIONS);
intent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, accountInfo.caldav.id);
startService(intent);
if (accountInfo != null && accountInfo.caldav != null) {
intent = new Intent(this, DavService.class);
intent.setAction(DavService.ACTION_REFRESH_COLLECTIONS);
intent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, accountInfo.caldav.id);
startService(intent);
}
break;
case R.id.create_calendar:
intent = new Intent(this, CreateCalendarActivity.class);
@@ -189,22 +229,10 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
boolean nowChecked = !info.selected;
if (list.getChoiceMode() == AbsListView.CHOICE_MODE_SINGLE)
// clear all other checked items
for (int i = adapter.getCount()-1; i >= 0; i--)
adapter.getItem(i).selected = false;
OpenHelper dbHelper = new OpenHelper(AccountActivity.this);
try {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransaction();
if (list.getChoiceMode() == AbsListView.CHOICE_MODE_SINGLE) {
// disable all other collections
ContentValues values = new ContentValues(1);
values.put(Collections.SYNC, 0);
db.update(Collections._TABLE, values, Collections.SERVICE_ID + "=?", new String[] { String.valueOf(info.serviceID) });
}
db.beginTransactionNonExclusive();
ContentValues values = new ContentValues(1);
values.put(Collections.SYNC, nowChecked ? 1 : 0);
@@ -265,7 +293,7 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
@Override
public Loader<AccountInfo> onCreateLoader(int id, Bundle args) {
return new AccountLoader(this, account.name);
return new AccountLoader(this, account);
}
public void reload() {
@@ -276,12 +304,6 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
public void onLoadFinished(Loader<AccountInfo> loader, final AccountInfo info) {
accountInfo = info;
if (accountInfo == null) {
// account doesn't exist anymore
finish();
return;
}
CardView card = (CardView)findViewById(R.id.carddav);
if (info.carddav != null) {
ProgressBar progress = (ProgressBar)findViewById(R.id.carddav_refreshing);
@@ -331,19 +353,20 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
}
private static class AccountLoader extends AsyncTaskLoader<AccountInfo> implements DavService.RefreshingStatusListener, ServiceConnection {
private final String accountName;
private final OpenHelper dbHelper;
private static class AccountLoader extends AsyncTaskLoader<AccountInfo> implements DavService.RefreshingStatusListener, ServiceConnection, SyncStatusObserver {
private final Account account;
private DavService.InfoBinder davService;
private Object syncStatusListener;
public AccountLoader(Context context, String accountName) {
public AccountLoader(Context context, Account account) {
super(context);
this.accountName = accountName;
dbHelper = new OpenHelper(context);
this.account = account;
}
@Override
protected void onStartLoading() {
syncStatusListener = ContentResolver.addStatusChangeListener(SYNC_OBSERVER_TYPE_ACTIVE, this);
getContext().bindService(new Intent(getContext(), DavService.class), this, Context.BIND_AUTO_CREATE);
}
@@ -351,6 +374,9 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
protected void onStopLoading() {
davService.removeRefreshingStatusListener(this);
getContext().unbindService(this);
if (syncStatusListener != null)
ContentResolver.removeStatusChangeListener(syncStatusListener);
}
@Override
@@ -371,46 +397,55 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
forceLoad();
}
@Override
public void onStatusChanged(int which) {
forceLoad();
}
@Override
public AccountInfo loadInBackground() {
// peek into AccountSettings to call possible 0.9 -> 1.0 migration
// The next line can be removed as soon as migration from 0.9 is not required anymore!
try {
new AccountSettings(getContext(), new Account(accountName, Constants.ACCOUNT_TYPE));
} catch (InvalidAccountException e) {
App.log.log(Level.INFO, "Account doesn't exist (anymore)", e);
return null;
}
AccountInfo info = new AccountInfo();
try {
SQLiteDatabase db = dbHelper.getReadableDatabase();
@Cleanup Cursor cursor = db.query(
Services._TABLE,
new String[] { Services.ID, Services.SERVICE },
Services.ACCOUNT_NAME + "=?", new String[] { accountName },
null, null, null);
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
String service = cursor.getString(1);
if (Services.SERVICE_CARDDAV.equals(service)) {
info.carddav = new AccountInfo.ServiceInfo();
info.carddav.id = id;
info.carddav.refreshing = davService.isRefreshing(id);
info.carddav.hasHomeSets = hasHomeSets(db, id);
info.carddav.collections = readCollections(db, id);
@Cleanup OpenHelper dbHelper = new OpenHelper(getContext());
SQLiteDatabase db = dbHelper.getReadableDatabase();
} else if (Services.SERVICE_CALDAV.equals(service)) {
info.caldav = new AccountInfo.ServiceInfo();
info.caldav.id = id;
info.caldav.refreshing = davService.isRefreshing(id);
info.caldav.hasHomeSets = hasHomeSets(db, id);
info.caldav.collections = readCollections(db, id);
@Cleanup Cursor cursor = db.query(
Services._TABLE,
new String[] { Services.ID, Services.SERVICE },
Services.ACCOUNT_NAME + "=?", new String[] { account.name },
null, null, null);
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
String service = cursor.getString(1);
if (Services.SERVICE_CARDDAV.equals(service)) {
info.carddav = new AccountInfo.ServiceInfo();
info.carddav.id = id;
info.carddav.refreshing = (davService != null && davService.isRefreshing(id)) ||
ContentResolver.isSyncActive(account, App.getAddressBooksAuthority());
AccountManager accountManager = AccountManager.get(getContext());
for (Account addrBookAccount : accountManager.getAccountsByType(App.getAddressBookAccountType())) {
LocalAddressBook addressBook = new LocalAddressBook(getContext(), addrBookAccount, null);
try {
if (account.equals(addressBook.getMainAccount()))
info.carddav.refreshing |= ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY);
} catch(ContactsStorageException e) {
}
}
info.carddav.hasHomeSets = hasHomeSets(db, id);
info.carddav.collections = readCollections(db, id);
} else if (Services.SERVICE_CALDAV.equals(service)) {
info.caldav = new AccountInfo.ServiceInfo();
info.caldav.id = id;
info.caldav.refreshing = (davService != null && davService.isRefreshing(id)) ||
ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) ||
ContentResolver.isSyncActive(account, TaskProvider.ProviderName.OpenTasks.getAuthority());
info.caldav.hasHomeSets = hasHomeSets(db, id);
info.caldav.collections = readCollections(db, id);
}
} finally {
dbHelper.close();
}
return info;
}
@@ -432,6 +467,7 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
}
return collections;
}
}
@@ -449,7 +485,7 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
final CollectionInfo info = getItem(position);
RadioButton checked = (RadioButton)v.findViewById(R.id.checked);
CheckBox checked = (CheckBox)v.findViewById(R.id.checked);
checked.setChecked(info.selected);
TextView tv = (TextView)v.findViewById(R.id.title);
@@ -517,6 +553,94 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
}
/* DIALOG FRAGMENTS */
public static class RenameAccountFragment extends DialogFragment {
private final static String ARG_ACCOUNT = "account";
static RenameAccountFragment newInstance(@NonNull Account account) {
RenameAccountFragment fragment = new RenameAccountFragment();
Bundle args = new Bundle(1);
args.putParcelable(ARG_ACCOUNT, account);
fragment.setArguments(args);
return fragment;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Account oldAccount = getArguments().getParcelable(ARG_ACCOUNT);
final EditText editText = new EditText(getContext());
editText.setText(oldAccount.name);
return new AlertDialog.Builder(getContext())
.setTitle(R.string.account_rename)
.setMessage(R.string.account_rename_new_name)
.setView(editText)
.setPositiveButton(R.string.account_rename_rename, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
final String newName = editText.getText().toString();
if (newName.equals(oldAccount.name))
return;
final AccountManager accountManager = AccountManager.get(getContext());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
accountManager.renameAccount(oldAccount, newName,
new AccountManagerCallback<Account>() {
@Override
public void run(AccountManagerFuture<Account> future) {
App.log.info("Updating account name references");
// cancel maybe running synchronization
ContentResolver.cancelSync(oldAccount, null);
for (Account addrBookAccount : accountManager.getAccountsByType(App.getAddressBookAccountType()))
ContentResolver.cancelSync(addrBookAccount, null);
// update account name references in database
@Cleanup OpenHelper dbHelper = new OpenHelper(getContext());
ServiceDB.onRenameAccount(dbHelper.getWritableDatabase(), oldAccount.name, newName);
// update main account of address book accounts
try {
for (Account addrBookAccount : accountManager.getAccountsByType(App.getAddressBookAccountType())) {
LocalAddressBook addressBook = new LocalAddressBook(getContext(), addrBookAccount, null);
if (oldAccount.equals(addressBook.getMainAccount()))
addressBook.setMainAccount(new Account(newName, oldAccount.type));
}
} catch(ContactsStorageException e) {
App.log.log(Level.SEVERE, "Couldn't update address book accounts", e);
}
// calendar provider doesn't allow changing account_name of Events
// update account_name of local tasks
try {
LocalTaskList.onRenameAccount(getContext().getContentResolver(), oldAccount.name, newName);
} catch(RemoteException e) {
App.log.log(Level.SEVERE, "Couldn't propagate new account name to tasks provider", e);
}
// synchronize again
requestSync(new Account(newName, oldAccount.type));
}
}, null
);
getActivity().finish();
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.create();
}
}
/* USER ACTIONS */
private void deleteAccount() {
@@ -548,19 +672,23 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
}, null);
}
private void requestSync() {
protected static void requestSync(Account account) {
String authorities[] = {
ContactsContract.AUTHORITY,
App.getAddressBooksAuthority(),
CalendarContract.AUTHORITY,
TaskProvider.ProviderName.OpenTasks.authority
TaskProvider.ProviderName.OpenTasks.getAuthority()
};
for (String authority : authorities) {
Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); // manual sync
extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); // run immediately (don't queue)
ContentResolver.requestSync(account, authority, extras);
}
}
private void requestSync() {
requestSync(account);
Snackbar.make(findViewById(R.id.parent), R.string.account_synchronizing_now, Snackbar.LENGTH_LONG).show();
}

View File

@@ -11,6 +11,7 @@ package at.bitfire.davdroid.ui;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.OnAccountsUpdateListener;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
@@ -28,7 +29,6 @@ import android.widget.ListView;
import android.widget.TextView;
import at.bitfire.davdroid.AccountsChangedReceiver;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
public class AccountListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Account[]>, AdapterView.OnItemClickListener {
@@ -103,8 +103,9 @@ public class AccountListFragment extends ListFragment implements LoaderManager.L
}
@Override
@SuppressLint("MissingPermission")
public Account[] loadInBackground() {
return accountManager.getAccountsByType(Constants.ACCOUNT_TYPE);
return accountManager.getAccountsByType(getContext().getString(R.string.account_type));
}
}

View File

@@ -9,28 +9,33 @@
package at.bitfire.davdroid.ui;
import android.accounts.Account;
import android.content.DialogInterface;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SyncStatusObserver;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.support.v4.app.LoaderManager;
import android.support.v4.app.NavUtils;
import android.support.v7.app.AlertDialog;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.preference.EditTextPreference;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.support.v7.preference.SwitchPreferenceCompat;
import android.text.TextUtils;
import android.view.MenuItem;
import java.util.logging.Level;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.R;
import at.bitfire.ical4android.TaskProvider;
import at.bitfire.vcard4android.GroupMethod;
public class AccountSettingsActivity extends AppCompatActivity {
public final static String EXTRA_ACCOUNT = "account";
@@ -64,7 +69,7 @@ public class AccountSettingsActivity extends AppCompatActivity {
}
public static class AccountSettingsFragment extends PreferenceFragmentCompat {
public static class AccountSettingsFragment extends PreferenceFragmentCompat implements LoaderManager.LoaderCallbacks<AccountSettings> {
Account account;
@Override
@@ -72,7 +77,8 @@ public class AccountSettingsActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
account = getArguments().getParcelable(EXTRA_ACCOUNT);
refresh();
getLoaderManager().initLoader(0, getArguments(), this);
}
@Override
@@ -80,13 +86,14 @@ public class AccountSettingsActivity extends AppCompatActivity {
addPreferencesFromResource(R.xml.settings_account);
}
public void refresh() {
final AccountSettings settings;
@Override
public Loader<AccountSettings> onCreateLoader(int id, Bundle args) {
return new AccountSettingsLoader(getContext(), (Account)args.getParcelable(EXTRA_ACCOUNT));
}
try {
settings = new AccountSettings(getActivity(), account);
} catch(InvalidAccountException e) {
App.log.log(Level.INFO, "Account is invalid or doesn't exist (anymore)", e);
@Override
public void onLoadFinished(Loader<AccountSettings> loader, final AccountSettings settings) {
if (settings == null) {
getActivity().finish();
return;
}
@@ -98,8 +105,9 @@ public class AccountSettingsActivity extends AppCompatActivity {
prefUserName.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.username((String) newValue);
refresh(); return false;
settings.username((String)newValue);
getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
return false;
}
});
@@ -107,24 +115,15 @@ public class AccountSettingsActivity extends AppCompatActivity {
prefPassword.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.password((String) newValue);
refresh(); return false;
}
});
final SwitchPreferenceCompat prefPreemptive = (SwitchPreferenceCompat)findPreference("preemptive");
prefPreemptive.setChecked(settings.preemptiveAuth());
prefPreemptive.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.preemptiveAuth((Boolean) newValue);
refresh(); return false;
settings.password((String)newValue);
getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
return false;
}
});
// category: synchronization
final ListPreference prefSyncContacts = (ListPreference)findPreference("sync_interval_contacts");
final Long syncIntervalContacts = settings.getSyncInterval(ContactsContract.AUTHORITY);
final Long syncIntervalContacts = settings.getSyncInterval(App.getAddressBooksAuthority());
if (syncIntervalContacts != null) {
prefSyncContacts.setValue(syncIntervalContacts.toString());
if (syncIntervalContacts == AccountSettings.SYNC_INTERVAL_MANUALLY)
@@ -134,8 +133,9 @@ public class AccountSettingsActivity extends AppCompatActivity {
prefSyncContacts.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.setSyncInterval(ContactsContract.AUTHORITY, Long.parseLong((String) newValue));
refresh(); return false;
settings.setSyncInterval(App.getAddressBooksAuthority(), Long.parseLong((String)newValue));
getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
return false;
}
});
} else {
@@ -154,8 +154,9 @@ public class AccountSettingsActivity extends AppCompatActivity {
prefSyncCalendars.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.setSyncInterval(CalendarContract.AUTHORITY, Long.parseLong((String) newValue));
refresh(); return false;
settings.setSyncInterval(CalendarContract.AUTHORITY, Long.parseLong((String)newValue));
getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
return false;
}
});
} else {
@@ -164,7 +165,7 @@ public class AccountSettingsActivity extends AppCompatActivity {
}
final ListPreference prefSyncTasks = (ListPreference)findPreference("sync_interval_tasks");
final Long syncIntervalTasks = settings.getSyncInterval(TaskProvider.ProviderName.OpenTasks.authority);
final Long syncIntervalTasks = settings.getSyncInterval(TaskProvider.ProviderName.OpenTasks.getAuthority());
if (syncIntervalTasks != null) {
prefSyncTasks.setValue(syncIntervalTasks.toString());
if (syncIntervalTasks == AccountSettings.SYNC_INTERVAL_MANUALLY)
@@ -174,8 +175,9 @@ public class AccountSettingsActivity extends AppCompatActivity {
prefSyncTasks.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.authority, Long.parseLong((String) newValue));
refresh(); return false;
settings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.getAuthority(), Long.parseLong((String)newValue));
getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
return false;
}
});
} else {
@@ -183,30 +185,148 @@ public class AccountSettingsActivity extends AppCompatActivity {
prefSyncTasks.setSummary(R.string.settings_sync_summary_not_available);
}
final EditTextPreference prefTimeRangePastDays = (EditTextPreference)findPreference("caldav_time_range_past_days");
Integer pastDays = settings.getTimeRangePastDays();
if (pastDays != null) {
prefTimeRangePastDays.setText(pastDays.toString());
prefTimeRangePastDays.setSummary(getResources().getQuantityString(R.plurals.settings_sync_time_range_past_days, pastDays, pastDays));
} else {
prefTimeRangePastDays.setText(null);
prefTimeRangePastDays.setSummary(R.string.settings_sync_time_range_past_none);
}
prefTimeRangePastDays.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
final SwitchPreferenceCompat prefWifiOnly = (SwitchPreferenceCompat)findPreference("sync_wifi_only");
prefWifiOnly.setChecked(settings.getSyncWifiOnly());
prefWifiOnly.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
int days;
try {
days = Integer.parseInt((String)newValue);
} catch(NumberFormatException ignored) {
days = -1;
}
settings.setTimeRangePastDays(days < 0 ? null : days);
refresh(); return false;
public boolean onPreferenceChange(Preference preference, Object wifiOnly) {
settings.setSyncWiFiOnly((Boolean)wifiOnly);
getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
return false;
}
});
final EditTextPreference prefWifiOnlySSID = (EditTextPreference)findPreference("sync_wifi_only_ssid");
final String onlySSID = settings.getSyncWifiOnlySSID();
prefWifiOnlySSID.setText(onlySSID);
if (onlySSID != null)
prefWifiOnlySSID.setSummary(getString(R.string.settings_sync_wifi_only_ssid_on, onlySSID));
else
prefWifiOnlySSID.setSummary(R.string.settings_sync_wifi_only_ssid_off);
prefWifiOnlySSID.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
String ssid = (String)newValue;
settings.setSyncWifiOnlySSID(!TextUtils.isEmpty(ssid) ? ssid : null);
getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
return false;
}
});
// category: CardDAV
final ListPreference prefGroupMethod = (ListPreference)findPreference("contact_group_method");
if (syncIntervalContacts != null) {
prefGroupMethod.setValue(settings.getGroupMethod().name());
prefGroupMethod.setSummary(prefGroupMethod.getEntry());
if (BuildConfig.settingContactGroupMethod == null)
prefGroupMethod.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object o) {
String name = (String)o;
settings.setGroupMethod(GroupMethod.valueOf(name));
getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
return false;
}
});
else
prefGroupMethod.setEnabled(false);
} else
prefGroupMethod.setEnabled(false);
// category: CalDAV
final EditTextPreference prefTimeRangePastDays = (EditTextPreference)findPreference("time_range_past_days");
if (syncIntervalCalendars != null) {
Integer pastDays = settings.getTimeRangePastDays();
if (pastDays != null) {
prefTimeRangePastDays.setText(pastDays.toString());
prefTimeRangePastDays.setSummary(getResources().getQuantityString(R.plurals.settings_sync_time_range_past_days, pastDays, pastDays));
} else {
prefTimeRangePastDays.setText(null);
prefTimeRangePastDays.setSummary(R.string.settings_sync_time_range_past_none);
}
prefTimeRangePastDays.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
int days;
try {
days = Integer.parseInt((String)newValue);
} catch(NumberFormatException ignored) {
days = -1;
}
settings.setTimeRangePastDays(days < 0 ? null : days);
getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
return false;
}
});
} else
prefTimeRangePastDays.setEnabled(false);
final SwitchPreferenceCompat prefManageColors = (SwitchPreferenceCompat)findPreference("manage_calendar_colors");
if (syncIntervalCalendars != null || syncIntervalTasks != null) {
prefManageColors.setChecked(settings.getManageCalendarColors());
prefManageColors.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.setManageCalendarColors((Boolean)newValue);
getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
return false;
}
});
} else
prefManageColors.setEnabled(false);
}
@Override
public void onLoaderReset(Loader<AccountSettings> loader) {
}
}
private static class AccountSettingsLoader extends AsyncTaskLoader<AccountSettings> implements SyncStatusObserver {
final Account account;
Object listenerHandle;
public AccountSettingsLoader(Context context, Account account) {
super(context);
this.account = account;
}
@Override
protected void onStartLoading() {
forceLoad();
listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this);
}
@Override
protected void onStopLoading() {
ContentResolver.removeStatusChangeListener(listenerHandle);
}
@Override
public void abandon() {
onStopLoading();
}
@Override
public AccountSettings loadInBackground() {
AccountSettings settings;
try {
settings = new AccountSettings(getContext(), account);
} catch(InvalidAccountException e) {
return null;
}
return settings;
}
@Override
public void onStatusChanged(int which) {
App.log.fine("Reloading account settings");
forceLoad();
}
}
}

View File

@@ -8,11 +8,15 @@
package at.bitfire.davdroid.ui;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.content.SyncStatusObserver;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.NavigationView;
import android.support.design.widget.Snackbar;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
@@ -22,11 +26,23 @@ import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import android.view.View;
import at.bitfire.davdroid.Constants;
import java.util.ServiceLoader;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.ui.setup.LoginActivity;
public class AccountsActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
import static android.content.ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS;
public class AccountsActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, SyncStatusObserver {
private static final ServiceLoader<IAccountsDrawerHandler> serviceLoader = ServiceLoader.load(IAccountsDrawerHandler.class);
private static final IAccountsDrawerHandler accountsDrawerHandler = serviceLoader.iterator().next();
private Snackbar syncStatusSnackbar;
private Object syncStatusObserver;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -62,6 +78,51 @@ public class AccountsActivity extends AppCompatActivity implements NavigationVie
}
}
@Override
protected void onResume() {
super.onResume();
if (BuildConfig.FLAVOR == App.FLAVOR_SOLDUPE) {
AccountManager accountManager = AccountManager.get(this);
Account[] accounts = accountManager.getAccountsByType(getString(R.string.account_type));
FloatingActionButton fab = (FloatingActionButton)findViewById(R.id.fab);
fab.setVisibility(accounts.length > 0 ? View.GONE : View.VISIBLE);
}
onStatusChanged(SYNC_OBSERVER_TYPE_SETTINGS);
syncStatusObserver = ContentResolver.addStatusChangeListener(SYNC_OBSERVER_TYPE_SETTINGS, this);
}
@Override
protected void onPause() {
super.onPause();
if (syncStatusObserver != null) {
ContentResolver.removeStatusChangeListener(syncStatusObserver);
syncStatusObserver = null;
}
}
@Override
public void onStatusChanged(int which) {
if (syncStatusSnackbar != null) {
syncStatusSnackbar.dismiss();
syncStatusSnackbar = null;
}
if (!ContentResolver.getMasterSyncAutomatically()) {
syncStatusSnackbar = Snackbar.make(findViewById(R.id.coordinator), R.string.accounts_global_sync_disabled, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.accounts_global_sync_enable, new View.OnClickListener() {
@Override
public void onClick(View v) {
ContentResolver.setMasterSyncAutomatically(true);
}
});
syncStatusSnackbar.show();
}
}
@Override
public void onBackPressed() {
DrawerLayout drawer = (DrawerLayout)findViewById(R.id.drawer_layout);
@@ -73,33 +134,13 @@ public class AccountsActivity extends AppCompatActivity implements NavigationVie
@Override
public boolean onNavigationItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.nav_about:
startActivity(new Intent(this, AboutActivity.class));
break;
case R.id.nav_app_settings:
startActivity(new Intent(this, AppSettingsActivity.class));
break;
case R.id.nav_twitter:
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://twitter.com/davdroidapp")));
break;
case R.id.nav_website:
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri));
break;
case R.id.nav_faq:
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("faq/").build()));
break;
case R.id.nav_forums:
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("forums/").build()));
break;
case R.id.nav_donate:
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("donate/").build()));
break;
}
boolean processed = accountsDrawerHandler.onNavigationItemSelected(this, item);
DrawerLayout drawer = (DrawerLayout)findViewById(R.id.drawer_layout);
drawer.closeDrawer(GravityCompat.START);
return true;
return processed;
}
}

View File

@@ -12,20 +12,18 @@ import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.preference.EditTextPreference;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.support.v7.preference.SwitchPreferenceCompat;
import java.security.KeyStoreException;
import java.util.Enumeration;
import java.util.logging.Level;
import java.net.URI;
import java.net.URISyntaxException;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.model.Settings;
import de.duenndns.ssl.MemorizingTrustManager;
import lombok.Cleanup;
public class AppSettingsActivity extends AppCompatActivity {
@@ -42,19 +40,102 @@ public class AppSettingsActivity extends AppCompatActivity {
public static class SettingsFragment extends PreferenceFragmentCompat {
Preference prefResetHints,
prefResetCertificates;
SwitchPreferenceCompat prefLogToExternalStorage;
ServiceDB.OpenHelper dbHelper;
Settings settings;
Preference
prefResetHints,
prefResetCertificates;
SwitchPreferenceCompat
prefOverrideProxy,
prefDistrustSystemCerts,
prefLogToExternalStorage;
EditTextPreference
prefProxyHost,
prefProxyPort;
@Override
public void onCreate(Bundle savedInstanceState) {
dbHelper = new ServiceDB.OpenHelper(getContext());
settings = new Settings(dbHelper.getReadableDatabase());
super.onCreate(savedInstanceState);
}
@Override
public void onDestroy() {
super.onDestroy();
dbHelper.close();
}
@Override
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.settings_app);
prefResetHints = findPreference("reset_hints");
prefResetCertificates = findPreference("reset_certificates");
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
Settings settings = new Settings(dbHelper.getReadableDatabase());
prefOverrideProxy = (SwitchPreferenceCompat)findPreference("override_proxy");
prefOverrideProxy.setChecked(settings.getBoolean(App.OVERRIDE_PROXY, false));
prefOverrideProxy.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.putBoolean(App.OVERRIDE_PROXY, (boolean)newValue);
return true;
}
});
prefProxyHost = (EditTextPreference)findPreference("proxy_host");
String proxyHost = settings.getString(App.OVERRIDE_PROXY_HOST, App.OVERRIDE_PROXY_HOST_DEFAULT);
prefProxyHost.setText(proxyHost);
prefProxyHost.setSummary(proxyHost);
prefProxyHost.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
String host = (String)newValue;
try {
URI uri = new URI(null, host, null, null);
} catch(URISyntaxException e) {
Snackbar.make(getView(), e.getLocalizedMessage(), Snackbar.LENGTH_LONG).show();
return false;
}
settings.putString(App.OVERRIDE_PROXY_HOST, host);
prefProxyHost.setSummary(host);
return true;
}
});
prefProxyPort = (EditTextPreference)findPreference("proxy_port");
String proxyPort = settings.getString(App.OVERRIDE_PROXY_PORT, String.valueOf(App.OVERRIDE_PROXY_PORT_DEFAULT));
prefProxyPort.setText(proxyPort);
prefProxyPort.setSummary(proxyPort);
prefProxyPort.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
int port;
try {
port = Integer.parseInt((String)newValue);
} catch(NumberFormatException e) {
port = App.OVERRIDE_PROXY_PORT_DEFAULT;
}
settings.putInt(App.OVERRIDE_PROXY_PORT, port);
prefProxyPort.setText(String.valueOf(port));
prefProxyPort.setSummary(String.valueOf(port));
return true;
}
});
prefDistrustSystemCerts = (SwitchPreferenceCompat)findPreference("distrust_system_certs");
App app = (App)getContext().getApplicationContext();
if (app.getCertManager() == null)
prefDistrustSystemCerts.setVisible(false);
else
prefDistrustSystemCerts.setChecked(settings.getBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, false));
prefResetCertificates = findPreference("reset_certificates");
if (app.getCertManager() == null)
prefResetCertificates.setVisible(false);
prefLogToExternalStorage = (SwitchPreferenceCompat)findPreference("log_to_external_storage");
prefLogToExternalStorage.setChecked(settings.getBoolean(App.LOG_TO_EXTERNAL_STORAGE, false));
}
@@ -63,6 +144,8 @@ public class AppSettingsActivity extends AppCompatActivity {
public boolean onPreferenceTreeClick(Preference preference) {
if (preference == prefResetHints)
resetHints();
else if (preference == prefDistrustSystemCerts)
setDistrustSystemCerts(((SwitchPreferenceCompat)preference).isChecked());
else if (preference == prefResetCertificates)
resetCertificates();
else if (preference == prefLogToExternalStorage)
@@ -73,31 +156,29 @@ public class AppSettingsActivity extends AppCompatActivity {
}
private void resetHints() {
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
Settings settings = new Settings(dbHelper.getWritableDatabase());
settings.remove(StartupDialogFragment.HINT_BATTERY_OPTIMIZATIONS);
settings.remove(StartupDialogFragment.HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED);
settings.remove(StartupDialogFragment.HINT_OPENTASKS_NOT_INSTALLED);
Snackbar.make(getView(), R.string.app_settings_reset_hints_success, Snackbar.LENGTH_LONG).show();
}
private void resetCertificates() {
MemorizingTrustManager mtm = App.getMemorizingTrustManager();
private void setDistrustSystemCerts(boolean distrust) {
settings.putBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, distrust);
int deleted = 0;
Enumeration<String> iterator = mtm.getCertificates();
while (iterator.hasMoreElements())
try {
mtm.deleteCertificate(iterator.nextElement());
deleted++;
} catch (KeyStoreException e) {
App.log.log(Level.SEVERE, "Couldn't distrust certificate", e);
}
Snackbar.make(getView(), getResources().getQuantityString(R.plurals.app_settings_reset_trusted_certificates_success, deleted, deleted), Snackbar.LENGTH_LONG).show();
// re-initialize certificate manager
App app = (App)getContext().getApplicationContext();
app.reinitCertManager();
// reinitialize certificate manager of :sync process
getContext().sendBroadcast(new Intent(App.ReinitSettingsReceiver.ACTION_REINIT_SETTINGS));
}
private void resetCertificates() {
((App)getContext().getApplicationContext()).getCertManager().resetCertificates();
Snackbar.make(getView(), getString(R.string.app_settings_reset_certificates_success), Snackbar.LENGTH_LONG).show();
}
private void setExternalLogging(boolean externalLogging) {
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
Settings settings = new Settings(dbHelper.getWritableDatabase());
settings.putBoolean(App.LOG_TO_EXTERNAL_STORAGE, externalLogging);
// reinitialize logger of default process
@@ -105,7 +186,7 @@ public class AppSettingsActivity extends AppCompatActivity {
app.reinitLogger();
// reinitialize logger of :sync process
getContext().sendBroadcast(new Intent("at.bitfire.davdroid.REINIT_LOGGER"));
getContext().sendBroadcast(new Intent(App.ReinitSettingsReceiver.ACTION_REINIT_SETTINGS));
}
}

View File

@@ -8,8 +8,10 @@
package at.bitfire.davdroid.ui;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.app.LoaderManager;
import android.content.AsyncTaskLoader;
import android.content.ContentResolver;
@@ -17,19 +19,15 @@ import android.content.Context;
import android.content.Intent;
import android.content.Loader;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.support.v4.database.DatabaseUtilsCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
@@ -47,21 +45,22 @@ import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.model.ServiceDB;
import at.bitfire.davdroid.resource.LocalAddressBook;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
public class DebugInfoActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<String> {
public static final String
KEY_EXCEPTION = "exception",
KEY_THROWABLE = "throwable",
KEY_LOGS = "logs",
KEY_ACCOUNT = "account",
KEY_AUTHORITY = "authority",
KEY_PHASE = "phase";
private static final int MAX_INLINE_REPORT_LENGTH = 8000;
KEY_PHASE = "phase",
KEY_LOCAL_RESOURCE = "localResource",
KEY_REMOTE_RESOURCE = "remoteResource";
TextView tvReport;
String report;
@@ -90,31 +89,41 @@ public class DebugInfoActivity extends AppCompatActivity implements LoaderManage
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.setType("text/plain");
sendIntent.putExtra(Intent.EXTRA_SUBJECT, "DAVdroid debug info");
sendIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME + " debug info");
boolean inline = false;
// since Android 4.1, FileProvider permissions are handled in a useful way (using ClipData)
boolean asAttachment = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
if (report.length() > MAX_INLINE_REPORT_LENGTH)
// report is too long for inline text, send it as an attachment
if (asAttachment)
try {
reportFile = File.createTempFile("davdroid-debug", ".txt", getExternalCacheDir());
File debugInfoDir = new File(getCacheDir(), "debug-info");
debugInfoDir.mkdir();
reportFile = new File(debugInfoDir, "debug.txt");
App.log.fine("Writing debug info to " + reportFile.getAbsolutePath());
FileWriter writer = new FileWriter(reportFile);
writer.write(report);
writer.close();
sendIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(reportFile));
} catch (IOException e) {
// let's hope the report is < 1 MB (Android IPC limit)
inline = true;
}
else
inline = true;
sendIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(this, getString(R.string.authority_log_provider), reportFile));
sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (inline)
} catch(IOException e) {
// creating an attachment failed, so send it inline
asAttachment = false;
StringBuilder builder = new StringBuilder();
builder .append("Couldn't write debug info file:\n")
.append(ExceptionUtils.getStackTrace(e))
.append("\n\n")
.append(report);
report = builder.toString();
}
if (!asAttachment)
sendIntent.putExtra(Intent.EXTRA_TEXT, report);
startActivity(sendIntent);
startActivity(Intent.createChooser(sendIntent, null));
}
}
@@ -150,22 +159,27 @@ public class DebugInfoActivity extends AppCompatActivity implements LoaderManage
}
@Override
@SuppressLint("MissingPermission")
public String loadInBackground() {
Exception exception = null;
Throwable throwable = null;
String logs = null,
authority = null;
Account account = null;
int phase = -1;
String localResource = null,
remoteResource = null;
if (extras != null) {
exception = (Exception)extras.getSerializable(KEY_EXCEPTION);
throwable = (Throwable)extras.getSerializable(KEY_THROWABLE);
logs = extras.getString(KEY_LOGS);
account = extras.getParcelable(KEY_ACCOUNT);
authority = extras.getString(KEY_AUTHORITY);
phase = extras.getInt(KEY_PHASE, -1);
localResource = extras.getString(KEY_LOCAL_RESOURCE);
remoteResource = extras.getString(KEY_REMOTE_RESOURCE);
}
StringBuilder report = new StringBuilder();
StringBuilder report = new StringBuilder("--- BEGIN DEBUG INFO ---\n");
// begin with most specific information
@@ -176,57 +190,103 @@ public class DebugInfoActivity extends AppCompatActivity implements LoaderManage
if (authority != null)
report.append("Authority: ").append(authority).append("\n");
if (exception instanceof HttpException) {
HttpException http = (HttpException)exception;
if (http.request != null)
report.append("\nHTTP REQUEST:\n").append(http.request).append("\n\n");
if (http.response != null)
report.append("HTTP RESPONSE:\n").append(http.response).append("\n");
if (throwable instanceof HttpException) {
HttpException http = (HttpException)throwable;
if (http.getRequest() != null)
report.append("\nHTTP REQUEST:\n").append(http.getRequest()).append("\n\n");
if (http.getResponse() != null)
report.append("HTTP RESPONSE:\n").append(http.getResponse()).append("\n");
}
if (exception != null)
if (localResource != null)
report.append("\nCURRENT LOCAL RESOURCE:\n")
.append(localResource)
.append("\n");
if (remoteResource != null)
report.append("\nCURRENT REMOTE RESOURCE:\n")
.append(remoteResource)
.append("\n");
if (throwable != null)
report .append("\nEXCEPTION:\n")
.append(ExceptionUtils.getStackTrace(exception));
.append(ExceptionUtils.getStackTrace(throwable));
if (logs != null)
report.append("\nLOGS:\n").append(logs).append("\n");
final Context context = getContext();
try {
PackageManager pm = getContext().getPackageManager();
PackageManager pm = context.getPackageManager();
String installedFrom = pm.getInstallerPackageName(BuildConfig.APPLICATION_ID);
if (TextUtils.isEmpty(installedFrom))
installedFrom = "APK (directly)";
boolean workaroundInstalled = false;
try {
workaroundInstalled = pm.getPackageInfo("at.bitfire.davdroid.jbworkaround", 0) != null;
workaroundInstalled = pm.getPackageInfo(BuildConfig.APPLICATION_ID + ".jbworkaround", 0) != null;
} catch(PackageManager.NameNotFoundException ignored) {}
report.append("\nSOFTWARE INFORMATION\n" +
"DAVdroid version: ").append(BuildConfig.VERSION_NAME).append(" (").append(BuildConfig.VERSION_CODE).append(") ").append(new Date(BuildConfig.buildTime)).append("\n")
.append("Installed from: ").append(installedFrom).append("\n")
.append("JB Workaround installed: ").append(workaroundInstalled ? "yes" : "no").append("\n\n");
"Package: ").append(BuildConfig.APPLICATION_ID).append("\n" +
"Version: ").append(BuildConfig.VERSION_NAME).append(" (").append(BuildConfig.VERSION_CODE).append(") ").append(new Date(BuildConfig.buildTime)).append("\n")
.append("Installed from: ").append(installedFrom).append("\n")
.append("JB Workaround installed: ").append(workaroundInstalled ? "yes" : "no").append("\n\n");
} catch(Exception ex) {
App.log.log(Level.SEVERE, "Couldn't get software information", ex);
}
report.append(
"CONFIGURATION\n" +
"System-wide synchronization: ").append(ContentResolver.getMasterSyncAutomatically() ? "automatically" : "manually").append("\n");
AccountManager accountManager = AccountManager.get(getContext());
for (Account acct : accountManager.getAccountsByType(Constants.ACCOUNT_TYPE))
report.append("CONFIGURATION\n");
// power saving
PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
if (powerManager != null && Build.VERSION.SDK_INT >= 23)
report.append("Power saving disabled: ")
.append(powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) ? "yes" : "no")
.append("\n");
// permissions
for (String permission : new String[] { Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR,
PermissionsActivity.PERMISSION_READ_TASKS, PermissionsActivity.PERMISSION_WRITE_TASKS })
report.append(permission).append(" permission: ")
.append(ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED ? "granted" : "denied")
.append("\n");
// system-wide sync settings
report.append("System-wide synchronization: ")
.append(ContentResolver.getMasterSyncAutomatically() ? "automatically" : "manually")
.append("\n");
// main accounts
AccountManager accountManager = AccountManager.get(context);
for (Account acct : accountManager.getAccountsByType(context.getString(R.string.account_type)))
try {
AccountSettings settings = new AccountSettings(getContext(), acct);
report.append(
"Account: ").append(acct.name).append("\n" +
" Address book sync. interval: ").append(syncStatus(settings, ContactsContract.AUTHORITY)).append("\n" +
" Calendar sync. interval: ").append(syncStatus(settings, CalendarContract.AUTHORITY)).append("\n" +
" OpenTasks sync. interval: ").append(syncStatus(settings, "org.dmfs.tasks")).append("\n");
AccountSettings settings = new AccountSettings(context, acct);
report.append("Account: ").append(acct.name).append("\n" +
" Address book sync. interval: ").append(syncStatus(settings, context.getString(R.string.address_books_authority))).append("\n" +
" Calendar sync. interval: ").append(syncStatus(settings, CalendarContract.AUTHORITY)).append("\n" +
" OpenTasks sync. interval: ").append(syncStatus(settings, "org.dmfs.tasks")).append("\n" +
" WiFi only: ").append(settings.getSyncWifiOnly());
if (settings.getSyncWifiOnlySSID() != null)
report.append(", SSID: ").append(settings.getSyncWifiOnlySSID());
report.append("\n [CardDAV] Contact group method: ").append(settings.getGroupMethod())
.append("\n [CalDAV] Time range (past days): ").append(settings.getTimeRangePastDays())
.append("\n Manage calendar colors: ").append(settings.getManageCalendarColors())
.append("\n");
} catch(InvalidAccountException e) {
report.append(acct).append(" is invalid (unsupported settings version) or does not exist\n");
}
// address book accounts
for (Account acct : accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)))
try {
LocalAddressBook addressBook = new LocalAddressBook(context, acct, null);
report.append("Address book account: ").append(acct.name).append("\n" +
" Main account: ").append(addressBook.getMainAccount()).append("\n" +
" URL: ").append(addressBook.getURL()).append("\n" +
" Sync automatically: ").append(ContentResolver.getSyncAutomatically(acct, ContactsContract.AUTHORITY)).append("\n");
} catch(ContactsStorageException e) {
report.append(acct).append(" is invalid: ").append(e.getMessage()).append("\n");
}
report.append("\n");
report.append("SQLITE DUMP\n");
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(context);
dbHelper.dump(report);
report.append("\n");
@@ -236,10 +296,11 @@ public class DebugInfoActivity extends AppCompatActivity implements LoaderManage
"Android version: ").append(Build.VERSION.RELEASE).append(" (").append(Build.DISPLAY).append(")\n" +
"Device: ").append(WordUtils.capitalize(Build.MANUFACTURER)).append(" ").append(Build.MODEL).append(" (").append(Build.DEVICE).append(")\n\n"
);
} catch (Exception ex) {
} catch(Exception ex) {
App.log.log(Level.SEVERE, "Couldn't get system details", ex);
}
report.append("--- END DEBUG INFO ---\n");
return report.toString();
}

View File

@@ -57,7 +57,7 @@ public class ExceptionInfoFragment extends DialogFragment {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(getContext(), DebugInfoActivity.class);
intent.putExtra(DebugInfoActivity.KEY_EXCEPTION, exception);
intent.putExtra(DebugInfoActivity.KEY_THROWABLE, exception);
if (account != null)
intent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account);
startActivity(intent);

View File

@@ -0,0 +1,11 @@
package at.bitfire.davdroid.ui;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.view.MenuItem;
public interface IAccountsDrawerHandler {
public boolean onNavigationItemSelected(@NonNull Activity activity, MenuItem item);
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright © 2013 2016 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.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.LocalTaskList;
public class PermissionsActivity extends AppCompatActivity {
public static final String
PERMISSION_READ_TASKS = "org.dmfs.permission.READ_TASKS",
PERMISSION_WRITE_TASKS = "org.dmfs.permission.WRITE_TASKS";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_permissions);
}
@Override
protected void onResume() {
super.onResume();
refresh();
}
protected void refresh() {
boolean noCalendarPermissions =
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_CALENDAR) != PackageManager.PERMISSION_GRANTED;
findViewById(R.id.calendar_permissions).setVisibility(noCalendarPermissions ? View.VISIBLE : View.GONE);
boolean noContactsPermissions =
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED;
findViewById(R.id.contacts_permissions).setVisibility(noContactsPermissions ? View.VISIBLE : View.GONE);
boolean noTaskPermissions;
if (LocalTaskList.tasksProviderAvailable(this)) {
noTaskPermissions =
ActivityCompat.checkSelfPermission(this, PERMISSION_READ_TASKS) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, PERMISSION_WRITE_TASKS) != PackageManager.PERMISSION_GRANTED;
findViewById(R.id.opentasks_permissions).setVisibility(noTaskPermissions ? View.VISIBLE : View.GONE);
} else {
findViewById(R.id.opentasks_permissions).setVisibility(View.GONE);
noTaskPermissions = false;
}
if (!noCalendarPermissions && !noContactsPermissions && !noTaskPermissions) {
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
nm.cancel(Constants.NOTIFICATION_PERMISSIONS);
finish();
}
}
public void requestCalendarPermissions(View v) {
ActivityCompat.requestPermissions(this, new String[] {
Manifest.permission.READ_CALENDAR,
Manifest.permission.WRITE_CALENDAR
}, 0);
}
public void requestContactsPermissions(View v) {
ActivityCompat.requestPermissions(this, new String[] {
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_CONTACTS
}, 0);
}
public void requestOpenTasksPermissions(View v) {
ActivityCompat.requestPermissions(this, new String[] {
PERMISSION_READ_TASKS,
PERMISSION_WRITE_TASKS
}, 0);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
refresh();
}
}

View File

@@ -8,15 +8,19 @@
package at.bitfire.davdroid.ui;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog;
@@ -37,14 +41,15 @@ import lombok.Cleanup;
public class StartupDialogFragment extends DialogFragment {
public static final String
HINT_BATTERY_OPTIMIZATIONS = "hint_BatteryOptimizations",
HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED = "hint_GooglePlayAccountsRemoved",
HINT_OPENTASKS_NOT_INSTALLED = "hint_OpenTasksNotInstalled";
private static final String ARGS_MODE = "mode";
enum Mode {
BATTERY_OPTIMIZATIONS,
DEVELOPMENT_VERSION,
FDROID_DONATE,
GOOGLE_PLAY_ACCOUNTS_REMOVED,
OPENTASKS_NOT_INSTALLED
}
@@ -59,19 +64,23 @@ public class StartupDialogFragment extends DialogFragment {
dialogs.add(StartupDialogFragment.instantiate(Mode.DEVELOPMENT_VERSION));
else {
// store-specific information
final String installedFrom = installedFrom(context);
if (installedFrom == null || installedFrom.startsWith("org.fdroid"))
dialogs.add(StartupDialogFragment.instantiate(Mode.FDROID_DONATE));
else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && // only on Android <5
"com.android.vending".equals(installedFrom) && // only when installed from Play Store
if (BuildConfig.FLAVOR == App.FLAVOR_GOOGLE_PLAY) {
// Play store
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && // only on Android <5
settings.getBoolean(HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED, true)) // and only when "Don't show again" hasn't been clicked yet
dialogs.add(StartupDialogFragment.instantiate(Mode.GOOGLE_PLAY_ACCOUNTS_REMOVED));
dialogs.add(StartupDialogFragment.instantiate(Mode.GOOGLE_PLAY_ACCOUNTS_REMOVED));
}
}
// battery optimization whitelisting
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && settings.getBoolean(HINT_BATTERY_OPTIMIZATIONS, true)) {
PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
if (powerManager != null && !powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID))
dialogs.add(StartupDialogFragment.instantiate(Mode.BATTERY_OPTIMIZATIONS));
}
// OpenTasks information
if (!LocalTaskList.tasksProviderAvailable(context.getContentResolver()) &&
settings.getBoolean(HINT_OPENTASKS_NOT_INSTALLED, true))
if (!LocalTaskList.tasksProviderAvailable(context) && settings.getBoolean(HINT_OPENTASKS_NOT_INSTALLED, true))
dialogs.add(StartupDialogFragment.instantiate(Mode.OPENTASKS_NOT_INSTALLED));
Collections.reverse(dialogs);
@@ -88,16 +97,44 @@ public class StartupDialogFragment extends DialogFragment {
@NonNull
@Override
@TargetApi(Build.VERSION_CODES.M)
@SuppressLint("BatteryLife")
public Dialog onCreateDialog(Bundle savedInstanceState) {
setCancelable(false);
final ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
Mode mode = Mode.valueOf(getArguments().getString(ARGS_MODE));
switch (mode) {
case BATTERY_OPTIMIZATIONS:
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.startup_battery_optimization)
.setMessage(R.string.startup_battery_optimization_message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.setNeutralButton(R.string.startup_battery_optimization_disable, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:" + BuildConfig.APPLICATION_ID));
if (intent.resolveActivity(getContext().getPackageManager()) != null)
getContext().startActivity(intent);
}
})
.setNegativeButton(R.string.startup_dont_show_again, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
Settings settings = new Settings(dbHelper.getWritableDatabase());
settings.putBoolean(HINT_BATTERY_OPTIMIZATIONS, false);
}
})
.create();
case DEVELOPMENT_VERSION:
return new AlertDialog.Builder(getActivity())
.setIcon(R.drawable.ic_launcher)
.setIcon(R.mipmap.ic_launcher)
.setTitle(R.string.startup_development_version)
.setMessage(R.string.startup_development_version_message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@@ -108,25 +145,7 @@ public class StartupDialogFragment extends DialogFragment {
.setNeutralButton(R.string.startup_development_version_give_feedback, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("forums/beta/").build()));
}
})
.create();
case FDROID_DONATE:
return new AlertDialog.Builder(getActivity())
.setIcon(R.drawable.ic_launcher)
.setTitle(R.string.startup_donate)
.setMessage(R.string.startup_donate_message)
.setPositiveButton(R.string.startup_donate_now, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("donate/").build()));
}
})
.setNegativeButton(R.string.startup_donate_later, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://forums.bitfire.at/category/9/beta-test-discussion")));
}
})
.create();
@@ -150,13 +169,14 @@ public class StartupDialogFragment extends DialogFragment {
.setNeutralButton(R.string.startup_google_play_accounts_removed_more_info, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("faq/").build());
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.navigation_drawer_faq_url)));
getContext().startActivity(intent);
}
})
.setNegativeButton(R.string.startup_dont_show_again, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
Settings settings = new Settings(dbHelper.getWritableDatabase());
settings.putBoolean(HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED, false);
}
@@ -164,10 +184,13 @@ public class StartupDialogFragment extends DialogFragment {
.create();
case OPENTASKS_NOT_INSTALLED:
StringBuilder builder = new StringBuilder(getString(R.string.startup_opentasks_not_installed_message));
if (Build.VERSION.SDK_INT < 23)
builder.append("\n\n").append(getString(R.string.startup_opentasks_reinstall_davdroid));
return new AlertDialog.Builder(getActivity())
.setIcon(R.drawable.ic_alarm_on_dark)
.setTitle(R.string.startup_opentasks_not_installed)
.setMessage(R.string.startup_opentasks_not_installed_message)
.setMessage(builder.toString())
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
@@ -177,12 +200,16 @@ public class StartupDialogFragment extends DialogFragment {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=org.dmfs.tasks"));
getContext().startActivity(intent);
if (intent.resolveActivity(getContext().getPackageManager()) != null)
getContext().startActivity(intent);
else
App.log.warning("No market app available, can't install OpenTasks");
}
})
.setNegativeButton(R.string.startup_dont_show_again, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
Settings settings = new Settings(dbHelper.getWritableDatabase());
settings.putBoolean(HINT_OPENTASKS_NOT_INSTALLED, false);
}

View File

@@ -10,6 +10,7 @@ package at.bitfire.davdroid.ui.setup;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
@@ -24,14 +25,17 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import java.net.URI;
import java.util.logging.Level;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.DavService;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.davdroid.model.ServiceDB.Collections;
@@ -40,12 +44,16 @@ import at.bitfire.davdroid.model.ServiceDB.OpenHelper;
import at.bitfire.davdroid.model.ServiceDB.Services;
import at.bitfire.davdroid.resource.LocalTaskList;
import at.bitfire.ical4android.TaskProvider;
import at.bitfire.vcard4android.GroupMethod;
import lombok.Cleanup;
public class AccountDetailsFragment extends Fragment {
private static final String KEY_CONFIG = "config";
Spinner spnrGroupMethod;
public static AccountDetailsFragment newInstance(DavResourceFinder.Configuration config) {
AccountDetailsFragment frag = new AccountDetailsFragment();
Bundle args = new Bundle(1);
@@ -54,6 +62,7 @@ public class AccountDetailsFragment extends Fragment {
return frag;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.login_account_details, container, false);
@@ -69,7 +78,13 @@ public class AccountDetailsFragment extends Fragment {
DavResourceFinder.Configuration config = (DavResourceFinder.Configuration)getArguments().getSerializable(KEY_CONFIG);
final EditText editName = (EditText)v.findViewById(R.id.account_name);
editName.setText(config.userName);
editName.setText((config.calDAV != null && config.calDAV.email != null) ? config.calDAV.email : config.userName);
// CardDAV-specific
v.findViewById(R.id.carddav).setVisibility(config.cardDAV != null ? View.VISIBLE : View.GONE);
spnrGroupMethod = (Spinner)v.findViewById(R.id.contact_group_method);
if (BuildConfig.settingContactGroupMethod != null)
spnrGroupMethod.setEnabled(false);
Button btnCreate = (Button)v.findViewById(R.id.create_account);
btnCreate.setOnClickListener(new View.OnClickListener() {
@@ -79,9 +94,10 @@ public class AccountDetailsFragment extends Fragment {
if (name.isEmpty())
editName.setError(getString(R.string.login_account_name_required));
else {
if (createAccount(name, (DavResourceFinder.Configuration)getArguments().getSerializable(KEY_CONFIG)))
if (createAccount(name, (DavResourceFinder.Configuration)getArguments().getSerializable(KEY_CONFIG))) {
getActivity().setResult(Activity.RESULT_OK);
getActivity().finish();
else
} else
Snackbar.make(v, R.string.login_account_not_created, Snackbar.LENGTH_LONG).show();
}
}
@@ -91,10 +107,10 @@ public class AccountDetailsFragment extends Fragment {
}
protected boolean createAccount(String accountName, DavResourceFinder.Configuration config) {
Account account = new Account(accountName, Constants.ACCOUNT_TYPE);
Account account = new Account(accountName, getString(R.string.account_type));
// create Android account
Bundle userData = AccountSettings.initialUserData(config.userName, config.preemptive);
Bundle userData = AccountSettings.initialUserData(config.userName);
App.log.log(Level.INFO, "Creating Android account with initial config", new Object[] { account, userData });
AccountManager accountManager = AccountManager.get(getContext());
@@ -105,45 +121,54 @@ public class AccountDetailsFragment extends Fragment {
App.log.log(Level.INFO, "Writing account configuration to database", config);
@Cleanup OpenHelper dbHelper = new OpenHelper(getContext());
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransactionNonExclusive();
try {
AccountSettings settings = new AccountSettings(getContext(), account);
Intent refreshIntent = new Intent(getActivity(), DavService.class);
refreshIntent.setAction(DavService.ACTION_REFRESH_COLLECTIONS);
if (config.cardDAV != null) {
// insert CardDAV service
long id = insertService(db, accountName, Services.SERVICE_CARDDAV, config.cardDAV);
// start CardDAV service detection (refresh collections)
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id);
getActivity().startService(refreshIntent);
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
// initial CardDAV account settings
if (BuildConfig.settingContactGroupMethod == null) {
int idx = spnrGroupMethod.getSelectedItemPosition();
String groupMethodName = getResources().getStringArray(R.array.settings_contact_group_method_values)[idx];
settings.setGroupMethod(GroupMethod.valueOf(groupMethodName));
}
// contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
settings.setSyncInterval(App.getAddressBooksAuthority(), Constants.DEFAULT_SYNC_INTERVAL);
} else
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0);
ContentResolver.setIsSyncable(account, App.getAddressBooksAuthority(), 0);
if (config.calDAV != null) {
// insert CalDAV service
long id = insertService(db, accountName, Services.SERVICE_CALDAV, config.calDAV);
// start CalDAV service detection (refresh collections)
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id);
getActivity().startService(refreshIntent);
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY, true);
// calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
settings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL);
if (LocalTaskList.tasksProviderAvailable(getContext().getContentResolver())) {
// will only do something if OpenTasks is installed and accessible
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 1);
ContentResolver.setSyncAutomatically(account, TaskProvider.ProviderName.OpenTasks.authority, true);
} else
// If OpenTasks is installed after DAVdroid, DAVdroid won't get task permissions and crash at every task sync
// unless we disable task sync here (before OpenTasks is available).
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 0);
} else {
// enable task sync if OpenTasks is installed
// further changes will be handled by PackageChangedReceiver
if (LocalTaskList.tasksProviderAvailable(getContext())) {
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.getAuthority(), 1);
settings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.getAuthority(), Constants.DEFAULT_SYNC_INTERVAL);
}
} else
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0);
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 0);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
} catch(InvalidAccountException e) {
App.log.log(Level.SEVERE, "Couldn't access account settings", e);
}
return true;
@@ -157,21 +182,21 @@ public class AccountDetailsFragment extends Fragment {
values.put(Services.SERVICE, service);
if (info.principal != null)
values.put(Services.PRINCIPAL, info.principal.toString());
long serviceID = db.insertOrThrow(Services._TABLE, null, values);
long serviceID = db.insertWithOnConflict(Services._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
// insert home sets
for (URI homeSet : info.homeSets) {
values.clear();
values.put(HomeSets.SERVICE_ID, serviceID);
values.put(HomeSets.URL, homeSet.toString());
db.insertOrThrow(HomeSets._TABLE, null, values);
db.insertWithOnConflict(HomeSets._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
// insert collections
for (CollectionInfo collection : info.collections.values()) {
values = collection.toDB();
values.put(Collections.SERVICE_ID, serviceID);
db.insertOrThrow(Collections._TABLE, null, values);
db.insertWithOnConflict(Collections._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
return serviceID;

View File

@@ -19,6 +19,7 @@ import org.xbill.DNS.Type;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
@@ -28,7 +29,9 @@ import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import at.bitfire.dav4android.Constants;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.Property;
import at.bitfire.dav4android.UrlUtils;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
@@ -39,6 +42,7 @@ import at.bitfire.dav4android.property.CalendarColor;
import at.bitfire.dav4android.property.CalendarDescription;
import at.bitfire.dav4android.property.CalendarHomeSet;
import at.bitfire.dav4android.property.CalendarTimezone;
import at.bitfire.dav4android.property.CalendarUserAddressSet;
import at.bitfire.dav4android.property.CurrentUserPrincipal;
import at.bitfire.dav4android.property.CurrentUserPrivilegeSet;
import at.bitfire.dav4android.property.DisplayName;
@@ -47,6 +51,7 @@ import at.bitfire.dav4android.property.SupportedCalendarComponentSet;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.log.StringHandler;
import at.bitfire.davdroid.model.CollectionInfo;
import kotlin.Pair;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import okhttp3.HttpUrl;
@@ -77,8 +82,8 @@ public class DavResourceFinder {
log.setLevel(Level.FINEST);
log.addHandler(logBuffer);
httpClient = HttpClient.create(log);
httpClient = HttpClient.addAuthentication(httpClient, credentials.userName, credentials.password, credentials.authPreemptive);
httpClient = HttpClient.create(context, log);
httpClient = HttpClient.addAuthentication(httpClient, credentials.userName, credentials.password);
}
@@ -88,7 +93,7 @@ public class DavResourceFinder {
calDavConfig = findInitialConfiguration(Service.CALDAV);
return new Configuration(
credentials.userName, credentials.password, credentials.authPreemptive,
credentials.userName, credentials.password,
cardDavConfig, calDavConfig,
logBuffer.toString()
);
@@ -140,6 +145,26 @@ public class DavResourceFinder {
}
}
if (config.principal != null && service == Service.CALDAV) {
// query email address (CalDAV scheduling: calendar-user-address-set)
DavResource davPrincipal = new DavResource(httpClient, HttpUrl.get(config.principal), log);
try {
davPrincipal.propfind(0, CalendarUserAddressSet.NAME);
CalendarUserAddressSet addressSet = (CalendarUserAddressSet)davPrincipal.getProperties().get(CalendarUserAddressSet.NAME);
if (addressSet != null)
for (String href : addressSet.getHrefs())
try {
URI uri = new URI(href);
if ("mailto".equals(uri.getScheme()))
config.email = uri.getSchemeSpecificPart();
} catch(URISyntaxException e) {
Constants.log.log(Level.WARNING, "Unparseable user address", e);
}
} catch(IOException | HttpException | DavException e) {
Constants.log.log(Level.WARNING, "Couldn't query user email address", e);
}
}
// return config or null if config doesn't contain useful information
boolean serviceAvailable = config.principal != null || !config.homeSets.isEmpty() || !config.collections.isEmpty();
return serviceAvailable ? config : null;
@@ -170,15 +195,20 @@ public class DavResourceFinder {
}
// check for current-user-principal
CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal)davBase.properties.get(CurrentUserPrincipal.NAME);
if (currentUserPrincipal != null && currentUserPrincipal.href != null)
principal = davBase.location.resolve(currentUserPrincipal.href);
Pair<DavResource, Property> result1 = davBase.findProperty(CurrentUserPrincipal.NAME);
if (result1 != null) {
CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal)result1.getSecond();
if (currentUserPrincipal.getHref() != null)
principal = result1.getFirst().getLocation().resolve(currentUserPrincipal.getHref());
}
// check for resource type "principal"
if (principal == null) {
ResourceType resourceType = (ResourceType)davBase.properties.get(ResourceType.NAME);
if (resourceType != null && resourceType.types.contains(ResourceType.PRINCIPAL))
principal = davBase.location;
for (Pair<DavResource, Property> result2 : davBase.findProperties(ResourceType.NAME)) {
ResourceType resourceType = (ResourceType)result2.getSecond();
if (resourceType != null && resourceType.getTypes().contains(ResourceType.PRINCIPAL))
principal = result2.getFirst().getLocation();
}
}
// If a principal has been detected successfully, ensure that it provides the required service.
@@ -190,46 +220,67 @@ public class DavResourceFinder {
}
}
/**
* If #dav references an address book or an address book home set, it will added to
* config.collections or config.homesets. Only evaluates already known properties,
* does not call dav.propfind()! URLs will be stored with trailing "/".
* @param dav resource whose properties are evaluated
* @param config structure where the address book (collection) and/or home set is stored into (if found)
*/
protected void rememberIfAddressBookOrHomeset(@NonNull DavResource dav, @NonNull Configuration.ServiceInfo config) {
// Is the collection an address book?
ResourceType resourceType = (ResourceType)dav.properties.get(ResourceType.NAME);
if (resourceType != null && resourceType.types.contains(ResourceType.ADDRESSBOOK)) {
dav.location = UrlUtils.withTrailingSlash(dav.location);
log.info("Found address book at " + dav.location);
config.collections.put(dav.location.uri(), CollectionInfo.fromDavResource(dav));
// Is there an address book?
for (Pair<DavResource, Property> result : dav.findProperties(ResourceType.NAME)) {
ResourceType resourceType = (ResourceType)result.getSecond();
if (resourceType.getTypes().contains(ResourceType.ADDRESSBOOK)) {
DavResource addressBook = result.getFirst();
addressBook.setLocation(UrlUtils.withTrailingSlash(addressBook.getLocation()));
log.info("Found address book at " + addressBook.getLocation());
config.collections.put(addressBook.getLocation().uri(), CollectionInfo.fromDavResource(addressBook));
}
}
// Does the collection refer to address book homesets?
AddressbookHomeSet homeSets = (AddressbookHomeSet)dav.properties.get(AddressbookHomeSet.NAME);
if (homeSets != null)
for (String href : homeSets.hrefs)
config.homeSets.add(UrlUtils.withTrailingSlash(dav.location.resolve(href)).uri());
// Is there an addressbook-home-set?
for (Pair<DavResource, Property> result : dav.findProperties(AddressbookHomeSet.NAME)) {
AddressbookHomeSet homeSet = (AddressbookHomeSet)result.getSecond();
for (String href : homeSet.getHrefs()) {
HttpUrl location = UrlUtils.withTrailingSlash(result.getFirst().getLocation().resolve(href));
log.info("Found address book home-set at " + location);
config.homeSets.add(location.uri());
}
}
}
protected void rememberIfCalendarOrHomeset(@NonNull DavResource dav, @NonNull Configuration.ServiceInfo config) {
// Is the collection a calendar collection?
ResourceType resourceType = (ResourceType)dav.properties.get(ResourceType.NAME);
if (resourceType != null && resourceType.types.contains(ResourceType.CALENDAR)) {
dav.location = UrlUtils.withTrailingSlash(dav.location);
log.info("Found calendar collection at " + dav.location);
config.collections.put(dav.location.uri(), CollectionInfo.fromDavResource(dav));
for (Pair<DavResource, Property> result : dav.findProperties(ResourceType.NAME)) {
ResourceType resourceType = (ResourceType)result.getSecond();
if (resourceType.getTypes().contains(ResourceType.CALENDAR)) {
DavResource calendar = result.getFirst();
calendar.setLocation(UrlUtils.withTrailingSlash(calendar.getLocation()));
log.info("Found calendar at " + calendar.getLocation());
config.collections.put(calendar.getLocation().uri(), CollectionInfo.fromDavResource(calendar));
}
}
// Does the collection refer to calendar homesets?
CalendarHomeSet homeSets = (CalendarHomeSet)dav.properties.get(CalendarHomeSet.NAME);
if (homeSets != null)
for (String href : homeSets.hrefs)
config.homeSets.add(UrlUtils.withTrailingSlash(dav.location.resolve(href)).uri());
// Is there an calendar-home-set?
for (Pair<DavResource, Property> result : dav.findProperties(CalendarHomeSet.NAME)) {
CalendarHomeSet homeSet = (CalendarHomeSet)result.getSecond();
for (String href : homeSet.getHrefs()) {
HttpUrl location = UrlUtils.withTrailingSlash(result.getFirst().getLocation().resolve(href));
log.info("Found calendar home-set at " + location);
config.homeSets.add(location.uri());
}
}
}
boolean providesService(HttpUrl url, Service service) throws IOException {
protected boolean providesService(HttpUrl url, Service service) throws IOException {
DavResource davPrincipal = new DavResource(httpClient, url, log);
try {
davPrincipal.options();
if ((service == Service.CARDDAV && davPrincipal.capabilities.contains("addressbook")) ||
(service == Service.CALDAV && davPrincipal.capabilities.contains("calendar-access")))
if ((service == Service.CARDDAV && davPrincipal.getCapabilities().contains("addressbook")) ||
(service == Service.CALDAV && davPrincipal.getCapabilities().contains("calendar-access")))
return true;
} catch (HttpException|DavException e) {
@@ -302,7 +353,7 @@ public class DavResourceFinder {
if (principal != null)
return principal;
} catch(NotFoundException e) {
} catch(NotFoundException|IllegalArgumentException e) {
log.log(Level.WARNING, "No resource found", e);
}
return null;
@@ -318,19 +369,22 @@ public class DavResourceFinder {
DavResource dav = new DavResource(httpClient, url, log);
dav.propfind(0, CurrentUserPrincipal.NAME);
CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal)dav.properties.get(CurrentUserPrincipal.NAME);
if (currentUserPrincipal != null && currentUserPrincipal.href != null) {
HttpUrl principal = dav.location.resolve(currentUserPrincipal.href);
if (principal != null) {
log.info("Found current-user-principal: " + principal);
Pair<DavResource, Property> result = dav.findProperty(CurrentUserPrincipal.NAME);
if (result != null) {
CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal)result.getSecond();
if (currentUserPrincipal.getHref() != null) {
HttpUrl principal = result.getFirst().getLocation().resolve(currentUserPrincipal.getHref());
if (principal != null) {
log.info("Found current-user-principal: " + principal);
// service check
if (service != null && !providesService(principal, service)) {
log.info(principal + " doesn't provide required " + service + " service");
principal = null;
// service check
if (service != null && !providesService(principal, service)) {
log.info(principal + " doesn't provide required " + service + " service");
principal = null;
}
return principal != null ? principal.uri() : null;
}
return principal != null ? principal.uri() : null;
}
}
return null;
@@ -358,10 +412,11 @@ public class DavResourceFinder {
public URI principal;
public final Set<URI> homeSets = new HashSet<>();
public final Map<URI, CollectionInfo> collections = new HashMap<>();
public String email;
}
public final String userName, password;
public final boolean preemptive;
public final ServiceInfo cardDAV;
public final ServiceInfo calDAV;

View File

@@ -22,7 +22,6 @@ import android.support.v4.content.Loader;
import android.support.v7.app.AlertDialog;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.ui.DebugInfoActivity;
import at.bitfire.davdroid.ui.setup.DavResourceFinder.Configuration;

View File

@@ -0,0 +1,9 @@
package at.bitfire.davdroid.ui.setup;
import android.support.v4.app.Fragment;
public interface ILoginCredentialsFragment {
Fragment getFragment();
}

View File

@@ -9,28 +9,76 @@
package at.bitfire.davdroid.ui.setup;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
import java.util.ServiceLoader;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
/**
* Activity to initially connect to a server and create an account.
* Fields for server/user data can be pre-filled with extras in the Intent.
*/
public class LoginActivity extends AppCompatActivity {
/**
* When set, "login by URL" will be activated by default, and the URL field will be set to this value.
* When not set, "login by email" will be activated by default.
*/
public static final String EXTRA_URL = "url";
/**
* When set, and {@link #EXTRA_PASSWORD} is set too, the user name field will be set to this value.
* When set, and {@link #EXTRA_URL} is not set, the email address field will be set to this value.
*/
public static final String EXTRA_USERNAME = "username";
/**
* When set, the password field will be set to this value.
*/
public static final String EXTRA_PASSWORD = "password";
private static final ServiceLoader<ILoginCredentialsFragment> loginFragmentLoader = ServiceLoader.load(ILoginCredentialsFragment.class);
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null)
// first call, add fragment
getSupportFragmentManager().beginTransaction()
.replace(android.R.id.content, new LoginCredentialsFragment())
.commit();
for (ILoginCredentialsFragment fragment : loginFragmentLoader)
getSupportFragmentManager().beginTransaction()
.replace(android.R.id.content, fragment.getFragment())
.commit();
}
@Override
protected void onResume() {
super.onResume();
App app = (App)getApplicationContext();
if (app.getCertManager() != null)
app.getCertManager().appInForeground = true;
}
@Override
protected void onPause() {
super.onPause();
App app = (App)getApplicationContext();
if (app.getCertManager() != null)
app.getCertManager().appInForeground = false;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_login, menu);
@@ -38,6 +86,6 @@ public class LoginActivity extends AppCompatActivity {
}
public void showHelp(MenuItem item) {
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("configuration/").build()));
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.login_help_url))));
}
}

View File

@@ -19,7 +19,6 @@ import lombok.RequiredArgsConstructor;
public class LoginCredentials implements Parcelable {
public final URI uri;
public final String userName, password;
public final boolean authPreemptive;
@Override
public int describeContents() {
@@ -31,7 +30,6 @@ public class LoginCredentials implements Parcelable {
dest.writeSerializable(uri);
dest.writeString(userName);
dest.writeString(password);
dest.writeValue(authPreemptive);
}
public static final Creator CREATOR = new Creator<LoginCredentials>() {
@@ -39,8 +37,7 @@ public class LoginCredentials implements Parcelable {
public LoginCredentials createFromParcel(Parcel source) {
return new LoginCredentials(
(URI)source.readSerializable(),
source.readString(), source.readString(),
(boolean)source.readValue(null)
source.readString(), source.readString()
);
}

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