Compare commits

...

121 Commits
v1.10 ... v2.0

Author SHA1 Message Date
Ricki Hirner
ce56047da3 Version bump to 2.0 2018-07-30 10:15:59 +02:00
Ricki Hirner
3186b89874 Fetch translations from Transifex 2018-07-28 14:34:09 +02:00
Ricki Hirner
1b0557d4b8 Version bump to 2.0-rc1 2018-07-28 14:29:32 +02:00
Ricki Hirner
5efe1dbed0 Vector drawable: fix Android 4.4 compatibility 2018-07-28 14:29:01 +02:00
Ricki Hirner
d5f1074e30 Fetch translations from Transifex 2018-07-27 16:52:57 +02:00
Ricki Hirner
5e32dc7c91 Fix/improve error handling; bump version to 1.12-beta4 2018-07-27 16:49:42 +02:00
Ricki Hirner
1914269811 Version bump to 1.12-beta3 2018-07-22 11:22:34 +02:00
Ricki Hirner
f1d92b0bfe Fetch translations from Transifex 2018-07-22 11:18:50 +02:00
Ricki Hirner
3c76eb79d6 Use Apache Commons ContextedException instead of own class 2018-07-22 11:15:53 +02:00
Ricki Hirner
86e9cb4ace Rewrite startup strings 2018-07-21 11:07:34 +02:00
Ricki Hirner
3c6ff6e6ac Managed: uses managed profiles feature 2018-07-20 11:47:10 +02:00
Ricki Hirner
1082dc367d Version bump to 1.12-beta2 2018-07-17 13:33:15 +02:00
Ricki Hirner
6e935c433c Collection info: allow copying URL to clipboard 2018-07-17 13:23:59 +02:00
Ricki Hirner
4536378ac9 Collection info fragment 2018-07-17 12:47:46 +02:00
Ricki Hirner
042fb6fefa Remove Espresso tests for now 2018-07-17 12:45:45 +02:00
Ricki Hirner
29faa422ba Account activity: show "Select collections to synchronize" hint when no collections are selected 2018-07-16 18:38:04 +02:00
Ricki Hirner
563737b410 Update to okhttp 3.11 2018-07-16 14:00:49 +02:00
Ricki Hirner
384b91ca77 Update build tools; enable parallel sync of address books authority 2018-07-15 17:10:14 +02:00
Ricki Hirner
1ec933128d Fetch translations from Transifex 2018-07-13 15:06:59 +02:00
Ricki Hirner
f7df046af9 New About activity
* new About activity using AboutLibraries library
* include DAVdroid version and other non-personal information in URL when DAVdroid homepage is opened
  (so that we know what DAVdroid versions are used out there and maybe can provide version-specific help)
2018-07-13 14:57:14 +02:00
Ricki Hirner
1622d6d53e New sync logic with XML streaming
* refactored dav4android to use XML streaming
* refactored collection detection
* refactored sync logic
2018-07-12 00:04:51 +02:00
Ricki Hirner
4ba318fa14 Enable strict mode for debugging; use thread for DB changes in AccountActivity 2018-06-27 12:46:28 +02:00
Ricki Hirner
1eecbad457 Change detection error message 2018-06-24 21:33:51 +02:00
Ricki Hirner
4b9aa376b3 Rename address book accounts correctly when renaming accounts; drop unparsable fields from vCards 2018-06-20 12:38:19 +02:00
Ricki Hirner
9d7c72c3ca Managed: move lambda expressions out of parentheses 2018-06-17 16:55:48 +02:00
Ricki Hirner
f41025d4d8 Version bump to 1.11.5 2018-06-17 16:36:54 +02:00
Ricki Hirner
59c765a6e8 move lambda expressions out of parentheses; use CREATOR-named companion objects for Parcelable 2018-06-17 16:34:45 +02:00
Ricki Hirner
6ff069ac57 Fix lateinit null value in resource detection; update Kotlin and gradle 2018-06-16 15:07:25 +02:00
Ricki Hirner
050f86f020 Version bump to 1.11.4.1-mgd1 2018-06-15 13:34:01 +02:00
Ricki Hirner
e45c8ab1eb Managed: Add login introduction message 2018-06-15 13:33:05 +02:00
Ricki Hirner
dd30677334 Trigger a full calendar sync when past event time limit is changed in account settings 2018-06-14 09:20:55 +02:00
Ricki Hirner
822b49cfcf Don't show contacts permission notification when no address book is selected for synchronization 2018-06-13 17:22:43 +02:00
Ricki Hirner
c667c18199 Version bump to 1.11.4.1 2018-06-12 11:08:51 +02:00
Ricki Hirner
33491336af Version bump to 1.11.4.1-beta1 2018-06-12 00:37:31 +02:00
Ricki Hirner
502a40c179 Collection sync: don't reset "present remotely" flag after enumerating resources 2018-06-12 00:35:39 +02:00
Ricki Hirner
c0bb073a57 Version bump to 1.11.4 2018-06-08 11:50:39 +02:00
Ricki Hirner
a5a3fbb969 Fetch translations from Transifex 2018-06-08 11:50:39 +02:00
Ricki Hirner
9c8219108b Update gradle plugin 2018-06-08 11:50:35 +02:00
Ricki Hirner
f85e435303 Fix crash when collection is deleted 2018-05-30 10:32:45 +02:00
Ricki Hirner
4e4d3b60aa Update copyright 2018-05-28 12:04:43 +02:00
Ricki Hirner
2848f0c466 Add maxOccurs to contacts.xml group membership (allows editing of contacts again) 2018-05-28 11:55:54 +02:00
Ricki Hirner
49512be453 Fetch translations from Transifex 2018-05-28 10:47:49 +02:00
Ricki Hirner
4cae527723 Add group memberships to contacts.xml so that they can be edited with some Contacts apps 2018-05-28 10:46:33 +02:00
Ricki Hirner
0baf10c962 Version bump to 1.11.4-beta2 2018-05-27 16:27:05 +02:00
Ricki Hirner
a8f0c8ea24 Show HTTP request/response in debug info 2018-05-27 16:26:31 +02:00
Ricki Hirner
baa044d61e Use US locale for date in User-Agent 2018-05-27 15:33:34 +02:00
Ricki Hirner
6352113c1e Fix lateinit problem 2018-05-26 12:41:41 +02:00
Ricki Hirner
99c1fd94c5 Use HttpUrl whenever possible
* HttpUrl is the preferred class because we use URLs mainly for okhttp
* don't use URI or String for URLs, if possible
* HttpUrl is not Serializable, so use Parcelable for data classes with HttpUrl

X
2018-05-26 12:30:02 +02:00
Ricki Hirner
4e3e281892 Use weak reference for sync adapter thread lock 2018-05-26 12:29:58 +02:00
Ricki Hirner
5445f2ab72 Don't show InterruptedIOException; tests 2018-05-25 10:56:47 +02:00
Ricki Hirner
c089c3d369 Update to version code 222 (1.11.4-beta1) 2018-05-24 14:09:46 +02:00
Ricki Hirner
6fef958d5f Update ical4android 2018-05-24 14:08:38 +02:00
Ricki Hirner
6d34def40c Integrate adaptive icon 2018-05-23 13:11:29 +02:00
Lokesh Krishna
ed06106c23 Adaptive icon 2018-05-23 12:58:49 +02:00
Ricki Hirner
866841afc0 dav4android update (immutable responses) 2018-05-23 12:50:44 +02:00
Ricki Hirner
8a76167eca Fetch translations from Transifex 2018-05-15 14:21:37 +02:00
Ricki Hirner
8587100853 Collection sync: handle 403 with valid-sync-token precondition 2018-05-14 12:54:26 +02:00
Ricki Hirner
fff7677703 Update build.gradle 2018-05-12 11:26:14 +02:00
Ricki Hirner
a7aded904d Version bump to 1.11.4-beta1 2018-05-11 23:30:27 +02:00
Ricki Hirner
a76cc5a805 ical4android: reduce size of sent VTIMEZONEs 2018-05-11 23:18:18 +02:00
Ricki Hirner
9ec5bd51f5 Version bump to 1.11.3 2018-05-10 12:58:51 +02:00
Ricki Hirner
9c15749257 Version bump to 1.11.3-beta1 2018-05-07 17:55:47 +02:00
Ricki Hirner
faced361d8 Enable Collection Synchronization for CalDAV when past time event limit is disabled 2018-05-07 17:54:35 +02:00
Ricki Hirner
b67e42b91a Version bump to 1.11.2 2018-05-04 11:00:30 +02:00
Ricki Hirner
1012dbfe4b Collection sync: mark skipped entries as locally present 2018-05-01 16:33:47 +02:00
Ricki Hirner
22bd34ce60 Version bump to 1.11.2-beta1 2018-05-01 10:18:28 +02:00
Ricki Hirner
330c2bd49d Improve collection sync for contacts 2018-04-30 12:40:29 +02:00
Ricki Hirner
bf2287550c Collection sync: don't download already available resources 2018-04-29 00:32:25 +02:00
Ricki Hirner
95033a20fd Support collection sync (RFC 6578) for contacts 2018-04-28 23:59:09 +02:00
Ricki Hirner
f8dec15c97 Code cleanup (lint) 2018-04-28 21:21:46 +02:00
Ricki Hirner
3ea1512f95 Fix memory leak in vcard4android 2018-04-27 00:51:22 +02:00
Ricki Hirner
a546823cb9 Add message to local storage error notification 2018-04-26 12:28:56 +02:00
Ricki Hirner
9582e07944 Fetch translations from Transifex 2018-04-26 10:54:20 +02:00
Ricki Hirner
a1b0427bfc Version bump to 1.11.1 2018-04-26 10:51:32 +02:00
Ricki Hirner
4cf4cecf0a Don't throw exception when content provider doesn't return all results; ignore RemoteException on getting contacts sync state 2018-04-26 10:47:46 +02:00
Ricki Hirner
842648d602 Show and use ical4j and okhttp version numbers when possible 2018-04-21 15:01:50 +02:00
Ricki Hirner
4fdcc077f4 Log remote resource info for tasks, too 2018-04-19 08:29:51 +02:00
Ricki Hirner
c0f0f8a83c Update support library and cert4android 2018-04-17 12:14:34 +02:00
Ricki Hirner
c586bab08b Handle exceptions when acquiring task provider 2018-04-15 11:51:11 +02:00
Ricki Hirner
e81ae958aa Treat InterruptedIOException like IOException 2018-04-13 15:23:49 +02:00
Ricki Hirner
051530fa7d Fetch translations from Transifex 2018-04-13 10:24:52 +02:00
Ricki Hirner
628937e109 Version bump to 1.11 2018-04-13 10:18:48 +02:00
Ricki Hirner
cd3662ce43 Update gradle plugin 2018-04-13 09:23:43 +02:00
Ricki Hirner
d694b480c4 Version bump to 1.11-rc1 2018-03-29 14:40:02 +02:00
Ricki Hirner
afc02d5ab5 Update gradle, kotlin, build tools 2018-03-29 14:07:31 +02:00
Ricki Hirner
171cda098a 1.11-beta2 2018-03-26 09:41:07 +02:00
Ricki Hirner
19b660333f Fetch translations from Transifex 2018-03-26 09:37:41 +02:00
Ricki Hirner
e6419ccefc Ask for LOCATION_COARSE permission for WiFi name detection on Android 8.1+ 2018-03-26 09:34:23 +02:00
Ricki Hirner
2a783bef3a Don't re-schedule non-existent master events of deleted exceptions 2018-03-24 23:12:51 +01:00
Ricki Hirner
34c08b299c Improve OpenTasks install message 2018-03-20 12:25:12 +01:00
Ricki Hirner
f2d9221239 Restrict more method names by interfaces 2018-03-16 13:16:04 +01:00
Ricki Hirner
32651978cc Fix sync bugs 2018-03-15 19:36:44 +01:00
Ricki Hirner
437a055c81 Minor fixes 2018-03-15 18:13:53 +01:00
Ricki Hirner
65ef5cd1d9 Use Google repo 2018-03-15 13:10:46 +01:00
Ricki Hirner
cac1339b61 Themeing, minor refactoring 2018-03-15 12:09:27 +01:00
Ricki Hirner
d6c11b7f39 Add "app auto-start permission" startup dialog for specific vendors 2018-03-14 14:54:09 +01:00
Ricki Hirner
4cfb0af588 Tests, minor refactoring 2018-03-12 13:42:17 +01:00
Ricki Hirner
b2ad46e41c Sync error notifications: retry action, lower importance of IOEXceptions 2018-03-09 11:36:43 +01:00
Ricki Hirner
b36731705a Account activity: remove SnackBar message when a read-only address book is selected 2018-03-08 15:19:27 +01:00
Ricki Hirner
4df8aba2ac Show startup fragments only once per AccountActivity lifecycle 2018-03-08 15:18:28 +01:00
Ricki Hirner
5e0ed389c2 Sync logic fix, theming, ProGuard 2018-03-07 23:24:49 +01:00
Ricki Hirner
486c7db99c Update to support library 27.1.0 and use it wherever possible
* Fragment transactions can now be done in onLoadFinished().
2018-03-06 13:58:43 +01:00
Ricki Hirner
58556447f5 Tests 2018-03-05 19:00:59 +01:00
Ricki Hirner
afd614fa19 Further improve notifications 2018-03-05 14:27:08 +01:00
Ricki Hirner
3a69f66ba8 Remove Project Lombok from About
* not used anymore because of Kotlin
* thanks to Project Lombok!
2018-03-05 13:09:30 +01:00
Ricki Hirner
edadc4e260 Remove PermissionsActivity
* asking for permissions is already (and better) done by AccountActivity
2018-03-05 13:04:28 +01:00
Ricki Hirner
0db859f3db Synchronization error messages / notifications 2018-03-05 12:36:48 +01:00
Ricki Hirner
a3a3cf8259 Rewrite sync algorithm, prepare for WebDAV collection sync 2018-02-27 12:24:01 +01:00
Ricki Hirner
c065c48702 Optimize notification icons
* don't show DAVdroid icon unless there's a strong relationship to DAVdroid itself
2018-02-06 12:07:59 +01:00
Ricki Hirner
0f5f2a3331 Fix image loader 2018-01-31 17:13:32 +01:00
Ricki Hirner
d7bf4f95a5 Espresso tests 2018-01-30 14:59:03 +01:00
Ricki Hirner
8c82d21ecc ical4android update 2018-01-27 22:56:50 +01:00
Ricki Hirner
619012e54e Prefer AutoCloseable over Closeable 2018-01-25 15:09:11 +01:00
Ricki Hirner
87ab0ca05b Basic Espresso tests 2018-01-25 15:03:26 +01:00
Ricki Hirner
6f7f35abcc Managed: don't continue to login credentials fragment when there are no settings 2018-01-22 19:09:56 +01:00
Ricki Hirner
3c6f6145f0 Version bump to 1.10.1.1 2018-01-20 17:09:14 +01:00
Ricki Hirner
1fb90762e0 Hotfix: don't clear event colors 2018-01-20 17:08:02 +01:00
Ricki Hirner
3fe1d428a3 Version bump to 1.10.1 2018-01-20 15:24:53 +01:00
Ricki Hirner
c47394b021 Fetch translations from Transifex 2018-01-20 15:23:58 +01:00
Ricki Hirner
faa49350c9 ical4android: work around missing account separation when updating events
* fixes removed eventColor_index fields when there is at least one DAVdroid
  account with enabled calendar colors and at least one account with disabled
  calendar colors
2018-01-20 14:59:52 +01:00
Ricki Hirner
511dde66ad Managed: add login_certificate_alias restriction 2018-01-18 23:35:03 +01:00
158 changed files with 4942 additions and 4540 deletions

View File

@@ -11,15 +11,19 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'org.jetbrains.dokka-android'
ext {
baseVersionName = '2.0'
}
android {
compileSdkVersion 27
buildToolsVersion '27.0.1'
buildToolsVersion '28.0.1'
defaultConfig {
applicationId "at.bitfire.davdroid"
resValue "string", "packageID", applicationId
versionCode 203
versionCode 241
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
minSdkVersion 19 // Android 4.4
@@ -33,17 +37,16 @@ android {
}
flavorDimensions "type"
productFlavors {
standard {
dimension "type"
versionName "1.10"
versionName baseVersionName
buildConfigField "boolean", "customCerts", "true"
}
managed {
dimension "type"
versionName "1.10-mgd"
versionName "$baseVersionName-mgd"
applicationId "com.davdroid.managed"
resValue "string", "packageID", applicationId
@@ -55,20 +58,20 @@ android {
gplay {
dimension "type"
versionName "1.10-gplay"
versionName "$baseVersionName-gplay"
buildConfigField "boolean", "customCerts", "true"
}
icloud {
dimension "type"
versionName "1.10-cloud"
versionName "$baseVersionName-icloud"
applicationId "at.bitfire.cloudsync"
resValue "string", "packageID", applicationId
}
soldupe {
dimension "type"
versionName "1.10-soldupe"
versionName "$baseVersionName-soldupe"
applicationId "com.soldupe.cloudsync"
resValue "string", "packageID", applicationId
@@ -81,6 +84,7 @@ android {
standard.res.srcDirs = [ "src/davdroid/res" ]
gplay.java.srcDirs = [ "src/gplay/java", "src/davdroid/java" ]
gplay.res.srcDirs = [ "src/gplay/res", "src/davdroid/res" ]
androidTest.java.srcDirs = [ "src/androidTest/java", "src/espressoTest/java" ]
}
signingConfigs {
@@ -116,7 +120,6 @@ android {
disable 'ImpliedQuantity', 'MissingQuantity' // quantities from Transifex may vary
disable 'MissingTranslation', 'ExtraTranslation' // translations from Transifex are not always up to date
disable "OnClick" // doesn't recognize Kotlin onClick methods
disable 'Recycle' // doesn't understand Lombok's @Cleanup
disable 'RtlEnabled'
disable 'RtlHardcoded'
disable 'Typos'
@@ -132,38 +135,33 @@ android {
}
dependencies {
compile project(':cert4android')
compile project(':dav4android')
compile project(':ical4android')
compile project(':vcard4android')
implementation project(':cert4android')
implementation project(':dav4android')
implementation project(':ical4android')
implementation project(':vcard4android')
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
compile 'com.android.support:appcompat-v7:27.0.2'
compile 'com.android.support:cardview-v7:27.0.2'
compile 'com.android.support:design:27.0.2'
compile 'com.android.support:preference-v14:27.0.2'
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:preference-v14:27.1.1'
compile 'com.github.yukuku:ambilwarna:2.0.1'
implementation 'com.github.yukuku:ambilwarna:2.0.1'
implementation 'com.mikepenz:aboutlibraries:6.0.9'
compile 'com.squareup.okhttp3:logging-interceptor:3.9.1'
compile 'commons-io:commons-io:2.6'
compile 'dnsjava:dnsjava:2.1.8'
compile 'org.apache.commons:commons-lang3:3.6'
compile 'org.apache.commons:commons-collections4:4.1'
implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
implementation 'commons-io:commons-io:2.6'
implementation 'dnsjava:dnsjava:2.1.8'
implementation 'org.apache.commons:commons-lang3:3.7'
implementation 'org.apache.commons:commons-collections4:4.1'
// for tests
//noinspection GradleDynamicVersion
androidTestCompile('com.android.support.test:runner:+') {
exclude group: 'com.android.support', module: 'support-annotations'
}
//noinspection GradleDynamicVersion
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.9.1'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'junit:junit:4.12'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.11.0'
testCompile 'junit:junit:4.12'
testCompile 'com.squareup.okhttp3:mockwebserver:3.9.1'
testImplementation 'junit:junit:4.12'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.11.0'
}

View File

@@ -19,7 +19,7 @@
-dontwarn ezvcard.io.json.** # JSON serializer (for jCards) not used
-dontwarn freemarker.** # freemarker templating library (for creating hCards) not used
-dontwarn org.jsoup.** # jsoup library (for hCard parsing) not used
-keep class ezvcard.property.** { *; } # keep all VCard properties (created at runtime)
-keep class ezvcard.property.** { *; } # keep all vCard properties (created at runtime)
# ical4j: ignore unused dynamic libraries
-dontwarn aQute.**
@@ -35,6 +35,7 @@
-dontwarn okio.**
-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault
-dontwarn org.conscrypt.**
# dnsjava
-dontwarn sun.net.spi.nameservice.** # not available on Android

1
app/src/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
espressoTest

View File

@@ -6,7 +6,7 @@
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid;
package at.bitfire.davdroid
import android.support.test.InstrumentationRegistry.getInstrumentation
import at.bitfire.cert4android.CustomCertManager
@@ -33,9 +33,9 @@ import javax.net.ssl.X509TrustManager
class CustomTlsSocketFactoryTest {
lateinit var certMgr: CustomCertManager
lateinit var factory: CustomTlsSocketFactory
val server = MockWebServer()
private lateinit var certMgr: CustomCertManager
private lateinit var factory: CustomTlsSocketFactory
private val server = MockWebServer()
@Before
fun startServer() {

View File

@@ -1,133 +0,0 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.model;
import android.content.ContentValues;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
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 {
HttpClient httpClient;
MockWebServer server = new MockWebServer();
@Before
public void setUp() {
httpClient = new HttpClient.Builder().build();
}
@After
public void shutDown() {
httpClient.close();
}
@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.getOkHttpClient(), server.url("/"));
dav.propfind(0, ResourceType.NAME);
CollectionInfo info = new CollectionInfo(dav);
assertEquals(CollectionInfo.Type.ADDRESS_BOOK, info.getType());
assertFalse(info.getReadOnly());
assertEquals("My Contacts", info.getDisplayName());
assertEquals("My Contacts Description", info.getDescription());
// 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.getOkHttpClient(), server.url("/"));
dav.propfind(0, ResourceType.NAME);
info = new CollectionInfo(dav);
assertEquals(CollectionInfo.Type.CALENDAR, info.getType());
assertTrue(info.getReadOnly());
assertNull(info.getDisplayName());
assertEquals("My Calendar", info.getDescription());
assertEquals(0xFFFF0000, (int)info.getColor());
assertEquals("tzdata", info.getTimeZone());
assertTrue(info.getSupportsVEVENT());
assertTrue(info.getSupportsVTODO());
}
@Test
public void testFromDB() {
ContentValues values = new ContentValues();
values.put(Collections.ID, 1);
values.put(Collections.SERVICE_ID, 1);
values.put(Collections.TYPE, CollectionInfo.Type.CALENDAR.name());
values.put(Collections.URL, "http://example.com");
values.put(Collections.READ_ONLY, 1);
values.put(Collections.DISPLAY_NAME, "display name");
values.put(Collections.DESCRIPTION, "description");
values.put(Collections.COLOR, 0xFFFF0000);
values.put(Collections.TIME_ZONE, "tzdata");
values.put(Collections.SUPPORTS_VEVENT, 1);
values.put(Collections.SUPPORTS_VTODO, 1);
values.put(Collections.SYNC, 1);
CollectionInfo info = new CollectionInfo(values);
assertEquals(CollectionInfo.Type.CALENDAR, info.getType());
assertEquals(1, (long)info.getId());
assertEquals(1, (long)info.getServiceID());
assertEquals("http://example.com", info.getUrl());
assertTrue(info.getReadOnly());
assertEquals("display name", info.getDisplayName());
assertEquals("description", info.getDescription());
assertEquals(0xFFFF0000, (int)info.getColor());
assertEquals("tzdata", info.getTimeZone());
assertTrue(info.getSupportsVEVENT());
assertTrue(info.getSupportsVTODO());
assertTrue(info.getSelected());
}
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.model
import android.content.ContentValues
import android.support.test.filters.SmallTest
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.property.ResourceType
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.model.ServiceDB.Collections
import okhttp3.HttpUrl
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
class CollectionInfoTest {
private lateinit var httpClient: HttpClient
private val server = MockWebServer()
@Before
fun setUp() {
httpClient = HttpClient.Builder().build()
}
@After
fun shutDown() {
httpClient.close()
}
@Test
@SmallTest
fun testFromDavResource() {
// r/w address book
server.enqueue(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>"))
var info: CollectionInfo? = null
DavResource(httpClient.okHttpClient, server.url("/"))
.propfind(0, ResourceType.NAME) { response, _ ->
info = CollectionInfo(response)
}
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(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>"))
info = null
DavResource(httpClient.okHttpClient, server.url("/"))
.propfind(0, ResourceType.NAME) { response, _ ->
info = CollectionInfo(response)
}
assertEquals(CollectionInfo.Type.CALENDAR, info?.type)
assertTrue(info!!.readOnly)
assertNull(info?.displayName)
assertEquals("My Calendar", info?.description)
assertEquals(0xFFFF0000.toInt(), info?.color)
assertEquals("tzdata", info?.timeZone)
assertTrue(info!!.supportsVEVENT)
assertTrue(info!!.supportsVTODO)
}
@Test
fun testFromDB() {
val values = ContentValues()
values.put(Collections.ID, 1)
values.put(Collections.SERVICE_ID, 1)
values.put(Collections.TYPE, CollectionInfo.Type.CALENDAR.name)
values.put(Collections.URL, "http://example.com")
values.put(Collections.READ_ONLY, 1)
values.put(Collections.DISPLAY_NAME, "display name")
values.put(Collections.DESCRIPTION, "description")
values.put(Collections.COLOR, 0xFFFF0000)
values.put(Collections.TIME_ZONE, "tzdata")
values.put(Collections.SUPPORTS_VEVENT, 1)
values.put(Collections.SUPPORTS_VTODO, 1)
values.put(Collections.SYNC, 1)
val info = CollectionInfo(values)
assertEquals(CollectionInfo.Type.CALENDAR, info.type)
assertEquals(1.toLong(), info.id)
assertEquals(1.toLong(), info.serviceID)
assertEquals(HttpUrl.parse("http://example.com/"), info.url)
assertTrue(info.readOnly)
assertEquals("display name", info.displayName)
assertEquals("description", info.description)
assertEquals(0xFFFF0000.toInt(), info.color)
assertEquals("tzdata", info.timeZone)
assertTrue(info.supportsVEVENT)
assertTrue(info.supportsVTODO)
assertTrue(info.selected)
}
}

View File

@@ -8,11 +8,11 @@
package at.bitfire.davdroid.settings
import android.content.ServiceConnection
import android.support.test.InstrumentationRegistry
import android.support.test.InstrumentationRegistry.getTargetContext
import at.bitfire.davdroid.App
import junit.framework.Assert.*
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.After
import org.junit.Before
import org.junit.Test

View File

@@ -1,201 +0,0 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.ui.setup;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
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.HttpClient;
import at.bitfire.davdroid.log.Logger;
import at.bitfire.davdroid.model.Credentials;
import at.bitfire.davdroid.ui.setup.DavResourceFinder.Configuration.ServiceInfo;
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;
HttpClient client;
LoginInfo loginInfo;
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();
loginInfo = new LoginInfo(URI.create("/"), new Credentials("mock", "12345"));
finder = new DavResourceFinder(getTargetContext(), loginInfo);
client = new HttpClient.Builder()
.addAuthentication(null, loginInfo.credentials)
.build();
}
@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.getOkHttpClient(), server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL));
finder.rememberIfAddressBookOrHomeset(dav, info = new ServiceInfo());
assertEquals(0, info.getCollections().size());
assertEquals(0, info.getHomeSets().size());
// recognize home set
dav.propfind(0, AddressbookHomeSet.NAME);
finder.rememberIfAddressBookOrHomeset(dav, info = new ServiceInfo());
assertEquals(0, info.getCollections().size());
assertEquals(1, info.getHomeSets().size());
assertEquals(server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET + "/").uri(), info.getHomeSets().iterator().next());
// recognize address book
dav = new DavResource(client.getOkHttpClient(), server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK));
dav.propfind(0, ResourceType.NAME);
finder.rememberIfAddressBookOrHomeset(dav, info = new ServiceInfo());
assertEquals(1, info.getCollections().size());
assertEquals(server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK + "/").uri(), info.getCollections().keySet().iterator().next());
assertEquals(0, info.getHomeSets().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;
}
Logger.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

@@ -0,0 +1,189 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.ui.setup
import android.support.test.InstrumentationRegistry.getTargetContext
import android.support.test.filters.SmallTest
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.property.AddressbookHomeSet
import at.bitfire.dav4android.property.ResourceType
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.Credentials
import at.bitfire.davdroid.ui.setup.DavResourceFinder.Configuration.ServiceInfo
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import java.net.URI
class DavResourceFinderTest {
companion object {
private const val PATH_NO_DAV = "/nodav"
private const val PATH_CALDAV = "/caldav"
private const val PATH_CARDDAV = "/carddav"
private const val PATH_CALDAV_AND_CARDDAV = "/both-caldav-carddav"
private const val SUBPATH_PRINCIPAL = "/principal"
private const val SUBPATH_ADDRESSBOOK_HOMESET = "/addressbooks"
private const val SUBPATH_ADDRESSBOOK = "/addressbooks/private-contacts"
}
val server = MockWebServer()
lateinit var finder: DavResourceFinder
lateinit var client: HttpClient
lateinit var loginInfo: LoginInfo
@Before
fun initServerAndClient() {
server.setDispatcher(TestDispatcher())
server.start()
loginInfo = LoginInfo(URI.create("/"), Credentials("mock", "12345"))
finder = DavResourceFinder(getTargetContext(), loginInfo)
client = HttpClient.Builder()
.addAuthentication(null, loginInfo.credentials)
.build()
}
@After
fun stopServer() {
server.shutdown()
}
@Test
@SmallTest
fun testRememberIfAddressBookOrHomeset() {
// recognize home set
var info = ServiceInfo()
DavResource(client.okHttpClient, server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL))
.propfind(0, AddressbookHomeSet.NAME) { response, _ ->
finder.scanCardDavResponse(response, info)
}
assertEquals(0, info.collections.size)
assertEquals(1, info.homeSets.size)
assertEquals(server.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET/"), info.homeSets.first())
// recognize address book
info = ServiceInfo()
DavResource(client.okHttpClient, server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK))
.propfind(0, ResourceType.NAME) { response, _ ->
finder.scanCardDavResponse(response, info)
}
assertEquals(1, info.collections.size)
assertEquals(server.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), info.collections.keys.first())
assertEquals(0, info.homeSets.size)
}
@Test
fun testProvidesService() {
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
fun testGetCurrentUserPrincipal() {
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),
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),
finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV)
)
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV))
}
// mock server
class TestDispatcher: Dispatcher() {
override fun dispatch(rq: RecordedRequest): MockResponse {
if (!checkAuth(rq)) {
val authenticate = MockResponse().setResponseCode(401)
authenticate.setHeader("WWW-Authenticate", "Basic realm=\"test\"")
return authenticate
}
val path = rq.path
if (rq.method.equals("OPTIONS", true)) {
val dav = when {
path.startsWith(PATH_CALDAV) -> "calendar-access"
path.startsWith(PATH_CARDDAV) -> "addressbook"
path.startsWith(PATH_CALDAV_AND_CARDDAV) -> "calendar-access, addressbook"
else -> null
}
val response = MockResponse().setResponseCode(200)
if (dav != null)
response.addHeader("DAV", dav)
return response
} else if (rq.method.equals("PROPFIND", true)) {
val props: String?
when (path) {
PATH_CALDAV,
PATH_CARDDAV ->
props = "<current-user-principal><href>$path$SUBPATH_PRINCIPAL</href></current-user-principal>"
PATH_CARDDAV + SUBPATH_PRINCIPAL ->
props = "<CARD:addressbook-home-set>" +
" <href>$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET</href>" +
"</CARD:addressbook-home-set>"
PATH_CARDDAV + SUBPATH_ADDRESSBOOK ->
props = "<resourcetype>" +
" <collection/>" +
" <CARD:addressbook/>" +
"</resourcetype>"
else -> props = null
}
Logger.log.info("Sending props: $props")
return MockResponse()
.setResponseCode(207)
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
"<response>" +
" <href>${rq.path}</href>" +
" <propstat><prop>$props</prop></propstat>" +
"</response>" +
"</multistatus>")
}
return MockResponse().setResponseCode(404)
}
private fun checkAuth(rq: RecordedRequest) =
rq.getHeader("Authorization") == "Basic bW9jazoxMjM0NQ=="
}
}

View File

@@ -21,7 +21,7 @@ import at.bitfire.davdroid.settings.ISettings
class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
companion object {
private val BETA_FEEDBACK_URI = "mailto:support@davdroid.com?subject=${BuildConfig.APPLICATION_ID} beta feedback ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
private const val BETA_FEEDBACK_URI = "mailto:support@davdroid.com?subject=${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} feedback (${BuildConfig.VERSION_CODE})"
}
@@ -44,19 +44,19 @@ class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
R.id.nav_twitter ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://twitter.com/davdroidapp")))
R.id.nav_website ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))))
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)))
R.id.nav_manual ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)
.buildUpon().appendEncodedPath("manual/").build()))
R.id.nav_faq ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)
.buildUpon().appendEncodedPath("faq/").build()))
R.id.nav_forums ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)
.buildUpon().appendEncodedPath("forums/").build()))
R.id.nav_donate ->
if (BuildConfig.FLAVOR != App.FLAVOR_GOOGLE_PLAY)
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.homepage_url))
activity.startActivity(Intent(Intent.ACTION_VIEW, App.homepageUrl(activity)
.buildUpon().appendEncodedPath("donate/").build()))
else ->
return false
@@ -65,4 +65,4 @@ class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
return true
}
}
}

View File

@@ -8,18 +8,16 @@
package at.bitfire.davdroid.ui.setup
import android.app.Fragment
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.security.KeyChain
import android.security.KeyChainAliasCallback
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.Toast
import at.bitfire.dav4android.Constants
import at.bitfire.davdroid.R
import kotlinx.android.synthetic.standard.login_credentials_fragment.view.*
@@ -55,12 +53,12 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang
}
v.urlcert_select_cert.setOnClickListener {
KeyChain.choosePrivateKeyAlias(activity, KeyChainAliasCallback { alias ->
Handler(Looper.getMainLooper()).post({
KeyChain.choosePrivateKeyAlias(activity, { alias ->
Handler(Looper.getMainLooper()).post {
v.urlcert_cert_alias.text = alias
v.urlcert_cert_alias.error = null
})
}, null, null, null, -1, view.urlcert_cert_alias.text.toString())
}
}, null, null, null, -1, view!!.urlcert_cert_alias.text.toString())
}
v.login.setOnClickListener {
@@ -80,7 +78,7 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang
}
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
onCheckedChanged(view)
onCheckedChanged(view!!)
}
private fun onCheckedChanged(v: View) {
@@ -90,6 +88,7 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang
}
private fun validateLoginData(): LoginInfo? {
val view = requireNotNull(view)
when {
// Login with email address
view.login_type_email.isChecked -> {
@@ -108,7 +107,7 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang
valid = false
}
val password = view.email_password.getText().toString()
val password = view.email_password.text.toString()
if (password.isEmpty()) {
view.email_password.error = getString(R.string.login_password_required)
valid = false
@@ -126,10 +125,10 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang
var valid = true
val baseUrl = Uri.parse(view.urlpwd_base_url.text.toString())
val uri = validateBaseUrl(baseUrl, false, { message ->
val uri = validateBaseUrl(baseUrl, false) { message ->
view.urlpwd_base_url.error = message
valid = false
})
}
val userName = view.urlpwd_user_name.text.toString()
if (userName.isBlank()) {
@@ -154,10 +153,10 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang
var valid = true
val baseUrl = Uri.parse(view.urlcert_base_url.text.toString())
val uri = validateBaseUrl(baseUrl, true, { message ->
val uri = validateBaseUrl(baseUrl, true) { message ->
view.urlcert_base_url.error = message
valid = false
})
}
val alias = view.urlcert_cert_alias.text.toString()
if (alias.isEmpty()) {

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2013 2016 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
@@ -130,6 +130,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_type_url_certificate"
android:layout_marginTop="16dp"
android:paddingLeft="14dp" tools:ignore="RtlSymmetry"
style="@style/login_type_headline"/>
<LinearLayout

View File

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

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -2,19 +2,110 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">DAVdroid Llibreta dAdreces</string>
<string name="address_books_authority_title">Llibreta dadreces</string>
<string name="help">Ajuda</string>
<string name="manage_accounts">Gestiona comptes</string>
<string name="please_wait">Esperi si us plau...</string>
<string name="send">Enviar</string>
<string name="notification_channel_debugging">Depurador</string>
<string name="notification_channel_general">Altres missatges importants</string>
<string name="notification_channel_sync">Sincronització</string>
<string name="notification_channel_sync_io_errors">Xarxa i errors E/S</string>
<string name="notification_channel_sync_status">Estat dels misatges</string>
<!--startup dialogs-->
<string name="startup_battery_optimization_disable">Desactiva per DAVdroid</string>
<string name="startup_dont_show_again">No mostrar de nou</string>
<string name="startup_donate">Informació de Codi Obert</string>
<string name="startup_donate_message">Som feliços de que utilitzis DAVdroid, el qual és programari lliure (GPLv3). Desenvolupar DAVdroid ens porte moltes hores i molta feina, si us plau, considera fer una donació.</string>
<string name="startup_donate_now">Mostra la pàgina de donació</string>
<string name="startup_donate_later">Més tard</string>
<string name="startup_google_play_accounts_removed">Informació d\'errors DRM a Play Store</string>
<string name="startup_more_info">Més informació</string>
<string name="startup_opentasks_not_installed">OpenTasks no està instal·lada</string>
<string name="startup_opentasks_not_installed_install">Instal·lar OpenTasks</string>
<!--AboutActivity-->
<!--global settings-->
<string name="logging_no_external_storage">Emmagatzematge extern no trobat</string>
<!--AccountsActivity-->
<string name="navigation_drawer_about">Quan a / Llicència</string>
<string name="navigation_drawer_beta_feedback">Retroacció de la Beta</string>
<string name="navigation_drawer_settings">Paràmetres</string>
<string name="navigation_drawer_news_updates">Novetats &amp; Actualitzacions</string>
<string name="navigation_drawer_external_links">Enllaços externs</string>
<string name="navigation_drawer_website">Pàgina web</string>
<string name="navigation_drawer_manual">Manual</string>
<string name="navigation_drawer_faq">Preguntes Freqüents</string>
<string name="navigation_drawer_forums">Ajuda / Fòrums</string>
<string name="navigation_drawer_donate">Fer donació</string>
<string name="accounts_global_sync_enable">Activar</string>
<!--DavService-->
<!--AppSettingsActivity-->
<string name="app_settings">Paràmetres</string>
<string name="app_settings_user_interface">Interfície dusuari</string>
<string name="app_settings_reset_hints">Reiniciar pistes</string>
<string name="app_settings_security">Seguretat</string>
<string name="app_settings_debug">Depurador</string>
<!--AccountActivity-->
<!--PermissionsActivity-->
<string name="account_synchronize_now">Sincronitza ara</string>
<string name="account_synchronizing_now">Sincronitzant ara</string>
<string name="account_settings">Paràmetres del compte</string>
<string name="account_rename">Canvia el nom al compte</string>
<string name="account_rename_rename">Canvia el nom</string>
<string name="account_delete">Esborra el compte</string>
<string name="account_delete_confirmation_title">Segur que vols esborrar el compte?</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_synchronize_this_collection">Sincronitzar aquesta col·lecció</string>
<string name="account_read_only">Només de lectura</string>
<string name="account_calendar">Calendari</string>
<string name="account_task_list">Llista de tasques</string>
<string name="account_refresh_address_book_list">Actualitza la llista de llibretes dadreces</string>
<string name="account_create_new_address_book">Crear nova llibreta dadreces</string>
<string name="account_refresh_calendar_list">Actualitzar llista de calendaris</string>
<string name="account_create_new_calendar">Crear nou calendari</string>
<string name="account_install_icsdroid">Instal·lar ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_title">Afegir compte</string>
<string name="login_type_email">Entra amb una adreça de correu electrònic</string>
<string name="login_email_address">Correu electrònic</string>
<string name="login_email_address_error">Es requereix un correu electrònic vàlid</string>
<string name="login_password">Contrasenya</string>
<string name="login_password_required">Es requereix contrasenya</string>
<string name="login_type_url">Entra amb una URL i un nom d\'usuari</string>
<string name="login_url_must_be_http_or_https">La URL hauria de començar amb http(s)://</string>
<string name="login_url_must_be_https">La URL hauria de començar amb https://</string>
<string name="login_url_host_name_required">Es requereix nom d\'amfitrió</string>
<string name="login_user_name">Nom d\'usuari/a</string>
<string name="login_user_name_required">Es requereix nom d\'usuari/a</string>
<string name="login_base_url">URL base</string>
<string name="login_select_certificate">Selecciona certificat</string>
<string name="login_login">Entrada</string>
<string name="login_back">Sortir</string>
<string name="login_create_account">Crear compte</string>
<string name="login_account_name">Nom del compte</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Paràmetres: %s</string>
<string name="settings_authentication">Autentificació</string>
<string name="settings_username">Nom d\'usuari/a</string>
<string name="settings_enter_username">Escriu el nom d\'usuari/a:</string>
<string name="settings_password">Contrasenya</string>
<string name="settings_enter_password">Escriu la teva contrasenya</string>
<string name="settings_sync">Sincronització</string>
<string name="settings_sync_summary_manually">Només a mà</string>
<string-array name="settings_sync_interval_names">
<item>Només a mà</item>
<item>Cada 15 minuts</item>
<item>Cada 30 minuts</item>
<item>Cada hora</item>
<item>Cada 2 hores</item>
<item>Cada 4 hores</item>
<item>Una vegada al dia</item>
</string-array>
<string name="settings_sync_wifi_only">Sincronitzar només amb WI-FI</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_caldav">CalDAV</string>
<!--collection management-->
<!--ExceptionInfoFragment-->
<!--sync adapters and DebugInfoActivity-->

View File

@@ -7,8 +7,6 @@
<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_donate">Open Source informace</string>
@@ -17,13 +15,10 @@
<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>
@@ -84,17 +79,6 @@
<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>
@@ -119,7 +103,6 @@
<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>
@@ -153,6 +136,7 @@
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Ignorovat události starší než 1 den</item>
<item quantity="few">Ignorovat události starší než %d dny</item>
<item quantity="many">Ignorovat události starší než %d dnů</item>
<item quantity="other">Ignorovat události starší než %d dnů</item>
</plurals>
<string name="settings_sync_time_range_past_message">Události z minulosti starší než vyznačený počet dnů budou ignorovány (lze zadat 0). Ponechte prázdné pro synchronizaci všech událostí.</string>
@@ -189,27 +173,6 @@
<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>

View File

@@ -2,58 +2,64 @@
<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="account_title_address_book">DAVdroid adressebog</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="manage_accounts">Administrere konti</string>
<string name="please_wait">Vent...</string>
<string name="send">Send</string>
<string name="notification_channel_debugging">Fejlsøgning</string>
<string name="notification_channel_general">Andre vigtige beskeder</string>
<string name="notification_channel_sync">Synkronisering</string>
<string name="notification_channel_sync_errors">Synkroniserings-fejl</string>
<string name="notification_channel_sync_io_errors">Netværks- og I/O-fejl</string>
<string name="notification_channel_sync_status">Status beskeder</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_battery_optimization_disable">Deaktivere DAVdroid</string>
<string name="startup_dont_show_again">Vis ikke igen</string>
<string name="startup_donate">Open-Source Information</string>
<string name="startup_donate">Open-Source information</string>
<string name="startup_donate_message">Det glæder os, at du bruger DAVdroid, som er open source-software (GPLv3). Det er hårdt arbejde at udvikle DAVdroid og taget tusindvis af arbejdstimer, så overvej at donere til projektet.</string>
<string name="startup_donate_now">Vis donationsside</string>
<string name="startup_donate_later">Måske senere</string>
<string name="startup_google_play_accounts_removed">Play Store DRM-fejl: Information</string>
<string name="startup_google_play_accounts_removed">Play Store DRM fejl information</string>
<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_more_info">Mere information</string>
<string name="startup_opentasks_not_installed">OpenTasks ikke installeret</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_not_installed_message">Du er nødt til at have den gratis app OpenTasks for at kunne synkronisere opgaver. (Ikke påkrævet for kontakter/begivenheder.)</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>
<string name="startup_opentasks_not_installed_install">Installere OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Licensforhold</string>
<string name="about_license_info_no_warranty">Dette program leveres ABSOLUT UDEN GARANTI. Det er fri software, og du er velkommen til at videredistribuere det under visse betingelse.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid fil-logning</string>
<string name="logging_to_external_storage">Logger til eksternt datalager: %s</string>
<string name="logging_couldnt_create_file">Kunne ikke oprette ekstern logfil: %s</string>
<string name="logging_no_external_storage">Eksternt lager ikke funder</string>
<string name="logging_no_external_storage">Eksternt lager ikke fundet</string>
<!--AccountsActivity-->
<string name="navigation_drawer_open">Åbn navigationsvindue</string>
<string name="navigation_drawer_close">Luk navigationsvindue</string>
<string name="navigation_drawer_subtitle">CalDAV/CardDAV Sync-adapter</string>
<string name="navigation_drawer_about">Om / Licens</string>
<string name="navigation_drawer_settings">Indstillinger</string>
<string name="navigation_drawer_subtitle">CalDAV/CardDAV synkroniseringsadapter</string>
<string name="navigation_drawer_about">Om / licens</string>
<string name="navigation_drawer_beta_feedback">Beta tilbagemelding</string>
<string name="navigation_drawer_settings">Opsætning</string>
<string name="navigation_drawer_news_updates">Nyheder &amp; opdateringer</string>
<string name="navigation_drawer_external_links">Eksterne links</string>
<string name="navigation_drawer_website">Hjemmeside</string>
<string name="navigation_drawer_faq">FAQ</string>
<string name="navigation_drawer_external_links">Eksterne henvisninger</string>
<string name="navigation_drawer_website">Netsted</string>
<string name="navigation_drawer_manual">Manual</string>
<string name="navigation_drawer_faq">OSS</string>
<string name="navigation_drawer_forums">Hjælp / fora</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="account_list_empty">Velkommen til DAVdroid!\n\nDu kan nu tilføje en CalDAV/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>
<string name="accounts_global_sync_enable">Aktivere</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>
<string name="dav_service_refresh_couldnt_refresh">Kunne ikke opdatere samling liste</string>
<!--AppSettingsActivity-->
<string name="app_settings">Indstillinger</string>
<string name="app_settings_user_interface">Brugerflade</string>
<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">Nulstil vejledende pop op</string>
<string name="app_settings_reset_hints_summary">Genaktivere tidligere lukket pop op</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>
@@ -68,79 +74,93 @@
<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_debug">Fejlsøgning</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>
<string name="app_settings_log_to_external_storage_off">Ekstern logning deaktiveret</string>
<string name="app_settings_show_debug_info">Vis debug-info</string>
<string name="app_settings_show_debug_info">Vis fejlsøgnings information</string>
<string name="app_settings_show_debug_info_details">Vis/del software og opsætningsoplysninger</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Synkroniser nu</string>
<string name="account_synchronizing_now">Synkroniserer nu</string>
<string name="account_synchronize_now">Synkronisere</string>
<string name="account_synchronizing_now">Synkroniserer</string>
<string name="account_settings">Opsætning af konti</string>
<string name="account_rename">Omdøb konto</string>
<string name="account_rename">Omdøbe 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_rename_rename">Omdøbe</string>
<string name="account_delete">Slette konto</string>
<string name="account_delete_confirmation_title">Slette konto?</string>
<string name="account_delete_confirmation_text">Alle lokale kopier af addessebøger, kalendere og opgavelister vil blive slettet.</string>
<string name="account_refresh_address_book_list">Opdater adressebogslister</string>
<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>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_synchronize_this_collection">synkronisere samling</string>
<string name="account_read_only">skrivebeskyttet</string>
<string name="account_calendar">kalender</string>
<string name="account_task_list">opgave liste</string>
<string name="account_refresh_address_book_list">Opdatere adressebogslister</string>
<string name="account_create_new_address_book">Oprette ny adressebog</string>
<string name="account_refresh_calendar_list">Opdatere kalenderliste</string>
<string name="account_create_new_calendar">Oprette ny kalender</string>
<string name="account_no_webcal_handler_found">Der er ikke fundet noget program der kan håndtere Webcal.</string>
<string name="account_install_icsdroid">Installere ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_title">Tilføj konto</string>
<string name="login_type_email">Log ind med emailadresse</string>
<string name="login_email_address">Emailadresse</string>
<string name="login_email_address_error">Gyldig emailadresse påkrævet</string>
<string name="login_title">Tilføje konto</string>
<string name="login_type_email">Logge ind med e-post adresse</string>
<string name="login_email_address">E-post adresse</string>
<string name="login_email_address_error">Gyldig e-post adresse påkrævet</string>
<string name="login_password">Adgangskode</string>
<string name="login_password_required">Adgangskode påkrævet</string>
<string name="login_type_url">Log ind med URL og brugernavn</string>
<string name="login_url_must_be_http_or_https">URL skal begynde med http(s)://</string>
<string name="login_type_url">Logge ind med URL og brugernavn</string>
<string name="login_url_must_be_http_or_https">URL skal starte med http(s)://</string>
<string name="login_url_must_be_https">URL skal starte med https://</string>
<string name="login_url_host_name_required">Værtsnavn påkrævet</string>
<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_login">Login</string>
<string name="login_base_url">Basis URL</string>
<string name="login_type_url_certificate">Logge ind med URL og klientcertifikat</string>
<string name="login_select_certificate">Vælge certifikat</string>
<string name="login_login">Logge ind</string>
<string name="login_back">Tilbage</string>
<string name="login_create_account">Opret konto</string>
<string name="login_create_account">Oprette 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>
<string name="login_querying_server">Vent, forespørger hos serveren...</string>
<string name="login_querying_server">Vent, forespørger serveren...</string>
<string name="login_no_caldav_carddav">Kunne ikke finde CalDAV- eller CardDAV-tjeneste.</string>
<string name="login_view_logs">Vis logs.</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Indstillinger: %s</string>
<string name="settings_authentication">Adgangsgodkendelse</string>
<string name="settings_username">Brugernavn</string>
<string name="settings_enter_username">Indtast brugernavn:</string>
<string name="settings_enter_username">Indtaste brugernavn:</string>
<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_certificate_alias">Alias for klientcertifikat</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>
<string name="settings_sync_summary_periodically" tools:ignore="PluralsCandidate">Hver %d minutter + øjeblikkeligt ved lokale ændringer</string>
<string name="settings_sync_interval_calendars">Synkroniseringsinterval for kalender</string>
<string name="settings_sync_interval_tasks">Synkroniseringsinterval for opgaver</string>
<string-array name="settings_sync_interval_names">
<item>Kun manuelt</item>
<item>Hvert 15. minut</item>
<item>Hver halve time</item>
<item>Hver time</item>
<item>Hver 2. time</item>
<item>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_ssids">WiFi SSID-begrænsning</string>
<string name="settings_sync_wifi_only_ssids_on">Synkroniserer kun over %s</string>
<string name="settings_sync_wifi_only_ssids_off">Alle WiFi-forbindelser vil blive anvendt</string>
<string name="settings_sync_wifi_only_ssids_message">Kommaseparerede navne (SSID\'er) over tilladte WiFi-netværk (efterlad blank for at bruge alle)</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">
@@ -151,6 +171,8 @@
<item>Grupper er særskilte VCards</item>
<item>Grupper er kategorier pr. kontakt</item>
</string-array>
<string name="settings_contact_group_method_change">Skift grupperingsmetode</string>
<string name="settings_contact_group_method_change_reload_contacts">Dette kræver, at alle kontakter genindlæses. Lokale ændringer, der ikke er gemt, vil blive slettet.</string>
<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>
@@ -162,6 +184,10 @@
<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>
<string name="settings_event_colors">Farver for begivenheder</string>
<string name="settings_event_colors_on">Synkroniser farver for begivenheder</string>
<string name="settings_event_colors_off">Synkroniser ikke farver for begivenheder</string>
<string name="settings_event_colors_off_confirm">Fjernes farver for begivenheder kan det fjerne farver, der allerede er synkroniseret.</string>
<!--collection management-->
<string name="create_addressbook">Opret adressebog</string>
<string name="create_addressbook_display_name_hint">Min adressebog</string>
@@ -183,6 +209,7 @@
<string name="delete_collection_confirm_title">Er du sikker?</string>
<string name="delete_collection_confirm_warning">DAV-sættet (%s) og dets data vil blive fjernet fra serveren.</string>
<string name="delete_collection_deleting_collection">Sletter CalDAV-sæt</string>
<string name="collection_force_read_only">Sæt skrivebeskyttet</string>
<!--ExceptionInfoFragment-->
<string name="exception">Der er opstået en fejl.</string>
<string name="exception_httpexception">Der er opstået en HTTP-fejl.</string>
@@ -190,29 +217,21 @@
<string name="exception_show_details">Vis detaljer</string>
<!--sync adapters and DebugInfoActivity-->
<string name="debug_info_title">Debug-info</string>
<string name="sync_contacts_read_only_address_book">Skrivebeskyttet adressebog</string>
<plurals name="sync_contacts_local_contact_changes_discarded">
<item quantity="one">Lokal ændring slettet</item>
<item quantity="other">%d lokale ændringer slettet</item>
</plurals>
<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>
<string name="sync_error">Fejl under %s</string>
<string name="sync_error_http_dav">Serverfejl under %s</string>
<string name="sync_error_local_storage">Databasefejl under %s</string>
<string-array name="sync_error_phases">
<item>forbereder synkronisering</item>
<item>checker understøttelse</item>
<item>behandler poster, der er slettet 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>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>
<string name="sync_error_opentasks_too_old">OpenTasks-version for gammel</string>
<string name="sync_error_opentasks_required_version">Påkrævet version: %1$s (aktuelt%2$s)</string>
<string name="sync_error_authentication_failed">Login mislykkedes (check loginoplysninger)</string>
<string name="sync_error_io">Netværks- eller I/O-fejl - %s</string>
<string name="sync_error_http_dav">HTTP-serverfejl - %s</string>
<string name="sync_error_local_storage">Lokal lagringsfejl - %s</string>
<string name="sync_error_retry">Gentag</string>
<string name="sync_error_view_item">Vis element</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>

View File

@@ -2,13 +2,19 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!--common strings-->
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">Agenda DAVdroid</string>
<string name="address_books_authority_title">Agendas</string>
<string name="help">Ayuda</string>
<string name="manage_accounts">Administrar cuentas</string>
<string name="please_wait">Por favor, espere...</string>
<string name="send">Enviar</string>
<string name="notification_channel_debugging">Depuración</string>
<string name="notification_channel_general">Otros mensajes importantes</string>
<string name="notification_channel_sync">Sincronización</string>
<string name="notification_channel_sync_errors">Errores de sincronización</string>
<string name="notification_channel_sync_io_errors">Errores de Red y E/S</string>
<string name="notification_channel_sync_status">Mensajes de estado</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_donate">Información de código abierto</string>
@@ -17,13 +23,12 @@
<string name="startup_donate_later">Quizás luego</string>
<string name="startup_google_play_accounts_removed">Información de error de DRM de Play Store</string>
<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_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á disponible. DAVdroid no podrá sincronizar listas de tareas.</string>
<string name="startup_opentasks_not_installed_message">Para sincronizar tareas, la aplicación libre OpenTasks es requerida. (No necesaria para contactos/eventos.)</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>
@@ -35,13 +40,18 @@
<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_beta_feedback">Retroalimentación Beta</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_manual">Manual</string>
<string name="navigation_drawer_faq">Preguntas frequentes</string>
<string name="navigation_drawer_forums">Ayuda / Foros</string>
<string name="navigation_drawer_donate">Donar</string>
<string name="account_list_empty">Bienvenido a DAVdroid!\n\nAhora puedes añadir una cuenta CalDAV/CardDAV.</string>
<string name="accounts_global_sync_disabled">Sincronización automática del sistema completo está deshabilitada</string>
<string name="accounts_global_sync_enable">Activar</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>
@@ -80,21 +90,19 @@
<string name="account_delete">Eliminar cuenta</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_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_synchronize_this_collection">sincronizar ésta colección</string>
<string name="account_read_only">solo lectura</string>
<string name="account_calendar">calendario</string>
<string name="account_task_list">lista de tareas</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_create_new_address_book">Crear nueva agenda</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>
<string name="account_no_webcal_handler_found">No se encontró aplicación para administrar Webcal</string>
<string name="account_install_icsdroid">Instale ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_title">Añadir cuenta</string>
<string name="login_type_email">Acceder con cuenta de correo</string>
@@ -104,10 +112,13 @@
<string name="login_password_required">Contraseña requerida</string>
<string name="login_type_url">Acceder con URL y nombre de usuario</string>
<string name="login_url_must_be_http_or_https">La URL debe comenzar con http(s)://</string>
<string name="login_url_must_be_https">El URL debe comenzar con https://</string>
<string name="login_url_host_name_required">Nombre de servidor requerido</string>
<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_type_url_certificate">Iniciar sesión con URL y certificado del cliente</string>
<string name="login_select_certificate">Seleccionar un certificado</string>
<string name="login_login">Registrar</string>
<string name="login_back">Volver</string>
<string name="login_create_account">Crear cuenta</string>
@@ -119,7 +130,6 @@
<string name="login_configuration_detection">Detectar configuración</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>
<string name="settings_authentication">Autenticación</string>
@@ -128,6 +138,7 @@
<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_certificate_alias">Alias del certificado cliente</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>
@@ -137,6 +148,10 @@
<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_ssids">Restricción WiFi SSID</string>
<string name="settings_sync_wifi_only_ssids_on">Solo se sincronizará a través de %s</string>
<string name="settings_sync_wifi_only_ssids_off">Todas las conexiones WiFi serán usadas</string>
<string name="settings_sync_wifi_only_ssids_message">Nombres separados por comas (SSIDs) de redes WiFi permitidas (deje vacío para todas)</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">
@@ -147,6 +162,8 @@
<item>Los groups tienen VCards separadas</item>
<item>Los groups tienen una categoría por contacto</item>
</string-array>
<string name="settings_contact_group_method_change">Cambie método de grupo</string>
<string name="settings_contact_group_method_change_reload_contacts">Esto requiere recargar todos los contactos. Cambios locales sin guardar serán descartados.</string>
<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>
@@ -158,8 +175,12 @@
<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>
<string name="settings_event_colors">Soporte de colores en eventos</string>
<string name="settings_event_colors_on">Sincronizar colores de eventos</string>
<string name="settings_event_colors_off">No sincronizar colores de eventos</string>
<string name="settings_event_colors_off_confirm">Desactivar colores de eventos podría remover colores de eventos ya sincronizados.</string>
<!--collection management-->
<string name="create_addressbook">Crear nueva lista de contactos</string>
<string name="create_addressbook">Crear nueva agenda</string>
<string name="create_addressbook_display_name_hint">Agendas</string>
<string name="create_calendar">Crear colección CalDAV</string>
<string name="create_calendar_display_name_hint">Mi calendario</string>
@@ -179,6 +200,7 @@
<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>
<string name="collection_force_read_only">Forzar solo-lectura</string>
<!--ExceptionInfoFragment-->
<string name="exception">Ocurrió un error.</string>
<string name="exception_httpexception">Ha ocurrido un error HTTP.</string>
@@ -186,29 +208,21 @@
<string name="exception_show_details">Mostrar detalles</string>
<!--sync adapters and DebugInfoActivity-->
<string name="debug_info_title">Información de depuración</string>
<string name="sync_contacts_read_only_address_book">Libro de direcciones solo-lectura</string>
<plurals name="sync_contacts_local_contact_changes_discarded">
<item quantity="one">Cambio de contacto local descartado</item>
<item quantity="other">%d cambios de contactos locales descartados</item>
</plurals>
<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>
<string name="sync_error">Error al %s</string>
<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>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>
<string name="sync_error_opentasks_too_old">OpenTasks muy viejo</string>
<string name="sync_error_opentasks_required_version">Versión requerida: %1$s (actual %2$s)</string>
<string name="sync_error_authentication_failed">Falló la autenticación (revise credenciales de inicio de sesión)</string>
<string name="sync_error_io">Error de red o E/S %s</string>
<string name="sync_error_http_dav">Error de servidor %s</string>
<string name="sync_error_local_storage">Error de almacenamiento local %s</string>
<string name="sync_error_retry">Reintentar</string>
<string name="sync_error_view_item">Ver item</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>

View File

@@ -9,8 +9,6 @@
<string name="please_wait">patientez ...</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_donate">Open-Source Information</string>
@@ -19,13 +17,10 @@
<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">L\'application OpenTasks n\'est pas installée</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>
@@ -96,17 +91,6 @@
<string name="account_create_new_calendar">Créer un nouveau calendrier</string>
<string name="account_no_webcal_handler_found">Aucune application compatible WebCal</string>
<string name="account_install_icsdroid">Installer ICSdroid</string>
<!--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 d\'accès aux 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_title">Ajouter un compte</string>
<string name="login_type_email">Connexion avec une adresse email</string>
@@ -131,7 +115,6 @@
<string name="login_configuration_detection">Détection de la configuration</string>
<string name="login_querying_server">Veuillez patienter, nous interrogeons le serveur ...</string>
<string name="login_no_caldav_carddav">Aucun accès possible au service CalDAV ou CardDAV.</string>
<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>
@@ -217,27 +200,6 @@
<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>

View File

@@ -8,9 +8,13 @@
<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>
<string name="notification_channel_debugging">Hibakeresés</string>
<string name="notification_channel_general">Egyéb fontos üzenetek</string>
<string name="notification_channel_sync">Szinkronizáció</string>
<string name="notification_channel_sync_errors">Szinkronizációs hibák</string>
<string name="notification_channel_sync_io_errors">Hálózati és I/O hibák</string>
<string name="notification_channel_sync_status">Státuszüzenetek</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_donate">A forrás nyíltságával kapcsolatos információk</string>
@@ -19,13 +23,12 @@
<string name="startup_donate_later">Talán később</string>
<string name="startup_google_play_accounts_removed">Play Áruház DRM hibával kapcsolatos információ</string>
<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_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, így a DAVdroid nem lesz képes szinkronizálni feladatlistákat.</string>
<string name="startup_opentasks_not_installed_message">A feladatok szinkronizálásához az ingyenes OpenTasks alkalmazásra van szükség. (A névjegyek és események szinkronizálásához erre nincs szükség.)</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>
<string name="about_license_info_no_warranty">Ehhez a program SEMMIFÉLE GARANCIA NEM JÁR. Ez a program szabad szoftver, ami a bizonyos feltételek mellett szabadon terjeszthető.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid fájlalapú naplózás</string>
@@ -37,10 +40,12 @@
<string name="navigation_drawer_close">Navigációs fiók lezárása</string>
<string name="navigation_drawer_subtitle">CalDAV/CardDAV szinkronizációs adapter</string>
<string name="navigation_drawer_about">Névjegy / Licenc</string>
<string name="navigation_drawer_beta_feedback">Tesztelői visszajelzés</string>
<string name="navigation_drawer_settings">Beállítások</string>
<string name="navigation_drawer_news_updates">Hírek és frissítések</string>
<string name="navigation_drawer_external_links">Weblapok</string>
<string name="navigation_drawer_website">Honlap</string>
<string name="navigation_drawer_manual">Kézikönyv</string>
<string name="navigation_drawer_faq">GYIK</string>
<string name="navigation_drawer_forums">Segítség / Fórumok</string>
<string name="navigation_drawer_donate">Támogatás</string>
@@ -88,23 +93,16 @@
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_synchronize_this_collection">a gyűjtemény szinkronizálása</string>
<string name="account_read_only">csak olvasható</string>
<string name="account_calendar">naptár</string>
<string name="account_task_list">feladatlista</string>
<string name="account_refresh_address_book_list">Címjegyzék-lista frissítése</string>
<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>
<string name="account_no_webcal_handler_found">Nem található Webcal-képes alkalmazás</string>
<string name="account_install_icsdroid">ICSdroid telepítése</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>
@@ -114,10 +112,13 @@
<string name="login_password_required">A jelszó megadása szükséges</string>
<string name="login_type_url">Bejelentkezés URL és felhasználónév segítségével</string>
<string name="login_url_must_be_http_or_https">Az URL elején szerepeljen http(s)://</string>
<string name="login_url_must_be_https">Az URL elején szerepeljen https://</string>
<string name="login_url_host_name_required">A szervernév megadása feltétlenül szükséges</string>
<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_type_url_certificate">Bejelentkezés URL és tanúsítvány segítségével</string>
<string name="login_select_certificate">Tanúsítvány kiválasztása</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>
@@ -129,7 +130,6 @@
<string name="login_configuration_detection">A konfiguráció felderítése</string>
<string name="login_querying_server">Kérjük, várjon, a szerver lekérdezése...</string>
<string name="login_no_caldav_carddav">Nem található CalDAV vagy CardDAV szolgáltatás.</string>
<string name="login_view_logs">Naplóbejegyzések megtekintése</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Beállítások: %s</string>
<string name="settings_authentication">Authentikáció</string>
@@ -138,6 +138,7 @@
<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_certificate_alias">Tanúsítvány alternatív megnevezése</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>
@@ -170,6 +171,8 @@
<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_contact_group_method_change">A csoportok kezelésének megváltoztatása</string>
<string name="settings_contact_group_method_change_reload_contacts">Az összes névjegyet újra be kell olvasni. Az el nem mentett helyi változások törlődni fognak.</string>
<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>
@@ -206,6 +209,7 @@
<string name="delete_collection_confirm_title">Biztos?</string>
<string name="delete_collection_confirm_warning">A gyűjtemény (%s) és a hozzá tartozó adatok törölve lesznek a szerverről.</string>
<string name="delete_collection_deleting_collection">Gyűjtemény törlése</string>
<string name="collection_force_read_only">Csak olvashatóvá tétel</string>
<!--ExceptionInfoFragment-->
<string name="exception">Hiba történt.</string>
<string name="exception_httpexception">HTTP hiba történt.</string>
@@ -213,29 +217,21 @@
<string name="exception_show_details">Részletek megjelenítése</string>
<!--sync adapters and DebugInfoActivity-->
<string name="debug_info_title">Hibakeresési információ</string>
<string name="sync_contacts_read_only_address_book">Csak olvasható címjegyzék</string>
<plurals name="sync_contacts_local_contact_changes_discarded">
<item quantity="one">Helyi névjegyváltozás elvetve</item>
<item quantity="other">%d helyi névjegyváltozás elvetve</item>
</plurals>
<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>
<string name="sync_error">Hiba az alábbi művelet közben: %s</string>
<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>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 gyűjtése </item>
<item>az új vagy módosított bejegyzések feltölté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>
<string name="sync_error_opentasks_too_old">Az OpenTask verzió nem megfelelő</string>
<string name="sync_error_opentasks_required_version">Szükséges verzió: %1$s (jelenlegi verzió: %2$s)</string>
<string name="sync_error_authentication_failed">Authentikáció nem sikerült (ellenőrizze a hitelesítéshez megadott adatokat)</string>
<string name="sync_error_io">Hálózati vagy I/O hiba %s</string>
<string name="sync_error_http_dav">HTTP szerver oldali hiba - %s</string>
<string name="sync_error_local_storage">Tárhelyhiba - %s</string>
<string name="sync_error_retry">Újbóli próbálkozás</string>
<string name="sync_error_view_item">Elem megtekintése</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>

View File

@@ -2,15 +2,18 @@
<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="account_title_address_book">Rubrica DAVdroid</string>
<string name="address_books_authority_title">Rubriche</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>
<string name="notification_channel_general">Altri messaggi importanti</string>
<string name="notification_channel_sync">Sincronizzazione</string>
<string name="notification_channel_sync_errors">Errori di sincronizzazione</string>
<string name="notification_channel_sync_io_errors">Errori di Rete e di I/O</string>
<string name="notification_channel_sync_status">Messaggi di stato</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_donate">Informazioni sull\'Open-Source</string>
@@ -19,13 +22,12 @@
<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_more_info">Più 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_not_installed_message">Per sincronizzare le attività è richiesta l\'app gratuita OpenTasks. (Non richiesta per contatti/eventi.)</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>
@@ -41,7 +43,9 @@
<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_manual">Manuale</string>
<string name="navigation_drawer_faq">Domande Frequenti</string>
<string name="navigation_drawer_forums">Aiuto / Forum</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>
@@ -87,22 +91,14 @@
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_read_only">sola lettura</string>
<string name="account_calendar">calendario</string>
<string name="account_task_list">elenco attività</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>
<string name="account_install_icsdroid">Installa ICSdroid</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>
@@ -112,10 +108,13 @@
<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_must_be_https">L\'URL deve iniziare con https://</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_type_url_certificate">Accedi con URL e certificato client</string>
<string name="login_select_certificate">Seleziona certificato</string>
<string name="login_login">Login</string>
<string name="login_back">Indietro</string>
<string name="login_create_account">Crea account</string>
@@ -127,7 +126,6 @@
<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>
@@ -154,6 +152,7 @@
<string name="settings_sync_wifi_only">Sincr. solo tramite WiFi</string>
<string name="settings_sync_wifi_only_on">La sincronizzazione è limitata alle connessioni WiFi</string>
<string name="settings_sync_wifi_only_off">Il tipo di connessione non è preso in considerazione</string>
<string name="settings_sync_wifi_only_ssids">Restrizione SSID WiFi</string>
<string name="settings_sync_wifi_only_ssids_message">Nomi (SSID) delle reti WiFi autorizzate separati da virgola (lascia vuoto per autorizzarle tutte)</string>
<string name="settings_carddav">CardDAV</string>
<string name="settings_contact_group_method">Organizzazione dei gruppi di contatto</string>
@@ -176,9 +175,12 @@
<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>
<string name="settings_event_colors">Supporto colore dell\'evento</string>
<string name="settings_event_colors_on">Sincronizza colori eventi</string>
<string name="settings_event_colors_off">Non sincronizza colori eventi</string>
<!--collection management-->
<string name="create_addressbook">Crea indirizzario</string>
<string name="create_addressbook_display_name_hint">Il mio indirizzario</string>
<string name="create_addressbook">Crea rubrica</string>
<string name="create_addressbook_display_name_hint">La mia rubrica</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>
@@ -197,6 +199,7 @@
<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>
<string name="collection_force_read_only">Forza sola lettura</string>
<!--ExceptionInfoFragment-->
<string name="exception">Si è verificato un errore.</string>
<string name="exception_httpexception">Si è verificato un errore HTTP.</string>
@@ -204,29 +207,12 @@
<string name="exception_show_details">Mostra dettagli</string>
<!--sync adapters and DebugInfoActivity-->
<string name="debug_info_title">Informazioni di debug</string>
<string name="sync_contacts_read_only_address_book">Rubrica in sola lettura</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>
<string name="sync_error_opentasks_too_old">OpenTasks troppo vecchia</string>
<string name="sync_error_opentasks_required_version">Versione richiesta: %1$s (attualmente %2$s)</string>
<string name="sync_error_authentication_failed">Autenticazione fallita (controlla credenziali login)</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>

View File

@@ -4,33 +4,41 @@
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">DAVdroid アドレス帳</string>
<string name="address_books_authority_title">アドレス帳</string>
<string name="copied_to_clipboard">クリップボードにコピーしました</string>
<string name="help">ヘルプ</string>
<string name="manage_accounts">アカウントの管理</string>
<string name="please_wait">しばらくお待ちください …</string>
<string name="send">送信</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">デバッグ中</string>
<string name="notification_channel_sync_status">同期ステータス</string>
<string name="notification_channel_sync_problems">同期の問題</string>
<string name="notification_channel_general">他の重要なメッセージ</string>
<string name="notification_channel_sync">同期</string>
<string name="notification_channel_sync_errors">同期エラー</string>
<string name="notification_channel_sync_io_errors">ネットワークおよび I/O エラー</string>
<string name="notification_channel_sync_status">ステータスメッセージ</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">バッテリー最適化</string>
<string name="startup_battery_optimization_message">Android は数日後に DAVdroid の同期を無効にする/減らすことがあります。これを防止するには、バッテリー最適化をオフにしてください。</string>
<string name="startup_autostart_permission">自動同期</string>
<string name="startup_autostart_permission_message">%s ファームウェアは自動同期をブロックすることがよくあります。 この場合、Android の設定で自動同期を許可してください。</string>
<string name="startup_battery_optimization">スケジュール同期</string>
<string name="startup_battery_optimization_message">お使いのデバイスは DAVdroid の同期を制限します。 通常の DAVdroid 同期間隔を適用するには、「バッテリ最適化」をオフにしてください。</string>
<string name="startup_battery_optimization_disable">DAVdroid 用にオフにする</string>
<string name="startup_dont_show_again">次回から表示しない</string>
<string name="startup_not_now">後で</string>
<string name="startup_donate">オープンソース情報</string>
<string name="startup_donate_message">あなたがオープンソース ソフトウェア (GPLv3) の DAVdroid を使用していただくことに、私たちは満足しています。 DAVdroid の開発はハードワークで、何千もの作業時間がかかりました。寄付をご検討ください。</string>
<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">特定の条件下で、DAVdroid を再起動後またはアップグレードした後、Play ストア DRM によりすべての DAVdroid アカウントがなくなる可能性があります。この問題の影響を受けている場合 (のみ)、Play ストアから「DAVdroid JB 回避策」をインストールしてください。</string>
<string name="startup_google_play_accounts_removed_more_info">追加情報</string>
<string name="startup_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_not_installed_message">タスクを同期するために、無料アプリのOpenTasksが必要です。 (連絡先/イベントには必要ありません)</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_libraries">ライブラリー</string>
<string name="about_version">バージョン %1s (%2d)</string>
<string name="about_build_date">コンパイル日時 %s</string>
<string name="about_flavor_info">このバージョンは Google Play での配信にのみ対応です。</string>
<string name="about_license_info_no_warranty">このプログラムは完全に無保証で提供されます。これはフリーソフトウェアで、特定の条件下での再頒布を歓迎します。</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVdroid ファイルログ</string>
@@ -92,6 +100,7 @@
<string name="account_delete">アカウントを削除</string>
<string name="account_delete_confirmation_title">アカウントを削除してもよろしいですか?</string>
<string name="account_delete_confirmation_text">アドレス帳、カレンダー、タスクリストのローカルコピーがすべて削除されます。</string>
<string name="account_select_collections_hint">同期するコレクションを選択</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
@@ -105,20 +114,7 @@
<string name="account_create_new_calendar">新しいカレンダーを作成</string>
<string name="account_no_webcal_handler_found">Webcal に対応するアプリが見つかりません</string>
<string name="account_install_icsdroid">ICSdroid をインストール</string>
<string name="account_read_only_address_book_selected">読み取り専用のアドレス帳 ローカルの変更は破棄されます!</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://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">アカウントを追加</string>
<string name="login_type_email">メールアドレスでログイン</string>
<string name="login_email_address">メールアドレス</string>
@@ -145,7 +141,7 @@
<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>
<string name="login_view_logs">詳細を表示</string>
<!--AccountSettingsActivity-->
<string name="settings_title">設定: %s</string>
<string name="settings_authentication">認証</string>
@@ -225,6 +221,9 @@
<string name="delete_collection_confirm_warning">このコレクション (%s) とそのすべてのデータがサーバーから削除されます。</string>
<string name="delete_collection_deleting_collection">コレクションの削除中</string>
<string name="collection_force_read_only">強制的に読み取り専用</string>
<string name="collection_properties">プロパティ</string>
<string name="collection_properties_url">アドレス (URL):</string>
<string name="collection_properties_copy_url">URL をコピー</string>
<!--ExceptionInfoFragment-->
<string name="exception">エラーが発生しました。</string>
<string name="exception_httpexception">HTTP エラーが発生しました。</string>
@@ -240,27 +239,12 @@
<string name="sync_error_permissions_text">追加のアクセス許可が必要です</string>
<string name="sync_error_opentasks_too_old">OpenTasks が古すぎます</string>
<string name="sync_error_opentasks_required_version">必要なバージョン: %1$s (現在 %2$s)</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>
<string name="sync_error_authentication_failed">認証に失敗しました (ログイン情報を確認してください)</string>
<string name="sync_error_io">ネットワークまたは I/O エラー %s</string>
<string name="sync_error_http_dav">HTTP サーバーエラー %s</string>
<string name="sync_error_local_storage">内蔵ストレージエラー %s</string>
<string name="sync_error_retry">再試行</string>
<string name="sync_error_view_item">アイテムを表示</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: 接続セキュリティ</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroidは、未知の証明書を検出しました。それを信頼しますか?</string>

View File

@@ -8,11 +8,7 @@
<string name="manage_accounts">Behandle kontoer</string>
<string name="please_wait">Vent…</string>
<string name="send">Send</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Batterioptimisering</string>
<string name="startup_battery_optimization_message">Det kan hende Android skrur av/reduserer DAVdroid-synkronisering etter et par dager. For å forhindre dette, skru av batterioptimisering.</string>
<string name="startup_battery_optimization_disable">Skru av for DAVdroid</string>
<string name="startup_dont_show_again">Ikke vis igjen</string>
<string name="startup_donate">Friprog-informasjon</string>
@@ -21,13 +17,10 @@
<string name="startup_donate_later">Kanskje senere</string>
<string name="startup_google_play_accounts_removed">DRM-feilinformasjon på Play-butikken</string>
<string name="startup_google_play_accounts_removed_message">Under gitte forhold vil DRM fra Play-butikken forårsake at alle DAVdroid-kontoer forsvinner etter omstart eller etter oppgradering av DAVdroid. Hvis du rammes av dette problemet (og bare da), installer \"DAVdroid JB Workaround\" fra Play-butikken.</string>
<string name="startup_google_play_accounts_removed_more_info">Mer informasjon</string>
<string name="startup_opentasks_not_installed">OpenTasks er ikke installert</string>
<string name="startup_opentasks_not_installed_message">OpenTasks-programmet er ikke tilgjengelig, så DAVdroid kan ikke synkronisere gjøremålslister.</string>
<string name="startup_opentasks_reinstall_davdroid">Etter å ha installert OpenTasks, må du reinstallere Davdroid og legge til kontoene dine igjen (Android-feil).</string>
<string name="startup_opentasks_not_installed_install">Installer OpenTasks</string>
<!--AboutActivity-->
<string name="about_license_terms">Lisensvilkår</string>
<string name="about_license_info_no_warranty">Dette programmet kommer uten NOEN FORM FOR GARANTI. Det er fri programvare, og du er velkommen til å redistribuere det under gitte forhold.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">DAVDroid fil-logging</string>
@@ -101,20 +94,7 @@
<string name="account_create_new_calendar">Opprett ny kalender</string>
<string name="account_no_webcal_handler_found">Fant ingen programmer med støtte for Webcal</string>
<string name="account_install_icsdroid">Installer ICSdroid</string>
<string name="account_read_only_address_book_selected">Adressebok uten skrivemuligheter - lokale endringer vil bli forkastet!</string>
<!--PermissionsActivity-->
<string name="permissions_title">DAVdroid-tilgang</string>
<string name="permissions_calendar">Kalender-tilgang</string>
<string name="permissions_calendar_details">For å synkronisere CalDAV-hendelser med dine lokale kalendere må DAVdroid ha tilgang til kalenderne dine.</string>
<string name="permissions_calendar_request">Forespør kalender-tilganger</string>
<string name="permissions_contacts">Kontakt-tilgang</string>
<string name="permissions_contacts_details">For å synkronisere CardDAV-adressebøker med dine lokale kontakter må DAVdroid ha tilgang til dine kontakter.</string>
<string name="permissions_contacts_request">Forespør kontakt-tilgang</string>
<string name="permissions_opentasks">OpenTasks-tilganger</string>
<string name="permissions_opentasks_details">For å synkronisere CalDAV-gjøremål med din lokale gjøremålslister må DAVdroid ha tilgang til OpenTasks.</string>
<string name="permissions_opentasks_request">Forespør OpenTasks-tilganger</string>
<!--AddAccountActivity-->
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">Legg til konto</string>
<string name="login_type_email">Innlogging med e-postadresse</string>
<string name="login_email_address">E-postadresse</string>
@@ -138,7 +118,6 @@
<string name="login_configuration_detection">Oppdagelse av oppsett</string>
<string name="login_querying_server">Vent, spør tjener…</string>
<string name="login_no_caldav_carddav">Fant ikke CalDAV eller CardDAV-tjeneste.</string>
<string name="login_view_logs">Vis logger</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Innstillinger: %s</string>
<string name="settings_authentication">Identitetsbekreftelse</string>
@@ -232,27 +211,6 @@
</plurals>
<string name="sync_error_permissions">DAVdroid-tilganger</string>
<string name="sync_error_permissions_text">Ytterligere tilganger kreves</string>
<string name="sync_error_calendar">Kalendersynkronisering feilet (%s)</string>
<string name="sync_error_contacts">Adresseboksynkronisering mislyktes (%s)</string>
<string name="sync_error_tasks">Gjøremålssynkronisering mislyktes (%s)</string>
<string name="sync_error">Feil under %s</string>
<string name="sync_error_http_dav">Tjener feil under %s</string>
<string name="sync_error_local_storage">Databasefeil under %s</string>
<string-array name="sync_error_phases">
<item>forbereder synkronisering</item>
<item>spør om muligheter</item>
<item>behandler lokalt slettede oppføringer</item>
<item>forbereder opprettede/endrede oppføringer</item>
<item>laster opp opprettede/modifiserte oppføringer</item>
<item>sjekker synkroniseringstilstand</item>
<item>listefører lokale oppføringer</item>
<item>listefører oppføringer annensteds hen</item>
<item>sammenlign lokale/oppføringer annensteds hen</item>
<item>laster ned oppføringer annensteds fra</item>
<item>etterbehandling</item>
<item>lagrer synkroniseringstilstand</item>
</string-array>
<string name="sync_error_unauthorized">Brukernavn-/passord feil</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: Tilkoblingssikkerhet</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid har støtt på et ukjent sertifikat. Har du tiltro til det?</string>

View File

@@ -8,10 +8,7 @@
<string name="manage_accounts">Beheer accounts</string>
<string name="please_wait">Een moment geduld...</string>
<string name="send">Verzenden</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?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_donate">Open-Source informatie</string>
@@ -20,13 +17,10 @@
<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>
@@ -96,17 +90,6 @@
<string name="account_create_new_calendar">Maak een nieuwe agenda</string>
<string name="account_no_webcal_handler_found">Geen mogelijke Webcal app gevonden</string>
<string name="account_install_icsdroid">ICSdroid installeren</string>
<!--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_title">Account toevoegen</string>
<string name="login_type_email">Inloggen met e-mailadres</string>
@@ -131,7 +114,6 @@
<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>
@@ -207,27 +189,6 @@
<string name="sync_contacts_read_only_address_book">Alleen-lezen adresboek</string>
<string name="sync_error_permissions">DAVdroid rechten</string>
<string name="sync_error_permissions_text">Aanvullende rechten vereist</string>
<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>

View File

@@ -8,11 +8,8 @@
<string name="manage_accounts">Zadządzaj kontami</string>
<string name="please_wait">Proszę czekać</string>
<string name="send">Wyślij</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">Debugowanie</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_donate">Informacje Open-Source</string>
@@ -21,13 +18,10 @@
<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>
@@ -44,6 +38,7 @@
<string name="navigation_drawer_news_updates">Nowości &amp; aktualizacje</string>
<string name="navigation_drawer_external_links">Zewnętrzne odnośniki</string>
<string name="navigation_drawer_website">Strona WWW</string>
<string name="navigation_drawer_manual">Ręcznie</string>
<string name="navigation_drawer_faq">Pytania i odpowiedzi</string>
<string name="navigation_drawer_forums">Pomoc / Forum</string>
<string name="navigation_drawer_donate">Dotacja</string>
@@ -101,20 +96,7 @@
<string name="account_create_new_calendar">Stwórz nowy kalendarz</string>
<string name="account_no_webcal_handler_found">Nie znaleziono aplikacji obsługującej Webcal</string>
<string name="account_install_icsdroid">Zainstaluj ICSdroid</string>
<string name="account_read_only_address_book_selected">Książka adresowa tylko do odczytu - lokalne zmiany zostaną odrzucone</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_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<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>
@@ -123,10 +105,13 @@
<string name="login_password_required">Wymagane hasło</string>
<string name="login_type_url">Logowanie za pomocą adresu URL i nazwy użytkownika</string>
<string name="login_url_must_be_http_or_https">URL musi zaczynać się od http(s)://</string>
<string name="login_url_must_be_https">Adres URL musi zaczynać się od https://</string>
<string name="login_url_host_name_required">Wymagana nazwa hosta</string>
<string name="login_user_name">Nazwa użytkownika</string>
<string name="login_user_name_required">Wymagana nazwa użytkownika</string>
<string name="login_base_url">Podstawowy URL</string>
<string name="login_type_url_certificate">Logowanie za pomocą adresu URL i certyfikatu klienta</string>
<string name="login_select_certificate">Wybierz certyfikat</string>
<string name="login_login">Zaloguj</string>
<string name="login_back">Wróć</string>
<string name="login_create_account">Stwórz konto</string>
@@ -138,7 +123,6 @@
<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>
@@ -147,6 +131,7 @@
<string name="settings_password">Hasło</string>
<string name="settings_password_summary">Zaktualizuj hasło zgodnie z serwerem.</string>
<string name="settings_enter_password">Wpisz hasło:</string>
<string name="settings_certificate_alias">Alias certyfikatu klienta</string>
<string name="settings_sync">Synchronizacja</string>
<string name="settings_sync_interval_contacts">Częstotliwość synchronizacji kontaktów</string>
<string name="settings_sync_summary_manually">Tylko ręcznie</string>
@@ -232,27 +217,8 @@
</plurals>
<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 powiodła się (%s)</string>
<string name="sync_error_contacts">Synchronizacja książki adresowej nie powiodła się (%s)</string>
<string name="sync_error_tasks">Synchronizacja zadań nie powiodła się (%s)</string>
<string name="sync_error">Błąd podczas %s</string>
<string name="sync_error_http_dav">Błąd serwera 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>
<string name="sync_error_opentasks_too_old">OpenTask jest niekompatybilny</string>
<string name="sync_error_opentasks_required_version">Wymagana wersja: %1$s (obecna %2$s)</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: Bezpieczeństwo połączenia</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid napotkał nieznany certyfikat. Czy chcesz go dodać?</string>

View File

@@ -4,31 +4,41 @@
<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="copied_to_clipboard">Copiado para a área de transferência</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>
<string name="notification_channel_debugging">Depuração</string>
<string name="notification_channel_sync_status">Status da sincronização</string>
<string name="notification_channel_sync_problems">Problemas de sincronização</string>
<string name="notification_channel_general">Outras mensagens importantes</string>
<string name="notification_channel_sync">Sincronização</string>
<string name="notification_channel_sync_errors">Erros de sincronização</string>
<string name="notification_channel_sync_io_errors">Erros de rede e E/S</string>
<string name="notification_channel_sync_status">Mensagens de status</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_autostart_permission">Sincronização automática</string>
<string name="startup_autostart_permission_message">O firmware%s frequentemente bloqueia a sincronização automática. Nesse caso, ative a sincronização automática nas configurações do seu Android.</string>
<string name="startup_battery_optimization">Sincronização agendada</string>
<string name="startup_battery_optimization_message">Seu aparelho irá restringir a sincronização do DAVdroid. Para forçar a sincronização do DAVdroid em intervalos regulares, 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_not_now">Não agora</string>
<string name="startup_donate">Informação sobre Código Aberto</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_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á disponível, não sendo possível sincronizar as listas de tarefas pelo DAVdroid.</string>
<string name="startup_opentasks_not_installed_message">Para sincronizar tarefas é necessário instalar o aplicativo livre OpenTasks. (Não é necessário para contatos/eventos)</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>
<string name="about_libraries">Bibliotecas</string>
<string name="about_version">Versão %1s (%2d)</string>
<string name="about_build_date">Compilado em %s</string>
<string name="about_flavor_info">Esta versão está disponível apenas para distribuição na Google Play.</string>
<string name="about_license_info_no_warranty">Este programa é distribuído SEM NENHUMA GARANTIA. Ele é software livre e pode ser redistribuído sob algumas condições.</string>
<!--global settings-->
<string name="logging_davdroid_file_logging">Registro do arquivo do DAVdroid</string>
@@ -90,6 +100,7 @@
<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>
<string name="account_select_collections_hint">Selecione as coleções a sincronizar</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
@@ -103,18 +114,6 @@
<string name="account_create_new_calendar">Criar novo calendário</string>
<string name="account_no_webcal_handler_found">Não foi encontrado um aplicativo capaz de lidar com Webcal</string>
<string name="account_install_icsdroid">Instalar ICSdroid</string>
<string name="account_read_only_address_book_selected">Livro de endereços somente leitura as alterações locais serão descartadas!</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>
@@ -142,7 +141,7 @@
<string name="login_configuration_detection">Detecção de configuração</string>
<string name="login_querying_server">Aguarde, procurando servidor...</string>
<string name="login_no_caldav_carddav">Não foi possível encontrar o serviço CalDAV ou CardDAV.</string>
<string name="login_view_logs">Exibir registros</string>
<string name="login_view_logs">Mostrar detalhes</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Configurações: %s</string>
<string name="settings_authentication">Autenticação</string>
@@ -223,6 +222,9 @@
<string name="delete_collection_confirm_warning">Esta coleção (%s) e todos os seus dados serão removidos do servidor.</string>
<string name="delete_collection_deleting_collection">Excluindo coleção</string>
<string name="collection_force_read_only">Forçar somente leitura</string>
<string name="collection_properties">Propriedades</string>
<string name="collection_properties_url">Endereço (URL):</string>
<string name="collection_properties_copy_url">Copiar URL</string>
<!--ExceptionInfoFragment-->
<string name="exception">Ocorreu um erro.</string>
<string name="exception_httpexception">Ocorreu um erro de HTTP.</string>
@@ -239,27 +241,12 @@
<string name="sync_error_permissions_text">É necessário permissões adicionais</string>
<string name="sync_error_opentasks_too_old">A versão do OpenTasks é muito antiga</string>
<string name="sync_error_opentasks_required_version">Versão necessária: %1$s (atual %2$s)</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>
<string name="sync_error">Erro ao %s</string>
<string name="sync_error_http_dav">Erro do servidor ao %s</string>
<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 habilidades</item>
<item>processando os itens excluídos localmente</item>
<item>preparando os itens criados/modificados</item>
<item>enviando os itens criados/modificados</item>
<item>verificando o estado da sincronização</item>
<item>listando os itens locais</item>
<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>
<string name="sync_error_authentication_failed">Falha de autenticação (verifique as credenciais)</string>
<string name="sync_error_io">Erro de rede ou E/S %s</string>
<string name="sync_error_http_dav">Erro no servidor HTTP %s</string>
<string name="sync_error_local_storage">Erro de armazenamento local %s</string>
<string name="sync_error_retry">Repetir</string>
<string name="sync_error_view_item">Ver item</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>

View File

@@ -10,7 +10,6 @@
<!--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>

View File

@@ -4,34 +4,42 @@
<string name="app_name">DAVdroid</string>
<string name="account_title_address_book">Адресная книга DAVdroid</string>
<string name="address_books_authority_title">Адресные книги</string>
<string name="copied_to_clipboard">Скопировано в буфер обмена</string>
<string name="help">Помощь</string>
<string name="manage_accounts">Управление аккаунтами</string>
<string name="please_wait">Пожалуйста, подождите …</string>
<string name="send">Отправить</string>
<string name="homepage_url">https://www.davdroid.com/?pk_campaign=davdroid-app</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">Отладка</string>
<string name="notification_channel_sync_status">Статус синхронизации</string>
<string name="notification_channel_sync_problems">Проблемы с синхронизацией</string>
<string name="notification_channel_general">Другие важные сообщения</string>
<string name="notification_channel_sync">Синхронизация</string>
<string name="notification_channel_sync_errors">Ошибки синхронизации</string>
<string name="notification_channel_sync_io_errors">Ошибки сети и ввода/вывода</string>
<string name="notification_channel_sync_status">Сообщения о состоянии</string>
<!--startup dialogs-->
<string name="startup_battery_optimization">Оптимизация батареи</string>
<string name="startup_battery_optimization_message">Андроид может отключить/понизить синхронизацию DAVdroid через несколько дней. Чтобы это предотвратить, отключите оптимизацию батареи.</string>
<string name="startup_autostart_permission">Автоматическая синхронизация</string>
<string name="startup_autostart_permission_message">%s ПО устройства часто блокирует автоматическую синхронизацию. В этом случае разрешите автоматическую синхронизацию в настройках Android.</string>
<string name="startup_battery_optimization">Синхронизация по расписанию</string>
<string name="startup_battery_optimization_message">Ваше устройство будет блокировать синхронизацию DAVdroid. Чтобы обеспечить регулярные интервалы синхронизации DAVdroid, отключите оптимизацию энергопотребления.</string>
<string name="startup_battery_optimization_disable">Отключить для DAVdroid</string>
<string name="startup_dont_show_again">Не показывать снова</string>
<string name="startup_not_now">Не сейчас</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_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_not_installed_message">Для синхронизации задач требуется бесплатное приложение OpenTasks. (Не требуется для контактов/событий.)</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>
<string name="about_libraries">Библиотеки</string>
<string name="about_version">Версия %1s (%2d)</string>
<string name="about_build_date">Скомпилировано %s</string>
<string name="about_flavor_info">Эта версия распространяется только через Google Play.</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>
@@ -80,7 +88,7 @@
<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">Показать отладочную информацию</string>
<string name="app_settings_show_debug_info_details">Просмотреть/поделиться программой и настройками</string>
<!--AccountActivity-->
<string name="account_synchronize_now">Синхронизировать</string>
@@ -92,6 +100,7 @@
<string name="account_delete">Удалить аккаунт</string>
<string name="account_delete_confirmation_title">Вы действительно хотите удалить аккаунт?</string>
<string name="account_delete_confirmation_text">Все локальные копии адресных книг, календарей и задач будут удалены.</string>
<string name="account_select_collections_hint">Выберите коллекции для синхронизации</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">WebСal</string>
@@ -105,20 +114,7 @@
<string name="account_create_new_calendar">Создать новый календарь</string>
<string name="account_no_webcal_handler_found">Не найдено приложение, поддерживающее WebCal</string>
<string name="account_install_icsdroid">Установить ICSdroid</string>
<string name="account_read_only_address_book_selected">Адресная книга только для чтения локальные изменения будут отменены!</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://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
<string name="login_title">Добавить аккаунт</string>
<string name="login_type_email">Вход с адресом электронной почты</string>
<string name="login_email_address">Адрес электронной почты</string>
@@ -145,14 +141,14 @@
<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>
<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_password_summary">Обновить пароль</string>
<string name="settings_enter_password">Введите свой пароль:</string>
<string name="settings_certificate_alias">Псевдоним сертификата клиента</string>
<string name="settings_sync">Синхронизация</string>
@@ -185,7 +181,7 @@
</string-array>
<string-array name="settings_contact_group_method_entries">
<item>Группы являются отдельными vCards</item>
<item>Группы относятся к категориям контактов</item>
<item>Группы являются категориями контактов</item>
</string-array>
<string name="settings_contact_group_method_change">Изменить метод группировки</string>
<string name="settings_contact_group_method_change_reload_contacts">Вам потребуется перезагрузить все контакты. Несохраненные локальные изменения будут отброшены.</string>
@@ -227,7 +223,10 @@
<string name="delete_collection_confirm_title">Вы уверены?</string>
<string name="delete_collection_confirm_warning">Эта коллекция (%s) и все ее данные будут удалены с сервера.</string>
<string name="delete_collection_deleting_collection">Удаление коллекции</string>
<string name="collection_force_read_only">Принудительно только для чтения</string>
<string name="collection_force_read_only">Только для чтения</string>
<string name="collection_properties">Свойства</string>
<string name="collection_properties_url">Адрес (URL):</string>
<string name="collection_properties_copy_url">Копировать URL</string>
<!--ExceptionInfoFragment-->
<string name="exception">Произошла ошибка.</string>
<string name="exception_httpexception">Произошла ошибка HTTP</string>
@@ -246,27 +245,12 @@
<string name="sync_error_permissions_text">Требуются дополнительные разрешения</string>
<string name="sync_error_opentasks_too_old">OpenTasks устарел</string>
<string name="sync_error_opentasks_required_version">Требуется версия: %1$s (текущая %2$s)</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>
<string name="sync_error_authentication_failed">Ошибка аутентификации (проверьте учетные данные)</string>
<string name="sync_error_io">Ошибка сети или ввода/вывода %s</string>
<string name="sync_error_http_dav">Ошибка HTTP-сервера %s</string>
<string name="sync_error_local_storage">Ошибка локального хранилища %s</string>
<string name="sync_error_retry">Повторить</string>
<string name="sync_error_view_item">Просмотр элемента</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: безопасность подключения</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid обнаружил неизвестный сертификат. Вы хотите доверять ему?</string>

View File

@@ -9,8 +9,6 @@
<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_donate">Подаци о отвореном кôду</string>
@@ -19,13 +17,10 @@
<string name="startup_donate_later">Можда касније</string>
<string name="startup_google_play_accounts_removed">Грешка ДРМ-а Плеј продавнице</string>
<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_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>
@@ -88,17 +83,6 @@
<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>
@@ -123,7 +107,6 @@
<string name="login_configuration_detection">Откривање конфигурације</string>
<string name="login_querying_server">Сачекајте, шаљем упит серверу…</string>
<string name="login_no_caldav_carddav">Не могох да нађем КалДАВ или КардДАВ услугу.</string>
<string name="login_view_logs">Прикажи записе</string>
<!--AccountSettingsActivity-->
<string name="settings_title">Поставке: %s</string>
<string name="settings_authentication">Аутентификација</string>
@@ -193,27 +176,6 @@
<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>
<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">ДАВдроид: Безбедност везе</string>
<string name="trust_certificate_unknown_certificate_found">ДАВдроид је наишао на непознат сертификат. Желите ли да се поуздате у њега?</string>

View File

@@ -14,13 +14,10 @@
<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>
@@ -66,18 +63,6 @@
<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>
@@ -101,7 +86,6 @@
<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>
@@ -123,6 +107,7 @@
<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="one">%d günden daha eski olaylar göz ardı edilecektir</item>
<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>
@@ -160,12 +145,5 @@
<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

@@ -8,9 +8,13 @@
<string name="manage_accounts">Керування обліковими записами</string>
<string name="please_wait">Будь ласка, зачекайте...</string>
<string name="send">Відправити</string>
<string name="notification_channel_debugging">Зневадження</string>
<string name="notification_channel_general">Інші важливі повідомлення</string>
<string name="notification_channel_sync">Синхронізація</string>
<string name="notification_channel_sync_errors">Помилки синхронізації</string>
<string name="notification_channel_sync_io_errors">Помилка мережі та вводу/виводу</string>
<string name="notification_channel_sync_status">Повідомлення про стан</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_donate">Інформація Open-Source</string>
@@ -19,13 +23,12 @@
<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_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_not_installed_message">Для синхронізації завдань необхідно встановити додаток OpenTasks. (Не має потреби для контактів/подій.)</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>
@@ -37,11 +40,14 @@
<string name="navigation_drawer_close">Закрити панель навігації</string>
<string name="navigation_drawer_subtitle">Адаптер синхронізації CalDAV/CardDAV</string>
<string name="navigation_drawer_about">Про / Ліцензія</string>
<string name="navigation_drawer_beta_feedback">Beta відгук</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_manual">Посібник</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>
@@ -84,21 +90,19 @@
<string name="account_delete">Видалити запис</string>
<string name="account_delete_confirmation_title">Дійсно видалити обліковий запис?</string>
<string name="account_delete_confirmation_text">Всі локальні копії адресних книг, календарів та завдань будуть вилучені.</string>
<string name="account_carddav">CardDAV</string>
<string name="account_caldav">CalDAV</string>
<string name="account_webcal">Webcal</string>
<string name="account_synchronize_this_collection">синхронізувати дану колекцію</string>
<string name="account_read_only">лише читання</string>
<string name="account_calendar">календар</string>
<string name="account_task_list">перелік завдань</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>
<string name="account_no_webcal_handler_found">Не знайдено додатку з підтримкою Webcal</string>
<string name="account_install_icsdroid">Встановити ICSdroid</string>
<!--AddAccountActivity-->
<string name="login_title">Додати запис</string>
<string name="login_type_email">Увійти за допомогою електронної пошти</string>
@@ -108,10 +112,13 @@
<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_must_be_https">Посилання повинно починатися з https://</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_type_url_certificate">Вхід по посиланню та сертифікату клієнта</string>
<string name="login_select_certificate">Обрати сертифікат</string>
<string name="login_login">Увійти</string>
<string name="login_back">Назад</string>
<string name="login_create_account">Створити запис</string>
@@ -123,7 +130,6 @@
<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>
@@ -132,33 +138,58 @@
<string name="settings_password">Пароль</string>
<string name="settings_password_summary">Оновити пароль, згідно налаштувань Вашого сервера.</string>
<string name="settings_enter_password">Введіть Ваш пароль:</string>
<string name="settings_certificate_alias">Прив\'язка сертифікату клієнта</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_interval_calendars">Інтервал синхронізації календарів</string>
<string name="settings_sync_interval_tasks">Інтервал синхронізації завдань</string>
<string-array name="settings_sync_interval_names">
<item>Вручну</item>
<item>Кожні 15 хвилин</item>
<item>Кожні 30 хвилин</item>
<item>Щогодинно</item>
<item>Кожні 2 години</item>
<item>Кожні 4 години</item>
<item>Щоденно</item>
</string-array>
<string name="settings_sync_wifi_only">Синхронізувати лише через 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_ssids">Обмеження WiFi SSID</string>
<string name="settings_sync_wifi_only_ssids_on">Синхронізувати лише через %s</string>
<string name="settings_sync_wifi_only_ssids_off">Може використовуватись всі Wi-Fi з\'єднання</string>
<string name="settings_sync_wifi_only_ssids_message">Назви (SSID) дозволених Wi-Fi мереж, розділені комами (залиште порожнім для всіх)</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>Групи як окремі VCard</item>
<item>Групи як категорії в середині контактів</item>
</string-array>
<string name="settings_contact_group_method_change">Змінити метод групування</string>
<string name="settings_contact_group_method_change_reload_contacts">Це потребує перезавантаження всіх контактів. Незбережені локальні зміни буде відхилено.</string>
<string name="settings_caldav">CalDAV</string>
<string name="settings_sync_time_range_past">Інтервал синхронізації</string>
<string name="settings_sync_time_range_past_none">Всі події будуть синхронізовані</string>
<plurals name="settings_sync_time_range_past_days">
<item quantity="one">Події старші одного дня будуть проігноровані</item>
<item quantity="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>
<string name="settings_event_colors">Підтримка кольорів подій</string>
<string name="settings_event_colors_on">Синхронізувати кольори подій</string>
<string name="settings_event_colors_off">Не синхронізувати кольори подій</string>
<string name="settings_event_colors_off_confirm">Вимикання кольорів подій може видалити вже синхронізовані кольори подій.</string>
<!--collection management-->
<string name="create_addressbook">Створити адресну книгу</string>
<string name="create_addressbook_display_name_hint">Моя адресна книга</string>
@@ -180,6 +211,7 @@
<string name="delete_collection_confirm_title">Ви впевнені?</string>
<string name="delete_collection_confirm_warning">Колекція (%s) та всі пов\'язані данні будуть вилучені з даного серверу.</string>
<string name="delete_collection_deleting_collection">Видалення колекції</string>
<string name="collection_force_read_only">Примусово лише читання</string>
<!--ExceptionInfoFragment-->
<string name="exception">Трапилась помилка.</string>
<string name="exception_httpexception">Трапилась помилка HTTP.</string>
@@ -187,29 +219,23 @@
<string name="exception_show_details">Показати подробиці</string>
<!--sync adapters and DebugInfoActivity-->
<string name="debug_info_title">Інформація зневадження</string>
<string name="sync_contacts_read_only_address_book">Адресна книга лише для читання</string>
<plurals name="sync_contacts_local_contact_changes_discarded">
<item quantity="one">Локальну зміну контакту відхилено</item>
<item quantity="few">%d локальні зміни контакту відхилено</item>
<item quantity="many">%d локальних змін контакту відхилено</item>
<item quantity="other">%d локальних змін контакту відхилено</item>
</plurals>
<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>
<string name="sync_error_opentasks_too_old">OpenTasks застарів</string>
<string name="sync_error_opentasks_required_version">Необхідна версія: %1$s (поточна %2$s)</string>
<string name="sync_error_authentication_failed">Помилка аутентифікації (перевірте обліковий запис)</string>
<string name="sync_error_io">Помилка мережі та вводу/виводу — %s</string>
<string name="sync_error_http_dav">Помилка сервера HTTP — %s</string>
<string name="sync_error_local_storage">Помилка локального сховища — %s</string>
<string name="sync_error_retry">Повторити</string>
<string name="sync_error_view_item">Перегляд елементу</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: Безпека з\'єднання</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid зіткнувся з невідомим сертифікатом. Чи довіряти йому?</string>

View File

@@ -1,5 +1,5 @@
<!--
~ Copyright © 2013 2016 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,11 +8,10 @@
<resources>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<style name="AppThemeExt" parent="AppTheme">
<item name="android:windowActivityTransitions">true</item>
<item name="android:windowEnterTransition">@android:transition/slide_right</item>
<item name="android:windowExitTransition">@android:transition/slide_left</item>
</style>
</resources>

View File

@@ -8,10 +8,8 @@
<string name="manage_accounts">管理账户</string>
<string name="please_wait">请稍等...</string>
<string name="send">发送</string>
<string name="beta_feedback_url">https://forums.bitfire.at/category/9/beta-test-discussion?pk_campaign=davdroid-app</string>
<string name="notification_channel_debugging">调试</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_donate">开源信息</string>
@@ -20,13 +18,10 @@
<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_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 后,由于 Android 的限制,请重新安装 DAVdroid 并重新创建账户。</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>
@@ -43,10 +38,11 @@
<string name="navigation_drawer_news_updates">最新消息</string>
<string name="navigation_drawer_external_links">外部链接</string>
<string name="navigation_drawer_website">应用网站</string>
<string name="navigation_drawer_manual">手动</string>
<string name="navigation_drawer_faq">常见问题</string>
<string name="navigation_drawer_forums">帮助 / 论坛</string>
<string name="navigation_drawer_donate">捐助</string>
<string name="account_list_empty">欢迎使用 DAVdroid\n\n现在你可以增加 CalDAV/CardDAV 账户。</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-->
@@ -100,18 +96,6 @@
<string name="account_create_new_calendar">创建日历</string>
<string name="account_no_webcal_handler_found">找不到支持 Webcal 的应用</string>
<string name="account_install_icsdroid">安装 ICSdroid</string>
<string name="account_read_only_address_book_selected">这是只读通讯录,本地修改会被自动撤销!</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>
@@ -120,11 +104,14 @@
<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_must_be_http_or_https">URL 以 http(s):// 为开头</string>
<string name="login_url_must_be_https">URL 需以 https:// 为开头</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">根地址</string>
<string name="login_type_url_certificate">使用 URL 和客户端证书登录</string>
<string name="login_select_certificate">选择证书</string>
<string name="login_login">登录</string>
<string name="login_back">返回</string>
<string name="login_create_account">创建账户</string>
@@ -136,7 +123,6 @@
<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>
@@ -145,6 +131,7 @@
<string name="settings_password">密码</string>
<string name="settings_password_summary">修改服务器密码</string>
<string name="settings_enter_password">输入密码</string>
<string name="settings_certificate_alias">客户端证书别名</string>
<string name="settings_sync">同步</string>
<string name="settings_sync_interval_contacts">通讯录自动同步间隔</string>
<string name="settings_sync_summary_manually">手动同步</string>
@@ -228,27 +215,8 @@
</plurals>
<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>
<string name="sync_error_opentasks_too_old">OpenTasks 版本太旧</string>
<string name="sync_error_opentasks_required_version">最低版本 %1$s (当前 %2$s</string>
<!--cert4android-->
<string name="certificate_notification_connection_security">DAVdroid: 连接安全性</string>
<string name="trust_certificate_unknown_certificate_found">DAVdroid 遇到了未知证书。你是否要信任该证书?</string>

View File

@@ -9,8 +9,6 @@
<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_donate">開源資訊</string>
@@ -19,13 +17,10 @@
<string name="startup_donate_later">下次再說</string>
<string name="startup_google_play_accounts_removed">Play商店數位權利管理錯誤訊息</string>
<string name="startup_google_play_accounts_removed_message">在某些情況下Play商店 的數位權利管理可能導致在重開機後或更新 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 這個 app 不存在您的裝置上,所以 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>
@@ -87,17 +82,6 @@
<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">用 Email 地址登入</string>
@@ -122,7 +106,6 @@
<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>
@@ -190,27 +173,6 @@
<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>

View File

@@ -1,7 +0,0 @@
<?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

@@ -40,6 +40,10 @@
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
<!-- android.permission-group.LOCATION -->
<!-- required since Android 8.1 to get the WiFi name (for "sync in Wifi only" feature) -->
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- ical4android declares task access permissions -->
<application
@@ -49,7 +53,7 @@
android:networkSecurityConfig="@xml/network_security_config"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:theme="@style/AppThemeExt"
tools:ignore="UnusedAttribute">
<service android:name=".DavService"/>
@@ -67,7 +71,7 @@
<activity
android:name=".ui.AboutActivity"
android:label="@string/app_name"
android:label="@string/navigation_drawer_about"
android:theme="@style/AppTheme.NoActionBar"
android:parentActivityName=".ui.AccountsActivity"/>
@@ -76,10 +80,6 @@
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"

View File

@@ -1,153 +0,0 @@
<h3>Apache License, Version 2.0, January 2004</h3>
<p><a href="http://www.apache.org/licenses/">http://www.apache.org/licenses/</a> </p>
<p>TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION</p>
<p><strong><a name="definitions">1. Definitions</a></strong>.</p>
<p>"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.</p>
<p>"Licensor" shall mean the copyright owner or entity authorized by the
copyright owner that is granting the License.</p>
<p>"Legal Entity" shall mean the union of the acting entity and all other
entities that control, are controlled by, or are under common control with
that entity. For the purposes of this definition, "control" means (i) the
power, direct or indirect, to cause the direction or management of such
entity, whether by contract or otherwise, or (ii) ownership of fifty
percent (50%) or more of the outstanding shares, or (iii) beneficial
ownership of such entity.</p>
<p>"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.</p>
<p>"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation source,
and configuration files.</p>
<p>"Object" form shall mean any form resulting from mechanical transformation
or translation of a Source form, including but not limited to compiled
object code, generated documentation, and conversions to other media types.</p>
<p>"Work" shall mean the work of authorship, whether in Source or Object form,
made available under the License, as indicated by a copyright notice that
is included in or attached to the work (an example is provided in the
Appendix below).</p>
<p>"Derivative Works" shall mean any work, whether in Source or Object form,
that is based on (or derived from) the Work and for which the editorial
revisions, annotations, elaborations, or other modifications represent, as
a whole, an original work of authorship. For the purposes of this License,
Derivative Works shall not include works that remain separable from, or
merely link (or bind by name) to the interfaces of, the Work and Derivative
Works thereof.</p>
<p>"Contribution" shall mean any work of authorship, including the original
version of the Work and any modifications or additions to that Work or
Derivative Works thereof, that is intentionally submitted to Licensor for
inclusion in the Work by the copyright owner or by an individual or Legal
Entity authorized to submit on behalf of the copyright owner. For the
purposes of this definition, "submitted" means any form of electronic,
verbal, or written communication sent to the Licensor or its
representatives, including but not limited to communication on electronic
mailing lists, source code control systems, and issue tracking systems that
are managed by, or on behalf of, the Licensor for the purpose of discussing
and improving the Work, but excluding communication that is conspicuously
marked or otherwise designated in writing by the copyright owner as "Not a
Contribution."</p>
<p>"Contributor" shall mean Licensor and any individual or Legal Entity on
behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.</p>
<p><strong><a name="copyright">2. Grant of Copyright License</a></strong>. Subject to the
terms and conditions of this License, each Contributor hereby grants to You
a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of, publicly
display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.</p>
<p><strong><a name="patent">3. Grant of Patent License</a></strong>. Subject to the terms
and conditions of this License, each Contributor hereby grants to You a
perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made, use,
offer to sell, sell, import, and otherwise transfer the Work, where such
license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by
combination of their Contribution(s) with the Work to which such
Contribution(s) was submitted. If You institute patent litigation against
any entity (including a cross-claim or counterclaim in a lawsuit) alleging
that the Work or a Contribution incorporated within the Work constitutes
direct or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate as of the
date such litigation is filed.</p>
<p><strong><a name="redistribution">4. Redistribution</a></strong>. You may reproduce and
distribute copies of the Work or Derivative Works thereof in any medium,
with or without modifications, and in Source or Object form, provided that
You meet the following conditions:</p>
<p>a. You must give any other recipients of the Work or Derivative Works a
copy of this License; and</p>
<p>b. You must cause any modified files to carry prominent notices stating
that You changed the files; and</p>
<p>c. You must retain, in the Source form of any Derivative Works that You
distribute, all copyright, patent, trademark, and attribution notices from
the Source form of the Work, excluding those notices that do not pertain to
any part of the Derivative Works; and</p>
<p>d. If the Work includes a "NOTICE" text file as part of its distribution,
then any Derivative Works that You distribute must include a readable copy
of the attribution notices contained within such NOTICE file, excluding
those notices that do not pertain to any part of the Derivative Works, in
at least one of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or documentation,
if provided along with the Derivative Works; or, within a display generated
by the Derivative Works, if and wherever such third-party notices normally
appear. The contents of the NOTICE file are for informational purposes only
and do not modify the License. You may add Your own attribution notices
within Derivative Works that You distribute, alongside or as an addendum to
the NOTICE text from the Work, provided that such additional attribution
notices cannot be construed as modifying the License.
<br/>
<br/>
You may add Your own copyright statement to Your modifications and may
provide additional or different license terms and conditions for use,
reproduction, or distribution of Your modifications, or for any such
Derivative Works as a whole, provided Your use, reproduction, and
distribution of the Work otherwise complies with the conditions stated in
this License.
</p>
<p><strong><a name="contributions">5. Submission of Contributions</a></strong>. Unless You
explicitly state otherwise, any Contribution intentionally submitted for
inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the
terms of any separate license agreement you may have executed with Licensor
regarding such Contributions.</p>
<p><strong><a name="trademarks">6. Trademarks</a></strong>. This License does not grant
permission to use the trade names, trademarks, service marks, or product
names of the Licensor, except as required for reasonable and customary use
in describing the origin of the Work and reproducing the content of the
NOTICE file.</p>
<p><strong><a name="no-warranty">7. Disclaimer of Warranty</a></strong>. Unless required by
applicable law or agreed to in writing, Licensor provides the Work (and
each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,
without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You
are solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise
of permissions under this License.</p>
<p><strong><a name="no-liability">8. Limitation of Liability</a></strong>. In no event and
under no legal theory, whether in tort (including negligence), contract, or
otherwise, unless required by applicable law (such as deliberate and
grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a result
of this License or out of the use or inability to use the Work (including
but not limited to damages for loss of goodwill, work stoppage, computer
failure or malfunction, or any and all other commercial damages or losses),
even if such Contributor has been advised of the possibility of such
damages.</p>
<p><strong><a name="additional">9. Accepting Warranty or Additional Liability</a></strong>.
While redistributing the Work or Derivative Works thereof, You may choose
to offer, and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this License.
However, in accepting such obligations, You may act only on Your own behalf
and on Your sole responsibility, not on behalf of any other Contributor,
and only if You agree to indemnify, defend, and hold each Contributor
harmless for any liability incurred by, or claims asserted against, such
Contributor by reason of your accepting any such warranty or additional
liability.</p>
<p>END OF TERMS AND CONDITIONS</p>

View File

@@ -1,28 +0,0 @@
<h3>BSD License (3-clause)</h3>
<p>Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:</p>
<p>o Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.</p>
<p>o Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.</p>
<p>o Neither the name of Ben Fortuna nor the names of any other contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.</p>
<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>

View File

@@ -1,23 +0,0 @@
<h3>BSD License</h3>
<p>Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:</p>
<p>1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.</p>
<p>2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.</p>
<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>

View File

@@ -1,7 +0,0 @@
<h3>The MIT License (MIT)</h3>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>
<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>

View File

@@ -9,6 +9,7 @@ package at.bitfire.davdroid
import android.accounts.Account
import android.accounts.AccountManager
import android.annotation.SuppressLint
import android.content.*
import android.os.Build
import android.os.Bundle
@@ -22,6 +23,7 @@ import at.bitfire.davdroid.model.Credentials
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.ServiceDB.*
import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.model.SyncState
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.resource.LocalTaskList
@@ -38,7 +40,12 @@ import org.dmfs.tasks.contract.TaskContract
import java.util.*
import java.util.logging.Level
class AccountSettings @Throws(InvalidAccountException::class) constructor(
/**
* Manages settings of an account.
*
* @throws InvalidAccountException on construction when the account doesn't exist (anymore)
*/
class AccountSettings(
val context: Context,
val settings: ISettings,
val account: Account
@@ -46,41 +53,40 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
companion object {
val CURRENT_VERSION = 8
val KEY_SETTINGS_VERSION = "version"
const val CURRENT_VERSION = 8
const val KEY_SETTINGS_VERSION = "version"
val KEY_USERNAME = "user_name"
val KEY_CERTIFICATE_ALIAS = "certificate_alias"
const val KEY_USERNAME = "user_name"
const val KEY_CERTIFICATE_ALIAS = "certificate_alias"
val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false)
val KEY_WIFI_ONLY_SSIDS = "wifi_only_ssids" // restrict sync to specific WiFi SSIDs
const val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false)
const val KEY_WIFI_ONLY_SSIDS = "wifi_only_ssids" // restrict sync to specific WiFi SSIDs
/** Time range limitation to the past [in days]
value = null default value (DEFAULT_TIME_RANGE_PAST_DAYS)
< 0 (-1) no limit
>= 0 entries more than n days in the past won't be synchronized
*/
val KEY_TIME_RANGE_PAST_DAYS = "time_range_past_days"
val DEFAULT_TIME_RANGE_PAST_DAYS = 90
const val KEY_TIME_RANGE_PAST_DAYS = "time_range_past_days"
const val DEFAULT_TIME_RANGE_PAST_DAYS = 90
/* Whether DAVdroid sets the local calendar color to the value from service DB at every sync
value = null (not existing) true (default)
"0" false */
val KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors"
const val KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors"
/* Whether DAVdroid populates and uses CalendarContract.Colors
value = null (not existing) false (default)
"1" true */
val KEY_EVENT_COLORS = "event_colors"
const val KEY_EVENT_COLORS = "event_colors"
/** Contact group method:
value = null (not existing) groups as separate VCards (default)
"CATEGORIES" groups are per-contact CATEGORIES
*/
val KEY_CONTACT_GROUP_METHOD = "contact_group_method"
const val KEY_CONTACT_GROUP_METHOD = "contact_group_method"
@JvmField
val SYNC_INTERVAL_MANUALLY = -1L
const val SYNC_INTERVAL_MANUALLY = -1L
fun initialUserData(credentials: Credentials): Bundle {
val bundle = Bundle(2)
@@ -172,11 +178,11 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
fun getTimeRangePastDays(): Int? {
val strDays = accountManager.getUserData(account, KEY_TIME_RANGE_PAST_DAYS)
if (strDays != null) {
return if (strDays != null) {
val days = Integer.valueOf(strDays)
return if (days < 0) null else days
if (days < 0) null else days
} else
return DEFAULT_TIME_RANGE_PAST_DAYS
DEFAULT_TIME_RANGE_PAST_DAYS
}
fun setTimeRangePastDays(days: Int?) =
@@ -234,27 +240,32 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
}
@Suppress("unused")
/**
* There is a mistake in this method. [TaskContract.Tasks.SYNC_VERSION] is used to store the
* SEQUENCE and should not be used for the eTag.
*/
private fun update_7_8() {
TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)?.let { provider ->
// ETag is now in sync_version instead of sync1
// UID is now in _uid instead of sync2
val cursor = provider.client.query(TaskProvider.syncAdapterUri(provider.tasksUri(), account),
provider.client.query(TaskProvider.syncAdapterUri(provider.tasksUri(), account),
arrayOf(TaskContract.Tasks._ID, TaskContract.Tasks.SYNC1, TaskContract.Tasks.SYNC2),
"${TaskContract.Tasks.ACCOUNT_TYPE}=? AND ${TaskContract.Tasks.ACCOUNT_NAME}=?",
arrayOf(account.type, account.name), null)
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
val eTag = cursor.getString(1)
val uid = cursor.getString(2)
val values = ContentValues(4)
values.put(TaskContract.Tasks._UID, uid)
values.put(TaskContract.Tasks.SYNC_VERSION, eTag)
values.putNull(TaskContract.Tasks.SYNC1)
values.putNull(TaskContract.Tasks.SYNC2)
Logger.log.log(Level.FINER, "Updating task $id", values)
provider.client.update(
TaskProvider.syncAdapterUri(ContentUris.withAppendedId(provider.tasksUri(), id), account),
values, null, null)
arrayOf(account.type, account.name), null).use { cursor ->
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
val eTag = cursor.getString(1)
val uid = cursor.getString(2)
val values = ContentValues(4)
values.put(TaskContract.Tasks._UID, uid)
values.put(TaskContract.Tasks.SYNC_VERSION, eTag)
values.putNull(TaskContract.Tasks.SYNC1)
values.putNull(TaskContract.Tasks.SYNC2)
Logger.log.log(Level.FINER, "Updating task $id", values)
provider.client.update(
TaskProvider.syncAdapterUri(ContentUris.withAppendedId(provider.tasksUri(), id), account),
values, null, null)
}
}
}
}
@@ -280,6 +291,7 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
}
@Suppress("unused")
@SuppressLint("ParcelClassLoader")
private fun update_5_6() {
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.let { provider ->
val parcel = Parcel.obtain()
@@ -297,7 +309,7 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
parcel.unmarshall(raw, 0, raw.size)
parcel.setDataPosition(0)
val params = parcel.readBundle()
val url = params.getString("url")
val url = params.getString("url")?.let { HttpUrl.parse(it) }
if (url == null)
Logger.log.info("No address book URL, ignoring account")
else {
@@ -307,7 +319,7 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
info.displayName = account.name
Logger.log.log(Level.INFO, "Creating new address book account", url)
val addressBookAccount = Account(LocalAddressBook.accountName(account, info), context.getString(R.string.account_type_address_book))
if (!accountManager.addAccountExplicitly(addressBookAccount, null, LocalAddressBook.initialUserData(account, info.url)))
if (!accountManager.addAccountExplicitly(addressBookAccount, null, LocalAddressBook.initialUserData(account, info.url.toString())))
throw ContactsStorageException("Couldn't create address book account")
// move contacts to new address book
@@ -374,7 +386,7 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.let { client ->
try {
val addrBook = LocalAddressBook(context, account, client)
val url = addrBook.getURL()
val url = addrBook.url
Logger.log.fine("Migrating address book $url")
// insert CardDAV service
@@ -487,6 +499,7 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
}
@Suppress("unused")
@SuppressLint("Recycle")
private fun update_1_2() {
/* - KEY_ADDRESSBOOK_URL ("addressbook_url"),
- KEY_ADDRESSBOOK_CTAG ("addressbook_ctag"),
@@ -504,16 +517,16 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
// until now, ContactsContract.Settings.UNGROUPED_VISIBLE was not set explicitly
val values = ContentValues()
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
addr.updateSettings(values)
addr.settings = values
val url = accountManager.getUserData(account, "addressbook_url")
if (!url.isNullOrEmpty())
addr.setURL(url)
addr.url = url
accountManager.setUserData(account, "addressbook_url", null)
val cTag = accountManager.getUserData (account, "addressbook_ctag")
if (!cTag.isNullOrEmpty())
addr.setCTag(cTag)
addr.lastSyncState = SyncState(SyncState.Type.CTAG, cTag)
accountManager.setUserData(account, "addressbook_ctag", null)
} finally {
if (Build.VERSION.SDK_INT >= 24)

View File

@@ -14,40 +14,42 @@ import android.content.Intent
import android.content.IntentFilter
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Build
import android.os.StrictMode
import android.support.v7.app.AppCompatDelegate
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.NotificationUtils
import kotlin.concurrent.thread
class App: Application() {
companion object {
@JvmField val FLAVOR_GOOGLE_PLAY = "gplay"
@JvmField val FLAVOR_ICLOUD = "icloud"
@JvmField val FLAVOR_MANAGED = "managed"
@JvmField val FLAVOR_SOLDUPE = "soldupe"
@JvmField val FLAVOR_STANDARD = "standard"
const val FLAVOR_GOOGLE_PLAY = "gplay"
const val FLAVOR_ICLOUD = "icloud"
const val FLAVOR_MANAGED = "managed"
const val FLAVOR_SOLDUPE = "soldupe"
const val FLAVOR_STANDARD = "standard"
val ORGANIZATION = "organization"
val ORGANIZATION_LOGO_URL = "logo_url"
const val ORGANIZATION = "organization"
const val ORGANIZATION_LOGO_URL = "logo_url"
val SUPPORT_HOMEPAGE = "support_homepage_url"
val SUPPORT_PHONE = "support_phone_number"
val SUPPORT_EMAIL = "support_email_address"
const val SUPPORT_HOMEPAGE = "support_homepage_url"
const val SUPPORT_PHONE = "support_phone_number"
const val SUPPORT_EMAIL = "support_email_address"
val MAX_ACCOUNTS = "max_accounts"
const val MAX_ACCOUNTS = "max_accounts"
@JvmField val DISTRUST_SYSTEM_CERTIFICATES = "distrust_system_certs"
@JvmField val OVERRIDE_PROXY = "override_proxy"
@JvmField val OVERRIDE_PROXY_HOST = "override_proxy_host"
@JvmField val OVERRIDE_PROXY_PORT = "override_proxy_port"
const val DISTRUST_SYSTEM_CERTIFICATES = "distrust_system_certs"
const val OVERRIDE_PROXY = "override_proxy"
const val OVERRIDE_PROXY_HOST = "override_proxy_host"
const val OVERRIDE_PROXY_PORT = "override_proxy_port"
@JvmField val OVERRIDE_PROXY_HOST_DEFAULT = "localhost"
@JvmField val OVERRIDE_PROXY_PORT_DEFAULT = 8118
const val OVERRIDE_PROXY_HOST_DEFAULT = "localhost"
const val OVERRIDE_PROXY_PORT_DEFAULT = 8118
@JvmStatic
fun getLauncherBitmap(context: Context): Bitmap? {
val drawableLogo = if (android.os.Build.VERSION.SDK_INT >= 21)
context.getDrawable(R.mipmap.ic_launcher)
@@ -60,6 +62,13 @@ class App: Application() {
null
}
fun homepageUrl(context: Context) =
Uri.parse(context.getString(R.string.homepage_url)).buildUpon()
.appendQueryParameter("pk_campaign", BuildConfig.APPLICATION_ID)
.appendQueryParameter("pk_kwd", context::class.java.simpleName)
.appendQueryParameter("app-version", BuildConfig.VERSION_NAME)
.build()!!
}
@@ -67,9 +76,28 @@ class App: Application() {
super.onCreate()
Logger.initialize(this)
if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectActivityLeaks()
.detectFileUriExposure()
.detectLeakedClosableObjects()
.detectLeakedRegistrationObjects()
.detectLeakedSqlLiteObjects()
.penaltyLog()
.build())
// main thread
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build())
}
if (Build.VERSION.SDK_INT <= 21)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
NotificationUtils.createChannels(this)
// don't block UI for some background checks
thread {
// watch installed/removed apps

View File

@@ -9,16 +9,22 @@ package at.bitfire.davdroid
object Constants {
// notification IDs
@JvmField val NOTIFICATION_EXTERNAL_FILE_LOGGING = 1
@JvmField val NOTIFICATION_REFRESH_COLLECTIONS = 2
@JvmField val NOTIFICATION_CONTACTS_SYNC = 10
@JvmField val NOTIFICATION_CALENDAR_SYNC = 11
@JvmField val NOTIFICATION_TASK_SYNC = 12
@JvmField val NOTIFICATION_PERMISSIONS = 20
@JvmField val NOTIFICATION_SUBSCRIPTION = 21
const val DAVDROID_GREEN_RGBA = 0xFF8bc34a.toInt()
@JvmField
val DEFAULT_SYNC_INTERVAL = 4 * 3600L // 4 hours
const val DEFAULT_SYNC_INTERVAL = 4 * 3600L // 4 hours
/**
* Context label for [org.apache.commons.lang3.exception.ContextedException].
* Context value is the [at.bitfire.davdroid.resource.LocalResource]
* which is related to the exception cause.
*/
const val EXCEPTION_CONTEXT_LOCAL_RESOURCE = "localResource"
/**
* Context label for [org.apache.commons.lang3.exception.ContextedException].
* Context value is the [okhttp3.HttpUrl] of the remote resource
* which is related to the exception cause.
*/
const val EXCEPTION_CONTEXT_REMOTE_RESOURCE = "remoteResource"
}

View File

@@ -11,13 +11,17 @@ package at.bitfire.davdroid
import android.accounts.Account
import android.app.PendingIntent
import android.app.Service
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Intent
import android.database.DatabaseUtils
import android.database.sqlite.SQLiteDatabase
import android.os.Binder
import android.os.Bundle
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.UrlUtils
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.exception.HttpException
@@ -30,8 +34,7 @@ import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import okhttp3.HttpUrl
import org.apache.commons.collections4.iterators.IteratorChain
import org.apache.commons.collections4.iterators.SingletonIterator
import okhttp3.OkHttpClient
import java.io.IOException
import java.lang.ref.WeakReference
import java.util.*
@@ -41,8 +44,14 @@ import kotlin.concurrent.thread
class DavService: Service() {
companion object {
@JvmField val ACTION_REFRESH_COLLECTIONS = "refreshCollections"
@JvmField val EXTRA_DAV_SERVICE_ID = "davServiceID"
const val ACTION_REFRESH_COLLECTIONS = "refreshCollections"
const val EXTRA_DAV_SERVICE_ID = "davServiceID"
/** Initialize a forced synchronization. Expects intent data
to be an URI of this format:
contents://<authority>/<account.type>/<account name>
**/
const val ACTION_FORCE_SYNC = "forceSync"
}
private val runningRefresh = HashSet<Long>()
@@ -59,6 +68,15 @@ class DavService: Service() {
thread { refreshCollections(id) }
refreshingStatusListeners.forEach { it.get()?.onDavRefreshStatusChanged(id, true) }
}
ACTION_FORCE_SYNC -> {
val authority = intent.data.authority
val account = Account(
intent.data.pathSegments[1],
intent.data.pathSegments[0]
)
forceSync(authority, account)
}
}
}
@@ -103,6 +121,14 @@ class DavService: Service() {
which actually do the work
*/
private fun forceSync(authority: String, account: Account) {
Logger.log.info("Forcing $authority synchronization of $account")
val extras = Bundle(2)
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true) // manual sync
extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue)
ContentResolver.requestSync(account, authority, extras)
}
private fun refreshCollections(service: Long) {
OpenHelper(this@DavService).use { dbHelper ->
val db = dbHelper.writableDatabase
@@ -155,24 +181,68 @@ class DavService: Service() {
/**
* Checks if the given URL defines home sets and adds them to the home set list.
* @param dav DavResource to check
*/
@Throws(IOException::class, HttpException::class, DavException::class)
fun queryHomeSets(dav: DavResource) {
when (serviceType) {
Services.SERVICE_CARDDAV -> {
dav.propfind(0, AddressbookHomeSet.NAME, GroupMembership.NAME)
for ((resource, addressbookHomeSet) in dav.findProperties(AddressbookHomeSet::class.java))
for (href in addressbookHomeSet.hrefs)
resource.location.resolve(href)?.let { homeSets += UrlUtils.withTrailingSlash(it) }
fun queryHomeSets(client: OkHttpClient, url: HttpUrl, recurse: Boolean = true) {
var related = setOf<HttpUrl>()
fun findRelated(root: HttpUrl, dav: Response) {
// refresh home sets: calendar-proxy-read/write-for
dav[CalendarProxyReadFor::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is a read-only proxy for $href, checking for home sets")
root.resolve(href)?.let {
related += it
}
}
}
Services.SERVICE_CALDAV -> {
dav.propfind(0, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME)
for ((resource, calendarHomeSet) in dav.findProperties(CalendarHomeSet::class.java))
for (href in calendarHomeSet.hrefs)
resource.location.resolve(href)?.let { homeSets.add(UrlUtils.withTrailingSlash(it)) }
dav[CalendarProxyWriteFor::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is a read/write proxy for $href, checking for home sets")
root.resolve(href)?.let {
related += it
}
}
}
// refresh home sets: direct group memberships
dav[GroupMembership::class.java]?.let {
for (href in it.hrefs) {
Logger.log.fine("Principal is member of group $href, checking for home sets")
root.resolve(href)?.let {
related += it
}
}
}
}
val dav = DavResource(client, url)
when (serviceType) {
Services.SERVICE_CARDDAV ->
dav.propfind(0, AddressbookHomeSet.NAME, GroupMembership.NAME) { response, _ ->
response[AddressbookHomeSet::class.java]?.let {
for (href in it.hrefs)
dav.location.resolve(href)?.let { homeSets += UrlUtils.withTrailingSlash(it) }
}
if (recurse)
findRelated(dav.location, response)
}
Services.SERVICE_CALDAV -> {
dav.propfind(0, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME) { response, _ ->
response[CalendarHomeSet::class.java]?.let {
for (href in it.hrefs)
dav.location.resolve(href)?.let { homeSets.add(UrlUtils.withTrailingSlash(it)) }
}
if (recurse)
findRelated(dav.location, response)
}
}
}
for (resource in related)
queryHomeSets(client, resource, false)
}
fun saveHomeSets() {
@@ -209,43 +279,14 @@ class DavService: Service() {
// refresh home set list (from principal)
readPrincipal()?.let { principalUrl ->
Logger.log.fine("Querying principal $principalUrl for home sets")
val principal = DavResource(httpClient, principalUrl)
queryHomeSets(principal)
// refresh home sets: calendar-proxy-read/write-for
for ((resource, proxyRead) in principal.findProperties(CalendarProxyReadFor::class.java))
for (href in proxyRead.hrefs) {
Logger.log.fine("Principal is a read-only proxy for $href, checking for home sets")
resource.location.resolve(href)?.let { queryHomeSets(DavResource(httpClient, it)) }
}
for ((resource, proxyWrite) in principal.findProperties(CalendarProxyWriteFor::class.java))
for (href in proxyWrite.hrefs) {
Logger.log.fine("Principal is a read/write proxy for $href, checking for home sets")
resource.location.resolve(href)?.let { queryHomeSets(DavResource(httpClient, it)) }
}
// refresh home sets: direct group memberships
principal.properties[GroupMembership::class.java]?.let { groupMembership ->
for (href in groupMembership.hrefs) {
Logger.log.fine("Principal is member of group $href, checking for home sets")
principal.location.resolve(href)?.let { url ->
try {
queryHomeSets(DavResource(httpClient, url))
} catch(e: HttpException) {
Logger.log.log(Level.WARNING, "Couldn't query member group", e)
} catch(e: DavException) {
Logger.log.log(Level.WARNING, "Couldn't query member group", e)
}
}
}
}
queryHomeSets(httpClient, principalUrl)
}
// remember selected collections
val selectedCollections = HashSet<HttpUrl>()
collections.values
.filter { it.selected }
.forEach { (url,_) -> HttpUrl.parse(url)?.let { selectedCollections.add(it) } }
.forEach { (url, _) -> selectedCollections.add(url) }
// now refresh collections (taken from home sets)
val itHomeSets = homeSets.iterator()
@@ -253,22 +294,21 @@ class DavService: Service() {
val homeSetUrl = itHomeSets.next()
Logger.log.fine("Listing home set $homeSetUrl")
val homeSet = DavResource(httpClient, homeSetUrl)
try {
homeSet.propfind(1, *CollectionInfo.DAV_PROPERTIES)
val itCollections = IteratorChain<DavResource>(homeSet.members.iterator(), homeSet.related.iterator(), SingletonIterator(homeSet))
while (itCollections.hasNext()) {
val member = itCollections.next()
val info = CollectionInfo(member)
DavResource(httpClient, homeSetUrl).propfind(1, *CollectionInfo.DAV_PROPERTIES) { response, _ ->
if (!response.isSuccess())
return@propfind
val info = CollectionInfo(response)
info.confirmed = true
Logger.log.log(Level.FINE, "Found collection", info)
if ((serviceType == Services.SERVICE_CARDDAV && info.type == CollectionInfo.Type.ADDRESS_BOOK) ||
(serviceType == Services.SERVICE_CALDAV && arrayOf(CollectionInfo.Type.CALENDAR, CollectionInfo.Type.WEBCAL).contains(info.type)))
collections[member.location] = info
(serviceType == Services.SERVICE_CALDAV && arrayOf(CollectionInfo.Type.CALENDAR, CollectionInfo.Type.WEBCAL).contains(info.type)))
collections[response.href] = info
}
} catch(e: HttpException) {
if (e.status in arrayOf(403, 404, 410))
if (e.code in arrayOf(403, 404, 410))
// delete home set only if it was not accessible (40x)
itHomeSets.remove()
}
@@ -280,18 +320,21 @@ class DavService: Service() {
val (url, info) = itCollections.next()
if (!info.confirmed)
try {
val collection = DavResource(httpClient, url)
collection.propfind(0, *CollectionInfo.DAV_PROPERTIES)
val info = CollectionInfo(collection)
info.confirmed = true
DavResource(httpClient, url).propfind(0, *CollectionInfo.DAV_PROPERTIES) { response, _ ->
if (!response.isSuccess())
return@propfind
// remove unusable collections
if ((serviceType == Services.SERVICE_CARDDAV && info.type != CollectionInfo.Type.ADDRESS_BOOK) ||
val info = CollectionInfo(response)
info.confirmed = true
// remove unusable collections
if ((serviceType == Services.SERVICE_CARDDAV && info.type != CollectionInfo.Type.ADDRESS_BOOK) ||
(serviceType == Services.SERVICE_CALDAV && !arrayOf(CollectionInfo.Type.CALENDAR, CollectionInfo.Type.WEBCAL).contains(info.type)) ||
(info.type == CollectionInfo.Type.WEBCAL && info.source == null))
itCollections.remove()
itCollections.remove()
}
} catch(e: HttpException) {
if (e.status in arrayOf(403, 404, 410))
if (e.code in arrayOf(403, 404, 410))
// delete collection only if it was not accessible (40x)
itCollections.remove()
else
@@ -320,19 +363,20 @@ class DavService: Service() {
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't refresh collection list", e)
val debugIntent = Intent(this@DavService, DebugInfoActivity::class.java)
val debugIntent = Intent(this, DebugInfoActivity::class.java)
debugIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e)
debugIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account)
val nm = NotificationUtils.createChannels(this)
val notify = NotificationCompat.Builder(this, NotificationUtils.CHANNEL_SYNC_PROBLEMS)
val notify = NotificationUtils.newBuilder(this)
.setSmallIcon(R.drawable.ic_sync_error_notification)
.setLargeIcon(App.getLauncherBitmap(this))
.setContentTitle(getString(R.string.dav_service_refresh_failed))
.setContentText(getString(R.string.dav_service_refresh_couldnt_refresh))
.setContentIntent(PendingIntent.getActivity(this, 0, debugIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setSubText(account.name)
.setCategory(NotificationCompat.CATEGORY_ERROR)
.build()
nm.notify(Constants.NOTIFICATION_REFRESH_COLLECTIONS, notify)
NotificationManagerCompat.from(this)
.notify(service.toString(), NotificationUtils.NOTIFY_REFRESH_COLLECTIONS, notify)
} finally {
runningRefresh.remove(service)
refreshingStatusListeners.forEach { it.get()?.onDavRefreshStatusChanged(service, false) }
@@ -341,4 +385,4 @@ class DavService: Service() {
}
}
}

View File

@@ -22,20 +22,16 @@ import java.util.*
*/
object DavUtils {
@JvmStatic
fun ARGBtoCalDAVColor(colorWithAlpha: Int): String {
val alpha = (colorWithAlpha shr 24) and 0xFF
val color = colorWithAlpha and 0xFFFFFF
return String.format("#%06X%02X", color, alpha)
}
@JvmStatic
fun lastSegmentOfUrl(url: String): String {
val httpUrl = HttpUrl.parse(url) ?: throw IllegalArgumentException("url not parsable")
fun lastSegmentOfUrl(url: HttpUrl): String {
// the list returned by HttpUrl.pathSegments() is unmodifiable, so we have to create a copy
val segments = LinkedList<String>(httpUrl.pathSegments())
Collections.reverse(segments)
val segments = LinkedList<String>(url.pathSegments())
segments.reverse()
return segments.firstOrNull { it.isNotEmpty() } ?: "/"
}

View File

@@ -13,6 +13,7 @@ import android.os.Build
import android.security.KeyChain
import at.bitfire.cert4android.CustomCertManager
import at.bitfire.dav4android.BasicDigestAuthHandler
import at.bitfire.dav4android.Constants
import at.bitfire.dav4android.UrlUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.Credentials
@@ -23,14 +24,13 @@ import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.internal.tls.OkHostnameVerifier
import okhttp3.logging.HttpLoggingInterceptor
import java.io.Closeable
import java.io.File
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.Socket
import java.security.KeyStore
import java.security.Principal
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.logging.Level
@@ -42,13 +42,13 @@ import javax.net.ssl.X509TrustManager
class HttpClient private constructor(
val okHttpClient: OkHttpClient,
private val certManager: CustomCertManager?
): Closeable {
): AutoCloseable {
companion object {
/** [OkHttpClient] singleton to build all clients from */
val sharedClient = OkHttpClient.Builder()
// set timeouts
.connectTimeout(30, TimeUnit.SECONDS)
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
@@ -65,7 +65,7 @@ class HttpClient private constructor(
certManager?.close()
}
class Builder @JvmOverloads constructor(
class Builder(
val context: Context? = null,
val settings: ISettings? = null,
accountSettings: AccountSettings? = null,
@@ -224,8 +224,11 @@ class HttpClient private constructor(
App.FLAVOR_SOLDUPE -> "Soldupe Sync"
else -> "DAVdroid"
}
private val userAgentDate = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US).format(Date(BuildConfig.buildTime))
private val userAgent = "$productName/${BuildConfig.VERSION_NAME} ($userAgentDate; dav4android; okhttp3) Android/${Build.VERSION.RELEASE}"
// use Locale.US because numbers may be encoded as non-ASCII characters in other locales
private val userAgentDateFormat = SimpleDateFormat("yyyy/MM/dd", Locale.US)
private val userAgentDate = userAgentDateFormat.format(Date(BuildConfig.buildTime))
private val userAgent = "$productName/${BuildConfig.VERSION_NAME} ($userAgentDate; dav4android; okhttp/${Constants.okHttpVersion}) Android/${Build.VERSION.RELEASE}"
override fun intercept(chain: Interceptor.Chain): Response {
val locale = Locale.getDefault()

View File

@@ -27,7 +27,7 @@ class MemoryCookieStore: CookieJar {
* This ensures that cookies can be overwritten. [RFC 6265 5.3 Storage Model]
* Not thread-safe!
*/
val storage = MultiKeyMap.multiKeyMap(HashedMap<MultiKey<out String>, Cookie>())!!
private val storage = MultiKeyMap.multiKeyMap(HashedMap<MultiKey<out String>, Cookie>())!!
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
synchronized(storage) {

View File

@@ -24,7 +24,6 @@ class PackageChangedReceiver: BroadcastReceiver() {
companion object {
@JvmStatic
fun updateTaskSync(context: Context) {
val tasksInstalled = LocalTaskList.tasksProviderAvailable(context)
Logger.log.info("Tasks provider available = $tasksInstalled")

View File

@@ -18,7 +18,7 @@ import java.util.logging.LogRecord
object LogcatHandler: Handler() {
private val MAX_LINE_LENGTH = 3000
private const val MAX_LINE_LENGTH = 3000
init {
formatter = PlainTextFormatter.LOGCAT

View File

@@ -15,9 +15,8 @@ import android.content.SharedPreferences
import android.os.Process
import android.preference.PreferenceManager
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import android.util.Log
import at.bitfire.davdroid.App
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.AppSettingsActivity
import at.bitfire.davdroid.ui.NotificationUtils
@@ -29,13 +28,12 @@ import java.util.logging.Level
object Logger {
val LOG_TO_EXTERNAL_STORAGE = "log_to_external_storage"
const val LOG_TO_EXTERNAL_STORAGE = "log_to_external_storage"
@JvmField
val log = java.util.logging.Logger.getLogger("davdroid")!!
lateinit private var preferences: SharedPreferences
private lateinit var preferences: SharedPreferences
fun initialize(context: Context) {
preferences = PreferenceManager.getDefaultSharedPreferences(context)
@@ -62,12 +60,11 @@ object Logger {
rootLogger.handlers.forEach { rootLogger.removeHandler(it) }
rootLogger.addHandler(LogcatHandler)
val nm = NotificationUtils.createChannels(context)
val nm = NotificationManagerCompat.from(context)
// log to external file according to preferences
if (logToFile) {
val builder = NotificationCompat.Builder(context, NotificationUtils.CHANNEL_DEBUG)
val builder = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_DEBUG)
builder .setSmallIcon(R.drawable.ic_sd_storage_notification)
.setLargeIcon(App.getLauncherBitmap(context))
.setContentTitle(context.getString(R.string.logging_davdroid_file_logging))
.setLocalOnly(true)
@@ -88,22 +85,22 @@ object Logger {
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setStyle(NotificationCompat.BigTextStyle()
.bigText(context.getString(R.string.logging_to_external_storage, dir.path)))
.setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(R.string.logging_to_external_storage, dir.path)))
.setOngoing(true)
} catch(e: IOException) {
log.log(Level.SEVERE, "Couldn't create external log file", e)
builder .setContentText(context.getString(R.string.logging_couldnt_create_file, e.localizedMessage))
val message = context.getString(R.string.logging_couldnt_create_file, e.localizedMessage)
builder .setContentText(message)
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
.setCategory(NotificationCompat.CATEGORY_ERROR)
}
else
builder.setContentText(context.getString(R.string.logging_no_external_storage))
nm.notify(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING, builder.build())
nm.notify(NotificationUtils.NOTIFY_EXTERNAL_FILE_LOGGING, builder.build())
} else
nm.cancel(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING)
nm.cancel(NotificationUtils.NOTIFY_EXTERNAL_FILE_LOGGING)
}
}

View File

@@ -19,10 +19,10 @@ class PlainTextFormatter private constructor(
): Formatter() {
companion object {
@JvmField val LOGCAT = PlainTextFormatter(true)
@JvmField val DEFAULT = PlainTextFormatter(false)
val LOGCAT = PlainTextFormatter(true)
val DEFAULT = PlainTextFormatter(false)
val MAX_MESSAGE_LENGTH = 20000
const val MAX_MESSAGE_LENGTH = 20000
}
override fun format(r: LogRecord): String {

View File

@@ -9,13 +9,25 @@
package at.bitfire.davdroid.model
import android.content.ContentValues
import at.bitfire.dav4android.DavResource
import android.os.Parcel
import android.os.Parcelable
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.UrlUtils
import at.bitfire.dav4android.property.*
import at.bitfire.davdroid.model.ServiceDB.Collections
import java.io.Serializable
import okhttp3.HttpUrl
data class CollectionInfo @JvmOverloads constructor(
val url: String,
/**
* Represents a WebDAV collection.
*
* @constructor always appends a trailing slash to the URL
*/
data class CollectionInfo(
/**
* URL of the collection (including trailing slash)
*/
val url: HttpUrl,
var id: Long? = null,
var serviceID: Long? = null,
@@ -38,7 +50,7 @@ data class CollectionInfo @JvmOverloads constructor(
// non-persistent properties
var confirmed: Boolean = false
): Serializable {
): Parcelable {
enum class Type {
ADDRESS_BOOK,
@@ -46,23 +58,8 @@ data class CollectionInfo @JvmOverloads constructor(
WEBCAL // iCalendar subscription
}
companion object {
@JvmField
val DAV_PROPERTIES = arrayOf(
ResourceType.NAME,
CurrentUserPrivilegeSet.NAME,
DisplayName.NAME,
AddressbookDescription.NAME, SupportedAddressData.NAME,
CalendarDescription.NAME, CalendarColor.NAME, SupportedCalendarComponentSet.NAME,
Source.NAME
)
}
constructor(dav: DavResource): this(dav.location.toString()) {
dav.properties[ResourceType::class.java]?.let { type ->
constructor(dav: Response): this(UrlUtils.withTrailingSlash(dav.href)) {
dav[ResourceType::class.java]?.let { type ->
when {
type.types.contains(ResourceType.ADDRESSBOOK) -> this.type = Type.ADDRESS_BOOK
type.types.contains(ResourceType.CALENDAR) -> this.type = Type.CALENDAR
@@ -70,33 +67,33 @@ data class CollectionInfo @JvmOverloads constructor(
}
}
dav.properties[CurrentUserPrivilegeSet::class.java]?.let { privilegeSet ->
dav[CurrentUserPrivilegeSet::class.java]?.let { privilegeSet ->
readOnly = !privilegeSet.mayWriteContent
}
dav.properties[DisplayName::class.java]?.let {
dav[DisplayName::class.java]?.let {
if (!it.displayName.isNullOrEmpty())
displayName = it.displayName
}
when (type) {
Type.ADDRESS_BOOK -> {
dav.properties[AddressbookDescription::class.java]?.let { description = it.description }
dav[AddressbookDescription::class.java]?.let { description = it.description }
}
Type.CALENDAR, Type.WEBCAL -> {
dav.properties[CalendarDescription::class.java]?.let { description = it.description }
dav.properties[CalendarColor::class.java]?.let { color = it.color }
dav.properties[CalendarTimezone::class.java]?.let { timeZone = it.vTimeZone }
dav[CalendarDescription::class.java]?.let { description = it.description }
dav[CalendarColor::class.java]?.let { color = it.color }
dav[CalendarTimezone::class.java]?.let { timeZone = it.vTimeZone }
if (type == Type.CALENDAR) {
supportsVEVENT = true
supportsVTODO = true
dav.properties[SupportedCalendarComponentSet::class.java]?.let {
dav[SupportedCalendarComponentSet::class.java]?.let {
supportsVEVENT = it.supportsEvents
supportsVTODO = it.supportsTasks
}
} else { // Type.WEBCAL
dav.properties[Source::class.java]?.let { source = it.hrefs.firstOrNull() }
dav[Source::class.java]?.let { source = it.hrefs.firstOrNull() }
supportsVEVENT = true
}
}
@@ -104,7 +101,7 @@ data class CollectionInfo @JvmOverloads constructor(
}
constructor(values: ContentValues): this(values.getAsString(Collections.URL)) {
constructor(values: ContentValues): this(UrlUtils.withTrailingSlash(HttpUrl.parse(values.getAsString(Collections.URL))!!)) {
id = values.getAsLong(Collections.ID)
serviceID = values.getAsLong(Collections.SERVICE_ID)
type = try {
@@ -134,7 +131,7 @@ data class CollectionInfo @JvmOverloads constructor(
// Collections.SERVICE_ID is never changed
type?.let { values.put(Collections.TYPE, it.name) }
values.put(Collections.URL, url)
values.put(Collections.URL, url.toString())
values.put(Collections.READ_ONLY, if (readOnly) 1 else 0)
values.put(Collections.FORCE_READ_ONLY, if (forceReadOnly) 1 else 0)
values.put(Collections.DISPLAY_NAME, displayName)
@@ -160,4 +157,87 @@ data class CollectionInfo @JvmOverloads constructor(
(i != 0)
}
override fun describeContents(): Int = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
fun<T> writeOrNull(value: T?, write: (T) -> Unit) {
if (value == null)
dest.writeByte(0)
else {
dest.writeByte(1)
write(value)
}
}
dest.writeString(url.toString())
writeOrNull(id) { dest.writeLong(it) }
writeOrNull(serviceID) { dest.writeLong(it) }
dest.writeString(type?.name)
dest.writeByte(if (readOnly) 1 else 0)
dest.writeByte(if (forceReadOnly) 1 else 0)
dest.writeString(displayName)
dest.writeString(description)
writeOrNull(color) { dest.writeInt(it) }
dest.writeString(timeZone)
dest.writeByte(if (supportsVEVENT) 1 else 0)
dest.writeByte(if (supportsVTODO) 1 else 0)
dest.writeByte(if (selected) 1 else 0)
dest.writeString(source)
dest.writeByte(if (confirmed) 1 else 0)
}
companion object CREATOR : Parcelable.Creator<CollectionInfo> {
val DAV_PROPERTIES = arrayOf(
ResourceType.NAME,
CurrentUserPrivilegeSet.NAME,
DisplayName.NAME,
AddressbookDescription.NAME, SupportedAddressData.NAME,
CalendarDescription.NAME, CalendarColor.NAME, SupportedCalendarComponentSet.NAME,
Source.NAME
)
override fun createFromParcel(parcel: Parcel): CollectionInfo {
fun<T> readOrNull(parcel: Parcel, read: () -> T): T? {
return if (parcel.readByte() == 0.toByte())
null
else
read()
}
return CollectionInfo(
HttpUrl.parse(parcel.readString())!!,
readOrNull(parcel) { parcel.readLong() },
readOrNull(parcel) { parcel.readLong() },
parcel.readString()?.let { Type.valueOf(it) },
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readString(),
parcel.readString(),
readOrNull(parcel) { parcel.readInt() },
parcel.readString(),
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readString(),
parcel.readByte() != 0.toByte()
)
}
override fun newArray(size: Int) = arrayOfNulls<CollectionInfo>(size)
}
}

View File

@@ -10,10 +10,10 @@ package at.bitfire.davdroid.model
import java.io.Serializable
class Credentials @JvmOverloads constructor(
@JvmField val userName: String? = null,
@JvmField val password: String? = null,
@JvmField val certificateAlias: String? = null
class Credentials(
val userName: String? = null,
val password: String? = null,
val certificateAlias: String? = null
): Serializable {
enum class Type {
@@ -34,8 +34,7 @@ class Credentials @JvmOverloads constructor(
}
}
override fun toString(): String {
return "Credentials(type=$type, userName=$userName, certificateAlias=$certificateAlias)"
}
override fun toString() =
"Credentials(type=$type, userName=$userName, certificateAlias=$certificateAlias)"
}

View File

@@ -17,57 +17,49 @@ import android.preference.PreferenceManager
import at.bitfire.davdroid.App
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.StartupDialogFragment
import java.io.Closeable
import java.util.logging.Level
class ServiceDB {
/*object Settings {
@JvmField val _TABLE = "settings"
@JvmField val NAME = "setting"
@JvmField val VALUE = "value"
}*/
object Services {
@JvmField val _TABLE = "services"
@JvmField val ID = "_id"
@JvmField val ACCOUNT_NAME = "accountName"
@JvmField val SERVICE = "service"
@JvmField val PRINCIPAL = "principal"
const val _TABLE = "services"
const val ID = "_id"
const val ACCOUNT_NAME = "accountName"
const val SERVICE = "service"
const val PRINCIPAL = "principal"
// allowed values for SERVICE column
@JvmField val SERVICE_CALDAV = "caldav"
@JvmField val SERVICE_CARDDAV = "carddav"
const val SERVICE_CALDAV = "caldav"
const val SERVICE_CARDDAV = "carddav"
}
object HomeSets {
@JvmField val _TABLE = "homesets"
@JvmField val ID = "_id"
@JvmField val SERVICE_ID = "serviceID"
@JvmField val URL = "url"
const val _TABLE = "homesets"
const val ID = "_id"
const val SERVICE_ID = "serviceID"
const val URL = "url"
}
object Collections {
@JvmField val _TABLE = "collections"
@JvmField val ID = "_id"
@JvmField val TYPE = "type"
@JvmField val SERVICE_ID = "serviceID"
@JvmField val URL = "url"
@JvmField val READ_ONLY = "readOnly"
@JvmField val FORCE_READ_ONLY = "forceReadOnly"
@JvmField val DISPLAY_NAME = "displayName"
@JvmField val DESCRIPTION = "description"
@JvmField val COLOR = "color"
@JvmField val TIME_ZONE = "timezone"
@JvmField val SUPPORTS_VEVENT = "supportsVEVENT"
@JvmField val SUPPORTS_VTODO = "supportsVTODO"
@JvmField val SOURCE = "source"
@JvmField val SYNC = "sync"
const val _TABLE = "collections"
const val ID = "_id"
const val TYPE = "type"
const val SERVICE_ID = "serviceID"
const val URL = "url"
const val READ_ONLY = "readOnly"
const val FORCE_READ_ONLY = "forceReadOnly"
const val DISPLAY_NAME = "displayName"
const val DESCRIPTION = "description"
const val COLOR = "color"
const val TIME_ZONE = "timezone"
const val SUPPORTS_VEVENT = "supportsVEVENT"
const val SUPPORTS_VTODO = "supportsVTODO"
const val SOURCE = "source"
const val SYNC = "sync"
}
companion object {
@JvmStatic
fun onRenameAccount(db: SQLiteDatabase, oldName: String, newName: String) {
val values = ContentValues(1)
values.put(Services.ACCOUNT_NAME, newName)
@@ -79,11 +71,11 @@ class ServiceDB {
class OpenHelper(
val context: Context
): SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION), Closeable {
): SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION), AutoCloseable {
companion object {
val DATABASE_NAME = "services.db"
val DATABASE_VERSION = 4
const val DATABASE_NAME = "services.db"
const val DATABASE_VERSION = 4
}
override fun onConfigure(db: SQLiteDatabase) {
@@ -228,4 +220,4 @@ class ServiceDB {
}
}
}
}

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.model
import at.bitfire.dav4android.property.SyncToken
import org.json.JSONException
import org.json.JSONObject
data class SyncState(
val type: Type,
val value: String,
/**
* Whether this sync state occurred during an initial sync as described
* in RFC 6578, which means the initial sync is not complete yet.
*/
var initialSync: Boolean? = null
) {
companion object {
private const val KEY_TYPE = "type"
private const val KEY_VALUE = "value"
private const val KEY_INITIAL_SYNC = "initialSync"
fun fromString(s: String?): SyncState? {
if (s == null)
return null
return try {
val json = JSONObject(s)
SyncState(
Type.valueOf(json.getString(KEY_TYPE)),
json.getString(KEY_VALUE),
try { json.getBoolean(KEY_INITIAL_SYNC) } catch(e: JSONException) { null }
)
} catch (e: JSONException) {
null
}
}
fun fromSyncToken(token: SyncToken, initialSync: Boolean? = null) =
SyncState(Type.SYNC_TOKEN, requireNotNull(token.token), initialSync)
}
enum class Type { CTAG, SYNC_TOKEN }
override fun toString(): String {
val json = JSONObject()
json.put(KEY_TYPE, type.name)
json.put(KEY_VALUE, value)
initialSync?.let { json.put(KEY_INITIAL_SYNC, it) }
return json.toString()
}
}

View File

@@ -12,17 +12,10 @@ import android.provider.ContactsContract.RawContacts
object UnknownProperties {
@JvmField
val CONTENT_ITEM_TYPE = "x.davdroid/unknown-properties"
const val CONTENT_ITEM_TYPE = "x.davdroid/unknown-properties"
@JvmField
val MIMETYPE = RawContacts.Data.MIMETYPE
@JvmField
val RAW_CONTACT_ID = RawContacts.Data.RAW_CONTACT_ID
@JvmField
val UNKNOWN_PROPERTIES = RawContacts.Data.DATA1
const val MIMETYPE = RawContacts.Data.MIMETYPE
const val RAW_CONTACT_ID = RawContacts.Data.RAW_CONTACT_ID
const val UNKNOWN_PROPERTIES = RawContacts.Data.DATA1
}

View File

@@ -0,0 +1,17 @@
/*
* 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.resource
import at.bitfire.vcard4android.Contact
interface LocalAddress: LocalResource<Contact> {
fun resetDeleted()
}

View File

@@ -23,9 +23,9 @@ import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.SyncState
import at.bitfire.vcard4android.*
import java.io.ByteArrayOutputStream
import java.io.FileNotFoundException
import java.util.*
import java.util.logging.Level
@@ -39,67 +39,50 @@ class LocalAddressBook(
private val context: Context,
account: Account,
provider: ContentProviderClient?
): AndroidAddressBook<LocalContact, LocalGroup>(account, provider, LocalContact.Factory, LocalGroup.Factory), LocalCollection<LocalResource> {
): AndroidAddressBook<LocalContact, LocalGroup>(account, provider, LocalContact.Factory, LocalGroup.Factory), LocalCollection<LocalAddress> {
companion object {
val USER_DATA_MAIN_ACCOUNT_TYPE = "real_account_type"
val USER_DATA_MAIN_ACCOUNT_NAME = "real_account_name"
val USER_DATA_URL = "url"
val USER_DATA_READ_ONLY = "read_only"
val USER_DATA_CTAG = "ctag"
const val USER_DATA_MAIN_ACCOUNT_TYPE = "real_account_type"
const val USER_DATA_MAIN_ACCOUNT_NAME = "real_account_name"
const val USER_DATA_URL = "url"
const val USER_DATA_READ_ONLY = "read_only"
@JvmStatic
@Throws(ContactsStorageException::class)
fun create(context: Context, provider: ContentProviderClient, mainAccount: Account, info: CollectionInfo): LocalAddressBook {
val accountManager = AccountManager.get(context)
val account = Account(accountName(mainAccount, info), context.getString(R.string.account_type_address_book))
if (!accountManager.addAccountExplicitly(account, null, initialUserData(mainAccount, info.url)))
throw ContactsStorageException("Couldn't create address book account")
if (!accountManager.addAccountExplicitly(account, null, initialUserData(mainAccount, info.url.toString())))
throw IllegalStateException("Couldn't create address book account")
val addressBook = LocalAddressBook(context, account, provider)
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
// set up Contacts Provider Settings
// initialize Contacts Provider Settings
val values = ContentValues(2)
values.put(ContactsContract.Settings.SHOULD_SYNC, 1)
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
addressBook.updateSettings(values)
addressBook.settings = values
return addressBook
}
@JvmStatic
@Throws(ContactsStorageException::class)
fun find(context: Context, provider: ContentProviderClient, mainAccount: Account?): List<LocalAddressBook> {
val accountManager = AccountManager.get(context)
fun findAll(context: Context, provider: ContentProviderClient, mainAccount: Account?) = AccountManager.get(context)
.getAccountsByType(context.getString(R.string.account_type_address_book))
.map { LocalAddressBook(context, it, provider) }
.filter { mainAccount == null || it.mainAccount == mainAccount }
.toList()
val result = LinkedList<LocalAddressBook>()
accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
.map { LocalAddressBook(context, it, provider) }
.filter { mainAccount == null || it.getMainAccount() == mainAccount }
.forEach { result += it }
return result
}
@JvmStatic
fun accountName(mainAccount: Account, info: CollectionInfo): String {
val baos = ByteArrayOutputStream()
baos.write(info.url.hashCode())
val hash = Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP or Base64.NO_PADDING)
val sb = StringBuilder(if (info.displayName.isNullOrEmpty()) DavUtils.lastSegmentOfUrl(info.url) else info.displayName)
sb .append(" (")
.append(mainAccount.name)
.append(" ")
.append(hash)
.append(")")
sb.append(" (${mainAccount.name} $hash)")
return sb.toString()
}
@JvmStatic
fun initialUserData(mainAccount: Account, url: String): Bundle {
val bundle = Bundle(3)
bundle.putString(USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
@@ -108,182 +91,212 @@ class LocalAddressBook(
return bundle
}
fun mainAccount(context: Context, account: Account): Account =
if (account.type == context.getString(R.string.account_type_address_book)) {
val manager = AccountManager.get(context)
Account(
manager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME),
manager.getUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE)
)
} else
account
}
override val title = account.name!!
/**
* Whether contact groups (LocalGroup resources) are included in query results for
* {@link #getAll()}, {@link #getDeleted()}, {@link #getDirty()} and
* {@link #getWithoutFileName()}.
* Whether contact groups ([LocalGroup]) are included in query results
* and are affected by updates/deletes on generic members.
*
* For instance, if this option is disabled, [findDirty] will find only dirty [LocalContact]s,
* but if it is enabled, [findDirty] will find dirty [LocalContact]s and [LocalGroup]s.
*/
var includeGroups = true
private var _mainAccount: Account? = null
var mainAccount: Account
get() {
_mainAccount?.let { return it }
AccountManager.get(context).let { accountManager ->
val name = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME)
val type = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE)
if (name != null && type != null)
return Account(name, type)
else
throw IllegalStateException("Address book doesn't exist anymore")
}
}
set(newMainAccount) {
AccountManager.get(context).let { accountManager ->
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, newMainAccount.name)
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, newMainAccount.type)
}
_mainAccount = newMainAccount
}
var url: String
get() = AccountManager.get(context).getUserData(account, USER_DATA_URL)
?: throw IllegalStateException("Address book has no URL")
set(url) = AccountManager.get(context).setUserData(account, USER_DATA_URL, url)
var readOnly: Boolean
get() = AccountManager.get(context).getUserData(account, USER_DATA_READ_ONLY) != null
set(readOnly) = AccountManager.get(context).setUserData(account, USER_DATA_READ_ONLY, if (readOnly) "1" else null)
override var lastSyncState: SyncState?
get() = syncState?.let { SyncState.fromString(String(it)) }
set(state) {
syncState = state?.toString()?.toByteArray()
}
/* operations on the collection (address book) itself */
@Throws(ContactsStorageException::class)
override fun markNotDirty(flags: Int): Int {
val values = ContentValues(1)
values.put(LocalContact.COLUMN_FLAGS, flags)
var number = provider!!.update(rawContactsSyncUri(), values, "${RawContacts.DIRTY}=0", null)
if (includeGroups) {
values.clear()
values.put(LocalGroup.COLUMN_FLAGS, flags)
number += provider.update(groupsSyncUri(), values, "${Groups.DIRTY}=0", null)
}
return number
}
override fun removeNotDirtyMarked(flags: Int): Int {
var number = provider!!.delete(rawContactsSyncUri(),
"${RawContacts.DIRTY}=0 AND ${LocalContact.COLUMN_FLAGS}=?", arrayOf(flags.toString()))
if (includeGroups)
number += provider.delete(groupsSyncUri(),
"${Groups.DIRTY}=0 AND ${LocalGroup.COLUMN_FLAGS}=?", arrayOf(flags.toString()))
return number
}
fun update(info: CollectionInfo) {
val newAccountName = accountName(getMainAccount(), info)
val newAccountName = accountName(mainAccount, info)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
if (account.name != newAccountName && Build.VERSION.SDK_INT >= 21) {
// no need to re-assign contacts to new account, because they will be deleted by contacts provider in any case
val accountManager = AccountManager.get(context)
val future = accountManager.renameAccount(account, newAccountName, {
try {
// update raw contacts to new account name
provider?.let { provider ->
val values = ContentValues(1)
values.put(RawContacts.ACCOUNT_NAME, newAccountName)
provider.update(syncAdapterURI(RawContacts.CONTENT_URI), values, "${RawContacts.ACCOUNT_NAME}=?", arrayOf(account.name))
}
} catch(e: RemoteException) {
Logger.log.log(Level.WARNING, "Couldn't re-assign contacts to new account name", e)
}
}, null)
val future = accountManager.renameAccount(account, newAccountName, null, null)
account = future.result
}
Constants.log.info("Address book read-only? = ${info.readOnly}")
setReadOnly(info.readOnly || info.forceReadOnly)
readOnly = info.readOnly || info.forceReadOnly
// make sure it will still be synchronized when contacts are updated
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
}
@Throws(ContactsStorageException::class)
fun delete() {
val accountManager = AccountManager.get(context)
try {
if (Build.VERSION.SDK_INT >= 22)
accountManager.removeAccount(account, null, null, null)
else
@Suppress("deprecation")
accountManager.removeAccount(account, null, null)
} catch(e: Exception) {
throw ContactsStorageException("Couldn't remove address book", e)
}
if (Build.VERSION.SDK_INT >= 22)
accountManager.removeAccount(account, null, null, null)
else
accountManager.removeAccount(account, null, null)
}
/* operations on members (contacts/groups) */
@Throws(ContactsStorageException::class, FileNotFoundException::class)
fun findContactByUID(uid: String): LocalContact {
val contacts = queryContacts("${AndroidContact.COLUMN_UID}=?", arrayOf(uid))
if (contacts.isEmpty())
throw FileNotFoundException()
return contacts.first()
override fun findByName(name: String): LocalAddress? {
val result = queryContacts("${AndroidContact.COLUMN_FILENAME}=?", arrayOf(name)).firstOrNull()
return if (includeGroups)
result ?: queryGroups("${AndroidGroup.COLUMN_FILENAME}=?", arrayOf(name)).firstOrNull()
else
result
}
@Throws(ContactsStorageException::class)
override fun getAll(): List<LocalResource> {
val all = LinkedList<LocalResource>()
all.addAll(queryContacts(null, null))
if (includeGroups)
all.addAll(queryGroups(null, null))
return all
}
/**
* Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0).
* @throws RemoteException on content provider errors
*/
@Throws(ContactsStorageException::class)
override fun getDeleted(): List<LocalResource> {
val deleted = LinkedList<LocalResource>()
deleted.addAll(getDeletedContacts())
if (includeGroups)
deleted.addAll(getDeletedGroups())
return deleted
override fun findDeleted() =
if (includeGroups)
findDeletedContacts() + findDeletedGroups()
else
findDeletedContacts()
fun findDeletedContacts() = queryContacts("${RawContacts.DELETED}!=0", null)
fun findDeletedGroups() = queryGroups("${Groups.DELETED}!=0", null)
/**
* Returns an array of local contacts/groups which have been changed locally (DIRTY != 0).
* @throws RemoteException on content provider errors
*/
override fun findDirty() =
if (includeGroups)
findDirtyContacts() + findDirtyGroups()
else
findDirtyContacts()
fun findDirtyContacts() = queryContacts("${RawContacts.DIRTY}!=0", null)
fun findDirtyGroups() = queryGroups("${Groups.DIRTY}!=0", null)
private fun queryContactsGroups(whereContacts: String?, whereArgsContacts: Array<String>?, whereGroups: String?, whereArgsGroups: Array<String>?): List<LocalAddress> {
val contacts = queryContacts(whereContacts, whereArgsContacts)
return if (includeGroups)
contacts + queryGroups(whereGroups, whereArgsGroups)
else
contacts
}
/**
* 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
* @throws RemoteException on content provider errors
*/
@Throws(ContactsStorageException::class)
fun verifyDirty(): Int {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
throw IllegalStateException("verifyDirty() should not be called on Android != 7")
var reallyDirty = 0
for (contact in getDirtyContacts())
try {
val lastHash = contact.getLastHashCode()
val currentHash = contact.dataHashCode()
if (lastHash == currentHash) {
// hash is code still the same, contact is not "really dirty" (only metadata been have changed)
Logger.log.log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact)
contact.resetDirty()
} else {
Logger.log.log(Level.FINE, "Contact data has changed from hash $lastHash to $currentHash", contact)
reallyDirty++
}
} catch(e: FileNotFoundException) {
throw ContactsStorageException("Couldn't calculate hash code", e)
for (contact in findDirtyContacts()) {
val lastHash = contact.getLastHashCode()
val currentHash = contact.dataHashCode()
if (lastHash == currentHash) {
// hash is code still the same, contact is not "really dirty" (only metadata been have changed)
Logger.log.log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact)
contact.resetDirty()
} else {
Logger.log.log(Level.FINE, "Contact data has changed from hash $lastHash to $currentHash", contact)
reallyDirty++
}
}
if (includeGroups)
reallyDirty += getDirtyGroups().size
reallyDirty += findDirtyGroups().size
return reallyDirty
}
/**
* Returns an array of local contacts/groups which have been changed locally (DIRTY != 0).
*/
@Throws(ContactsStorageException::class)
override fun getDirty(): List<LocalResource> {
val dirty = LinkedList<LocalResource>()
dirty.addAll(getDirtyContacts())
if (includeGroups)
dirty.addAll(getDirtyGroups())
return dirty
}
/**
* Returns an array of local contacts which don't have a file name yet.
*/
@Throws(ContactsStorageException::class)
override fun getWithoutFileName(): List<LocalResource> {
val nameless = LinkedList<LocalResource>()
nameless.addAll(queryContacts("${AndroidContact.COLUMN_FILENAME} IS NULL", null))
if (includeGroups)
nameless.addAll(queryGroups("${AndroidGroup.COLUMN_FILENAME} IS NULL", null))
return nameless
}
@Throws(ContactsStorageException::class)
fun getDeletedContacts() = queryContacts("${RawContacts.DELETED} != 0", null)
@Throws(ContactsStorageException::class)
fun getDirtyContacts() = queryContacts("${RawContacts.DIRTY} != 0", null)
@Throws(ContactsStorageException::class)
fun getDeletedGroups() = queryGroups("${Groups.DELETED} != 0", null)
@Throws(ContactsStorageException::class)
fun getDirtyGroups() = queryGroups("${Groups.DIRTY} != 0", null)
@Throws(ContactsStorageException::class)
fun getByGroupMembership(groupID: Long): List<LocalContact> {
try {
val ids = HashSet<Long>()
provider!!.query(syncAdapterURI(ContactsContract.Data.CONTENT_URI),
arrayOf(RawContacts.Data.RAW_CONTACT_ID),
"(${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?) OR (${CachedGroupMembership.MIMETYPE}=? AND ${CachedGroupMembership.GROUP_ID}=?)",
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, groupID.toString(), CachedGroupMembership.CONTENT_ITEM_TYPE, groupID.toString()),
null)?.use { cursor ->
while (cursor.moveToNext())
ids += cursor.getLong(0)
}
return ids.map { id -> LocalContact(this, id, null, null) }
} catch (e: RemoteException) {
throw ContactsStorageException("Couldn't query contacts", e)
val ids = HashSet<Long>()
provider!!.query(syncAdapterURI(ContactsContract.Data.CONTENT_URI),
arrayOf(RawContacts.Data.RAW_CONTACT_ID),
"(${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?) OR (${CachedGroupMembership.MIMETYPE}=? AND ${CachedGroupMembership.GROUP_ID}=?)",
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, groupID.toString(), CachedGroupMembership.CONTENT_ITEM_TYPE, groupID.toString()),
null)?.use { cursor ->
while (cursor.moveToNext())
ids += cursor.getLong(0)
}
return ids.map { findContactByID(it) }
}
@@ -292,29 +305,23 @@ class LocalAddressBook(
/**
* 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
* @param title title of the group to look for
* @return id of the group with given title
* @throws RemoteException on content provider errors
*/
@Throws(ContactsStorageException::class)
fun findOrCreateGroup(title: String): Long {
try {
provider!!.query(syncAdapterURI(Groups.CONTENT_URI), arrayOf(Groups._ID),
"${Groups.TITLE}=?", arrayOf(title), null)?.use { cursor ->
if (cursor.moveToNext())
return cursor.getLong(0)
}
val values = ContentValues(1)
values.put(Groups.TITLE, title)
val uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values)
return ContentUris.parseId(uri)
} catch(e: RemoteException) {
throw ContactsStorageException("Couldn't find local contact group", e)
provider!!.query(syncAdapterURI(Groups.CONTENT_URI), arrayOf(Groups._ID),
"${Groups.TITLE}=?", arrayOf(title), null)?.use { cursor ->
if (cursor.moveToNext())
return cursor.getLong(0)
}
val values = ContentValues(1)
values.put(Groups.TITLE, title)
val uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values)
return ContentUris.parseId(uri)
}
@Throws(ContactsStorageException::class)
fun removeEmptyGroups() {
// find groups without members
/** should be done using {@link Groups.SUMMARY_COUNT}, but it's not implemented in Android yet */
@@ -324,43 +331,4 @@ class LocalAddressBook(
}
}
// SETTINGS
@Throws(ContactsStorageException::class)
fun getMainAccount(): Account {
val accountManager = AccountManager.get(context)
val name = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME)
val type = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE)
if (name != null && type != null)
return Account(name, type)
else
throw ContactsStorageException("Address book doesn't exist anymore")
}
fun setMainAccount(mainAccount: Account) {
val accountManager = AccountManager.get(context)
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type)
}
@Throws(ContactsStorageException::class)
fun getURL() =
AccountManager.get(context).getUserData(account, USER_DATA_URL) ?: throw ContactsStorageException("Address book has no URL")
fun setURL(url: String) =
AccountManager.get(context).setUserData(account, USER_DATA_URL, url)
fun getReadOnly() =
AccountManager.get(context).getUserData(account, USER_DATA_READ_ONLY) != null
fun setReadOnly(readOnly: Boolean) =
AccountManager.get(context).setUserData(account, USER_DATA_READ_ONLY, if (readOnly) "1" else null)
override fun getCTag(): String? =
AccountManager.get(context).getUserData(account, USER_DATA_CTAG)
override fun setCTag(cTag: String?) =
AccountManager.get(context).setUserData(account, USER_DATA_CTAG, cTag)
}
}

View File

@@ -14,14 +14,17 @@ import android.content.ContentProviderOperation
import android.content.ContentUris
import android.content.ContentValues
import android.net.Uri
import android.os.RemoteException
import android.provider.CalendarContract
import android.provider.CalendarContract.*
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.ical4android.*
import java.io.FileNotFoundException
import at.bitfire.davdroid.model.SyncState
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.AndroidCalendarFactory
import at.bitfire.ical4android.BatchOperation
import at.bitfire.ical4android.DateUtils
import java.util.*
import java.util.logging.Level
@@ -33,18 +36,8 @@ class LocalCalendar private constructor(
companion object {
val defaultColor = 0xFF8bc34a.toInt() // light green 500
private const val COLUMN_SYNC_STATE = Calendars.CAL_SYNC1
val COLUMN_CTAG = Calendars.CAL_SYNC1
val BASE_INFO_COLUMNS = arrayOf(
Events._ID,
Events._SYNC_ID,
LocalEvent.COLUMN_ETAG
)
@Throws(CalendarStorageException::class)
fun create(account: Account, provider: ContentProviderClient, info: CollectionInfo): Uri {
val values = valuesFromCollectionInfo(info, true)
@@ -61,11 +54,11 @@ class LocalCalendar private constructor(
private fun valuesFromCollectionInfo(info: CollectionInfo, withColor: Boolean): ContentValues {
val values = ContentValues()
values.put(Calendars.NAME, info.url)
values.put(Calendars.NAME, info.url.toString())
values.put(Calendars.CALENDAR_DISPLAY_NAME, if (info.displayName.isNullOrBlank()) DavUtils.lastSegmentOfUrl(info.url) else info.displayName)
if (withColor)
values.put(Calendars.CALENDAR_COLOR, info.color ?: defaultColor)
values.put(Calendars.CALENDAR_COLOR, info.color ?: Constants.DAVDROID_GREEN_RGBA)
if (info.readOnly || info.forceReadOnly)
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
@@ -90,31 +83,33 @@ class LocalCalendar private constructor(
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, "${CalendarContract.Attendees.TYPE_OPTIONAL},${CalendarContract.Attendees.TYPE_REQUIRED},${CalendarContract.Attendees.TYPE_RESOURCE}")
return values
}
}
override val title: String
get() = displayName ?: id.toString()
override fun eventBaseInfoColumns() = BASE_INFO_COLUMNS
override var lastSyncState: SyncState?
get() = provider.query(calendarSyncURI(), arrayOf(COLUMN_SYNC_STATE), null, null, null)?.let { cursor ->
if (cursor.moveToNext())
return SyncState.fromString(cursor.getString(0))
else
null
}
set(state) {
val values = ContentValues(1)
values.put(COLUMN_SYNC_STATE, state.toString())
provider.update(calendarSyncURI(), values, null, null)
}
@Throws(CalendarStorageException::class)
fun update(info: CollectionInfo, updateColor: Boolean) =
update(valuesFromCollectionInfo(info, updateColor))
@Throws(CalendarStorageException::class)
override fun getAll(): List<LocalEvent> =
queryEvents("${Events.ORIGINAL_ID} IS NULL", null)
@Throws(CalendarStorageException::class)
override fun getDeleted() =
override fun findDeleted() =
queryEvents("${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null)
@Throws(CalendarStorageException::class)
override fun getWithoutFileName() =
queryEvents("${Events._SYNC_ID} IS NULL AND ${Events.ORIGINAL_ID} IS NULL", null)
@Throws(FileNotFoundException::class, CalendarStorageException::class)
override fun getDirty(): List<LocalEvent> {
override fun findDirty(): List<LocalEvent> {
val dirty = LinkedList<LocalEvent>()
// get dirty events which are required to have an increased SEQUENCE value
@@ -131,50 +126,46 @@ class LocalCalendar private constructor(
return dirty
}
@Throws(CalendarStorageException::class)
override fun getCTag(): String? =
try {
provider.query(calendarSyncURI(), arrayOf(COLUMN_CTAG), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
return cursor.getString(0)
}
null
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't read local (last known) CTag", e)
}
override fun findByName(name: String) =
queryEvents("${Events._SYNC_ID}=?", arrayOf(name)).firstOrNull()
@Throws(CalendarStorageException::class)
override fun setCTag(cTag: String?) {
try {
val values = ContentValues(1)
values.put(COLUMN_CTAG, cTag)
provider.update(calendarSyncURI(), values, null, null)
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't write local (last known) CTag", e)
}
override fun markNotDirty(flags: Int): Int {
val values = ContentValues(1)
values.put(LocalEvent.COLUMN_FLAGS, flags)
return provider.update(eventsSyncURI(), values,
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY}=0 AND ${Events.ORIGINAL_ID} IS NULL",
arrayOf(id.toString()))
}
@Throws(CalendarStorageException::class)
override fun removeNotDirtyMarked(flags: Int) =
provider.delete(eventsSyncURI(),
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY}=0 AND ${Events.ORIGINAL_ID} IS NULL AND ${LocalEvent.COLUMN_FLAGS}=?",
arrayOf(id.toString(), flags.toString()))
fun processDirtyExceptions() {
try {
// process deleted exceptions
Logger.log.info("Processing deleted exceptions")
provider.query(
syncAdapterURI(Events.CONTENT_URI),
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
"${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NOT NULL", null, null)?.use { cursor ->
while (cursor.moveToNext()) {
Logger.log.fine("Found deleted exception, removing; then re-scheduling original event")
val id = cursor.getLong(0) // can't be null (by definition)
val originalID = cursor.getLong(1) // can't be null (by query)
// process deleted exceptions
Logger.log.info("Processing deleted exceptions")
provider.query(
syncAdapterURI(Events.CONTENT_URI),
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
"${Events.CALENDAR_ID}=? AND ${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NOT NULL",
arrayOf(id.toString()), null)?.use { cursor ->
while (cursor.moveToNext()) {
Logger.log.fine("Found deleted exception, removing and re-scheduling original event (if available)")
val id = cursor.getLong(0) // can't be null (by definition)
val originalID = cursor.getLong(1) // can't be null (by query)
val batch = BatchOperation(provider)
val batch = BatchOperation(provider)
// get original event's SEQUENCE
provider.query(
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)),
arrayOf(LocalEvent.COLUMN_SEQUENCE),
null, null, null)?.use { cursor2 ->
// get original event's SEQUENCE
provider.query(
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)),
arrayOf(LocalEvent.COLUMN_SEQUENCE),
null, null, null)?.use { cursor2 ->
if (cursor2.moveToNext()) {
// original event is available
val originalSequence = if (cursor2.isNull(0)) 0 else cursor2.getInt(0)
// re-schedule original event and set it to DIRTY
@@ -184,44 +175,43 @@ class LocalCalendar private constructor(
.withValue(Events.DIRTY, 1)
))
}
// remove exception
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newDelete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
))
batch.commit()
}
}
// process dirty exceptions
Logger.log.info("Processing dirty exceptions")
provider.query(
syncAdapterURI(Events.CONTENT_URI),
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
"${Events.DIRTY}!=0 AND ${Events.ORIGINAL_ID} IS NOT NULL", null, null)?.use { cursor ->
while (cursor.moveToNext()) {
Logger.log.fine("Found dirty exception, increasing SEQUENCE to re-schedule")
val id = cursor.getLong(0) // can't be null (by definition)
val originalID = cursor.getLong(1) // can't be null (by query)
val sequence = if (cursor.isNull(2)) 0 else cursor.getInt(2)
val batch = BatchOperation(provider)
// original event to DIRTY
batch.enqueue(BatchOperation.Operation (
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
.withValue(Events.DIRTY, 1)
))
// increase SEQUENCE and set DIRTY to 0
batch.enqueue(BatchOperation.Operation (
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
.withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1)
.withValue(Events.DIRTY, 0)
))
batch.commit()
}
// completely remove deleted exception
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newDelete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
))
batch.commit()
}
}
// process dirty exceptions
Logger.log.info("Processing dirty exceptions")
provider.query(
syncAdapterURI(Events.CONTENT_URI),
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
"${Events.CALENDAR_ID}=? AND ${Events.DIRTY}!=0 AND ${Events.ORIGINAL_ID} IS NOT NULL",
arrayOf(id.toString()), null)?.use { cursor ->
while (cursor.moveToNext()) {
Logger.log.fine("Found dirty exception, increasing SEQUENCE to re-schedule")
val id = cursor.getLong(0) // can't be null (by definition)
val originalID = cursor.getLong(1) // can't be null (by query)
val sequence = if (cursor.isNull(2)) 0 else cursor.getInt(2)
val batch = BatchOperation(provider)
// original event to DIRTY
batch.enqueue(BatchOperation.Operation (
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
.withValue(Events.DIRTY, 1)
))
// increase SEQUENCE and set DIRTY to 0
batch.enqueue(BatchOperation.Operation (
ContentProviderOperation.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
.withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1)
.withValue(Events.DIRTY, 0)
))
batch.commit()
}
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't process locally modified exception", e)
}
}
@@ -233,4 +223,4 @@ class LocalCalendar private constructor(
}
}
}

View File

@@ -8,28 +8,31 @@
package at.bitfire.davdroid.resource
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.ContactsStorageException
import java.io.FileNotFoundException
import at.bitfire.davdroid.model.SyncState
interface LocalCollection<out T: LocalResource> {
interface LocalCollection<out T: LocalResource<*>> {
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun getDeleted(): List<T>
/** collection title (used for user notifications etc.) **/
val title: String
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun getWithoutFileName(): List<T>
var lastSyncState: SyncState?
@Throws(CalendarStorageException::class, ContactsStorageException::class, FileNotFoundException::class)
fun getDirty(): List<T>
fun findDeleted(): List<T>
fun findDirty(): List<T>
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun getAll(): List<T>
fun findByName(name: String): T?
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun getCTag(): String?
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun setCTag(cTag: String?)
/**
* Marks all entries which are not dirty with the given flags only.
* @return number of marked entries
**/
fun markNotDirty(flags: Int): Int
/**
* Removes all entries with are not dirty and are marked with exactly the given flags.
* @return number of removed entries
*/
fun removeNotDirtyMarked(flags: Int): Int
}

View File

@@ -23,129 +23,110 @@ import ezvcard.Ezvcard
import java.io.FileNotFoundException
import java.util.*
class LocalContact: AndroidContact, LocalResource {
class LocalContact: AndroidContact, LocalAddress {
companion object {
init {
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ez-vcard/" + Ezvcard.VERSION
}
val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3
const val COLUMN_FLAGS = ContactsContract.RawContacts.SYNC4
const val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3
}
private val cachedGroupMemberships = HashSet<Long>()
private val groupMemberships = HashSet<Long>()
override var flags: Int = 0
constructor(addressBook: AndroidAddressBook<LocalContact,*>, id: Long, fileName: String?, eTag: String?):
super(addressBook, id, fileName, eTag)
constructor(addressBook: AndroidAddressBook<LocalContact,*>, contact: Contact, fileName: String?, eTag: String?):
super(addressBook, contact, fileName, eTag)
@Throws(ContactsStorageException::class)
fun resetDeleted() {
val values = ContentValues(1)
values.put(ContactsContract.RawContacts.DELETED, 0)
try {
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
} catch(e: RemoteException) {
throw ContactsStorageException("Couldn't clear deleted flag", e)
}
constructor(addressBook: AndroidAddressBook<LocalContact,*>, values: ContentValues)
: super(addressBook, values) {
flags = values.getAsInteger(COLUMN_FLAGS) ?: 0
}
constructor(addressBook: AndroidAddressBook<LocalContact,*>, contact: Contact, fileName: String?, eTag: String?, flags: Int)
: super(addressBook, contact, fileName, eTag) {
this.flags = flags
}
override fun assignNameAndUID() {
val uid = UUID.randomUUID().toString()
val newFileName = "$uid.vcf"
val values = ContentValues(2)
values.put(COLUMN_FILENAME, newFileName)
values.put(COLUMN_UID, uid)
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
fileName = newFileName
}
override fun clearDirty(eTag: String?) {
val values = ContentValues(3)
values.put(COLUMN_ETAG, eTag)
values.put(ContactsContract.RawContacts.DIRTY, 0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
val hashCode = dataHashCode()
values.put(COLUMN_HASHCODE, hashCode)
Logger.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode")
}
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
this.eTag = eTag
}
override fun resetDeleted() {
val values = ContentValues(1)
values.put(ContactsContract.Groups.DELETED, 0)
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
}
@Throws(ContactsStorageException::class)
fun resetDirty() {
val values = ContentValues(1)
values.put(ContactsContract.RawContacts.DIRTY, 0)
try {
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
} catch(e: RemoteException) {
throw ContactsStorageException("Couldn't clear dirty flag", e)
}
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
}
@Throws(ContactsStorageException::class)
override fun clearDirty(eTag: String?) {
try {
val values = ContentValues(3)
values.put(COLUMN_ETAG, eTag)
values.put(ContactsContract.RawContacts.DIRTY, 0)
override fun updateFlags(flags: Int) {
val values = ContentValues(1)
values.put(LocalContact.COLUMN_FLAGS, flags)
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
val hashCode = dataHashCode()
values.put(COLUMN_HASHCODE, hashCode)
Logger.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode")
}
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
this.eTag = eTag
} catch(e: Exception) {
throw ContactsStorageException("Couldn't clear dirty flag", e)
}
}
@Throws(ContactsStorageException::class)
override fun prepareForUpload() {
try {
val uid = UUID.randomUUID().toString()
val newFileName = uid + ".vcf"
val values = ContentValues(2)
values.put(COLUMN_FILENAME, newFileName)
values.put(COLUMN_UID, uid)
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
fileName = newFileName
} catch(e: RemoteException) {
throw ContactsStorageException("Couldn't update UID", e)
}
this.flags = flags
}
@Throws(ContactsStorageException::class)
override fun populateData(mimeType: String, row: ContentValues) {
when (mimeType) {
CachedGroupMembership.CONTENT_ITEM_TYPE ->
cachedGroupMemberships.add(row.getAsLong(CachedGroupMembership.GROUP_ID))
cachedGroupMemberships += row.getAsLong(CachedGroupMembership.GROUP_ID)
GroupMembership.CONTENT_ITEM_TYPE ->
groupMemberships.add(row.getAsLong(GroupMembership.GROUP_ROW_ID))
groupMemberships += row.getAsLong(GroupMembership.GROUP_ROW_ID)
UnknownProperties.CONTENT_ITEM_TYPE ->
try {
contact!!.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES)
} catch(e: FileNotFoundException) {
throw ContactsStorageException("Couldn't fetch data rows", e)
}
contact!!.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES)
}
}
@Throws(ContactsStorageException::class)
override fun insertDataRows(batch: BatchOperation) {
super.insertDataRows(batch)
try {
contact!!.unknownProperties?.let { unknownProperties ->
val op: BatchOperation.Operation
val builder = ContentProviderOperation.newInsert(dataSyncURI())
if (id == null)
op = BatchOperation.Operation(builder, UnknownProperties.RAW_CONTACT_ID, 0)
else {
op = BatchOperation.Operation(builder)
builder.withValue(UnknownProperties.RAW_CONTACT_ID, id)
}
builder .withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE)
.withValue(UnknownProperties.UNKNOWN_PROPERTIES, unknownProperties)
batch.enqueue(op)
contact!!.unknownProperties?.let { unknownProperties ->
val op: BatchOperation.Operation
val builder = ContentProviderOperation.newInsert(dataSyncURI())
if (id == null)
op = BatchOperation.Operation(builder, UnknownProperties.RAW_CONTACT_ID, 0)
else {
op = BatchOperation.Operation(builder)
builder.withValue(UnknownProperties.RAW_CONTACT_ID, id)
}
} catch(e: FileNotFoundException) {
throw ContactsStorageException("Couldn't insert data rows", e)
builder .withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE)
.withValue(UnknownProperties.UNKNOWN_PROPERTIES, unknownProperties)
batch.enqueue(op)
}
}
@@ -154,7 +135,6 @@ class LocalContact: AndroidContact, LocalResource {
* Attention: re-reads {@link #contact} from the database, discarding all changes in memory
* @return hash code of contact data (including group memberships)
*/
@Throws(FileNotFoundException::class, ContactsStorageException::class)
internal fun dataHashCode(): Int {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
throw IllegalStateException("dataHashCode() should not be called on Android != 7")
@@ -169,44 +149,34 @@ class LocalContact: AndroidContact, LocalResource {
return dataHash xor groupHash
}
@Throws(ContactsStorageException::class)
fun updateHashCode(batch: BatchOperation?) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
throw IllegalStateException("updateHashCode() should not be called on Android != 7")
val values = ContentValues(1)
try {
val hashCode = dataHashCode()
Logger.log.fine("Storing contact hash = $hashCode")
values.put(COLUMN_HASHCODE, hashCode)
val hashCode = dataHashCode()
Logger.log.fine("Storing contact hash = $hashCode")
values.put(COLUMN_HASHCODE, hashCode)
if (batch == null)
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
else {
val builder = ContentProviderOperation
.newUpdate(rawContactSyncURI())
.withValues(values)
batch.enqueue(BatchOperation.Operation(builder))
}
} catch(e: Exception) {
throw ContactsStorageException("Couldn't store contact checksum", e)
if (batch == null)
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
else {
val builder = ContentProviderOperation
.newUpdate(rawContactSyncURI())
.withValues(values)
batch.enqueue(BatchOperation.Operation(builder))
}
}
@Throws(ContactsStorageException::class)
fun getLastHashCode(): Int {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
throw IllegalStateException("getLastHashCode() should not be called on Android != 7")
try {
addressBook.provider!!.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null)?.use { c ->
if (c.moveToNext() && !c.isNull(0))
return c.getInt(0)
}
return 0
} catch(e: RemoteException) {
throw ContactsStorageException("Could't read last hash code", e)
addressBook.provider!!.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null)?.use { c ->
if (c.moveToNext() && !c.isNull(0))
return c.getInt(0)
}
return 0
}
@@ -217,7 +187,7 @@ class LocalContact: AndroidContact, LocalResource {
.withValue(GroupMembership.RAW_CONTACT_ID, id)
.withValue(GroupMembership.GROUP_ROW_ID, groupID)
))
groupMemberships.add(groupID)
groupMemberships += groupID
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newInsert(dataSyncURI())
@@ -226,7 +196,7 @@ class LocalContact: AndroidContact, LocalResource {
.withValue(CachedGroupMembership.GROUP_ID, groupID)
.withYieldAllowed(true)
))
cachedGroupMemberships.add(groupID)
cachedGroupMemberships += groupID
}
fun removeGroupMemberships(batch: BatchOperation) {
@@ -247,10 +217,9 @@ class LocalContact: AndroidContact, LocalResource {
* 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
* @throws FileNotFoundException if the current contact can't be found
* @throws RemoteException on contacts provider errors
*/
@Throws(FileNotFoundException::class, ContactsStorageException::class)
fun getCachedGroupMemberships(): Set<Long> {
contact
return cachedGroupMemberships
@@ -259,26 +228,26 @@ class LocalContact: AndroidContact, LocalResource {
/**
* 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
* @throws FileNotFoundException if the current contact can't be found
* @throws RemoteException on contacts provider errors
*/
@Throws(FileNotFoundException::class, ContactsStorageException::class)
fun getGroupMemberships(): Set<Long> {
contact
return groupMemberships
}
// data rows
override fun buildContact(builder: ContentProviderOperation.Builder, update: Boolean) {
builder.withValue(COLUMN_FLAGS, flags)
super.buildContact(builder, update)
}
// factory
object Factory: AndroidContactFactory<LocalContact> {
override fun newInstance(addressBook: AndroidAddressBook<LocalContact, *>, id: Long, fileName: String?, eTag: String?) =
LocalContact(addressBook, id, fileName, eTag)
override fun newInstance(addressBook: AndroidAddressBook<LocalContact, *>, contact: Contact, fileName: String?, eTag: String?) =
LocalContact(addressBook, contact, fileName, eTag)
override fun fromProvider(addressBook: AndroidAddressBook<LocalContact, *>, values: ContentValues) =
LocalContact(addressBook, values)
}
}

View File

@@ -15,56 +15,54 @@ import android.provider.CalendarContract.Events
import at.bitfire.davdroid.BuildConfig
import at.bitfire.ical4android.*
import net.fortuna.ical4j.model.property.ProdId
import java.io.FileNotFoundException
import java.util.*
class LocalEvent: AndroidEvent, LocalResource {
class LocalEvent: AndroidEvent, LocalResource<Event> {
companion object {
init {
iCalendar.prodId = ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4j/2.x")
ICalendar.prodId = ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4j/" + Constants.ical4jVersion)
}
val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1
val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3
const val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1
const val COLUMN_FLAGS = CalendarContract.Events.SYNC_DATA2
const val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3
}
override var fileName: String? = null
private set
override var eTag: String? = null
override var flags: Int = 0
private set
var weAreOrganizer = true
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?): super(calendar, event) {
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?, flags: Int): super(calendar, event) {
this.fileName = fileName
this.eTag = eTag
this.flags = flags
}
private constructor(calendar: AndroidCalendar<*>, id: Long, baseInfo: ContentValues?): super(calendar, id, baseInfo) {
baseInfo?.let {
fileName = it.getAsString(Events._SYNC_ID)
eTag = it.getAsString(COLUMN_ETAG)
}
private constructor(calendar: AndroidCalendar<*>, values: ContentValues): super(calendar, values) {
fileName = values.getAsString(Events._SYNC_ID)
eTag = values.getAsString(COLUMN_ETAG)
flags = values.getAsInteger(COLUMN_FLAGS) ?: 0
}
/* process LocalEvent-specific fields */
@Throws(FileNotFoundException::class, CalendarStorageException::class)
override fun populateEvent(row: ContentValues) {
super.populateEvent(row)
val event = requireNotNull(event)
fileName = row.getAsString(Events._SYNC_ID)
eTag = row.getAsString(COLUMN_ETAG)
event.uid = row.getAsString(Events.UID_2445)
event.sequence = row.getAsInteger(COLUMN_SEQUENCE)
val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER)
weAreOrganizer = isOrganizer != null && isOrganizer != 0
}
@Throws(FileNotFoundException::class, CalendarStorageException::class)
override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) {
super.buildEvent(recurrence, builder)
val event = requireNotNull(event)
@@ -76,6 +74,7 @@ class LocalEvent: AndroidEvent, LocalResource {
.withValue(COLUMN_SEQUENCE, eventToBuild.sequence)
.withValue(CalendarContract.Events.DIRTY, 0)
.withValue(CalendarContract.Events.DELETED, 0)
.withValue(LocalEvent.COLUMN_FLAGS, flags)
if (buildException)
builder .withValue(Events.ORIGINAL_SYNC_ID, fileName)
@@ -85,57 +84,48 @@ class LocalEvent: AndroidEvent, LocalResource {
}
/* custom queries */
@Throws(CalendarStorageException::class)
override fun prepareForUpload() {
try {
var uid: String? = null
calendar.provider.query(eventSyncURI(), arrayOf(Events.UID_2445), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
uid = cursor.getString(0)
}
if (uid == null)
uid = UUID.randomUUID().toString()
val newFileName = "$uid.ics"
val values = ContentValues(2)
values.put(Events._SYNC_ID, newFileName)
values.put(Events.UID_2445, uid)
calendar.provider.update(eventSyncURI(), values, null, null)
fileName = newFileName
event!!.uid = uid
} catch(e: Exception) {
throw CalendarStorageException("Couldn't update UID", e)
override fun assignNameAndUID() {
var uid: String? = null
calendar.provider.query(eventSyncURI(), arrayOf(Events.UID_2445), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
uid = cursor.getString(0)
}
if (uid == null)
uid = UUID.randomUUID().toString()
val newFileName = "$uid.ics"
val values = ContentValues(2)
values.put(Events._SYNC_ID, newFileName)
values.put(Events.UID_2445, uid)
calendar.provider.update(eventSyncURI(), values, null, null)
fileName = newFileName
event!!.uid = uid
}
@Throws(CalendarStorageException::class)
override fun clearDirty(eTag: String?) {
try {
val values = ContentValues(2)
values.put(CalendarContract.Events.DIRTY, 0)
values.put(COLUMN_ETAG, eTag)
values.put(COLUMN_SEQUENCE, event!!.sequence)
calendar.provider.update(eventSyncURI(), values, null, null)
val values = ContentValues(2)
values.put(CalendarContract.Events.DIRTY, 0)
values.put(COLUMN_ETAG, eTag)
values.put(COLUMN_SEQUENCE, event!!.sequence)
calendar.provider.update(eventSyncURI(), values, null, null)
this.eTag = eTag
} catch (e: Exception) {
throw CalendarStorageException("Couldn't update UID", e)
}
this.eTag = eTag
}
override fun updateFlags(flags: Int) {
val values = ContentValues(1)
values.put(COLUMN_FLAGS, flags)
calendar.provider.update(eventSyncURI(), values, null, null)
this.flags = flags
}
object Factory: AndroidEventFactory<LocalEvent> {
override fun newInstance(calendar: AndroidCalendar<*>, id: Long, baseInfo: ContentValues?) =
LocalEvent(calendar, id, baseInfo)
override fun newInstance(calendar: AndroidCalendar<*>, event: Event) =
LocalEvent(calendar, event, null, null)
override fun fromProvider(calendar: AndroidCalendar<*>, values: ContentValues) =
LocalEvent(calendar, values)
}
}

View File

@@ -11,6 +11,7 @@ package at.bitfire.davdroid.resource
import android.content.ContentProviderOperation
import android.content.ContentUris
import android.content.ContentValues
import android.net.Uri
import android.os.Build
import android.os.Parcel
import android.os.RemoteException
@@ -21,116 +22,126 @@ import android.provider.ContactsContract.RawContacts
import android.provider.ContactsContract.RawContacts.Data
import at.bitfire.dav4android.Constants
import at.bitfire.vcard4android.*
import java.io.FileNotFoundException
import java.util.*
import java.util.logging.Level
class LocalGroup: AndroidGroup, LocalResource {
class LocalGroup: AndroidGroup, LocalAddress {
companion object {
const val COLUMN_FLAGS = Groups.SYNC4
/** marshaled list of member UIDs, as sent by server */
val COLUMN_PENDING_MEMBERS = Groups.SYNC3
const val COLUMN_PENDING_MEMBERS = Groups.SYNC3
/**
* 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
*/
@JvmStatic
@Throws(ContactsStorageException::class)
fun applyPendingMemberships(addressBook: LocalAddressBook) {
try {
addressBook.provider!!.query(
addressBook.syncAdapterURI(Groups.CONTENT_URI),
arrayOf(Groups._ID, COLUMN_PENDING_MEMBERS),
"$COLUMN_PENDING_MEMBERS IS NOT NULL", null,
null
)?.use { cursor ->
val batch = BatchOperation(addressBook.provider)
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
Constants.log.fine("Assigning members to group $id")
addressBook.provider!!.query(
addressBook.groupsSyncUri(),
arrayOf(Groups._ID, COLUMN_PENDING_MEMBERS),
"$COLUMN_PENDING_MEMBERS IS NOT NULL", null,
null
)?.use { cursor ->
val batch = BatchOperation(addressBook.provider)
while (cursor.moveToNext()) {
val 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
val changeContactIDs = HashSet<Long>()
// required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed
val changeContactIDs = HashSet<Long>()
// delete all memberships and cached memberships for this group
for (contact in addressBook.getByGroupMembership(id)) {
contact.removeGroupMemberships(batch)
changeContactIDs.add(contact.id!!)
}
// extract list of member UIDs
val members = LinkedList<String>()
val raw = cursor.getBlob(1)
val parcel = Parcel.obtain()
try {
parcel.unmarshall(raw, 0, raw.size)
parcel.setDataPosition(0)
parcel.readStringList(members)
} finally {
parcel.recycle()
}
// insert memberships
for (uid in members) {
Constants.log.fine("Assigning member: $uid")
try {
val member = addressBook.findContactByUID(uid)
member.addToGroup(batch, id)
changeContactIDs.add(member.id!!)
} catch(e: FileNotFoundException) {
Constants.log.log(Level.WARNING, "Group member not found: $uid", e)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
changeContactIDs
.map { LocalContact(addressBook, it, null, null) }
.forEach { it.updateHashCode(batch) }
// remove pending memberships
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
.withValue(COLUMN_PENDING_MEMBERS, null)
.withYieldAllowed(true)
))
batch.commit()
// delete all memberships and cached memberships for this group
for (contact in addressBook.getByGroupMembership(id)) {
contact.removeGroupMemberships(batch)
changeContactIDs += contact.id!!
}
// extract list of member UIDs
val members = LinkedList<String>()
val raw = cursor.getBlob(1)
val parcel = Parcel.obtain()
try {
parcel.unmarshall(raw, 0, raw.size)
parcel.setDataPosition(0)
parcel.readStringList(members)
} finally {
parcel.recycle()
}
// insert memberships
for (uid in members) {
Constants.log.fine("Assigning member: $uid")
addressBook.findContactByUID(uid)?.let { member ->
member.addToGroup(batch, id)
changeContactIDs += member.id!!
} ?: Constants.log.warning("Group member not found: $uid")
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
changeContactIDs
.map { addressBook.findContactByID(it) }
.forEach { it.updateHashCode(batch) }
// remove pending memberships
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
.withValue(COLUMN_PENDING_MEMBERS, null)
.withYieldAllowed(true)
))
batch.commit()
}
} catch(e: RemoteException) {
throw ContactsStorageException("Couldn't get pending memberships", e)
}
}
}
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, id: Long, fileName: String?, eTag: String?):
super(addressBook, id, fileName, eTag)
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, contact: Contact, fileName: String?, eTag: String?):
super(addressBook, contact, fileName, eTag)
override var flags: Int = 0
@Throws(ContactsStorageException::class)
fun resetDeleted() {
val uri = ContentUris.withAppendedId(addressBook.syncAdapterURI(ContactsContract.Groups.CONTENT_URI), requireNotNull(id))
val values = ContentValues(1)
values.put(ContactsContract.Groups.DELETED, 0)
try {
addressBook.provider!!.update(uri, values, null, null)
} catch(e: RemoteException) {
throw ContactsStorageException("Couldn't clear deleted flag", e)
}
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, values: ContentValues)
: super(addressBook, values) {
flags = values.getAsInteger(COLUMN_FLAGS) ?: 0
}
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, contact: Contact, fileName: String?, eTag: String?, flags: Int)
: super(addressBook, contact, fileName, eTag) {
this.flags = flags
}
override fun contentValues(): ContentValues {
val values = super.contentValues()
values.put(COLUMN_FLAGS, flags)
val members = Parcel.obtain()
try {
members.writeStringList(contact!!.members)
values.put(COLUMN_PENDING_MEMBERS, members.marshall())
} finally {
members.recycle()
}
return values
}
override fun assignNameAndUID() {
val uid = UUID.randomUUID().toString()
val newFileName = "$uid.vcf"
val values = ContentValues(2)
values.put(COLUMN_FILENAME, newFileName)
values.put(COLUMN_UID, uid)
update(values)
fileName = newFileName
}
@Throws(ContactsStorageException::class)
override fun clearDirty(eTag: String?) {
val id = requireNotNull(id)
@@ -165,38 +176,9 @@ class LocalGroup: AndroidGroup, LocalResource {
batch.commit()
}
@Throws(ContactsStorageException::class)
override fun prepareForUpload() {
val uid = UUID.randomUUID().toString()
val newFileName = "$uid.vcf"
val values = ContentValues(2)
values.put(COLUMN_FILENAME, newFileName)
values.put(COLUMN_UID, uid)
update(values)
fileName = newFileName
}
@Throws(FileNotFoundException::class, ContactsStorageException::class)
override fun contentValues(): ContentValues {
val values = super.contentValues()
val members = Parcel.obtain()
try {
members.writeStringList(contact!!.members)
values.put(COLUMN_PENDING_MEMBERS, members.marshall())
} finally {
members.recycle()
}
return values
}
/**
* Marks all members of the current group as dirty.
*/
@Throws(ContactsStorageException::class)
fun markMembersDirty() {
val batch = BatchOperation(addressBook.provider!!)
@@ -210,31 +192,45 @@ class LocalGroup: AndroidGroup, LocalResource {
batch.commit()
}
override fun resetDeleted() {
val values = ContentValues(1)
values.put(Groups.DELETED, 0)
addressBook.provider!!.update(groupSyncUri(), values, null, null)
}
override fun updateFlags(flags: Int) {
val values = ContentValues(1)
values.put(LocalGroup.COLUMN_FLAGS, flags)
addressBook.provider!!.update(groupSyncUri(), values, null, null)
this.flags = flags
}
// helpers
private fun groupSyncUri(): Uri {
val id = requireNotNull(id)
return ContentUris.withAppendedId(addressBook.groupsSyncUri(), id)
}
/**
* Lists all members of this group.
* @return list of all members' raw contact IDs
* @throws ContactsStorageException on contact provider errorst
* @throws RemoteException on contact provider errors
*/
@Throws(ContactsStorageException::class)
internal fun getMembers(): List<Long> {
val id = requireNotNull(id)
val members = LinkedList<Long>()
try {
addressBook.provider!!.query(
addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI),
arrayOf(Data.RAW_CONTACT_ID),
"${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?",
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()),
null
)?.use { cursor ->
while (cursor.moveToNext())
members.add(cursor.getLong(0))
}
} catch(e: RemoteException) {
throw ContactsStorageException("Couldn't list group members", e)
addressBook.provider!!.query(
addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI),
arrayOf(Data.RAW_CONTACT_ID),
"${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?",
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()),
null
)?.use { cursor ->
while (cursor.moveToNext())
members += cursor.getLong(0)
}
return members
}
@@ -243,13 +239,8 @@ class LocalGroup: AndroidGroup, LocalResource {
// factory
object Factory: AndroidGroupFactory<LocalGroup> {
override fun newInstance(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, id: Long, fileName: String?, eTag: String?) =
LocalGroup(addressBook, id, fileName, eTag)
override fun newInstance(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, contact: Contact, fileName: String?, eTag: String?) =
LocalGroup(addressBook, contact, fileName, eTag)
override fun fromProvider(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, values: ContentValues) =
LocalGroup(addressBook, values)
}
}

View File

@@ -8,23 +8,50 @@
package at.bitfire.davdroid.resource
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.ContactsStorageException
import android.net.Uri
interface LocalResource {
interface LocalResource<in TData: Any> {
companion object {
/**
* Resource is present on remote server. This flag is used to identify resources
* which are not present on the remote server anymore and can be deleted at the end
* of the synchronization.
*/
const val FLAG_REMOTELY_PRESENT = 1
}
/**
* Unique ID which identifies the resource in the local storage. May be null if the
* resource has not been saved yet.
*/
val id: Long?
var fileName: String?
val fileName: String?
var eTag: String?
val flags: Int
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun assignNameAndUID()
fun clearDirty(eTag: String?)
fun updateFlags(flags: Int)
/**
* Adds the data object to the content provider and ensures that the dirty flag is clear.
* @return content URI of the created row (e.g. event URI)
*/
fun add(): Uri
/**
* Updates the data object in the content provider and ensures that the dirty flag is clear.
* @return content URI of the updated row (e.g. event URI)
*/
fun update(data: TData): Uri
/**
* Deletes the data object from the content provider.
* @return number of affected rows
*/
fun delete(): Int
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun prepareForUpload()
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun clearDirty(eTag: String?)
}

View File

@@ -10,105 +10,93 @@ package at.bitfire.davdroid.resource
import android.content.ContentProviderOperation
import android.content.ContentValues
import android.provider.CalendarContract.Events
import at.bitfire.ical4android.*
import at.bitfire.ical4android.AndroidTask
import at.bitfire.ical4android.AndroidTaskFactory
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.Task
import org.dmfs.tasks.contract.TaskContract.Tasks
import java.io.FileNotFoundException
import java.text.ParseException
import java.util.*
class LocalTask: AndroidTask, LocalResource {
class LocalTask: AndroidTask, LocalResource<Task> {
companion object {
val COLUMN_ETAG = Tasks.SYNC_VERSION
val COLUMN_SEQUENCE = Tasks.SYNC3
const val COLUMN_ETAG = Tasks.SYNC1
const val COLUMN_FLAGS = Tasks.SYNC2
}
override var fileName: String? = null
override var eTag: String? = null
constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?): super(taskList, task) {
override var flags = 0
private set
constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?, flags: Int)
: super(taskList, task) {
this.fileName = fileName
this.eTag = eTag
this.flags = flags
}
private constructor(taskList: AndroidTaskList<*>, id: Long, baseInfo: ContentValues?): super(taskList, id) {
baseInfo?.let {
fileName = it.getAsString(Events._SYNC_ID)
eTag = it.getAsString(COLUMN_ETAG)
}
private constructor(taskList: AndroidTaskList<*>, values: ContentValues): super(taskList) {
id = values.getAsLong(Tasks._ID)
fileName = values.getAsString(Tasks._SYNC_ID)
eTag = values.getAsString(COLUMN_ETAG)
flags = values.getAsInteger(COLUMN_FLAGS) ?: 0
}
/* process LocalTask-specific fields */
@Throws(ParseException::class)
override fun populateTask(values: ContentValues) {
super.populateTask(values)
fileName = values.getAsString(Events._SYNC_ID)
eTag = values.getAsString(COLUMN_ETAG)
val task = requireNotNull(task)
task.sequence = values.getAsInteger(COLUMN_SEQUENCE)
}
@Throws(FileNotFoundException::class, CalendarStorageException::class)
override fun buildTask(builder: ContentProviderOperation.Builder, update: Boolean) {
super.buildTask(builder, update)
val task = requireNotNull(task)
builder .withValue(Tasks._SYNC_ID, fileName)
.withValue(COLUMN_SEQUENCE, task.sequence)
.withValue(COLUMN_ETAG, eTag)
.withValue(COLUMN_FLAGS, flags)
}
/* custom queries */
@Throws(CalendarStorageException::class)
override fun prepareForUpload() {
try {
val uid = UUID.randomUUID().toString()
val newFileName = uid + ".ics"
override fun assignNameAndUID() {
val uid = UUID.randomUUID().toString()
val newFileName = "$uid.ics"
val values = ContentValues(2)
values.put(Tasks._SYNC_ID, newFileName)
values.put(Tasks._UID, uid)
taskList.provider.client.update(taskSyncURI(), values, null, null)
val values = ContentValues(2)
values.put(Tasks._SYNC_ID, newFileName)
values.put(Tasks._UID, uid)
taskList.provider.client.update(taskSyncURI(), values, null, null)
fileName = newFileName
fileName = newFileName
task!!.uid = uid
} catch (e: Exception) {
throw CalendarStorageException("Couldn't update UID", e)
}
task!!.uid = uid
}
@Throws(CalendarStorageException::class)
override fun clearDirty(eTag: String?) {
try {
val values = ContentValues(2)
values.put(Tasks._DIRTY, 0)
values.put(COLUMN_ETAG, eTag)
val values = ContentValues(3)
values.put(Tasks._DIRTY, 0)
values.put(COLUMN_ETAG, eTag)
values.put(Tasks.SYNC_VERSION, task!!.sequence)
taskList.provider.client.update(taskSyncURI(), values, null, null)
values.put(COLUMN_SEQUENCE, task!!.sequence)
this.eTag = eTag
}
override fun updateFlags(flags: Int) {
if (id != null) {
val values = ContentValues(1)
values.put(COLUMN_FLAGS, flags)
taskList.provider.client.update(taskSyncURI(), values, null, null)
this.eTag = eTag
} catch (e: Exception) {
throw CalendarStorageException("Couldn't update _DIRTY/ETag/SEQUENCE", e)
}
this.flags = flags
}
object Factory: AndroidTaskFactory<LocalTask> {
override fun newInstance(calendar: AndroidTaskList<*>, id: Long, baseInfo: ContentValues?) =
LocalTask(calendar, id, baseInfo)
override fun newInstance(calendar: AndroidTaskList<*>, task: Task) =
LocalTask(calendar, task, null, null)
override fun fromProvider(taskList: AndroidTaskList<*>, values: ContentValues) =
LocalTask(taskList, values)
}
}

View File

@@ -15,15 +15,17 @@ import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.os.Build
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.SyncState
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.AndroidTaskListFactory
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.TaskProvider
import org.dmfs.tasks.contract.TaskContract.TaskLists
import org.dmfs.tasks.contract.TaskContract.Tasks
import java.io.FileNotFoundException
import java.util.logging.Level
class LocalTaskList private constructor(
account: Account,
@@ -33,29 +35,20 @@ class LocalTaskList private constructor(
companion object {
val defaultColor = 0xFFC3EA6E.toInt() // "DAVdroid green"
val COLUMN_CTAG = TaskLists.SYNC_VERSION
val BASE_INFO_COLUMNS = arrayOf(
Tasks._ID,
Tasks._SYNC_ID,
LocalTask.COLUMN_ETAG
)
@JvmStatic
fun tasksProviderAvailable(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
return context.packageManager.resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null
else {
val provider = TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)
provider?.use { return true }
else
try {
TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)?.use {
return true
}
} catch (e: Exception) {
// couldn't acquire task provider
}
return false
}
}
@JvmStatic
@Throws(CalendarStorageException::class)
fun create(account: Account, provider: TaskProvider, info: CollectionInfo): Uri {
val values = valuesFromCollectionInfo(info, true)
values.put(TaskLists.OWNER, account.name)
@@ -64,7 +57,6 @@ class LocalTaskList private constructor(
return create(account, provider, values)
}
@JvmStatic
@Throws(Exception::class)
fun onRenameAccount(resolver: ContentResolver, oldName: String, newName: String) {
var client: ContentProviderClient? = null
@@ -85,38 +77,49 @@ class LocalTaskList private constructor(
private fun valuesFromCollectionInfo(info: CollectionInfo, withColor: Boolean): ContentValues {
val values = ContentValues(3)
values.put(TaskLists._SYNC_ID, info.url)
values.put(TaskLists._SYNC_ID, info.url.toString())
values.put(TaskLists.LIST_NAME, if (info.displayName.isNullOrBlank()) DavUtils.lastSegmentOfUrl(info.url) else info.displayName)
if (withColor)
values.put(TaskLists.LIST_COLOR, info.color ?: defaultColor)
values.put(TaskLists.LIST_COLOR, info.color ?: Constants.DAVDROID_GREEN_RGBA)
return values
}
}
override val title: String
get() = name ?: id.toString()
override fun taskBaseInfoColumns() = BASE_INFO_COLUMNS
override var lastSyncState: SyncState?
get() {
try {
provider.client.query(taskListSyncUri(), arrayOf(TaskLists.SYNC_VERSION),
null, null, null)?.use { cursor ->
if (cursor.moveToNext())
cursor.getString(0)?.let {
return SyncState.fromString(it)
}
}
} catch (e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't read sync state", e)
}
return null
}
set(state) {
val values = ContentValues(1)
values.put(TaskLists.SYNC_VERSION, state?.toString())
provider.client.update(taskListSyncUri(), values, null, null)
}
@Throws(CalendarStorageException::class)
fun update(info: CollectionInfo, updateColor: Boolean) {
update(valuesFromCollectionInfo(info, updateColor))
}
fun update(info: CollectionInfo, updateColor: Boolean) =
update(valuesFromCollectionInfo(info, updateColor))
@Throws(CalendarStorageException::class)
override fun getAll() = queryTasks(null, null)
override fun findDeleted() = queryTasks("${Tasks._DELETED}!=0", null)
@Throws(CalendarStorageException::class)
override fun getDeleted() = queryTasks("${Tasks._DELETED}!=0", null)
@Throws(CalendarStorageException::class)
override fun getWithoutFileName() = queryTasks("${Tasks._SYNC_ID} IS NULL", null)
@Throws(FileNotFoundException::class, CalendarStorageException::class)
override fun getDirty(): List<LocalTask> {
override fun findDirty(): List<LocalTask> {
val tasks = queryTasks("${Tasks._DIRTY}!=0", null)
for (localTask in tasks) {
val task = requireNotNull(localTask.task)
@@ -129,30 +132,23 @@ class LocalTaskList private constructor(
return tasks
}
override fun findByName(name: String) =
queryTasks("${Tasks._SYNC_ID}=?", arrayOf(name)).firstOrNull()
@Throws(CalendarStorageException::class)
override fun getCTag(): String? =
try {
provider.client.query(taskListSyncUri(), arrayOf(COLUMN_CTAG), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
return cursor.getString(0)
}
null
} catch(e: Exception) {
throw CalendarStorageException("Couldn't read local (last known) CTag", e)
}
@Throws(CalendarStorageException::class)
override fun setCTag(cTag: String?) {
try {
val values = ContentValues(1)
values.put(COLUMN_CTAG, cTag)
provider.client.update(taskListSyncUri(), values, null, null)
} catch (e: Exception) {
throw CalendarStorageException("Couldn't write local (last known) CTag", e)
}
override fun markNotDirty(flags: Int): Int {
val values = ContentValues(1)
values.put(LocalTask.COLUMN_FLAGS, flags)
return provider.client.update(tasksSyncUri(), values,
"${Tasks.LIST_ID}=? AND ${Tasks._DIRTY}=0",
arrayOf(id.toString()))
}
override fun removeNotDirtyMarked(flags: Int) =
provider.client.delete(tasksSyncUri(),
"${Tasks.LIST_ID}=? AND ${Tasks._DIRTY}=0 AND ${LocalTask.COLUMN_FLAGS}=?",
arrayOf(id.toString(), flags.toString()))
object Factory: AndroidTaskListFactory<LocalTaskList> {
@@ -161,4 +157,4 @@ class LocalTaskList private constructor(
}
}
}

View File

@@ -19,7 +19,6 @@ import android.os.Handler
import android.os.IBinder
import android.os.Looper
import at.bitfire.davdroid.log.Logger
import java.io.Closeable
import java.lang.ref.WeakReference
import java.util.*
import java.util.logging.Level
@@ -111,16 +110,16 @@ class Settings: Service(), Provider.Observer {
}
fun getBoolean(key: String) =
getValue(key, { provider -> provider.getBoolean(key) })
getValue(key) { provider -> provider.getBoolean(key) }
fun getInt(key: String) =
getValue(key, { provider -> provider.getInt(key) })
getValue(key) { provider -> provider.getInt(key) }
fun getLong(key: String) =
getValue(key, { provider -> provider.getLong(key) })
getValue(key) { provider -> provider.getLong(key) }
fun getString(key: String) =
getValue(key, { provider -> provider.getString(key) })
getValue(key) { provider -> provider.getString(key) }
fun isWritable(key: String): Boolean {
@@ -153,16 +152,16 @@ class Settings: Service(), Provider.Observer {
}
fun putBoolean(key: String, value: Boolean?) =
putValue(key, value, { provider -> provider.putBoolean(key, value) })
putValue(key, value) { provider -> provider.putBoolean(key, value) }
fun putInt(key: String, value: Int?) =
putValue(key, value, { provider -> provider.putInt(key, value) })
putValue(key, value) { provider -> provider.putInt(key, value) }
fun putLong(key: String, value: Long?) =
putValue(key, value, { provider -> provider.putLong(key, value) })
putValue(key, value) { provider -> provider.putLong(key, value) }
fun putString(key: String, value: String?) =
putValue(key, value, { provider -> provider.putString(key, value) })
putValue(key, value) { provider -> provider.putString(key, value) }
fun remove(key: String): Boolean {
var deleted = false
@@ -226,7 +225,7 @@ class Settings: Service(), Provider.Observer {
delegate: ISettings,
private val context: Context,
private val serviceConn: ServiceConnection?
): ISettings by delegate, Closeable {
): ISettings by delegate, AutoCloseable {
override fun close() {
serviceConn?.let {

View File

@@ -20,8 +20,8 @@ class SharedPreferencesProvider(
): Provider {
companion object {
private val META_VERSION = "version"
private val CURRENT_VERSION = 0
private const val META_VERSION = "version"
private const val CURRENT_VERSION = 0
}
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
@@ -56,44 +56,44 @@ class SharedPreferencesProvider(
}
override fun getBoolean(key: String): Pair<Boolean?, Boolean> =
getValue(key, { preferences -> preferences.getBoolean(key, /* will never be used: */ false) })
getValue(key) { preferences -> preferences.getBoolean(key, /* will never be used: */ false) }
override fun getInt(key: String): Pair<Int?, Boolean> =
getValue(key, { preferences -> preferences.getInt(key, /* will never be used: */ -1) })
getValue(key) { preferences -> preferences.getInt(key, /* will never be used: */ -1) }
override fun getLong(key: String): Pair<Long?, Boolean> =
getValue(key, { preferences -> preferences.getLong(key, /* will never be used: */ -1) })
getValue(key) { preferences -> preferences.getLong(key, /* will never be used: */ -1) }
override fun getString(key: String): Pair<String?, Boolean> =
getValue(key, { preferences -> preferences.getString(key, /* will never be used: */ null) })
getValue(key) { preferences -> preferences.getString(key, /* will never be used: */ null) }
override fun isWritable(key: String) =
Pair(true, true)
private fun<T> putValue(key: String, value: T?, writer: (SharedPreferences.Editor, T) -> Unit): Boolean {
if (value == null)
return remove(key)
return if (value == null)
remove(key)
else {
Logger.log.fine("Writing setting $key = $value")
val edit = preferences.edit()
writer(edit, value)
edit.apply()
return true
true
}
}
override fun putBoolean(key: String, value: Boolean?) =
putValue(key, value, { editor, v -> editor.putBoolean(key, v) })
putValue(key, value) { editor, v -> editor.putBoolean(key, v) }
override fun putInt(key: String, value: Int?) =
putValue(key, value, { editor, v -> editor.putInt(key, v) })
putValue(key, value) { editor, v -> editor.putInt(key, v) }
override fun putLong(key: String, value: Long?) =
putValue(key, value, { editor, v -> editor.putLong(key, v) })
putValue(key, value) { editor, v -> editor.putLong(key, v) }
override fun putString(key: String, value: String?) =
putValue(key, value, { editor, v -> editor.putString(key, v) })
putValue(key, value) { editor, v -> editor.putString(key, v) }
override fun remove(key: String): Boolean {
Logger.log.fine("Removing setting $key")

View File

@@ -18,7 +18,6 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.ui.setup.LoginActivity
import at.bitfire.vcard4android.ContactsStorageException
import java.util.*
import java.util.logging.Level
@@ -52,10 +51,10 @@ class AccountAuthenticatorService: Service(), OnAccountsUpdateListener {
.map { LocalAddressBook(context, it, null) }
.forEach {
try {
if (!accountNames.contains(it.getMainAccount().name))
if (!accountNames.contains(it.mainAccount.name))
it.delete()
} catch(e: ContactsStorageException) {
Logger.log.log(Level.SEVERE, "Couldn't get address book main account", e)
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e)
}
}

View File

@@ -7,12 +7,16 @@
*/
package at.bitfire.davdroid.syncadapter
import android.Manifest
import android.accounts.Account
import android.accounts.AccountManager
import android.content.*
import android.content.pm.PackageManager
import android.database.DatabaseUtils
import android.os.Build
import android.os.Bundle
import android.provider.ContactsContract
import android.support.v4.content.ContextCompat
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
@@ -21,26 +25,20 @@ import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.vcard4android.ContactsStorageException
import at.bitfire.davdroid.ui.AccountActivity
import okhttp3.HttpUrl
import java.util.logging.Level
class AddressBooksSyncAdapterService: SyncAdapterService() {
class AddressBooksSyncAdapterService : SyncAdapterService() {
override fun syncAdapter() = AddressBooksSyncAdapter(this)
protected class AddressBooksSyncAdapter(
class AddressBooksSyncAdapter(
context: Context
): SyncAdapter(context) {
override fun sync(settings: ISettings, account: Account, extras: Bundle, authority: String, addressBooksProvider: ContentProviderClient, syncResult: SyncResult) {
val contactsProvider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
if (contactsProvider == null) {
Logger.log.severe("Couldn't access contacts provider")
syncResult.databaseError = true
return
}
) : SyncAdapter(context) {
override fun sync(settings: ISettings, account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
try {
val accountSettings = AccountSettings(context, settings, account)
@@ -51,7 +49,7 @@ class AddressBooksSyncAdapterService: SyncAdapterService() {
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
return
updateLocalAddressBooks(contactsProvider, account)
updateLocalAddressBooks(provider, account, syncResult)
val accountManager = AccountManager.get(context)
for (addressBookAccount in accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))) {
@@ -61,32 +59,32 @@ class AddressBooksSyncAdapterService: SyncAdapterService() {
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true)
ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras)
}
} catch(e: Exception) {
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't sync address books", e)
}
Logger.log.info("Address book sync complete")
}
private fun updateLocalAddressBooks(provider: ContentProviderClient, account: Account) {
private fun updateLocalAddressBooks(provider: ContentProviderClient, account: Account, syncResult: SyncResult) {
ServiceDB.OpenHelper(context).use { dbHelper ->
val db = dbHelper.readableDatabase
fun getService() =
db.query(ServiceDB.Services._TABLE, arrayOf(ServiceDB.Services.ID),
"${ServiceDB.Services.ACCOUNT_NAME}=? AND ${ServiceDB.Services.SERVICE}=?",
arrayOf(account.name, ServiceDB.Services.SERVICE_CARDDAV), null, null, null)?.use { c ->
"${ServiceDB.Services.ACCOUNT_NAME}=? AND ${ServiceDB.Services.SERVICE}=?",
arrayOf(account.name, ServiceDB.Services.SERVICE_CARDDAV), null, null, null)?.use { c ->
if (c.moveToNext())
c.getLong(0)
else
null
}
fun remoteAddressBooks(service: Long?): MutableMap<String, CollectionInfo> {
val collections = mutableMapOf<String, CollectionInfo>()
fun remoteAddressBooks(service: Long?): MutableMap<HttpUrl, CollectionInfo> {
val collections = mutableMapOf<HttpUrl, CollectionInfo>()
service?.let {
db.query(Collections._TABLE, null,
Collections.SERVICE_ID + "=? AND " + Collections.SYNC, arrayOf(service.toString()), null, null, null)?.use { cursor ->
Collections.SERVICE_ID + "=? AND " + Collections.SYNC, arrayOf(service.toString()), null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
val values = ContentValues(cursor.columnCount)
DatabaseUtils.cursorRowToContentValues(cursor, values)
@@ -102,30 +100,58 @@ class AddressBooksSyncAdapterService: SyncAdapterService() {
val service = getService()
val remote = remoteAddressBooks(service)
// delete/update local address books
for (addressBook in LocalAddressBook.find(context, provider, account)) {
val url = addressBook.getURL()
val info = remote[url]
if (info == null) {
Logger.log.log(Level.INFO, "Deleting obsolete local address book", url)
addressBook.delete()
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
if (remote.isEmpty()) {
Logger.log.info("No contacts permission, but no address book selected for synchronization")
return
} else {
// remote CollectionInfo found for this local collection, update data
try {
Logger.log.log(Level.FINE, "Updating local address book $url", info)
addressBook.update(info)
} catch(e: ContactsStorageException) {
Logger.log.log(Level.WARNING, "Couldn't rename address book account", e)
}
// we already have a local address book for this remote collection, don't take into consideration anymore
remote -= url
// no contacts permission, but address books should be synchronized -> show notification
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
notifyPermissions(intent)
}
}
// create new local address books
for ((_, info) in remote) {
Logger.log.log(Level.INFO, "Adding local address book", info)
LocalAddressBook.create(context, provider, account, info)
val contactsProvider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
try {
if (contactsProvider == null) {
Logger.log.severe("Couldn't access contacts provider")
syncResult.databaseError = true
return
}
// delete/update local address books
for (addressBook in LocalAddressBook.findAll(context, contactsProvider, account)) {
val url = HttpUrl.parse(addressBook.url)!!
val info = remote[url]
if (info == null) {
Logger.log.log(Level.INFO, "Deleting obsolete local address book", url)
addressBook.delete()
} else {
// remote CollectionInfo found for this local collection, update data
try {
Logger.log.log(Level.FINE, "Updating local address book $url", info)
addressBook.update(info)
} catch (e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't rename address book account", e)
}
// we already have a local address book for this remote collection, don't take into consideration anymore
remote -= url
}
}
// create new local address books
for ((_, info) in remote) {
Logger.log.log(Level.INFO, "Adding local address book", info)
LocalAddressBook.create(context, contactsProvider, account, info)
}
} finally {
if (Build.VERSION.SDK_INT >= 24)
contactsProvider?.close()
else
contactsProvider?.release()
}
}
}

View File

@@ -9,19 +9,19 @@
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.Context
import android.content.SyncResult
import android.os.Bundle
import at.bitfire.dav4android.DavCalendar
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.DavResponseCallback
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.property.CalendarData
import at.bitfire.dav4android.property.GetCTag
import at.bitfire.dav4android.property.GetETag
import at.bitfire.dav4android.property.*
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.SyncState
import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.resource.LocalEvent
import at.bitfire.davdroid.resource.LocalResource
@@ -30,7 +30,6 @@ import at.bitfire.ical4android.Event
import at.bitfire.ical4android.InvalidCalendarException
import okhttp3.HttpUrl
import okhttp3.RequestBody
import org.apache.commons.collections4.ListUtils
import java.io.ByteArrayOutputStream
import java.io.Reader
import java.io.StringReader
@@ -38,7 +37,7 @@ import java.util.*
import java.util.logging.Level
/**
* Synchronization manager for CalDAV collections; handles events ({@code VEVENT}).
* Synchronization manager for CalDAV collections; handles events (VEVENT)
*/
class CalendarSyncManager(
context: Context,
@@ -48,55 +47,55 @@ class CalendarSyncManager(
extras: Bundle,
authority: String,
syncResult: SyncResult,
val provider: ContentProviderClient,
val localCalendar: LocalCalendar
): SyncManager(context, settings, account, accountSettings, extras, authority, syncResult, "calendar/${localCalendar.id}") {
companion object {
private val MAX_MULTIGET = 20
}
init {
localCollection = localCalendar
}
override fun notificationId() = Constants.NOTIFICATION_CALENDAR_SYNC
override fun getSyncErrorTitle() = context.getString(R.string.sync_error_calendar, account.name)!!
localCalendar: LocalCalendar
): SyncManager<LocalEvent, LocalCalendar, DavCalendar>(context, settings, account, accountSettings, extras, authority, syncResult, localCalendar) {
override fun prepare(): Boolean {
collectionURL = HttpUrl.parse(localCalendar.name ?: return false) ?: return false
collectionURL = HttpUrl.parse(localCollection.name ?: return false) ?: return false
davCollection = DavCalendar(httpClient.okHttpClient, collectionURL)
// if there are dirty exceptions for events, mark their master events as dirty, too
localCollection.processDirtyExceptions()
return true
}
override fun queryCapabilities() {
davCollection.propfind(0, GetCTag.NAME)
override fun queryCapabilities(): SyncState? =
useRemoteCollection {
var syncState: SyncState? = null
it.propfind(0, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation ->
if (relation == Response.HrefRelation.SELF) {
response[SupportedReportSet::class.java]?.let {
hasCollectionSync = it.reports.contains(SupportedReportSet.SYNC_COLLECTION)
}
syncState = syncState(response)
}
}
Logger.log.info("Server supports Collection Sync: $hasCollectionSync")
syncState
}
override fun syncAlgorithm() = if (accountSettings.getTimeRangePastDays() != null || !hasCollectionSync)
SyncAlgorithm.PROPFIND_REPORT
else
SyncAlgorithm.COLLECTION_SYNC
override fun prepareUpload(resource: LocalEvent): RequestBody = useLocal(resource) {
val event = requireNotNull(resource.event)
Logger.log.log(Level.FINE, "Preparing upload of event ${resource.fileName}", event)
val os = ByteArrayOutputStream()
event.write(os)
RequestBody.create(
DavCalendar.MIME_ICALENDAR_UTF8,
os.toByteArray()
)
}
override fun prepareDirty() {
super.prepareDirty()
localCalendar.processDirtyExceptions()
}
override fun prepareUpload(resource: LocalResource): RequestBody {
if (resource is LocalEvent) {
val event = requireNotNull(resource.event)
Logger.log.log(Level.FINE, "Preparing upload of event ${resource.fileName}", event)
val os = ByteArrayOutputStream()
event.write(os)
return RequestBody.create(
DavCalendar.MIME_ICALENDAR,
os.toByteArray()
)
} else
throw IllegalArgumentException("resource must be a LocalEvent")
}
override fun listRemote() {
override fun listAllRemote(callback: DavResponseCallback) {
// calculate time range limits
var limitStart: Date? = null
accountSettings.getTimeRangePastDays()?.let { pastDays ->
@@ -105,75 +104,52 @@ class CalendarSyncManager(
limitStart = calendar.time
}
// fetch list of remote VEVENTs and build hash table to index file name
val calendar = davCalendar()
currentDavResource = calendar
calendar.calendarQuery("VEVENT", limitStart, null)
remoteResources = HashMap(davCollection.members.size)
for (iCal in davCollection.members) {
val fileName = iCal.fileName()
Logger.log.fine("Found remote VEVENT: $fileName")
remoteResources[fileName] = iCal
return useRemoteCollection { remote ->
Logger.log.info("Querying events since $limitStart")
remote.calendarQuery("VEVENT", limitStart, null, callback)
}
currentDavResource = null
}
override fun downloadRemote() {
Logger.log.info("Downloading ${toDownload.size} events ($MAX_MULTIGET at once)")
override fun downloadRemote(bunch: List<HttpUrl>) {
Logger.log.info("Downloading ${bunch.size} iCalendars: $bunch")
if (bunch.size == 1) {
val remote = bunch.first()
// only one contact, use GET
useRemote(DavResource(httpClient.okHttpClient, remote)) { resource ->
resource.get(DavCalendar.MIME_ICALENDAR.toString()) { response ->
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
val eTag = response.header("ETag")?.let { GetETag(it).eTag }
?: throw DavException("Received CalDAV GET response without ETag")
// download new/updated iCalendars from server
for (bunch in ListUtils.partition(toDownload.toList(), MAX_MULTIGET)) {
abortIfCancelled()
Logger.log.info("Downloading ${bunch.joinToString(", ")}")
if (bunch.size == 1) {
// only one contact, use GET
val remote = bunch.first()
currentDavResource = remote
val body = remote.get("text/calendar")
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
val eTag = remote.properties[GetETag::class.java]
if (eTag == null || eTag.eTag.isNullOrEmpty())
throw DavException("Received CalDAV GET response without ETag for ${remote.location}")
body.charStream()?.use { reader ->
processVEvent(remote.fileName(), eTag.eTag!!, reader)
}
} else {
// multiple contacts, use multi-get
val calendar = davCalendar()
currentDavResource = calendar
calendar.multiget(bunch.map { it.location })
// process multiget results
for (remote in davCollection.members) {
currentDavResource = remote
val eTag = remote.properties[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val calendarData = remote.properties[CalendarData::class.java]
val iCalendar = calendarData?.iCalendar
?: throw DavException("Received multi-get response without event data")
processVEvent(remote.fileName(), eTag, StringReader(iCalendar))
response.body()!!.use {
processVEvent(resource.fileName(), eTag, it.charStream())
}
}
}
} else
// multiple iCalendars, use calendar-multi-get
useRemoteCollection {
it.multiget(bunch) { response, _ ->
useRemote(response) {
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
currentDavResource = null
}
val calendarData = response[CalendarData::class.java]
val iCal = calendarData?.iCalendar
?: throw DavException("Received multi-get response without address data")
processVEvent(DavUtils.lastSegmentOfUrl(response.href), eTag, StringReader(iCal))
}
}
}
}
override fun postProcess() {
}
// helpers
private fun davCalendar() = davCollection as DavCalendar
private fun processVEvent(fileName: String, eTag: String, reader: Reader) {
val events: List<Event>
try {
@@ -187,24 +163,22 @@ class CalendarSyncManager(
val newData = events.first()
// delete local event, if it exists
val localEvent = localResources[fileName] as LocalEvent?
currentLocalResource = localEvent
if (localEvent != null) {
Logger.log.info("Updating $fileName in local calendar")
localEvent.eTag = eTag
localEvent.update(newData)
syncResult.stats.numUpdates++
} else {
Logger.log.info("Adding $fileName to local calendar")
val newEvent = LocalEvent(localCalendar, newData, fileName, eTag)
currentLocalResource = newEvent
newEvent.add()
syncResult.stats.numInserts++
useLocal(localCollection.findByName(fileName)) { local ->
if (local != null) {
Logger.log.info("Updating $fileName in local calendar")
local.eTag = eTag
local.update(newData)
syncResult.stats.numUpdates++
} else {
Logger.log.info("Adding $fileName to local calendar")
useLocal(LocalEvent(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
it.add()
}
syncResult.stats.numInserts++
}
}
} else
Logger.log.severe("Received VCALENDAR with not exactly one VEVENT with UID, but without RECURRENCE-ID; ignoring $fileName")
currentLocalResource = null
Logger.log.info("Received VCALENDAR with not exactly one VEVENT with UID and without RECURRENCE-ID; ignoring $fileName")
}
}

View File

@@ -20,6 +20,7 @@ import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.ical4android.AndroidCalendar
import okhttp3.HttpUrl
import java.util.logging.Level
class CalendarsSyncAdapterService: SyncAdapterService() {
@@ -51,7 +52,7 @@ class CalendarsSyncAdapterService: SyncAdapterService() {
for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, "${CalendarContract.Calendars.SYNC_EVENTS}!=0", null)) {
Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}")
CalendarSyncManager(context, settings, account, accountSettings, extras, authority, syncResult, provider, calendar).use {
CalendarSyncManager(context, settings, account, accountSettings, extras, authority, syncResult, calendar).use {
it.performSync()
}
}
@@ -75,8 +76,8 @@ class CalendarsSyncAdapterService: SyncAdapterService() {
null
}
fun remoteCalendars(service: Long?): MutableMap<String, CollectionInfo> {
val collections = mutableMapOf<String, CollectionInfo>()
fun remoteCalendars(service: Long?): MutableMap<HttpUrl, CollectionInfo> {
val collections = mutableMapOf<HttpUrl, CollectionInfo>()
service?.let {
db.query(Collections._TABLE, null,
"${Collections.SERVICE_ID}=? AND ${Collections.SUPPORTS_VEVENT}!=0 AND ${Collections.SYNC}",
@@ -100,7 +101,8 @@ class CalendarsSyncAdapterService: SyncAdapterService() {
val updateColors = settings.getManageCalendarColors()
for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null))
calendar.name?.let { url ->
calendar.name?.let {
val url = HttpUrl.parse(it)!!
val info = remote[url]
if (info == null) {
Logger.log.log(Level.INFO, "Deleting obsolete local calendar", url)

View File

@@ -24,7 +24,7 @@ import java.util.logging.Level
class ContactsSyncAdapterService: SyncAdapterService() {
companion object {
val PREVIOUS_GROUP_METHOD = "previous_group_method"
const val PREVIOUS_GROUP_METHOD = "previous_group_method"
}
override fun syncAdapter() = ContactsSyncAdapter(this)
@@ -37,7 +37,7 @@ class ContactsSyncAdapterService: SyncAdapterService() {
override fun sync(settings: ISettings, account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
try {
val addressBook = LocalAddressBook(context, account, provider)
val accountSettings = AccountSettings(context, settings, addressBook.getMainAccount())
val accountSettings = AccountSettings(context, settings, addressBook.mainAccount)
// handle group method change
val groupMethod = accountSettings.getGroupMethod().name
@@ -49,8 +49,8 @@ class ContactsSyncAdapterService: SyncAdapterService() {
provider.delete(addressBook.syncAdapterURI(ContactsContract.RawContacts.CONTENT_URI), null, null)
provider.delete(addressBook.syncAdapterURI(ContactsContract.Groups.CONTENT_URI), null, null)
// reset CTag
addressBook.setCTag(null)
// reset sync state
addressBook.syncState = null
}
}
accountSettings.accountManager.setUserData(account, PREVIOUS_GROUP_METHOD, groupMethod)
@@ -58,8 +58,8 @@ class ContactsSyncAdapterService: SyncAdapterService() {
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
return
Logger.log.info("Synchronizing address book: ${addressBook.getURL()}")
Logger.log.info("Taking settings from: ${addressBook.getMainAccount()}")
Logger.log.info("Synchronizing address book: ${addressBook.url}")
Logger.log.info("Taking settings from: ${addressBook.mainAccount}")
ContactsSyncManager(context, settings, account, accountSettings, extras, authority, syncResult, provider, addressBook).use {
it.performSync()

View File

@@ -12,31 +12,31 @@ import android.accounts.Account
import android.content.*
import android.os.Build
import android.os.Bundle
import android.os.RemoteException
import android.provider.ContactsContract.Groups
import android.support.v4.app.NotificationCompat
import at.bitfire.dav4android.DavAddressBook
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.DavResponseCallback
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.property.*
import at.bitfire.davdroid.*
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalContact
import at.bitfire.davdroid.resource.LocalGroup
import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.model.SyncState
import at.bitfire.davdroid.resource.*
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.vcard4android.BatchOperation
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.ContactsStorageException
import at.bitfire.vcard4android.GroupMethod
import ezvcard.VCardVersion
import okhttp3.HttpUrl
import okhttp3.Request
import okhttp3.RequestBody
import org.apache.commons.collections4.ListUtils
import java.io.*
import java.util.*
import java.util.logging.Level
/**
@@ -45,34 +45,34 @@ import java.util.logging.Level
* Group handling differs according to the {@link #groupMethod}. There are two basic methods to
* handle/manage groups:
*
* 1. 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 [prepareDirty]. Empty groups can be deleted without further processing,
* which is done in [postProcess] because groups may become empty after downloading
* updated remote contacts.
* 1. 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 [uploadDirty]. Empty groups can be deleted without further processing,
* which is done in [postProcess] because groups may become empty after downloading
* updated remote contacts.
*
* 2. 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.
* 2. 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.
*
* * 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
* [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 [android.provider.ContactsContract.CommonDataKinds.GroupMembership]
* data rows in respective [at.bitfire.vcard4android.CachedGroupMembership] rows.
* If the cached group memberships are not the same as the current group member ships, the
* difference set (in our example G, because its in the cached memberships, but not in the
* actual ones) is marked as dirty. This is done in [prepareDirty].
* 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
* [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 [android.provider.ContactsContract.CommonDataKinds.GroupMembership]
* data rows in respective [at.bitfire.vcard4android.CachedGroupMembership] rows.
* If the cached group memberships are not the same as the current group member ships, the
* difference set (in our example G, because its in the cached memberships, but not in the
* actual ones) is marked as dirty. This is done in [uploadDirty].
*
* * 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
* [LocalGroup.COLUMN_PENDING_MEMBERS]. In [postProcess],
* these "pending memberships" are assigned to the actual contacts and then cleaned up.
* 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
* [LocalGroup.COLUMN_PENDING_MEMBERS]. In [postProcess],
* these "pending memberships" are assigned to the actual contacts and then cleaned up.
*/
class ContactsSyncManager(
context: Context,
@@ -83,96 +83,110 @@ class ContactsSyncManager(
authority: String,
syncResult: SyncResult,
val provider: ContentProviderClient,
private val localAddressBook: LocalAddressBook
): SyncManager(context, settings, account, accountSettings, extras, authority, syncResult, "addressBook") {
localAddressBook: LocalAddressBook
): SyncManager<LocalAddress, LocalAddressBook, DavAddressBook>(context, settings, account, accountSettings, extras, authority, syncResult, localAddressBook) {
companion object {
private val MAX_MULTIGET = 10
infix fun <T> Set<T>.disjunct(other: Set<T>) = (this - other) union (other - this)
}
private val readOnly = localAddressBook.getReadOnly()
private val readOnly = localAddressBook.readOnly
private var numDiscarded = 0
private var hasVCard4 = false
private val groupMethod = accountSettings.getGroupMethod()
init {
localCollection = localAddressBook
}
override fun notificationId() = Constants.NOTIFICATION_CONTACTS_SYNC
override fun getSyncErrorTitle() = context.getString(R.string.sync_error_contacts, account.name)!!
/**
* Used to download images which are referenced by URL
*/
private lateinit var resourceDownloader: ResourceDownloader
override fun prepare(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
val reallyDirty = localAddressBook.verifyDirty()
val deleted = localAddressBook.getDeleted().size
val reallyDirty = localCollection.verifyDirty()
val deleted = localCollection.findDeleted().size
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_UPLOAD) && reallyDirty == 0 && deleted == 0) {
Logger.log.info("This sync was called to up-sync dirty/deleted contacts, but no contacts have been changed")
return false
}
}
collectionURL = HttpUrl.parse(localAddressBook.getURL()) ?: return false
collectionURL = HttpUrl.parse(localCollection.url) ?: return false
davCollection = DavAddressBook(httpClient.okHttpClient, collectionURL)
resourceDownloader = ResourceDownloader(davCollection.location)
return true
}
override fun queryCapabilities() {
// prepare remote address book
davCollection.propfind(0, SupportedAddressData.NAME, GetCTag.NAME)
val properties = davCollection.properties
properties[SupportedAddressData::class.java]?.let {
hasVCard4 = it.hasVCard4()
}
Logger.log.info("Server advertises VCard/4 support: $hasVCard4")
override fun queryCapabilities(): SyncState? {
Logger.log.info("Contact group method: $groupMethod")
// in case of GROUP_VCARDs, treat groups as contacts in the local address book
localAddressBook.includeGroups = groupMethod == GroupMethod.GROUP_VCARDS
localCollection.includeGroups = groupMethod == GroupMethod.GROUP_VCARDS
return useRemoteCollection {
var syncState: SyncState? = null
it.propfind(0, SupportedAddressData.NAME, SupportedReportSet.NAME, GetCTag.NAME, SyncToken.NAME) { response, relation ->
if (relation == Response.HrefRelation.SELF) {
response[SupportedAddressData::class.java]?.let {
hasVCard4 = it.hasVCard4()
}
response[SupportedReportSet::class.java]?.let {
hasCollectionSync = it.reports.contains(SupportedReportSet.SYNC_COLLECTION)
}
syncState = syncState(response)
}
}
Logger.log.info("Server supports vCard/4: $hasVCard4")
Logger.log.info("Server supports Collection Sync: $hasCollectionSync")
syncState
}
}
override fun processLocallyDeleted() {
override fun syncAlgorithm() = if (hasCollectionSync)
SyncAlgorithm.COLLECTION_SYNC
else
SyncAlgorithm.PROPFIND_REPORT
override fun processLocallyDeleted(): Boolean {
if (readOnly) {
for (group in localAddressBook.getDeletedGroups()) {
for (group in localCollection.findDeletedGroups()) {
Logger.log.warning("Restoring locally deleted group (read-only address book!)")
group.resetDeleted()
useLocal(group) { it.resetDeleted() }
numDiscarded++
}
for (contact in localAddressBook.getDeletedContacts()) {
for (contact in localCollection.findDeletedContacts()) {
Logger.log.warning("Restoring locally deleted contact (read-only address book!)")
contact.resetDeleted()
useLocal(contact) { it.resetDeleted() }
numDiscarded++
}
if (numDiscarded > 0)
notifyDiscardedChange()
return false
} else
// mirror deletions to remote collection (DELETE)
super.processLocallyDeleted()
return super.processLocallyDeleted()
}
override fun prepareDirty() {
// generate UID/file name for newly created contacts
super.prepareDirty()
override fun uploadDirty(): Boolean {
if (readOnly) {
for (group in localAddressBook.getDirtyGroups()) {
for (group in localCollection.findDirtyGroups()) {
Logger.log.warning("Resetting locally modified group to ETag=null (read-only address book!)")
group.clearDirty(null)
useLocal(group) { it.clearDirty(null) }
numDiscarded++
}
for (contact in localAddressBook.getDirtyContacts()) {
for (contact in localCollection.findDirtyContacts()) {
Logger.log.warning("Resetting locally modified contact to ETag=null (read-only address book!)")
contact.clearDirty(null)
useLocal(contact) { it.clearDirty(null) }
numDiscarded++
}
@@ -184,25 +198,27 @@ class ContactsSyncManager(
/* groups memberships are represented as contact CATEGORIES */
// groups with DELETED=1: set all members to dirty, then remove group
for (group in localAddressBook.getDeletedGroups()) {
for (group in localCollection.findDeletedGroups()) {
Logger.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()
useLocal(group) { it.delete() }
}
// groups with DIRTY=1: mark all memberships as dirty, then clean DIRTY flag of group
for (group in localAddressBook.getDirtyGroups()) {
for (group in localCollection.findDirtyGroups()) {
Logger.log.fine("Marking members of modified group $group as dirty")
group.markMembersDirty()
group.clearDirty(null)
useLocal(group) {
it.markMembersDirty()
it.clearDirty(null)
}
}
} else {
/* groups as separate VCards: there are group contacts and individual contacts */
// mark groups with changed members as dirty
val batch = BatchOperation(localAddressBook.provider!!)
for (contact in localAddressBook.getDirtyContacts())
val batch = BatchOperation(localCollection.provider!!)
for (contact in localCollection.findDirtyContacts())
try {
Logger.log.fine("Looking for changed group memberships of contact ${contact.fileName}")
val cachedGroups = contact.getCachedGroupMemberships()
@@ -210,7 +226,7 @@ class ContactsSyncManager(
for (groupID in cachedGroups disjunct currentGroups) {
Logger.log.fine("Marking group as dirty: $groupID")
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newUpdate(localAddressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)))
ContentProviderOperation.newUpdate(localCollection.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)))
.withValue(Groups.DIRTY, 1)
.withYieldAllowed(true)
))
@@ -220,24 +236,26 @@ class ContactsSyncManager(
batch.commit()
}
}
// generate UID/file name for newly created contacts
return super.uploadDirty()
}
private fun notifyDiscardedChange() {
val notification = NotificationCompat.Builder(context, NotificationUtils.CHANNEL_SYNC_STATUS)
val notification = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_STATUS)
.setSmallIcon(R.drawable.ic_delete_notification)
.setLargeIcon(App.getLauncherBitmap(context))
.setContentTitle(context.getString(R.string.sync_contacts_read_only_address_book))
.setContentText(context.resources.getQuantityString(R.plurals.sync_contacts_local_contact_changes_discarded, numDiscarded, numDiscarded))
.setNumber(numDiscarded)
.setSubText(account.name)
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setLocalOnly(true)
.setAutoCancel(true)
.build()
notificationManager.notify("discarded_${account.name}", 0, notification)
}
override fun prepareUpload(resource: LocalResource): RequestBody {
override fun prepareUpload(resource: LocalAddress): RequestBody = useLocal(resource) {
val contact: Contact
if (resource is LocalContact) {
contact = resource.contact!!
@@ -245,114 +263,71 @@ class ContactsSyncManager(
if (groupMethod == GroupMethod.CATEGORIES) {
// add groups as CATEGORIES
for (groupID in resource.getGroupMemberships()) {
try {
provider.query(
localAddressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)),
arrayOf(Groups.TITLE), null, null, null
)?.use { cursor ->
if (cursor.moveToNext()) {
val title = cursor.getString(0)
if (!title.isNullOrEmpty())
contact.categories.add(title)
}
provider.query(
localCollection.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)),
arrayOf(Groups.TITLE), null, null, null
)?.use { cursor ->
if (cursor.moveToNext()) {
val title = cursor.getString(0)
if (!title.isNullOrEmpty())
contact.categories.add(title)
}
} catch(e: RemoteException) {
throw ContactsStorageException("Couldn't find group for adding CATEGORIES", e)
}
}
}
} else if (resource is LocalGroup)
contact = resource.contact!!
else
throw IllegalArgumentException("resource must be a LocalContact or a LocalGroup")
throw IllegalArgumentException("resource must be LocalContact or LocalGroup")
Logger.log.log(Level.FINE, "Preparing upload of VCard ${resource.fileName}", contact)
val os = ByteArrayOutputStream()
contact.write(if (hasVCard4) VCardVersion.V4_0 else VCardVersion.V3_0, groupMethod, os)
return RequestBody.create(
RequestBody.create(
if (hasVCard4) DavAddressBook.MIME_VCARD4 else DavAddressBook.MIME_VCARD3_UTF8,
os.toByteArray()
)
}
override fun listRemote() {
val addressBook = davAddressBook()
currentDavResource = addressBook
// fetch list of remote VCards and build hash table to index file name
addressBook.propfind(1, ResourceType.NAME, GetETag.NAME)
remoteResources = HashMap(davCollection.members.size)
for (vCard in davCollection.members) {
// ignore member collections
var ignore = false
vCard.properties[ResourceType::class.java]?.let { type ->
if (type.types.contains(ResourceType.COLLECTION))
ignore = true
}
if (ignore)
continue
val fileName = vCard.fileName()
Logger.log.fine("Found remote VCard: $fileName")
remoteResources[fileName] = vCard
}
currentDavResource = null
}
override fun downloadRemote() {
Logger.log.info("Downloading ${toDownload.size} contacts ($MAX_MULTIGET at once)")
// prepare downloader which may be used to download external resource like contact photos
val downloader = ResourceDownloader(collectionURL)
// download new/updated VCards from server
for (bunch in ListUtils.partition(toDownload.toList(), MAX_MULTIGET)) {
abortIfCancelled()
Logger.log.info("Downloading ${bunch.joinToString(", ")}")
if (bunch.size == 1) {
// only one contact, use GET
val remote = bunch.first()
currentDavResource = remote
val body = remote.get("text/vcard;version=4.0, text/vcard;charset=utf-8;q=0.8, text/vcard;q=0.5")
// CardDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc6352#section-6.3.2.3]
val eTag = remote.properties[GetETag::class.java]
if (eTag == null || eTag.eTag.isNullOrEmpty())
throw DavException("Received CardDAV GET response without ETag for ${remote.location}")
body.charStream().use { reader ->
processVCard(remote.fileName(), eTag.eTag!!, reader, downloader)
}
} else {
// multiple contacts, use multi-get
val addressBook = davAddressBook()
currentDavResource = addressBook
addressBook.multiget(bunch.map { it.location }, hasVCard4)
// process multi-get results
for (remote in davCollection.members) {
currentDavResource = remote
val eTag = remote.properties[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val addressData = remote.properties[AddressData::class.java]
val vCard = addressData?.vCard
?: throw DavException("Received multi-get response without address data")
processVCard(remote.fileName(), eTag, StringReader(vCard), downloader)
}
override fun listAllRemote(callback: DavResponseCallback) =
useRemoteCollection {
it.propfind(1, ResourceType.NAME, GetETag.NAME, callback = callback)
}
currentDavResource = null
}
override fun downloadRemote(bunch: List<HttpUrl>) {
Logger.log.info("Downloading ${bunch.size} vCards: $bunch")
if (bunch.size == 1) {
val remote = bunch.first()
// only one contact, use GET
useRemote(DavResource(httpClient.okHttpClient, remote)) { resource ->
resource.get("text/vcard;version=4.0, text/vcard;charset=utf-8;q=0.8, text/vcard;q=0.5") { response ->
// CardDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc6352#section-6.3.2.3]
val eTag = response.header("ETag")?.let { GetETag(it).eTag }
?: throw DavException("Received CardDAV GET response without ETag")
response.body()!!.use {
processVCard(resource.fileName(), eTag, it.charStream(), resourceDownloader)
}
}
}
} else
// multiple vCards, use addressbook-multi-get
useRemoteCollection {
it.multiget(bunch, hasVCard4) { response, _ ->
useRemote(response) {
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val addressData = response[AddressData::class.java]
val vCard = addressData?.vCard
?: throw DavException("Received multi-get response without address data")
processVCard(DavUtils.lastSegmentOfUrl(response.href), eTag, StringReader(vCard), resourceDownloader)
}
}
}
}
override fun postProcess() {
@@ -361,20 +336,18 @@ class ContactsSyncManager(
// remove empty groups
Logger.log.info("Removing empty groups")
localAddressBook.removeEmptyGroups()
localCollection.removeEmptyGroups()
} else {
/* VCard4 group handling: there are group contacts and individual contacts */
Logger.log.info("Assigning memberships of downloaded contact groups")
LocalGroup.applyPendingMemberships(localAddressBook)
LocalGroup.applyPendingMemberships(localCollection)
}
}
// helpers
private fun davAddressBook() = davCollection as DavAddressBook
private fun processVCard(fileName: String, eTag: String, reader: Reader, downloader: Contact.Downloader) {
Logger.log.info("Processing CardDAV resource $fileName")
val contacts = Contact.fromReader(reader, downloader)
@@ -392,71 +365,69 @@ class ContactsSyncManager(
}
// update local contact, if it exists
var local = localResources[fileName]
currentLocalResource = local
if (local != null) {
Logger.log.log(Level.INFO, "Updating $fileName in local address book", newData)
useLocal(localCollection.findByName(fileName)) {
var local = it
if (local != null) {
Logger.log.log(Level.INFO, "Updating $fileName in local address book", newData)
if (local is LocalGroup && newData.group) {
// update group
local.eTag = eTag
local.updateFromServer(newData)
syncResult.stats.numUpdates++
if (local is LocalGroup && newData.group) {
// update group
local.eTag = eTag
local.flags = LocalResource.FLAG_REMOTELY_PRESENT
local.update(newData)
syncResult.stats.numUpdates++
} else if (local is LocalContact && !newData.group) {
// update contact
local.eTag = eTag
local.update(newData)
syncResult.stats.numUpdates++
} else if (local is LocalContact && !newData.group) {
// update contact
local.eTag = eTag
local.flags = LocalResource.FLAG_REMOTELY_PRESENT
local.update(newData)
syncResult.stats.numUpdates++
} else {
// group has become an individual contact or vice versa
local.delete()
local = null
}
}
if (local == null) {
if (newData.group) {
Logger.log.log(Level.INFO, "Creating local group", newData)
val group = LocalGroup(localAddressBook, newData, fileName, eTag)
currentLocalResource = group
group.create()
local = group
} else {
Logger.log.log(Level.INFO, "Creating local contact", newData)
val contact = LocalContact(localAddressBook, newData, fileName, eTag)
currentLocalResource = contact
contact.create()
local = contact
}
syncResult.stats.numInserts++
}
if (groupMethod == GroupMethod.CATEGORIES && local is LocalContact) {
// VCard3: update group memberships from CATEGORIES
currentLocalResource = local
val batch = BatchOperation(provider)
Logger.log.log(Level.FINE, "Removing contact group memberships")
local.removeGroupMemberships(batch)
for (category in local.contact!!.categories) {
val groupID = localAddressBook.findOrCreateGroup(category)
Logger.log.log(Level.FINE, "Adding membership in group $category ($groupID)")
local.addToGroup(batch, groupID)
} else {
// group has become an individual contact or vice versa
local.delete()
local = null
}
}
batch.commit()
}
if (local == null) {
if (newData.group) {
Logger.log.log(Level.INFO, "Creating local group", newData)
useLocal(LocalGroup(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
it.add()
local = it
}
} else {
Logger.log.log(Level.INFO, "Creating local contact", newData)
useLocal(LocalContact(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
it.add()
local = it
}
}
syncResult.stats.numInserts++
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O && local is LocalContact)
if (groupMethod == GroupMethod.CATEGORIES)
(local as? LocalContact)?.let { localContact ->
// VCard3: update group memberships from CATEGORIES
val batch = BatchOperation(provider)
Logger.log.log(Level.FINE, "Removing contact group memberships")
localContact.removeGroupMemberships(batch)
for (category in localContact.contact!!.categories) {
val groupID = localCollection.findOrCreateGroup(category)
Logger.log.log(Level.FINE, "Adding membership in group $category ($groupID)")
localContact.addToGroup(batch, groupID)
}
batch.commit()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
local.updateHashCode(null)
currentLocalResource = null
(local as? LocalContact)?.updateHashCode(null)
}
}

View File

@@ -8,34 +8,42 @@
package at.bitfire.davdroid.syncadapter
import android.Manifest
import android.accounts.Account
import android.app.PendingIntent
import android.app.Service
import android.content.*
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import android.support.v4.content.ContextCompat
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.App
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.ui.AccountActivity
import at.bitfire.davdroid.ui.AccountSettingsActivity
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.PermissionsActivity
import org.apache.commons.collections4.IteratorUtils
import java.lang.ref.WeakReference
import java.util.*
import java.util.logging.Level
abstract class SyncAdapterService: Service() {
companion object {
val runningSyncs = Collections.synchronizedSet(mutableSetOf<Pair<String, Account>>())!!
/** Keep a list of running syncs to block multiple calls at the same time,
* like run by some devices. Weak references are used for the case that a thread
* is terminated and the `finally` block which cleans up [runningSyncs] is not
* executed. */
private val runningSyncs = mutableListOf<WeakReference<Pair<String, Account>>>()
}
abstract protected fun syncAdapter(): AbstractThreadedSyncAdapter
protected abstract fun syncAdapter(): AbstractThreadedSyncAdapter
override fun onBind(intent: Intent?) = syncAdapter().syncAdapterBinder!!
@@ -48,7 +56,7 @@ abstract class SyncAdapterService: Service() {
private val syncPluginLoader = ServiceLoader.load(ISyncPlugin::class.java)
}
private val syncPlugins = IteratorUtils.toList(syncPluginLoader.iterator())
private val syncPlugins = syncPluginLoader.iterator().asSequence().toList()
init {
syncPlugins.forEach { Logger.log.info("Registered sync plugin: ${it::class.java.name}") }
@@ -62,9 +70,12 @@ abstract class SyncAdapterService: Service() {
// prevent multiple syncs of the same authority to be run for the same account
val currentSync = Pair(authority, account)
if (!runningSyncs.add(currentSync)) {
Logger.log.warning("There's already another $authority sync running for $account, aborting")
return
synchronized(runningSyncs) {
if (runningSyncs.any { it.get() == currentSync }) {
Logger.log.warning("There's already another $authority sync running for $account, aborting")
return
}
runningSyncs += WeakReference(currentSync)
}
try {
@@ -81,34 +92,31 @@ abstract class SyncAdapterService: Service() {
val runSync = syncPlugins.all { it.beforeSync(context, settings, syncResult) }
if (runSync)
if (runSync) {
SyncManager.cancelNotifications(NotificationManagerCompat.from(context), authority, account)
sync(settings, account, extras, authority, provider, syncResult)
}
syncPlugins.forEach { it.afterSync(context, settings, syncResult) }
}
Logger.log.info("Sync for $authority complete")
} finally {
runningSyncs -= currentSync
synchronized(runningSyncs) {
runningSyncs.removeAll { it.get() == null || it.get() == currentSync }
}
}
Logger.log.info("Sync for $currentSync finished")
}
override fun onSecurityException(account: Account, extras: Bundle, authority: String, syncResult: SyncResult) {
Logger.log.log(Level.WARNING, "Security exception when opening content provider for $authority")
syncResult.databaseError = true
val intent = Intent(context, PermissionsActivity::class.java)
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val notify = NotificationCompat.Builder(context, NotificationUtils.CHANNEL_SYNC_PROBLEMS)
.setSmallIcon(R.drawable.ic_sync_error_notification)
.setLargeIcon(App.getLauncherBitmap(context))
.setContentTitle(context.getString(R.string.sync_error_permissions))
.setContentText(context.getString(R.string.sync_error_permissions_text))
.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.build()
val nm = NotificationUtils.createChannels(context)
nm.notify(Constants.NOTIFICATION_PERMISSIONS, notify)
notifyPermissions(intent)
}
protected fun checkSyncConditions(settings: AccountSettings): Boolean {
@@ -121,6 +129,17 @@ abstract class SyncAdapterService: Service() {
}
settings.getSyncWifiOnlySSIDs()?.let { onlySSIDs ->
// getting the WiFi name requires location permission (and active location services) since Android 8.1
// see https://issuetracker.google.com/issues/70633700
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 &&
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
val intent = Intent(context, AccountSettingsActivity::class.java)
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, settings.account)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
notifyPermissions(intent)
}
val wifi = context.applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
val info = wifi.connectionInfo
if (info == null || !onlySSIDs.contains(info.ssid.trim('"'))) {
@@ -132,6 +151,18 @@ abstract class SyncAdapterService: Service() {
return true
}
protected fun notifyPermissions(intent: Intent) {
val notify = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_ERRORS)
.setSmallIcon(R.drawable.ic_sync_error_notification)
.setContentTitle(context.getString(R.string.sync_error_permissions))
.setContentText(context.getString(R.string.sync_error_permissions_text))
.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setAutoCancel(true)
.build()
NotificationManagerCompat.from(context).notify(NotificationUtils.NOTIFY_PERMISSIONS, notify)
}
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,6 @@
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.*
import android.content.pm.PackageManager
@@ -17,8 +16,8 @@ import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
@@ -30,6 +29,7 @@ import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.TaskProvider
import okhttp3.HttpUrl
import org.dmfs.tasks.contract.TaskContract
import java.util.logging.Level
@@ -60,16 +60,18 @@ class TasksSyncAdapterService: SyncAdapterService() {
for (taskList in AndroidTaskList.find(account, taskProvider, LocalTaskList.Factory, "${TaskContract.TaskLists.SYNC_ENABLED}!=0", null)) {
Logger.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]")
TasksSyncManager(context, settings, account, accountSettings, extras, authority, syncResult, taskProvider, taskList).use {
TasksSyncManager(context, settings, account, accountSettings, extras, authority, syncResult, taskList).use {
it.performSync()
}
}
} catch (e: TaskProvider.ProviderTooOldException) {
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notify = NotificationCompat.Builder(context, NotificationUtils.CHANNEL_SYNC_PROBLEMS)
val nm = NotificationManagerCompat.from(context)
val message = context.getString(R.string.sync_error_opentasks_required_version, e.provider.minVersionName, e.installedVersionName)
val notify = NotificationUtils.newBuilder(context)
.setSmallIcon(R.drawable.ic_sync_error_notification)
.setContentTitle(context.getString(R.string.sync_error_opentasks_too_old))
.setContentText(context.getString(R.string.sync_error_opentasks_required_version, e.provider.minVersionName, e.installedVersionName))
.setContentText(message)
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
.setCategory(NotificationCompat.CATEGORY_ERROR)
try {
@@ -83,7 +85,7 @@ class TasksSyncAdapterService: SyncAdapterService() {
notify .setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
.setAutoCancel(true)
nm.notify(Constants.NOTIFICATION_TASK_SYNC, notify.build())
nm.notify(NotificationUtils.NOTIFY_OPENTASKS, notify.build())
syncResult.databaseError = true
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't sync task lists", e)
@@ -107,8 +109,8 @@ class TasksSyncAdapterService: SyncAdapterService() {
null
}
fun remoteTaskLists(service: Long?): MutableMap<String, CollectionInfo> {
val collections = mutableMapOf<String, CollectionInfo>()
fun remoteTaskLists(service: Long?): MutableMap<HttpUrl, CollectionInfo> {
val collections = mutableMapOf<HttpUrl, CollectionInfo>()
service?.let {
db.query(Collections._TABLE, null,
"${Collections.SERVICE_ID}=? AND ${Collections.SUPPORTS_VTODO}!=0 AND ${Collections.SYNC}",
@@ -132,7 +134,8 @@ class TasksSyncAdapterService: SyncAdapterService() {
val updateColors = settings.getManageCalendarColors()
for (list in AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null))
list.syncId?.let { url ->
list.syncId?.let {
val url = HttpUrl.parse(it)!!
val info = remote[url]
if (info == null) {
Logger.log.fine("Deleting obsolete local task list $url")

View File

@@ -13,30 +13,34 @@ import android.content.Context
import android.content.SyncResult
import android.os.Bundle
import at.bitfire.dav4android.DavCalendar
import at.bitfire.dav4android.DavResource
import at.bitfire.dav4android.DavResponseCallback
import at.bitfire.dav4android.Response
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.property.CalendarData
import at.bitfire.dav4android.property.GetCTag
import at.bitfire.dav4android.property.GetETag
import at.bitfire.dav4android.property.SyncToken
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.R
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.SyncState
import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.resource.LocalTask
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.ical4android.InvalidCalendarException
import at.bitfire.ical4android.Task
import at.bitfire.ical4android.TaskProvider
import okhttp3.HttpUrl
import okhttp3.RequestBody
import org.apache.commons.collections4.ListUtils
import java.io.ByteArrayOutputStream
import java.io.Reader
import java.io.StringReader
import java.util.*
import java.util.logging.Level
/**
* Synchronization manager for CalDAV collections; handles tasks (VTODO)
*/
class TasksSyncManager(
context: Context,
settings: ISettings,
@@ -45,118 +49,87 @@ class TasksSyncManager(
extras: Bundle,
authority: String,
syncResult: SyncResult,
val provider: TaskProvider,
val localTaskList: LocalTaskList
): SyncManager(context, settings, account, accountSettings, extras, authority, syncResult, "taskList/${localTaskList.id}") {
val MAX_MULTIGET = 30
init {
localCollection = localTaskList
}
override fun notificationId() = Constants.NOTIFICATION_TASK_SYNC
override fun getSyncErrorTitle() = context.getString(R.string.sync_error_tasks, account.name)!!
localCollection: LocalTaskList
): SyncManager<LocalTask, LocalTaskList, DavCalendar>(context, settings, account, accountSettings, extras, authority, syncResult, localCollection) {
override fun prepare(): Boolean {
collectionURL = HttpUrl.parse(localTaskList.syncId ?: return false) ?: return false
collectionURL = HttpUrl.parse(localCollection.syncId ?: return false) ?: return false
davCollection = DavCalendar(httpClient.okHttpClient, collectionURL)
return true
}
override fun queryCapabilities() {
davCollection.propfind(0, GetCTag.NAME)
}
override fun prepareUpload(resource: LocalResource): RequestBody {
if (resource is LocalTask) {
val task = requireNotNull(resource.task)
Logger.log.log(Level.FINE, "Preparing upload of task ${resource.fileName}", task)
val os = ByteArrayOutputStream()
task.write(os)
return RequestBody.create(
DavCalendar.MIME_ICALENDAR,
os.toByteArray()
)
} else
throw IllegalArgumentException("resource must be a LocalTask")
}
override fun listRemote() {
// fetch list of remote VTODOs and build hash table to index file name
val calendar = davCalendar()
currentDavResource = calendar
calendar.calendarQuery("VTODO", null, null)
remoteResources = HashMap(davCollection.members.size)
for (vCard in davCollection.members) {
val fileName = vCard.fileName()
Logger.log.fine("Found remote VTODO: $fileName")
remoteResources[fileName] = vCard
}
currentDavResource = null
}
override fun downloadRemote() {
Logger.log.info("Downloading ${toDownload.size} tasks ($MAX_MULTIGET at once)")
// download new/updated iCalendars from server
for (bunch in ListUtils.partition(toDownload.toList(), MAX_MULTIGET)) {
abortIfCancelled()
Logger.log.info("Downloading ${bunch.joinToString(", ")}")
if (bunch.size == 1) {
// only one contact, use GET
val remote = bunch.first()
currentDavResource = remote
val body = remote.get("text/calendar")
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
val eTag = remote.properties[GetETag::class.java]
if (eTag == null || eTag.eTag.isNullOrEmpty())
throw DavException("Received CalDAV GET response without ETag for ${remote.location}")
body.charStream().use { reader ->
processVTodo(remote.fileName(), eTag.eTag!!, reader)
}
} else {
// multiple contacts, use multi-get
val calendar = davCalendar()
currentDavResource = calendar
calendar.multiget(bunch.map { it.location })
// process multiget results
for (remote in davCollection.members) {
currentDavResource = remote
val eTag = remote.properties[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val calendarData = remote.properties[CalendarData::class.java]
val iCalendar = calendarData?.iCalendar
?: throw DavException("Received multi-get response without task data")
processVTodo(remote.fileName(), eTag, StringReader(iCalendar))
override fun queryCapabilities() =
useRemoteCollection {
var syncState: SyncState? = null
it.propfind(0, GetCTag.NAME, SyncToken.NAME) { response, relation ->
if (relation == Response.HrefRelation.SELF)
syncState = syncState(response)
}
syncState
}
currentDavResource = null
override fun syncAlgorithm() = SyncAlgorithm.PROPFIND_REPORT
override fun prepareUpload(resource: LocalTask): RequestBody = useLocal(resource) {
val task = requireNotNull(resource.task)
Logger.log.log(Level.FINE, "Preparing upload of task ${resource.fileName}", task)
val os = ByteArrayOutputStream()
task.write(os)
RequestBody.create(
DavCalendar.MIME_ICALENDAR_UTF8,
os.toByteArray()
)
}
override fun listAllRemote(callback: DavResponseCallback) {
useRemoteCollection { remote ->
Logger.log.info("Querying tasks")
remote.calendarQuery("VTODO", null, null, callback)
}
}
override fun downloadRemote(bunch: List<HttpUrl>) {
Logger.log.info("Downloading ${bunch.size} iCalendars: $bunch")
if (bunch.size == 1) {
val remote = bunch.first()
// only one contact, use GET
useRemote(DavResource(httpClient.okHttpClient, remote)) { resource ->
resource.get(DavCalendar.MIME_ICALENDAR.toString()) { response ->
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
val eTag = response.header("ETag")?.let { GetETag(it).eTag }
?: throw DavException("Received CalDAV GET response without ETag")
response.body()!!.use {
processVTodo(resource.fileName(), eTag, it.charStream())
}
}
}
} else
// multiple iCalendars, use calendar-multi-get
useRemoteCollection {
it.multiget(bunch) { response, _ ->
useRemote(response) {
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val calendarData = response[CalendarData::class.java]
val iCal = calendarData?.iCalendar
?: throw DavException("Received multi-get response without address data")
processVTodo(DavUtils.lastSegmentOfUrl(response.href), eTag, StringReader(iCal))
}
}
}
}
override fun postProcess() {
}
// helpers
private fun davCalendar() = davCollection as DavCalendar
private fun processVTodo(fileName: String, eTag: String, reader: Reader) {
val tasks: List<Task>
try {
@@ -170,22 +143,22 @@ class TasksSyncManager(
val newData = tasks.first()
// update local task, if it exists
val localTask = localResources[fileName] as LocalTask?
currentLocalResource = localTask
if (localTask != null) {
Logger.log.info("Updating $fileName in local tasklist")
localTask.eTag = eTag
localTask.update(newData)
syncResult.stats.numUpdates++
} else {
Logger.log.info("Adding $fileName to local task list")
val newTask = LocalTask(localTaskList, newData, fileName, eTag)
currentLocalResource = newTask
newTask.add()
syncResult.stats.numInserts++
useLocal(localCollection.findByName(fileName)) { local ->
if (local != null) {
Logger.log.info("Updating $fileName in local task list")
local.eTag = eTag
local.update(newData)
syncResult.stats.numUpdates++
} else {
Logger.log.info("Adding $fileName to local task list")
useLocal(LocalTask(localCollection, newData, fileName, eTag, LocalResource.FLAG_REMOTELY_PRESENT)) {
it.add()
}
syncResult.stats.numInserts++
}
}
} else
Logger.log.severe("Received VCALENDAR with not exactly one VTODO; ignoring $fileName")
Logger.log.info("Received VCALENDAR with not exactly one VTODO; ignoring $fileName")
}
}
}

View File

@@ -8,86 +8,55 @@
package at.bitfire.davdroid.ui
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentPagerAdapter
import android.support.v4.app.LoaderManager
import android.support.v4.content.AsyncTaskLoader
import android.support.v4.content.Loader
import android.support.v7.app.AppCompatActivity
import android.text.Html
import android.text.Spanned
import android.text.util.Linkify
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import at.bitfire.davdroid.App
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import ezvcard.Ezvcard
import kotlinx.android.synthetic.main.about_component.view.*
import com.mikepenz.aboutlibraries.LibsBuilder
import kotlinx.android.synthetic.main.about_davdroid.*
import kotlinx.android.synthetic.main.activity_about.*
import org.apache.commons.io.IOUtils
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.logging.Level
class AboutActivity: AppCompatActivity() {
private class ComponentInfo(
val title: String?,
val version: String?,
val website: String,
val copyright: String,
val licenseInfo: Int?,
val licenseTextFile: String?
)
companion object {
private lateinit var components: Array<ComponentInfo>
const val pixelsHtml = "<font color=\"#fff433\">■</font>" +
"<font color=\"#ffffff\">■</font>" +
"<font color=\"#9b59d0\">■</font>" +
"<font color=\"#000000\">■</font>"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
components = arrayOf(
ComponentInfo(
null, BuildConfig.VERSION_NAME, getString(R.string.homepage_url),
"Ricki Hirner, Bernhard Stockmann (bitfire web engineering)",
null, null
), ComponentInfo(
"AmbilWarna", null, "https://github.com/yukuku/ambilwarna",
"Yuku", R.string.about_license_info_no_warranty, "apache2.html"
), ComponentInfo(
"Apache Commons", null, "http://commons.apache.org/",
"Apache Software Foundation", R.string.about_license_info_no_warranty, "apache2.html"
), ComponentInfo(
"dnsjava", null, "http://dnsjava.org/",
"Brian Wellington", R.string.about_license_info_no_warranty, "bsd.html"
), ComponentInfo(
"ez-vcard", Ezvcard.VERSION, "https://github.com/mangstadt/ez-vcard",
"Michael Angstadt", R.string.about_license_info_no_warranty, "bsd.html"
), ComponentInfo(
"ical4j", "2.x", "https://ical4j.github.io/",
"Ben Fortuna", R.string.about_license_info_no_warranty, "bsd-3clause.html"
), ComponentInfo(
"OkHttp", null, "https://square.github.io/okhttp/",
"Square, Inc.", R.string.about_license_info_no_warranty, "apache2.html"
), 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)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
viewpager.adapter = TabsAdapter(supportFragmentManager)
tabs.setupWithViewPager(viewpager)
tabs.setupWithViewPager(viewpager, false)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.about_davdroid, menu)
return true
}
fun showWebsite(item: MenuItem) {
val intent = Intent(Intent.ACTION_VIEW, App.homepageUrl(this))
if (intent.resolveActivity(packageManager) != null)
startActivity(intent)
}
@@ -95,118 +64,59 @@ class AboutActivity: AppCompatActivity() {
fm: FragmentManager
): FragmentPagerAdapter(fm) {
override fun getCount() = components.size
override fun getCount() = 2
override fun getPageTitle(position: Int) =
components[position].title ?: getString(R.string.app_name)!!
when (position) {
1 -> getString(R.string.about_libraries)
else -> getString(R.string.app_name)
}!!
override fun getItem(position: Int) = ComponentFragment.instantiate(position)
override fun getItem(position: Int) =
when (position) {
1 -> LibsBuilder()
.withAutoDetect(false)
.withFields(R.string::class.java.fields)
.withLicenseShown(true)
.supportFragment()
else -> DavdroidFragment()
}!!
}
class ComponentFragment: Fragment(), LoaderManager.LoaderCallbacks<Spanned> {
class DavdroidFragment: Fragment() {
companion object {
val KEY_POSITION = "position"
val KEY_FILE_NAME = "fileName"
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
inflater.inflate(R.layout.about_davdroid, container, false)!!
fun instantiate(position: Int): ComponentFragment {
val frag = ComponentFragment()
val args = Bundle(1)
args.putInt(KEY_POSITION, position)
frag.arguments = args
return frag
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
app_name.text = getString(R.string.app_name)
app_version.text = getString(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
build_time.text = getString(R.string.about_build_date, SimpleDateFormat.getDateInstance().format(BuildConfig.buildTime))
private val licenseFragmentLoader = ServiceLoader.load(ILicenseFragment::class.java)!!.firstOrNull()
pixels.text = Html.fromHtml(pixelsHtml)
if (false /* open-source version */) {
warranty.text = Html.fromHtml(getString(R.string.about_license_info_no_warranty))
license_text.text = Html.fromHtml(getString(R.string.gpl_v3))
} else /* non-ose builds */ {
when (BuildConfig.FLAVOR) {
App.FLAVOR_GOOGLE_PLAY,
App.FLAVOR_ICLOUD,
App.FLAVOR_SOLDUPE ->
warranty.setText(R.string.about_flavor_info)
else ->
warranty.visibility = View.GONE
}
@SuppressLint("SetTextI18n")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val info = (activity as AboutActivity).components[arguments!!.getInt(KEY_POSITION)]
val v = inflater.inflate(R.layout.about_component, container, false)
var title = info.title ?: getString(R.string.app_name)
info.version?.let { title += " ${info.version}" }
v.title.text = title
v.website.autoLinkMask = Linkify.WEB_URLS
v.website.text = info.website
v.copyright.text = "© ${info.copyright}"
if (info.licenseInfo == null && info.licenseTextFile == null) {
// No license text, so this must be the app's tab. Show the license fragment here, if available.
licenseFragmentLoader?.let { factory ->
fragmentManager!!.beginTransaction()
.add(R.id.license_fragment, factory.getFragment())
val licenseFragment = ServiceLoader.load(ILicenseFragment::class.java)!!.firstOrNull()
if (savedInstanceState == null && licenseFragment != null)
requireFragmentManager().beginTransaction()
.replace(R.id.license_fragment, licenseFragment.getFragment())
.commit()
}
v.license_terms.visibility = View.GONE
} else {
// show license info
if (info.licenseInfo == null)
v.license_info.visibility = View.GONE
else
v.license_info.setText(info.licenseInfo)
// load and format license text
if (info.licenseTextFile == null) {
v.license_header.visibility = View.GONE
v.license_text.visibility = View.GONE
} else {
val args = Bundle(1)
args.putString(KEY_FILE_NAME, info.licenseTextFile)
loaderManager.initLoader(0, args, this)
}
}
return v
}
override fun onCreateLoader(id: Int, args: Bundle) =
LicenseLoader(activity!!, args.getString(KEY_FILE_NAME))
override fun onLoadFinished(loader: Loader<Spanned>, license: Spanned?) {
view?.let { v ->
v.license_text.autoLinkMask = Linkify.EMAIL_ADDRESSES or Linkify.WEB_URLS
v.license_text.text = license
}
}
override fun onLoaderReset(loader: Loader<Spanned>) {}
}
class LicenseLoader(
context: Context,
val fileName: String
): AsyncTaskLoader<Spanned>(context) {
private var content: Spanned? = null
override fun onStartLoading() {
if (content != null)
deliverResult(content)
else
forceLoad()
}
override fun loadInBackground(): Spanned? {
Logger.log.fine("Loading license file $fileName")
try {
context.resources.assets.open(fileName).use {
content = Html.fromHtml(IOUtils.toString(it, Charsets.UTF_8))
return content
}
} catch(e: IOException) {
Logger.log.log(Level.SEVERE, "Couldn't read license file", e)
return null
}
}
}

View File

@@ -42,16 +42,16 @@ import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.ContactsStorageException
import kotlinx.android.synthetic.main.account_caldav_item.view.*
import kotlinx.android.synthetic.main.activity_account.*
import java.util.*
import java.util.concurrent.Executors
import java.util.logging.Level
class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, PopupMenu.OnMenuItemClickListener, LoaderManager.LoaderCallbacks<AccountActivity.AccountInfo> {
companion object {
@JvmField val EXTRA_ACCOUNT = "account"
const val EXTRA_ACCOUNT = "account"
private fun requestSync(context: Context, account: Account) {
val authorities = arrayOf(
@@ -73,11 +73,15 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
lateinit var account: Account
private var accountInfo: AccountInfo? = null
private val dbExecutor = Executors.newSingleThreadExecutor()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
account = intent.getParcelableExtra(EXTRA_ACCOUNT)
// account may be a DAVdroid address book account -> use main account in this case
account = LocalAddressBook.mainAccount(this,
requireNotNull(intent.getParcelableExtra(EXTRA_ACCOUNT)))
title = account.name
setContentView(R.layout.activity_account)
@@ -106,6 +110,12 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
loaderManager.initLoader(0, null, this)
}
override fun onDestroy() {
super.onDestroy()
dbExecutor.shutdown()
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (grantResults.any { it == PackageManager.PERMISSION_GRANTED })
// we've got additional permissions; try to load everything again
@@ -141,9 +151,9 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
.setTitle(R.string.account_delete_confirmation_title)
.setMessage(R.string.account_delete_confirmation_text)
.setNegativeButton(android.R.string.no, null)
.setPositiveButton(android.R.string.yes, { _, _ ->
.setPositiveButton(android.R.string.yes) { _, _ ->
deleteAccount()
})
}
.show()
}
else ->
@@ -192,23 +202,24 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
val info = adapter.getItem(position)
val nowChecked = !info.selected
OpenHelper(this).use { dbHelper ->
val db = dbHelper.writableDatabase
db.beginTransactionNonExclusive()
dbExecutor.execute {
OpenHelper(this).use { dbHelper ->
val db = dbHelper.writableDatabase
db.beginTransactionNonExclusive()
val values = ContentValues(1)
values.put(Collections.SYNC, if (nowChecked) 1 else 0)
db.update(Collections._TABLE, values, "${Collections.ID}=?", arrayOf(info.id.toString()))
val values = ContentValues(1)
values.put(Collections.SYNC, if (nowChecked) 1 else 0)
db.update(Collections._TABLE, values, "${Collections.ID}=?", arrayOf(info.id.toString()))
db.setTransactionSuccessful()
db.endTransaction()
db.setTransactionSuccessful()
db.endTransaction()
}
info.selected = nowChecked
runOnUiThread {
adapter.notifyDataSetChanged()
}
}
info.selected = nowChecked
adapter.notifyDataSetChanged()
if (nowChecked && info.type == CollectionInfo.Type.ADDRESS_BOOK && info.readOnly)
Snackbar.make(parent, R.string.account_read_only_address_book_selected, Snackbar.LENGTH_LONG).show()
}
private val onActionOverflowListener = { anchor: View, info: CollectionInfo ->
@@ -222,28 +233,32 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
isChecked = info.forceReadOnly
}
popup.setOnMenuItemClickListener({ item ->
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.force_read_only -> {
val nowChecked = !item.isChecked
OpenHelper(this).use { dbHelper ->
val db = dbHelper.writableDatabase
db.beginTransactionNonExclusive()
dbExecutor.execute {
OpenHelper(this).use { dbHelper ->
val db = dbHelper.writableDatabase
db.beginTransactionNonExclusive()
val values = ContentValues(1)
values.put(Collections.FORCE_READ_ONLY, nowChecked)
db.update(Collections._TABLE, values, "${Collections.ID}=?", arrayOf(info.id.toString()))
val values = ContentValues(1)
values.put(Collections.FORCE_READ_ONLY, nowChecked)
db.update(Collections._TABLE, values, "${Collections.ID}=?", arrayOf(info.id.toString()))
db.setTransactionSuccessful()
db.endTransaction()
reload()
db.setTransactionSuccessful()
db.endTransaction()
reload()
}
}
}
R.id.delete_collection ->
DeleteCollectionFragment.ConfirmDeleteCollectionFragment.newInstance(account, info).show(supportFragmentManager, null)
R.id.properties ->
CollectionInfoFragment.newInstance(info).show(supportFragmentManager, null)
}
true
})
}
popup.show()
// long click was handled
@@ -272,9 +287,9 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
val installIntent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=at.bitfire.icsdroid"))
if (packageManager.resolveActivity(installIntent, 0) != null)
snack.setAction(R.string.account_install_icsdroid, {
snack.setAction(R.string.account_install_icsdroid) {
startActivity(installIntent)
})
}
snack.show()
}
@@ -318,6 +333,10 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
override fun onLoadFinished(loader: Loader<AccountInfo>, info: AccountInfo?) {
accountInfo = info
if (info?.caldav?.collections?.any { it.selected } != true &&
info?.carddav?.collections?.any { it.selected} != true)
select_collections_hint.visibility = View.VISIBLE
carddav.visibility = info?.carddav?.let { carddav ->
carddav_refreshing.visibility = if (carddav.refreshing) View.VISIBLE else View.GONE
@@ -474,9 +493,9 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
for (addrBookAccount in accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))) {
val addressBook = LocalAddressBook(context, addrBookAccount, null)
try {
if (account == addressBook.getMainAccount())
if (account == addressBook.mainAccount)
carddav.refreshing = carddav.refreshing || ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY)
} catch(e: ContactsStorageException) {
} catch(e: Exception) {
}
}
@@ -558,7 +577,7 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
checked.isChecked = info.selected
var tv: TextView = v.findViewById(R.id.title)
tv.text = if (!info.displayName.isNullOrBlank()) info.displayName else info.url
tv.text = if (!info.displayName.isNullOrBlank()) info.displayName else info.url.toString()
tv = v.findViewById(R.id.description)
if (info.description.isNullOrBlank())
@@ -571,11 +590,11 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
v.findViewById<ImageView>(R.id.read_only).visibility =
if (info.readOnly || info.forceReadOnly) View.VISIBLE else View.GONE
v.findViewById<ImageView>(R.id.action_overflow).setOnClickListener({ view ->
v.findViewById<ImageView>(R.id.action_overflow).setOnClickListener { view ->
(context as? AccountActivity)?.let {
it.onActionOverflowListener(view, info)
}
})
}
return v
}
@@ -602,7 +621,7 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
} ?: View.INVISIBLE
var tv: TextView = v.findViewById(R.id.title)
tv.text = if (!info.displayName.isNullOrBlank()) info.displayName else info.url
tv.text = if (!info.displayName.isNullOrBlank()) info.displayName else info.url.toString()
tv = v.findViewById(R.id.description)
if (info.description.isNullOrBlank())
@@ -625,11 +644,11 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
if (info.type == CollectionInfo.Type.WEBCAL)
overflow.visibility = View.GONE
else
overflow.setOnClickListener({ view ->
overflow.setOnClickListener { view ->
(context as? AccountActivity)?.let {
it.onActionOverflowListener(view, info)
}
})
}
return v
}
@@ -642,7 +661,7 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
companion object {
val ARG_ACCOUNT = "account"
const val ARG_ACCOUNT = "account"
fun newInstance(account: Account): RenameAccountFragment {
val fragment = RenameAccountFragment()
@@ -681,31 +700,29 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
ContentResolver.cancelSync(addrBookAccount, null)
// update account name references in database
OpenHelper(activity!!).use { dbHelper ->
OpenHelper(requireActivity()).use { dbHelper ->
ServiceDB.onRenameAccount(dbHelper.writableDatabase, oldAccount.name, newName)
}
// update main account of address book accounts
try {
for (addrBookAccount in accountManager.getAccountsByType(getString(R.string.account_type_address_book))) {
val provider = activity!!.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
try {
if (provider != null) {
val addressBook = LocalAddressBook(activity!!, addrBookAccount, provider)
if (oldAccount == addressBook.getMainAccount())
addressBook.setMainAccount(Account(newName, oldAccount.type))
}
} finally {
if (Build.VERSION.SDK_INT >= 24)
provider?.close()
else
provider?.release()
if (ContextCompat.checkSelfPermission(requireActivity(), Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED)
try {
requireActivity().contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.let { provider ->
for (addrBookAccount in accountManager.getAccountsByType(getString(R.string.account_type_address_book)))
try {
val addressBook = LocalAddressBook(requireActivity(), addrBookAccount, provider)
if (oldAccount == addressBook.mainAccount)
addressBook.mainAccount = Account(newName, oldAccount.type)
} finally {
if (Build.VERSION.SDK_INT >= 24)
provider.close()
else
provider.release()
}
}
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't update address book accounts", e)
}
} catch(e: ContactsStorageException) {
Logger.log.log(Level.SEVERE, "Couldn't update address book accounts", e)
}
// calendar provider doesn't allow changing account_name of Events
// (all events will have to be downloaded again)
@@ -722,7 +739,7 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
}, null)
activity!!.finish()
})
.setNegativeButton(android.R.string.cancel, { _, _ -> })
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.create()
}
}

View File

@@ -8,29 +8,36 @@
package at.bitfire.davdroid.ui
import android.Manifest
import android.accounts.Account
import android.app.DialogFragment
import android.app.LoaderManager
import android.content.*
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.SyncStatusObserver
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.CalendarContract
import android.security.KeyChain
import android.support.v14.preference.PreferenceFragment
import android.support.v4.app.ActivityCompat
import android.support.v4.app.DialogFragment
import android.support.v4.app.LoaderManager
import android.support.v4.app.NavUtils
import android.support.v4.content.ContextCompat
import android.support.v4.content.Loader
import android.support.v7.app.AlertDialog
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.SwitchPreferenceCompat
import android.support.v7.preference.*
import android.view.MenuItem
import at.bitfire.davdroid.AccountSettings
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.model.Credentials
import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.GroupMethod
import org.apache.commons.lang3.StringUtils
@@ -38,7 +45,7 @@ import org.apache.commons.lang3.StringUtils
class AccountSettingsActivity: AppCompatActivity() {
companion object {
val EXTRA_ACCOUNT = "account"
const val EXTRA_ACCOUNT = "account"
}
private lateinit var account: Account
@@ -53,7 +60,7 @@ class AccountSettingsActivity: AppCompatActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null)
fragmentManager.beginTransaction()
supportFragmentManager.beginTransaction()
.replace(android.R.id.content, DialogFragment.instantiate(this, AccountSettingsFragment::class.java.name, intent.extras))
.commit()
}
@@ -68,13 +75,13 @@ class AccountSettingsActivity: AppCompatActivity() {
false
class AccountSettingsFragment: PreferenceFragment(), LoaderManager.LoaderCallbacks<Pair<ISettings, AccountSettings>?> {
class AccountSettingsFragment: PreferenceFragmentCompat(), LoaderManager.LoaderCallbacks<Pair<ISettings, AccountSettings>> {
lateinit var account: Account
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
account = arguments.getParcelable(EXTRA_ACCOUNT)
account = arguments!!.getParcelable(EXTRA_ACCOUNT)
loaderManager.initLoader(0, arguments, this)
}
@@ -82,10 +89,10 @@ class AccountSettingsActivity: AppCompatActivity() {
addPreferencesFromResource(R.xml.settings_account)
}
override fun onCreateLoader(id: Int, args: Bundle) =
AccountSettingsLoader(activity, args.getParcelable(EXTRA_ACCOUNT))
override fun onCreateLoader(id: Int, args: Bundle?) =
AccountSettingsLoader(requireActivity(), args!!.getParcelable(EXTRA_ACCOUNT))
override fun onLoadFinished(loader: Loader<Pair<ISettings, AccountSettings>?>, result: Pair<ISettings, AccountSettings>?) {
override fun onLoadFinished(loader: Loader<Pair<ISettings, AccountSettings>>, result: Pair<ISettings, AccountSettings>?) {
val (settings, accountSettings) = result ?: return
// preference group: authentication
@@ -123,9 +130,9 @@ class AccountSettingsActivity: AppCompatActivity() {
prefCertAlias.setOnPreferenceClickListener {
KeyChain.choosePrivateKeyAlias(activity, { alias ->
accountSettings.credentials(Credentials(certificateAlias = alias))
Handler(Looper.getMainLooper()).post({
Handler(Looper.getMainLooper()).post {
loaderManager.restartLoader(0, arguments, this)
})
}
}, null, null, null, -1, credentials.certificateAlias)
true
}
@@ -211,6 +218,13 @@ class AccountSettingsActivity: AppCompatActivity() {
false
}
// getting the WiFi name requires location permission (and active location services) since Android 8.1
// see https://issuetracker.google.com/issues/70633700
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 &&
accountSettings.getSyncWifiOnly() && onlySSIDs != null &&
ContextCompat.checkSelfPermission(requireActivity(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)
ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), 0)
// preference group: CardDAV
(findPreference("contact_group_method") as ListPreference).let {
if (syncIntervalContacts != null) {
@@ -222,11 +236,11 @@ class AccountSettingsActivity: AppCompatActivity() {
else {
it.isEnabled = true
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, groupMethod ->
AlertDialog.Builder(activity)
AlertDialog.Builder(requireActivity())
.setIcon(R.drawable.ic_error_dark)
.setTitle(R.string.settings_contact_group_method_change)
.setMessage(R.string.settings_contact_group_method_change_reload_contacts)
.setPositiveButton(android.R.string.ok, { _, _ ->
.setPositiveButton(android.R.string.ok) { _, _ ->
// change group method
accountSettings.setGroupMethod(GroupMethod.valueOf(groupMethod as String))
loaderManager.restartLoader(0, arguments, this)
@@ -235,7 +249,7 @@ class AccountSettingsActivity: AppCompatActivity() {
val args = Bundle(1)
args.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true)
ContentResolver.requestSync(account, getString(R.string.address_books_authority), args)
})
}
.setNegativeButton(android.R.string.cancel, null)
.show()
false
@@ -258,13 +272,29 @@ class AccountSettingsActivity: AppCompatActivity() {
it.setSummary(R.string.settings_sync_time_range_past_none)
}
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
var days: Int
try {
days = (newValue as String).toInt()
} catch(ignored: NumberFormatException) {
days = -1
val days = try {
(newValue as String).toInt()
} catch(e: NumberFormatException) {
-1
}
accountSettings.setTimeRangePastDays(if (days < 0) null else days)
// reset sync state of all calendars in this account to trigger a full sync
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED) {
requireContext().contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { provider ->
try {
AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null).forEach { calendar ->
calendar.lastSyncState = null
}
} finally {
if (Build.VERSION.SDK_INT >= 24)
provider.close()
else
provider.release()
}
}
}
loaderManager.restartLoader(0, arguments, this)
false
}
@@ -296,15 +326,15 @@ class AccountSettingsActivity: AppCompatActivity() {
accountSettings.setEventColors(true)
loaderManager.restartLoader(0, arguments, this)
} else
AlertDialog.Builder(activity)
AlertDialog.Builder(requireActivity())
.setIcon(R.drawable.ic_error_dark)
.setTitle(R.string.settings_event_colors)
.setMessage(R.string.settings_event_colors_off_confirm)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, { _, _ ->
.setPositiveButton(android.R.string.ok) { _, _ ->
accountSettings.setEventColors(false)
loaderManager.restartLoader(0, arguments, this)
})
}
.show()
false
}
@@ -313,7 +343,7 @@ class AccountSettingsActivity: AppCompatActivity() {
}
}
override fun onLoaderReset(loader: Loader<Pair<ISettings, AccountSettings>?>) {
override fun onLoaderReset(loader: Loader<Pair<ISettings, AccountSettings>>) {
}
}
@@ -330,7 +360,7 @@ class AccountSettingsActivity: AppCompatActivity() {
super.onStartLoading()
if (listenerHandle == null)
listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this@AccountSettingsLoader)
listenerHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this)
}
override fun onReset() {

View File

@@ -9,11 +9,15 @@
package at.bitfire.davdroid.ui
import android.accounts.AccountManager
import android.app.LoaderManager
import android.content.*
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.SyncStatusObserver
import android.os.Bundle
import android.support.design.widget.NavigationView
import android.support.design.widget.Snackbar
import android.support.v4.app.LoaderManager
import android.support.v4.content.Loader
import android.support.v4.view.GravityCompat
import android.support.v7.app.ActionBarDrawerToggle
import android.support.v7.app.AppCompatActivity
@@ -33,7 +37,7 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
private val serviceLoader = ServiceLoader.load(IAccountsDrawerHandler::class.java)!!
val accountsDrawerHandler = serviceLoader.iterator().next()!!
private val EXTRA_CREATE_STARTUP_FRAGMENTS = "createStartupFragments"
private const val fragTagStartup = "startup"
}
private var syncStatusSnackbar: Snackbar? = null
@@ -46,9 +50,9 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
setSupportActionBar(toolbar)
fab.setOnClickListener({
startActivity(Intent(this@AccountsActivity, LoginActivity::class.java))
})
fab.setOnClickListener {
startActivity(Intent(this, LoginActivity::class.java))
}
val toggle = ActionBarDrawerToggle(
this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
@@ -64,20 +68,19 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
startService(settingsIntent)
val args = Bundle(1)
args.putBoolean(EXTRA_CREATE_STARTUP_FRAGMENTS, savedInstanceState == null && packageName != callingPackage)
loaderManager.initLoader(0, args, this)
supportLoaderManager.initLoader(0, args, this)
}
override fun onCreateLoader(code: Int, args: Bundle) =
SettingsLoader(this, args.getBoolean(EXTRA_CREATE_STARTUP_FRAGMENTS))
override fun onCreateLoader(code: Int, args: Bundle?) =
SettingsLoader(this)
override fun onLoadFinished(loader: Loader<Settings>?, result: Settings?) {
override fun onLoadFinished(loader: Loader<Settings>, result: Settings?) {
val result = result ?: return
if (result.createStartupFragments) {
val ft = fragmentManager.beginTransaction()
StartupDialogFragment.getStartupDialogs(this, result.settings).forEach { ft.add(it, null) }
ft.commitAllowingStateLoss()
if (supportFragmentManager.findFragmentByTag(fragTagStartup) == null) {
val ft = supportFragmentManager.beginTransaction()
StartupDialogFragment.getStartupDialogs(this, result.settings).forEach { ft.add(it, fragTagStartup) }
ft.commit()
}
fab.visibility = if (result.allowAddAccount) View.VISIBLE else View.GONE
@@ -87,7 +90,7 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
}
}
override fun onLoaderReset(loader: Loader<Settings>?) {
override fun onLoaderReset(loader: Loader<Settings>) {
nav_view?.menu?.let {
accountsDrawerHandler.onSettingsChanged(null, it)
}
@@ -118,9 +121,9 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
if (!ContentResolver.getMasterSyncAutomatically()) {
val snackbar = Snackbar
.make(findViewById(R.id.coordinator), R.string.accounts_global_sync_disabled, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.accounts_global_sync_enable, {
ContentResolver.setMasterSyncAutomatically(true)
})
.setAction(R.string.accounts_global_sync_enable) {
ContentResolver.setMasterSyncAutomatically(true)
}
syncStatusSnackbar = snackbar
snackbar.show()
}
@@ -143,13 +146,11 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
class Settings(
val settings: ISettings,
val createStartupFragments: Boolean,
val allowAddAccount: Boolean
)
class SettingsLoader(
context: Context,
private val createStartupFragments: Boolean
context: Context
): at.bitfire.davdroid.ui.SettingsLoader<Settings>(context) {
override fun loadInBackground(): Settings? {
@@ -159,7 +160,6 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
return Settings(
it,
createStartupFragments,
accounts.size < it.getInt(App.MAX_ACCOUNTS, Int.MAX_VALUE)
)
}

View File

@@ -36,7 +36,7 @@ import java.net.URISyntaxException
class AppSettingsActivity: AppCompatActivity() {
companion object {
val EXTRA_SCROLL_TO = "scrollTo"
const val EXTRA_SCROLL_TO = "scrollTo"
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -164,8 +164,11 @@ class AppSettingsActivity: AppCompatActivity() {
// debugging settings
val prefLogToExternalStorage = findPreference(Logger.LOG_TO_EXTERNAL_STORAGE) as SwitchPreferenceCompat
prefLogToExternalStorage.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val context = activity!!
Logger.initialize(context)
// kill a potential :sync process, so that the new logger settings will be used
val am = activity!!.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
am.runningAppProcesses.forEach {
if (it.pid != Process.myPid()) {
Logger.log.info("Killing ${it.processName} process, pid = ${it.pid}")
@@ -177,6 +180,7 @@ class AppSettingsActivity: AppCompatActivity() {
}
private fun resetHints() {
settings?.remove(StartupDialogFragment.HINT_AUTOSTART_PERMISSIONS)
settings?.remove(StartupDialogFragment.HINT_BATTERY_OPTIMIZATIONS)
settings?.remove(StartupDialogFragment.HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED)
settings?.remove(StartupDialogFragment.HINT_OPENTASKS_NOT_INSTALLED)

View File

@@ -0,0 +1,61 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.ui
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v7.app.AlertDialog
import android.widget.Toast
import at.bitfire.davdroid.R
import at.bitfire.davdroid.model.CollectionInfo
import kotlinx.android.synthetic.main.collection_properties.view.*
class CollectionInfoFragment : DialogFragment() {
companion object {
private const val ARGS_INFO = "info"
fun newInstance(info: CollectionInfo): CollectionInfoFragment {
val frag = CollectionInfoFragment()
val args = Bundle(1)
args.putParcelable(ARGS_INFO, info)
frag.arguments = args
return frag
}
}
@SuppressLint("InflateParams")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val info = arguments!![ARGS_INFO] as CollectionInfo
val view = requireActivity().layoutInflater.inflate(R.layout.collection_properties, null)
view.url.text = info.url.toString()
view.url_copy.setOnClickListener {
val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val text = ClipData.newPlainText(info.displayName, info.url.toString())
clipboard.primaryClip = text
Toast.makeText(requireActivity(), R.string.copied_to_clipboard, Toast.LENGTH_LONG).show()
}
return AlertDialog.Builder(requireActivity())
.setTitle(info.displayName)
.setView(view)
.create()
}
}

View File

@@ -31,7 +31,7 @@ import java.util.*
class CreateAddressBookActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<CreateAddressBookActivity.AccountInfo> {
companion object {
val EXTRA_ACCOUNT = "account"
const val EXTRA_ACCOUNT = "account"
}
private lateinit var account: Account
@@ -67,7 +67,7 @@ class CreateAddressBookActivity: AppCompatActivity(), LoaderManager.LoaderCallba
var ok = true
HttpUrl.parse(homeSet)?.let {
val info = CollectionInfo(it.resolve(UUID.randomUUID().toString() + "/").toString())
val info = CollectionInfo(it.resolve(UUID.randomUUID().toString() + "/")!!)
info.displayName = display_name.text.toString()
if (info.displayName.isNullOrBlank()) {
display_name.error = getString(R.string.create_collection_display_name_required)

View File

@@ -35,7 +35,7 @@ import java.util.*
class CreateCalendarActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<CreateCalendarActivity.AccountInfo> {
companion object {
val EXTRA_ACCOUNT = "account"
const val EXTRA_ACCOUNT = "account"
}
private lateinit var account: Account
@@ -78,7 +78,7 @@ class CreateCalendarActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks
var ok = true
HttpUrl.parse(homeSet)?.let {
val info = CollectionInfo(it.resolve(UUID.randomUUID().toString() + "/").toString())
val info = CollectionInfo(it.resolve(UUID.randomUUID().toString() + "/")!!)
info.displayName = display_name.text.toString()
if (info.displayName.isNullOrBlank()) {
display_name.error = getString(R.string.create_collection_display_name_required)

View File

@@ -27,7 +27,6 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.settings.Settings
import okhttp3.HttpUrl
import java.io.IOException
import java.io.StringWriter
import java.util.logging.Level
@@ -37,14 +36,14 @@ class CreateCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<
companion object {
val ARG_ACCOUNT = "account"
val ARG_COLLECTION_INFO = "collectionInfo"
const val ARG_ACCOUNT = "account"
const val ARG_COLLECTION_INFO = "collectionInfo"
fun newInstance(account: Account, info: CollectionInfo): CreateCollectionFragment {
val frag = CreateCollectionFragment()
val args = Bundle(2)
args.putParcelable(ARG_ACCOUNT, account)
args.putSerializable(ARG_COLLECTION_INFO, info)
args.putParcelable(ARG_COLLECTION_INFO, info)
frag.arguments = args
return frag
}
@@ -57,8 +56,9 @@ class CreateCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
account = arguments!!.getParcelable(ARG_ACCOUNT)
info = arguments!!.getSerializable(ARG_COLLECTION_INFO) as CollectionInfo
val args = requireNotNull(arguments)
account = args.getParcelable(ARG_ACCOUNT)
info = args.getParcelable(ARG_COLLECTION_INFO)
loaderManager.initLoader(0, null, this)
}
@@ -74,16 +74,16 @@ class CreateCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<
}
override fun onCreateLoader(id: Int, args: Bundle?) = CreateCollectionLoader(activity!!, account, info)
override fun onCreateLoader(id: Int, args: Bundle?) = CreateCollectionLoader(requireActivity(), account, info)
override fun onLoadFinished(loader: Loader<Exception>, exception: Exception?) {
dismissAllowingStateLoss()
dismiss()
activity?.let { parent ->
if (exception != null)
fragmentManager!!.beginTransaction()
requireFragmentManager().beginTransaction()
.add(ExceptionInfoFragment.newInstance(exception, account), null)
.commitAllowingStateLoss()
.commit()
else
parent.finish()
}
@@ -189,10 +189,10 @@ class CreateCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<
.setForeground(true)
.build().use { httpClient ->
try {
val collection = DavResource(httpClient.okHttpClient, HttpUrl.parse(info.url)!!)
val collection = DavResource(httpClient.okHttpClient, info.url)
// create collection on remote server
collection.mkCol(writer.toString())
collection.mkCol(writer.toString()) {}
// now insert collection into database:
ServiceDB.OpenHelper(context).use { dbHelper ->

View File

@@ -11,8 +11,9 @@ package at.bitfire.davdroid.ui
import android.Manifest
import android.accounts.Account
import android.accounts.AccountManager
import android.app.LoaderManager
import android.content.*
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.os.Build
@@ -20,8 +21,11 @@ import android.os.Bundle
import android.os.PowerManager
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.support.v4.app.LoaderManager
import android.support.v4.content.AsyncTaskLoader
import android.support.v4.content.ContextCompat
import android.support.v4.content.FileProvider
import android.support.v4.content.Loader
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.view.Menu
@@ -36,7 +40,6 @@ import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.Settings
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.ContactsStorageException
import kotlinx.android.synthetic.main.activity_debug_info.*
import java.io.File
import java.io.FileWriter
@@ -48,13 +51,13 @@ import java.util.logging.Level
class DebugInfoActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<String> {
companion object {
@JvmField val KEY_THROWABLE = "throwable"
@JvmField val KEY_LOGS = "logs"
@JvmField val KEY_ACCOUNT = "account"
@JvmField val KEY_AUTHORITY = "authority"
@JvmField val KEY_PHASE = "phase"
@JvmField val KEY_LOCAL_RESOURCE = "localResource"
@JvmField val KEY_REMOTE_RESOURCE = "remoteResource"
const val KEY_THROWABLE = "throwable"
const val KEY_LOGS = "logs"
const val KEY_ACCOUNT = "account"
const val KEY_AUTHORITY = "authority"
const val KEY_PHASE = "phase"
const val KEY_LOCAL_RESOURCE = "localResource"
const val KEY_REMOTE_RESOURCE = "remoteResource"
}
private var report: String? = null
@@ -63,7 +66,7 @@ class DebugInfoActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<Stri
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_debug_info)
loaderManager.initLoader(0, intent.extras, this)
supportLoaderManager.initLoader(0, intent.extras, this)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -148,10 +151,14 @@ class DebugInfoActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<Stri
val throwable = extras?.getSerializable(KEY_THROWABLE) as Throwable?
if (throwable is HttpException) {
throwable.request?.let {
report.append("\nHTTP REQUEST:\n$it\n\n")
report.append("\nHTTP REQUEST:\n$it\n")
throwable.requestBody?.let { report.append(it) }
report.append("\n\n")
}
throwable.response?.let {
report.append("HTTP RESPONSE:\n$it\n")
throwable.responseBody?.let { report.append(it) }
report.append("\n\n")
}
}
@@ -253,10 +260,10 @@ class DebugInfoActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<Stri
try {
val addressBook = LocalAddressBook(context, acct, null)
report.append("Address book account: ${acct.name}\n" +
" Main account: ${addressBook.getMainAccount()}\n" +
" URL: ${addressBook.getURL()}\n" +
" Main account: ${addressBook.mainAccount}\n" +
" URL: ${addressBook.url}\n" +
" Sync automatically: ").append(ContentResolver.getSyncAutomatically(acct, ContactsContract.AUTHORITY)).append("\n")
} catch(e: ContactsStorageException) {
} catch(e: Exception) {
report.append("$acct is invalid: ${e.message}\n")
}
report.append("\n")

View File

@@ -25,14 +25,13 @@ import at.bitfire.davdroid.R
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.settings.Settings
import okhttp3.HttpUrl
@Suppress("DEPRECATION")
class DeleteCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<Exception> {
companion object {
val ARG_ACCOUNT = "account"
val ARG_COLLECTION_INFO = "collectionInfo"
const val ARG_ACCOUNT = "account"
const val ARG_COLLECTION_INFO = "collectionInfo"
}
private lateinit var account: Account
@@ -42,7 +41,7 @@ class DeleteCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<
super.onCreate(savedInstanceState)
account = arguments!!.getParcelable(ARG_ACCOUNT)
collectionInfo = arguments!!.getSerializable(ARG_COLLECTION_INFO) as CollectionInfo
collectionInfo = arguments!!.getParcelable(ARG_COLLECTION_INFO)
loaderManager.initLoader(0, null, this)
}
@@ -62,12 +61,12 @@ class DeleteCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<
DeleteCollectionLoader(activity!!, account, collectionInfo)
override fun onLoadFinished(loader: Loader<Exception>, exception: Exception?) {
dismissAllowingStateLoss()
dismiss()
if (exception != null)
fragmentManager!!.beginTransaction()
requireFragmentManager().beginTransaction()
.add(ExceptionInfoFragment.newInstance(exception, account), null)
.commitAllowingStateLoss()
.commit()
else
(activity as? AccountActivity)?.reload()
}
@@ -89,10 +88,10 @@ class DeleteCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<
.setForeground(true)
.build().use { httpClient ->
try {
val collection = DavResource(httpClient.okHttpClient, HttpUrl.parse(collectionInfo.url)!!)
val collection = DavResource(httpClient.okHttpClient, collectionInfo.url)
// delete collection from server
collection.delete(null)
collection.delete(null) {}
// delete collection locally
ServiceDB.OpenHelper(context).use { dbHelper ->
@@ -117,7 +116,7 @@ class DeleteCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<
val frag = ConfirmDeleteCollectionFragment()
val args = Bundle(2)
args.putParcelable(ARG_ACCOUNT, account)
args.putSerializable(ARG_COLLECTION_INFO, collectionInfo)
args.putParcelable(ARG_COLLECTION_INFO, collectionInfo)
frag.arguments = args
return frag
}
@@ -125,23 +124,23 @@ class DeleteCollectionFragment: DialogFragment(), LoaderManager.LoaderCallbacks<
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val collectionInfo = arguments!!.getSerializable(ARG_COLLECTION_INFO) as CollectionInfo
val collectionInfo = arguments!!.getParcelable(ARG_COLLECTION_INFO) as CollectionInfo
val name = if (collectionInfo.displayName.isNullOrBlank())
collectionInfo.url
collectionInfo.url.toString()
else
collectionInfo.displayName
return AlertDialog.Builder(activity!!)
.setTitle(R.string.delete_collection_confirm_title)
.setMessage(getString(R.string.delete_collection_confirm_warning, name))
.setPositiveButton(android.R.string.yes, { _, _ ->
.setPositiveButton(android.R.string.yes) { _, _ ->
val frag = DeleteCollectionFragment()
frag.arguments = arguments
frag.show(fragmentManager, null)
})
.setNegativeButton(android.R.string.no, { _, _ ->
}
.setNegativeButton(android.R.string.no) { _, _ ->
dismiss()
})
}
.create()
}
}

View File

@@ -21,8 +21,8 @@ import java.io.IOException
class ExceptionInfoFragment: DialogFragment() {
companion object {
val ARG_ACCOUNT = "account"
val ARG_EXCEPTION = "exception"
const val ARG_ACCOUNT = "account"
const val ARG_EXCEPTION = "exception"
fun newInstance(exception: Exception, account: Account?): ExceptionInfoFragment {
val frag = ExceptionInfoFragment()
@@ -35,8 +35,9 @@ class ExceptionInfoFragment: DialogFragment() {
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val exception = arguments!!.getSerializable(ARG_EXCEPTION) as Exception
val account: Account? = arguments!!.getParcelable(ARG_ACCOUNT)
val args = requireNotNull(arguments)
val exception = args.getSerializable(ARG_EXCEPTION) as Exception
val account: Account? = args.getParcelable(ARG_ACCOUNT)
val title = when (exception) {
is HttpException -> R.string.exception_httpexception
@@ -44,17 +45,17 @@ class ExceptionInfoFragment: DialogFragment() {
else -> R.string.exception
}
val dialog = AlertDialog.Builder(activity!!)
val dialog = AlertDialog.Builder(requireActivity())
.setIcon(R.drawable.ic_error_dark)
.setTitle(title)
.setMessage(exception::class.java.name + "\n" + exception.localizedMessage)
.setNegativeButton(R.string.exception_show_details, { _, _ ->
.setNegativeButton(R.string.exception_show_details) { _, _ ->
val intent = Intent(activity, DebugInfoActivity::class.java)
intent.putExtra(DebugInfoActivity.KEY_THROWABLE, exception)
account?.let { intent.putExtra(DebugInfoActivity.KEY_ACCOUNT, it) }
startActivity(intent)
})
.setPositiveButton(android.R.string.ok, { _, _ -> })
}
.setPositiveButton(android.R.string.ok) { _, _ -> }
.create()
isCancelable = false
return dialog

View File

@@ -8,29 +8,68 @@
package at.bitfire.davdroid.ui
import android.annotation.TargetApi
import android.app.NotificationChannel
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.support.v4.app.NotificationCompat
import at.bitfire.davdroid.App
import at.bitfire.davdroid.R
object NotificationUtils {
val CHANNEL_DEBUG = "debug"
val CHANNEL_SYNC_STATUS = "syncStatus"
val CHANNEL_SYNC_PROBLEMS = "syncProblems"
// notification IDs
const val NOTIFY_EXTERNAL_FILE_LOGGING = 1
const val NOTIFY_REFRESH_COLLECTIONS = 2
const val NOTIFY_SYNC_ERROR = 10
const val NOTIFY_OPENTASKS = 20
const val NOTIFY_PERMISSIONS = 21
const val NOTIFY_LICENSE = 100
fun createChannels(context: Context): NotificationManager {
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// notification channels
const val CHANNEL_GENERAL = "general"
const val CHANNEL_DEBUG = "debug"
private const val CHANNEL_SYNC = "sync"
const val CHANNEL_SYNC_ERRORS = "syncProblems"
const val CHANNEL_SYNC_IO_ERRORS = "syncIoErrors"
const val CHANNEL_SYNC_STATUS = "syncStatus"
fun createChannels(context: Context) {
@TargetApi(Build.VERSION_CODES.O)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val syncChannelGroup = NotificationChannelGroup(CHANNEL_SYNC, context.getString(R.string.notification_channel_sync))
nm.createNotificationChannelGroup(syncChannelGroup)
val syncChannels = arrayOf(
NotificationChannel(CHANNEL_SYNC_ERRORS, context.getString(R.string.notification_channel_sync_errors), NotificationManager.IMPORTANCE_DEFAULT),
NotificationChannel(CHANNEL_SYNC_IO_ERRORS, context.getString(R.string.notification_channel_sync_io_errors), NotificationManager.IMPORTANCE_MIN),
NotificationChannel(CHANNEL_SYNC_STATUS, context.getString(R.string.notification_channel_sync_status), NotificationManager.IMPORTANCE_MIN)
)
syncChannels.forEach {
it.group = CHANNEL_SYNC
}
if (Build.VERSION.SDK_INT >= 26)
nm.createNotificationChannels(listOf(
NotificationChannel(CHANNEL_DEBUG, context.getString(R.string.notification_channel_debugging), NotificationManager.IMPORTANCE_LOW),
NotificationChannel(CHANNEL_SYNC_STATUS, context.getString(R.string.notification_channel_sync_status), NotificationManager.IMPORTANCE_LOW),
NotificationChannel(CHANNEL_SYNC_PROBLEMS, context.getString(R.string.notification_channel_sync_problems), NotificationManager.IMPORTANCE_DEFAULT)
NotificationChannel(CHANNEL_DEBUG, context.getString(R.string.notification_channel_debugging), NotificationManager.IMPORTANCE_HIGH),
NotificationChannel(CHANNEL_GENERAL, context.getString(R.string.notification_channel_general), NotificationManager.IMPORTANCE_DEFAULT),
*syncChannels
))
}
}
return nm
fun newBuilder(context: Context, channel: String = CHANNEL_GENERAL): NotificationCompat.Builder {
val builder = NotificationCompat.Builder(context, channel)
.setColor(context.resources.getColor(R.color.primaryColor))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
builder.setLargeIcon(App.getLauncherBitmap(context))
return builder
}
}

View File

@@ -1,90 +0,0 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.ui
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.support.v4.app.ActivityCompat
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
import at.bitfire.ical4android.TaskProvider
import kotlinx.android.synthetic.main.activity_permissions.*
class PermissionsActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_permissions)
}
override fun onResume() {
super.onResume()
refresh()
}
private fun refresh() {
val noCalendarPermissions =
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_CALENDAR) != PackageManager.PERMISSION_GRANTED
calendar_permissions.visibility = if (noCalendarPermissions) View.VISIBLE else View.GONE
val noContactsPermissions =
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED
contacts_permissions.visibility = if (noContactsPermissions) View.VISIBLE else View.GONE
val noTaskPermissions: Boolean
if (LocalTaskList.tasksProviderAvailable(this)) {
noTaskPermissions =
ActivityCompat.checkSelfPermission(this, TaskProvider.PERMISSION_READ_TASKS) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, TaskProvider.PERMISSION_WRITE_TASKS) != PackageManager.PERMISSION_GRANTED
findViewById<View>(R.id.opentasks_permissions).visibility = if (noTaskPermissions) View.VISIBLE else View.GONE
} else {
findViewById<View>(R.id.opentasks_permissions).visibility = View.GONE
noTaskPermissions = false
}
if (!noCalendarPermissions && !noContactsPermissions && !noTaskPermissions) {
val nm = NotificationUtils.createChannels(this)
nm.cancel(Constants.NOTIFICATION_PERMISSIONS)
finish()
}
}
fun requestCalendarPermissions(v: View) {
ActivityCompat.requestPermissions(this, arrayOf(
Manifest.permission.READ_CALENDAR,
Manifest.permission.WRITE_CALENDAR
), 0)
}
fun requestContactsPermissions(v: View) {
ActivityCompat.requestPermissions(this, arrayOf(
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_CONTACTS
), 0)
}
fun requestOpenTasksPermissions(v: View) {
ActivityCompat.requestPermissions(this, arrayOf(
TaskProvider.PERMISSION_READ_TASKS,
TaskProvider.PERMISSION_WRITE_TASKS
), 0)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
refresh()
}
}

View File

@@ -8,9 +8,13 @@
package at.bitfire.davdroid.ui
import android.content.*
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Handler
import android.os.IBinder
import android.support.v4.content.AsyncTaskLoader
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.davdroid.settings.ISettingsObserver
import at.bitfire.davdroid.settings.Settings

View File

@@ -11,18 +11,18 @@ package at.bitfire.davdroid.ui
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Dialog
import android.app.DialogFragment
import android.app.LoaderManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.Loader
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.support.v4.app.DialogFragment
import android.support.v4.app.LoaderManager
import android.support.v4.content.Loader
import android.support.v7.app.AlertDialog
import at.bitfire.davdroid.App
import at.bitfire.davdroid.BuildConfig
@@ -30,12 +30,14 @@ import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.settings.ISettings
import org.apache.commons.lang3.text.WordUtils
import java.util.*
import java.util.logging.Level
class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISettings?> {
class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISettings> {
enum class Mode {
AUTOSTART_PERMISSIONS,
BATTERY_OPTIMIZATIONS,
GOOGLE_PLAY_ACCOUNTS_REMOVED,
OPENTASKS_NOT_INSTALLED,
@@ -44,13 +46,17 @@ class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISe
companion object {
private val SETTING_NEXT_DONATION_POPUP = "time_nextDonationPopup"
private const val SETTING_NEXT_DONATION_POPUP = "time_nextDonationPopup"
@JvmField val HINT_BATTERY_OPTIMIZATIONS = "hint_BatteryOptimizations"
@JvmField val HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED = "hint_GooglePlayAccountsRemoved"
@JvmField val HINT_OPENTASKS_NOT_INSTALLED = "hint_OpenTasksNotInstalled"
const val HINT_AUTOSTART_PERMISSIONS = "hint_AutostartPermissions"
// see https://github.com/jaredrummler/AndroidDeviceNames/blob/master/json/ for manufacturer values
private val autostartManufacturers = arrayOf("huawei", "letv", "oneplus", "vivo", "xiaomi", "zte")
val ARGS_MODE = "mode"
const val HINT_BATTERY_OPTIMIZATIONS = "hint_BatteryOptimizations"
const val HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED = "hint_GooglePlayAccountsRemoved"
const val HINT_OPENTASKS_NOT_INSTALLED = "hint_OpenTasksNotInstalled"
const val ARGS_MODE = "mode"
fun getStartupDialogs(context: Context, settings: ISettings): List<StartupDialogFragment> {
val dialogs = LinkedList<StartupDialogFragment>()
@@ -73,6 +79,10 @@ class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISe
dialogs.add(StartupDialogFragment.instantiate(Mode.BATTERY_OPTIMIZATIONS))
}
// vendor-specific auto-start information
if (autostartManufacturers.contains(Build.MANUFACTURER.toLowerCase()) && settings.getBoolean(HINT_AUTOSTART_PERMISSIONS, true))
dialogs.add(StartupDialogFragment.instantiate(Mode.AUTOSTART_PERMISSIONS))
// OpenTasks information
if (!LocalTaskList.tasksProviderAvailable(context) && settings.getBoolean(HINT_OPENTASKS_NOT_INSTALLED, true))
dialogs.add(StartupDialogFragment.instantiate(Mode.OPENTASKS_NOT_INSTALLED))
@@ -98,14 +108,14 @@ class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISe
loaderManager.initLoader(0, null, this)
}
override fun onCreateLoader(code: Int, args: Bundle?): Loader<ISettings?> =
SettingsLoader(activity)
override fun onCreateLoader(code: Int, args: Bundle?) =
SettingsLoader(requireActivity())
override fun onLoadFinished(loader: Loader<ISettings?>?, result: ISettings?) {
override fun onLoadFinished(loader: Loader<ISettings>, result: ISettings?) {
settings = result
}
override fun onLoaderReset(loader: Loader<ISettings?>?) {
override fun onLoaderReset(loader: Loader<ISettings>) {
settings = null
}
@@ -114,23 +124,41 @@ class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISe
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
isCancelable = false
val mode = Mode.valueOf(arguments.getString(ARGS_MODE))
val activity = requireActivity()
val mode = Mode.valueOf(arguments!!.getString(ARGS_MODE))
return when (mode) {
Mode.AUTOSTART_PERMISSIONS ->
AlertDialog.Builder(activity)
.setIcon(R.drawable.ic_error_dark)
.setTitle(R.string.startup_autostart_permission)
.setMessage(getString(R.string.startup_autostart_permission_message, WordUtils.capitalize(Build.MANUFACTURER.toLowerCase())))
.setPositiveButton(R.string.startup_more_info) { _, _ ->
val intent = Intent(Intent.ACTION_VIEW, App.homepageUrl(requireActivity()).buildUpon()
.appendPath("faq").appendEncodedPath("automatic-synchronization-is-not-run-as-expected/").build())
if (intent.resolveActivity(activity.packageManager) != null)
activity.startActivity(intent)
}
.setNeutralButton(R.string.startup_not_now) { _, _ -> }
.setNegativeButton(R.string.startup_dont_show_again) { _, _ ->
settings?.putBoolean(HINT_AUTOSTART_PERMISSIONS, false)
}
.create()
Mode.BATTERY_OPTIMIZATIONS ->
AlertDialog.Builder(activity)
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.startup_battery_optimization)
.setMessage(R.string.startup_battery_optimization_message)
.setPositiveButton(android.R.string.ok, { _, _ -> })
.setNeutralButton(R.string.startup_battery_optimization_disable, @TargetApi(Build.VERSION_CODES.M) { _, _ ->
.setPositiveButton(R.string.startup_battery_optimization_disable) @TargetApi(Build.VERSION_CODES.M) { _, _ ->
val intent = Intent(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:" + BuildConfig.APPLICATION_ID))
if (intent.resolveActivity(activity.packageManager) != null)
activity.startActivity(intent)
})
.setNegativeButton(R.string.startup_dont_show_again, { _: DialogInterface, _: Int ->
Uri.parse("package:" + BuildConfig.APPLICATION_ID))
if (intent.resolveActivity(activity.packageManager) != null)
activity.startActivity(intent)
}
.setNeutralButton(R.string.startup_not_now) { _, _ -> }
.setNegativeButton(R.string.startup_dont_show_again) { _: DialogInterface, _: Int ->
settings?.putBoolean(HINT_BATTERY_OPTIMIZATIONS, false)
})
}
.create()
Mode.GOOGLE_PLAY_ACCOUNTS_REMOVED -> {
@@ -144,15 +172,16 @@ class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISe
.setIcon(icon)
.setTitle(R.string.startup_google_play_accounts_removed)
.setMessage(R.string.startup_google_play_accounts_removed_message)
.setPositiveButton(android.R.string.ok, { _, _ -> })
.setNeutralButton(R.string.startup_google_play_accounts_removed_more_info, { _, _ ->
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.homepage_url)).buildUpon()
.appendPath("faq").appendPath("accounts-gone-after-reboot-or-update").build())
activity.startActivity(intent)
})
.setNegativeButton(R.string.startup_dont_show_again, { _, _ ->
.setPositiveButton(R.string.startup_more_info) { _, _ ->
val intent = Intent(Intent.ACTION_VIEW, App.homepageUrl(requireActivity()).buildUpon()
.appendPath("faq").appendEncodedPath("accounts-gone-after-reboot-or-update/").build())
if (intent.resolveActivity(activity.packageManager) != null)
activity.startActivity(intent)
}
.setNeutralButton(R.string.startup_not_now) { _, _ -> }
.setNegativeButton(R.string.startup_dont_show_again) { _, _ ->
settings?.putBoolean(HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED, false)
})
}
.create()
}
@@ -164,17 +193,17 @@ class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISe
.setIcon(R.drawable.ic_playlist_add_check_dark)
.setTitle(R.string.startup_opentasks_not_installed)
.setMessage(builder.toString())
.setPositiveButton(android.R.string.ok, { _, _ -> })
.setNeutralButton(R.string.startup_opentasks_not_installed_install, { _, _ ->
.setPositiveButton(R.string.startup_opentasks_not_installed_install) { _, _ ->
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=org.dmfs.tasks"))
if (intent.resolveActivity(activity.packageManager) != null)
activity.startActivity(intent)
else
Logger.log.warning("No market app available, can't install OpenTasks")
})
.setNegativeButton(R.string.startup_dont_show_again, { _: DialogInterface, _: Int ->
}
.setNeutralButton(R.string.startup_not_now) { _, _ -> }
.setNegativeButton(R.string.startup_dont_show_again) { _: DialogInterface, _: Int ->
settings?.putBoolean(HINT_OPENTASKS_NOT_INSTALLED, false)
})
}
.create()
}
@@ -183,17 +212,16 @@ class StartupDialogFragment: DialogFragment(), LoaderManager.LoaderCallbacks<ISe
.setIcon(R.mipmap.ic_launcher)
.setTitle(R.string.startup_donate)
.setMessage(R.string.startup_donate_message)
.setPositiveButton(R.string.startup_donate_now, { _, _ ->
val uri = Uri.parse(getString(R.string.homepage_url))
.buildUpon()
.setPositiveButton(R.string.startup_donate_now) { _, _ ->
val uri = App.homepageUrl(requireActivity()).buildUpon()
.appendEncodedPath("donate/")
.build()
startActivity(Intent(Intent.ACTION_VIEW, uri))
settings?.putLong(SETTING_NEXT_DONATION_POPUP, System.currentTimeMillis() + 30 * 86400000L) // 30 days
})
.setNegativeButton(R.string.startup_donate_later, { _, _ ->
}
.setNegativeButton(R.string.startup_donate_later) { _, _ ->
settings?.putLong(SETTING_NEXT_DONATION_POPUP, System.currentTimeMillis() + 14 * 86400000L) // 14 days
})
}
.create()
}

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