mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2026-01-06 05:47:50 -05:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e81ae958aa | ||
|
|
051530fa7d | ||
|
|
628937e109 | ||
|
|
cd3662ce43 | ||
|
|
d694b480c4 | ||
|
|
afc02d5ab5 | ||
|
|
171cda098a | ||
|
|
19b660333f | ||
|
|
e6419ccefc | ||
|
|
2a783bef3a | ||
|
|
34c08b299c | ||
|
|
f2d9221239 | ||
|
|
32651978cc | ||
|
|
437a055c81 | ||
|
|
65ef5cd1d9 | ||
|
|
cac1339b61 | ||
|
|
d6c11b7f39 | ||
|
|
4cfb0af588 | ||
|
|
b2ad46e41c | ||
|
|
b36731705a | ||
|
|
4df8aba2ac | ||
|
|
5e0ed389c2 | ||
|
|
486c7db99c | ||
|
|
58556447f5 | ||
|
|
afd614fa19 | ||
|
|
3a69f66ba8 | ||
|
|
edadc4e260 | ||
|
|
0db859f3db | ||
|
|
a3a3cf8259 | ||
|
|
c065c48702 | ||
|
|
0f5f2a3331 | ||
|
|
d7bf4f95a5 | ||
|
|
8c82d21ecc | ||
|
|
619012e54e | ||
|
|
87ab0ca05b | ||
|
|
6f7f35abcc | ||
|
|
3c6f6145f0 | ||
|
|
1fb90762e0 | ||
|
|
3fe1d428a3 | ||
|
|
c47394b021 | ||
|
|
faa49350c9 | ||
|
|
511dde66ad |
@@ -13,13 +13,13 @@ apply plugin: 'org.jetbrains.dokka-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion '27.0.1'
|
||||
buildToolsVersion '27.0.3'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "at.bitfire.davdroid"
|
||||
resValue "string", "packageID", applicationId
|
||||
|
||||
versionCode 203
|
||||
versionCode 213
|
||||
buildConfigField "long", "buildTime", System.currentTimeMillis() + "L"
|
||||
|
||||
minSdkVersion 19 // Android 4.4
|
||||
@@ -37,13 +37,13 @@ android {
|
||||
productFlavors {
|
||||
standard {
|
||||
dimension "type"
|
||||
versionName "1.10"
|
||||
versionName "1.11"
|
||||
|
||||
buildConfigField "boolean", "customCerts", "true"
|
||||
}
|
||||
managed {
|
||||
dimension "type"
|
||||
versionName "1.10-mgd"
|
||||
versionName "1.11-mgd"
|
||||
|
||||
applicationId "com.davdroid.managed"
|
||||
resValue "string", "packageID", applicationId
|
||||
@@ -55,20 +55,20 @@ android {
|
||||
|
||||
gplay {
|
||||
dimension "type"
|
||||
versionName "1.10-gplay"
|
||||
versionName "1.11-gplay"
|
||||
|
||||
buildConfigField "boolean", "customCerts", "true"
|
||||
}
|
||||
icloud {
|
||||
dimension "type"
|
||||
versionName "1.10-cloud"
|
||||
versionName "1.11-rc1-cloud"
|
||||
|
||||
applicationId "at.bitfire.cloudsync"
|
||||
resValue "string", "packageID", applicationId
|
||||
}
|
||||
soldupe {
|
||||
dimension "type"
|
||||
versionName "1.10-soldupe"
|
||||
versionName "1.11-soldupe"
|
||||
|
||||
applicationId "com.soldupe.cloudsync"
|
||||
resValue "string", "packageID", applicationId
|
||||
@@ -81,6 +81,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 +117,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'
|
||||
@@ -139,31 +139,28 @@ dependencies {
|
||||
|
||||
compile "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'
|
||||
compile 'com.android.support:appcompat-v7:27.1.0'
|
||||
compile 'com.android.support:cardview-v7:27.1.0'
|
||||
compile 'com.android.support:design:27.1.0'
|
||||
compile 'com.android.support:preference-v14:27.1.0'
|
||||
compile 'com.android.support.test.espresso:espresso-idling-resource:3.0.1'
|
||||
|
||||
compile 'com.github.yukuku:ambilwarna:2.0.1'
|
||||
|
||||
compile 'com.squareup.okhttp3:logging-interceptor:3.9.1'
|
||||
compile 'com.squareup.okhttp3:logging-interceptor:3.10.0'
|
||||
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'
|
||||
|
||||
// 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 'com.android.support.test:runner:1.0.1'
|
||||
androidTestCompile 'com.android.support.test:rules:1.0.1'
|
||||
androidTestCompile 'com.android.support.test.espresso:espresso-core:3.0.1'
|
||||
androidTestCompile 'com.android.support.test.espresso:espresso-intents:3.0.1'
|
||||
androidTestCompile 'junit:junit:4.12'
|
||||
androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.9.1'
|
||||
androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.10.0'
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'com.squareup.okhttp3:mockwebserver:3.9.1'
|
||||
testCompile 'com.squareup.okhttp3:mockwebserver:3.10.0'
|
||||
}
|
||||
|
||||
@@ -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
1
app/src/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
espressoTest
|
||||
@@ -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() {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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.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 dav = DavResource(httpClient.okHttpClient, server.url("/"))
|
||||
dav.propfind(0, ResourceType.NAME)
|
||||
var info = CollectionInfo(dav)
|
||||
assertEquals(CollectionInfo.Type.ADDRESS_BOOK, info.type)
|
||||
assertFalse(info.readOnly)
|
||||
assertEquals("My Contacts", info.displayName)
|
||||
assertEquals("My Contacts Description", info.description)
|
||||
|
||||
// read-only calendar, no display name
|
||||
server.enqueue(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 = DavResource(httpClient.okHttpClient, server.url("/"))
|
||||
dav.propfind(0, ResourceType.NAME)
|
||||
info = CollectionInfo(dav)
|
||||
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("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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* 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() {
|
||||
// before dav.propfind(), no info is available
|
||||
var dav = DavResource(client.okHttpClient, server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL))
|
||||
ServiceInfo().let { info ->
|
||||
finder.rememberIfAddressBookOrHomeset(dav, info)
|
||||
assertEquals(0, info.collections.size)
|
||||
assertEquals(0, info.homeSets.size)
|
||||
}
|
||||
|
||||
// recognize home set
|
||||
dav.propfind(0, AddressbookHomeSet.NAME)
|
||||
ServiceInfo().let { info ->
|
||||
finder.rememberIfAddressBookOrHomeset(dav, info)
|
||||
assertEquals(0, info.collections.size)
|
||||
assertEquals(1, info.homeSets.size)
|
||||
assertEquals(server.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET/").uri(), info.homeSets.first())
|
||||
}
|
||||
|
||||
// recognize address book
|
||||
dav = DavResource(client.okHttpClient, server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK))
|
||||
dav.propfind(0, ResourceType.NAME)
|
||||
ServiceInfo().let { info ->
|
||||
finder.rememberIfAddressBookOrHomeset(dav, info)
|
||||
assertEquals(1, info.collections.size)
|
||||
assertEquals(server.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/").uri(), 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).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
|
||||
|
||||
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=="
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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})"
|
||||
}
|
||||
|
||||
|
||||
@@ -65,4 +65,4 @@ class DefaultAccountsDrawerHandler: IAccountsDrawerHandler {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 ->
|
||||
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 -> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<!--DavService-->
|
||||
<!--AppSettingsActivity-->
|
||||
<!--AccountActivity-->
|
||||
<!--PermissionsActivity-->
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_type_email">Entra amb una adreça de correu electrònic</string>
|
||||
<string name="login_type_url">Entra amb una URL i un nom d\'usuari</string>
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
<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-->
|
||||
@@ -84,17 +82,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>
|
||||
@@ -189,27 +176,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>
|
||||
|
||||
@@ -2,16 +2,26 @@
|
||||
<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="manage_accounts">Administrere konti</string>
|
||||
<string name="please_wait">Vent venligst ...</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>
|
||||
<string name="notification_channel_debugging">Debugging</string>
|
||||
<string name="notification_channel_general">Andre vigtige meddelelser</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">Statusmeddelelser</string>
|
||||
<!--startup dialogs-->
|
||||
<string name="startup_autostart_permission">%s tilladelse til at starte automatisk</string>
|
||||
<string name="startup_autostart_permission_message">Enhedens firmware kan forbyde automatisk synkronisering. Du er muligvis nødt til at tillade automatisk synkronisering manuelt.</string>
|
||||
<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_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>
|
||||
@@ -19,11 +29,11 @@
|
||||
<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_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>
|
||||
@@ -31,21 +41,24 @@
|
||||
<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_subtitle">CalDAV/CardDAV synkroniseringsadapter</string>
|
||||
<string name="navigation_drawer_about">Om / Licens</string>
|
||||
<string name="navigation_drawer_beta_feedback">Beta feedback</string>
|
||||
<string name="navigation_drawer_settings">Indstillinger</string>
|
||||
<string name="navigation_drawer_news_updates">Nyheder & opdateringer</string>
|
||||
<string name="navigation_drawer_external_links">Eksterne links</string>
|
||||
<string name="navigation_drawer_external_links">Eksterne henvisninger</string>
|
||||
<string name="navigation_drawer_website">Hjemmeside</string>
|
||||
<string name="navigation_drawer_manual">Manual</string>
|
||||
<string name="navigation_drawer_faq">FAQ</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>
|
||||
@@ -84,22 +97,21 @@
|
||||
<string name="account_delete">Slet konto</string>
|
||||
<string name="account_delete_confirmation_title">Ønsker du at slette konto?</string>
|
||||
<string name="account_delete_confirmation_text">Alle lokale kopier af addessebøger, kalendere og opgavelister vil blive slettet.</string>
|
||||
<string name="account_carddav">CardDAV</string>
|
||||
<string name="account_caldav">CalDAV</string>
|
||||
<string name="account_webcal">Webcal</string>
|
||||
<string name="account_synchronize_this_collection">Synkroniser denne 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">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_no_webcal_handler_found">Der er ikke fundet nogen app, der kan håndtere Webcal.</string>
|
||||
<string name="account_install_icsdroid">Installer ICSdroid</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
|
||||
<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>
|
||||
@@ -108,10 +120,13 @@
|
||||
<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_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_type_url_certificate">Log ind med URL og klientcertifikat</string>
|
||||
<string name="login_select_certificate">Vælg certifikat</string>
|
||||
<string name="login_login">Login</string>
|
||||
<string name="login_back">Tilbage</string>
|
||||
<string name="login_create_account">Opret konto</string>
|
||||
@@ -132,15 +147,29 @@
|
||||
<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 +180,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 +193,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 +218,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 +226,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>
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
<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_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_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-->
|
||||
@@ -84,17 +82,6 @@
|
||||
<string name="account_create_new_address_book">Crear nueva lista de contactos</string>
|
||||
<string name="account_refresh_calendar_list">Refrescar calendario</string>
|
||||
<string name="account_create_new_calendar">Crear nuevo calendario</string>
|
||||
<!--PermissionsActivity-->
|
||||
<string name="permissions_title">Permisos de DAVdroid</string>
|
||||
<string name="permissions_calendar">Permisos de calendario</string>
|
||||
<string name="permissions_calendar_details">Para sincronizar eventos CalDAV con tus calendarios locales, DAVdroid necesita acceder a los mismos.</string>
|
||||
<string name="permissions_calendar_request">Solicitar permisos sobre calendario</string>
|
||||
<string name="permissions_contacts">Permisos de contactos</string>
|
||||
<string name="permissions_contacts_details">Para sincronizar libretas de contactos CadDAV con tus contactos locales, DAVdroid necesita acceder a los mismos.</string>
|
||||
<string name="permissions_contacts_request">Solicitar permisos sobre contactos</string>
|
||||
<string name="permissions_opentasks">Permisos de OpenTasks</string>
|
||||
<string name="permissions_opentasks_details">Para sincronizar listas de tareas CalDAV con tus listas de tareas locales, DAVdroid necesita acceder a OpenTasks.</string>
|
||||
<string name="permissions_opentasks_request">Solicitar permisos sobre OpenTasks</string>
|
||||
<!--AddAccountActivity-->
|
||||
<string name="login_title">Añadir cuenta</string>
|
||||
<string name="login_type_email">Acceder con cuenta de correo</string>
|
||||
@@ -188,27 +175,6 @@
|
||||
<string name="debug_info_title">Información de depuración</string>
|
||||
<string name="sync_error_permissions">Permisos de DAVdroid</string>
|
||||
<string name="sync_error_permissions_text">Permisos adicionales requeridos</string>
|
||||
<string name="sync_error_calendar">La sincronización de calendario falló (%s)</string>
|
||||
<string name="sync_error_contacts">La sincronización de agenda falló (%s)</string>
|
||||
<string name="sync_error_tasks">La sincronización de tareas falló (%s)</string>
|
||||
<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>
|
||||
<!--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>
|
||||
|
||||
@@ -19,9 +19,7 @@
|
||||
<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-->
|
||||
@@ -96,17 +94,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>
|
||||
@@ -217,27 +204,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>
|
||||
|
||||
@@ -8,7 +8,16 @@
|
||||
<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="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">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_autostart_permission_message">Lehetséges,hogy az eszköz nem teszi lehetővé az automatikus szinkronizálást és az automatikus szinkronizálást lehetőségét kézzel kell beállítani.</string>
|
||||
<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>
|
||||
@@ -19,9 +28,9 @@
|
||||
<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-->
|
||||
@@ -37,10 +46,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,24 +99,18 @@
|
||||
<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_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
|
||||
<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>
|
||||
<string name="login_email_address">Email cím:</string>
|
||||
@@ -114,10 +119,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>
|
||||
@@ -138,6 +146,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 +179,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 +217,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 +225,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>
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
<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">Ottimizzazione 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>
|
||||
@@ -19,9 +24,9 @@
|
||||
<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-->
|
||||
@@ -41,7 +46,9 @@
|
||||
<string name="navigation_drawer_news_updates">Notizie & 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 +94,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 +111,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>
|
||||
@@ -154,6 +156,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 +179,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 +203,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 +211,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>
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
<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>
|
||||
<!--startup dialogs-->
|
||||
<string name="startup_battery_optimization">バッテリー最適化</string>
|
||||
<string name="startup_battery_optimization_message">Android は数日後に DAVdroid の同期を無効にする/減らすことがあります。これを防止するには、バッテリー最適化をオフにしてください。</string>
|
||||
@@ -24,9 +22,7 @@
|
||||
<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_opentasks_not_installed">OpenTasks がインストールされていません</string>
|
||||
<string name="startup_opentasks_not_installed_message">OpenTasks アプリが利用できないため、DAVdroid はタスクリストを同期することができません。</string>
|
||||
<string name="startup_opentasks_reinstall_davdroid">OpenTasks をインストールした後で、DAVdroidを再インストールして、再度アカウントを追加してください (Android のバグ)。</string>
|
||||
<string name="startup_opentasks_not_installed_install">OpenTasks をインストール</string>
|
||||
<!--AboutActivity-->
|
||||
@@ -105,18 +101,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">ローカルの連絡先と 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>
|
||||
@@ -240,27 +224,6 @@
|
||||
<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>
|
||||
<!--cert4android-->
|
||||
<string name="certificate_notification_connection_security">DAVdroid: 接続セキュリティ</string>
|
||||
<string name="trust_certificate_unknown_certificate_found">DAVdroidは、未知の証明書を検出しました。それを信頼しますか?</string>
|
||||
|
||||
@@ -21,9 +21,7 @@
|
||||
<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-->
|
||||
@@ -101,18 +99,6 @@
|
||||
<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>
|
||||
@@ -232,27 +218,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>
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
<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-->
|
||||
@@ -96,17 +94,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>
|
||||
@@ -207,27 +194,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>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<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>
|
||||
@@ -21,9 +22,7 @@
|
||||
<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-->
|
||||
@@ -44,6 +43,7 @@
|
||||
<string name="navigation_drawer_news_updates">Nowości & 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,18 +101,6 @@
|
||||
<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>
|
||||
@@ -123,10 +111,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>
|
||||
@@ -147,6 +138,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 +224,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>
|
||||
|
||||
@@ -9,9 +9,14 @@
|
||||
<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_autostart_permission">%s permissão de início automático</string>
|
||||
<string name="startup_autostart_permission_message">O firmware do aparelho pode impedir a sincronização automática. Você pode ter que definir a sincronização automática de forma manual.</string>
|
||||
<string name="startup_battery_optimization">Otimização da bateria</string>
|
||||
<string name="startup_battery_optimization_message">O Android pode desativar/reduzir a sincronização do DAVdroid depois de alguns dias. Para evitar isso, desligue a otimização da bateria.</string>
|
||||
<string name="startup_battery_optimization_disable">Desligar para o DAVdroid</string>
|
||||
@@ -22,9 +27,9 @@
|
||||
<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-->
|
||||
@@ -103,18 +108,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>
|
||||
@@ -239,27 +232,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,11 +11,16 @@
|
||||
<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_autostart_permission">Разрешение автозапуска %s</string>
|
||||
<string name="startup_autostart_permission_message">Прошивка устройства может препятствовать автоматической синхронизации. Возможно, придется разрешить автоматическую синхронизацию вручную.</string>
|
||||
<string name="startup_battery_optimization">Оптимизация батареи</string>
|
||||
<string name="startup_battery_optimization_message">Андроид может отключить/понизить синхронизацию DAVdroid через несколько дней. Чтобы это предотвратить, отключите оптимизацию батареи.</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>
|
||||
@@ -24,14 +29,14 @@
|
||||
<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_license_info_no_warranty">Эта программа поставляется БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ. Это свободное программное обеспечение и вы можете распространять его при соблюдении определенных условий.</string>
|
||||
<!--global settings-->
|
||||
<string name="logging_davdroid_file_logging">Файл журнала DAVdroid</string>
|
||||
<string name="logging_to_external_storage">Сохранение логов во внешнем хранилище: %s</string>
|
||||
@@ -105,18 +110,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_help_url">https://www.davdroid.com/tested-with/?pk_campaign=davdroid-app</string>
|
||||
<string name="login_title">Добавить аккаунт</string>
|
||||
@@ -152,7 +145,7 @@
|
||||
<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 +178,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 +220,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>
|
||||
<string name="collection_force_read_only">Только для чтения</string>
|
||||
<!--ExceptionInfoFragment-->
|
||||
<string name="exception">Произошла ошибка.</string>
|
||||
<string name="exception_httpexception">Произошла ошибка HTTP</string>
|
||||
@@ -246,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">Ошибка сети или ввода/вывода – %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>
|
||||
|
||||
@@ -19,9 +19,7 @@
|
||||
<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-->
|
||||
@@ -88,17 +86,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>
|
||||
@@ -193,27 +180,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>
|
||||
|
||||
@@ -14,9 +14,7 @@
|
||||
<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-->
|
||||
@@ -66,18 +64,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>
|
||||
@@ -123,6 +109,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 +147,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>
|
||||
|
||||
@@ -8,7 +8,17 @@
|
||||
<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_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_autostart_permission">Дозвіл автозапуску %s</string>
|
||||
<string name="startup_autostart_permission_message">Програмне забезпечення пристрою може запобігати автоматичні синхронізації. Можливо доведеться дозволити автоматичну синхронізацію вручну.</string>
|
||||
<string name="startup_battery_optimization">Оптимізація енергоспоживання</string>
|
||||
<string name="startup_battery_optimization_message">Android може вимкнути, чи призупинити синхронізацію DAVdroid через деякий час. Аби запобігти цьому, вимкніть оптимізацію енергоспоживання для додатку.</string>
|
||||
<string name="startup_battery_optimization_disable">Вимкнути для DAVdroid</string>
|
||||
@@ -19,9 +29,9 @@
|
||||
<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-->
|
||||
@@ -37,11 +47,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,22 +97,21 @@
|
||||
<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_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>
|
||||
@@ -108,10 +120,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>
|
||||
@@ -132,21 +147,41 @@
|
||||
<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>
|
||||
@@ -159,6 +194,10 @@
|
||||
<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 +219,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 +227,22 @@
|
||||
<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="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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
<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>
|
||||
<!--startup dialogs-->
|
||||
<string name="startup_battery_optimization">电池优化</string>
|
||||
<string name="startup_battery_optimization_message">系统可能会在几天后减少或停用 DAVdroid 同步。为了避免这一情况,请禁用对 DAVdroid 的电池优化。</string>
|
||||
@@ -20,9 +22,7 @@
|
||||
<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-->
|
||||
@@ -43,10 +43,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,19 +101,8 @@
|
||||
<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">Email 地址</string>
|
||||
@@ -120,11 +110,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>
|
||||
@@ -145,6 +138,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 +222,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>
|
||||
|
||||
@@ -19,9 +19,7 @@
|
||||
<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-->
|
||||
@@ -87,17 +85,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>
|
||||
@@ -190,27 +177,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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"/>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -22,6 +22,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 +39,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 +52,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)
|
||||
@@ -234,27 +239,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,7 +384,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
|
||||
@@ -504,16 +514,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)
|
||||
|
||||
@@ -17,37 +17,37 @@ import android.graphics.drawable.BitmapDrawable
|
||||
import android.os.Build
|
||||
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)
|
||||
@@ -70,6 +70,8 @@ class App: Application() {
|
||||
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
|
||||
|
||||
@@ -9,16 +9,8 @@ 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
|
||||
|
||||
}
|
||||
|
||||
@@ -11,12 +11,15 @@ 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.UrlUtils
|
||||
import at.bitfire.dav4android.exception.DavException
|
||||
@@ -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
|
||||
@@ -320,19 +346,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) }
|
||||
|
||||
@@ -22,14 +22,12 @@ 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")
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ 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
|
||||
@@ -42,7 +41,7 @@ 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 */
|
||||
@@ -65,7 +64,7 @@ class HttpClient private constructor(
|
||||
certManager?.close()
|
||||
}
|
||||
|
||||
class Builder @JvmOverloads constructor(
|
||||
class Builder(
|
||||
val context: Context? = null,
|
||||
val settings: ISettings? = null,
|
||||
accountSettings: AccountSettings? = null,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -14,7 +14,7 @@ import at.bitfire.dav4android.property.*
|
||||
import at.bitfire.davdroid.model.ServiceDB.Collections
|
||||
import java.io.Serializable
|
||||
|
||||
data class CollectionInfo @JvmOverloads constructor(
|
||||
data class CollectionInfo(
|
||||
val url: String,
|
||||
|
||||
var id: Long? = null,
|
||||
@@ -48,7 +48,6 @@ data class CollectionInfo @JvmOverloads constructor(
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmField
|
||||
val DAV_PROPERTIES = arrayOf(
|
||||
ResourceType.NAME,
|
||||
CurrentUserPrivilegeSet.NAME,
|
||||
|
||||
@@ -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)"
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
47
app/src/main/java/at/bitfire/davdroid/model/SyncState.kt
Normal file
47
app/src/main/java/at/bitfire/davdroid/model/SyncState.kt
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright © Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.model
|
||||
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import java.util.logging.Level
|
||||
|
||||
data class SyncState(
|
||||
val type: Type,
|
||||
val value: String
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromString(s: String?): SyncState? {
|
||||
if (s == null)
|
||||
return null
|
||||
|
||||
val pos = s.indexOf(':')
|
||||
if (pos == -1)
|
||||
return null
|
||||
|
||||
return try {
|
||||
SyncState(
|
||||
Type.valueOf(s.substring(0, pos)),
|
||||
s.substring(pos + 1)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.WARNING, "Couldn't restore SyncState", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum class Type { CTAG, SYNC_TOKEN }
|
||||
|
||||
override fun toString() =
|
||||
"${type.name}:$value"
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
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,21 +91,95 @@ 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 }
|
||||
|
||||
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 IllegalStateException("Address book doesn't exist anymore")
|
||||
}
|
||||
set(account) {
|
||||
val accountManager = AccountManager.get(context)
|
||||
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, account.name)
|
||||
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, account.type)
|
||||
|
||||
_mainAccount = account
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -135,7 +192,7 @@ class LocalAddressBook(
|
||||
values.put(RawContacts.ACCOUNT_NAME, newAccountName)
|
||||
provider.update(syncAdapterURI(RawContacts.CONTENT_URI), values, "${RawContacts.ACCOUNT_NAME}=?", arrayOf(account.name))
|
||||
}
|
||||
} catch(e: RemoteException) {
|
||||
} catch (e: RemoteException) {
|
||||
Logger.log.log(Level.WARNING, "Couldn't re-assign contacts to new account name", e)
|
||||
}
|
||||
}, null)
|
||||
@@ -143,147 +200,111 @@ class LocalAddressBook(
|
||||
}
|
||||
|
||||
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 +313,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 +339,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)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -65,7 +58,7 @@ class LocalCalendar private constructor(
|
||||
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(
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -23,129 +23,112 @@ 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>()
|
||||
|
||||
|
||||
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)
|
||||
override var flags: Int = 0
|
||||
private set
|
||||
|
||||
|
||||
@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 +137,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 +151,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 +189,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 +198,7 @@ class LocalContact: AndroidContact, LocalResource {
|
||||
.withValue(CachedGroupMembership.GROUP_ID, groupID)
|
||||
.withYieldAllowed(true)
|
||||
))
|
||||
cachedGroupMemberships.add(groupID)
|
||||
cachedGroupMemberships += groupID
|
||||
}
|
||||
|
||||
fun removeGroupMemberships(batch: BatchOperation) {
|
||||
@@ -247,10 +219,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,10 +230,9 @@ 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
|
||||
@@ -272,13 +242,8 @@ class LocalContact: AndroidContact, LocalResource {
|
||||
// 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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/2.x")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
private set
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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?)
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,17 +35,6 @@ 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
|
||||
@@ -54,8 +45,6 @@ class LocalTaskList private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@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 +53,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
|
||||
@@ -89,34 +77,45 @@ class LocalTaskList private constructor(
|
||||
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 +128,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 +153,4 @@ class LocalTaskList private constructor(
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ 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 java.util.logging.Level
|
||||
|
||||
class AddressBooksSyncAdapterService: SyncAdapterService() {
|
||||
@@ -29,7 +28,7 @@ class AddressBooksSyncAdapterService: SyncAdapterService() {
|
||||
override fun syncAdapter() = AddressBooksSyncAdapter(this)
|
||||
|
||||
|
||||
protected class AddressBooksSyncAdapter(
|
||||
class AddressBooksSyncAdapter(
|
||||
context: Context
|
||||
): SyncAdapter(context) {
|
||||
|
||||
@@ -103,8 +102,8 @@ class AddressBooksSyncAdapterService: SyncAdapterService() {
|
||||
val remote = remoteAddressBooks(service)
|
||||
|
||||
// delete/update local address books
|
||||
for (addressBook in LocalAddressBook.find(context, provider, account)) {
|
||||
val url = addressBook.getURL()
|
||||
for (addressBook in LocalAddressBook.findAll(context, provider, account)) {
|
||||
val url = addressBook.url
|
||||
val info = remote[url]
|
||||
if (info == null) {
|
||||
Logger.log.log(Level.INFO, "Deleting obsolete local address book", url)
|
||||
@@ -114,7 +113,7 @@ class AddressBooksSyncAdapterService: SyncAdapterService() {
|
||||
try {
|
||||
Logger.log.log(Level.FINE, "Updating local address book $url", info)
|
||||
addressBook.update(info)
|
||||
} catch(e: ContactsStorageException) {
|
||||
} 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
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* Copyright © Ricki Hirner (bitfire web engineering).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the GNU Public License v3.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import at.bitfire.dav4android.DavCollection
|
||||
import at.bitfire.dav4android.DavResource
|
||||
import at.bitfire.dav4android.exception.ConflictException
|
||||
import at.bitfire.dav4android.exception.DavException
|
||||
import at.bitfire.dav4android.exception.HttpException
|
||||
import at.bitfire.dav4android.exception.PreconditionFailedException
|
||||
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.HttpClient
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.model.SyncState
|
||||
import at.bitfire.davdroid.resource.LocalCollection
|
||||
import at.bitfire.davdroid.resource.LocalResource
|
||||
import at.bitfire.davdroid.settings.ISettings
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.RequestBody
|
||||
import java.util.logging.Level
|
||||
|
||||
abstract class BaseDavSyncManager<ResourceType: LocalResource<*>, out CollectionType: LocalCollection<ResourceType>, RemoteType: DavCollection>(
|
||||
context: Context,
|
||||
settings: ISettings,
|
||||
account: Account,
|
||||
accountSettings: AccountSettings,
|
||||
extras: Bundle,
|
||||
authority: String,
|
||||
syncResult: SyncResult,
|
||||
localCollection: CollectionType
|
||||
): SyncManager<ResourceType, CollectionType>(context, settings, account, accountSettings, extras, authority, syncResult, localCollection), AutoCloseable {
|
||||
|
||||
protected val httpClient = HttpClient.Builder(context, settings, accountSettings).build()
|
||||
|
||||
protected lateinit var collectionURL: HttpUrl
|
||||
protected lateinit var davCollection: RemoteType
|
||||
|
||||
override fun close() {
|
||||
httpClient.close()
|
||||
}
|
||||
|
||||
override fun prepare(): Boolean {
|
||||
// always re-sync on manual syncs
|
||||
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL))
|
||||
localCollection.lastSyncState = null
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Process locally deleted entries (DELETE them on the server as well).
|
||||
* Checks for thread interruption before each request to allow quick sync cancellation.
|
||||
*/
|
||||
override fun processLocallyDeleted(): Boolean {
|
||||
var numDeleted = 0
|
||||
|
||||
// Remove locally deleted entries from server (if they have a name, i.e. if they were uploaded before),
|
||||
// but only if they don't have changed on the server. Then finally remove them from the local address book.
|
||||
val localList = localCollection.findDeleted()
|
||||
for (local in localList)
|
||||
useLocal(local, {
|
||||
abortIfCancelled()
|
||||
|
||||
val fileName = local.fileName
|
||||
if (fileName != null) {
|
||||
Logger.log.info("$fileName has been deleted locally -> deleting from server")
|
||||
|
||||
useRemote(DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build()), { remote ->
|
||||
try {
|
||||
remote.delete(local.eTag)
|
||||
numDeleted++
|
||||
} catch (e: HttpException) {
|
||||
Logger.log.warning("Couldn't delete $fileName from server; ignoring (may be downloaded again)")
|
||||
}
|
||||
})
|
||||
} else
|
||||
Logger.log.info("Removing local record #${local.id} which has been deleted locally and was never uploaded")
|
||||
local.delete()
|
||||
syncResult.stats.numDeletes++
|
||||
})
|
||||
Logger.log.info("Removed $numDeleted record(s) from server")
|
||||
return numDeleted > 0
|
||||
}
|
||||
|
||||
protected abstract fun prepareUpload(resource: ResourceType): RequestBody
|
||||
|
||||
/**
|
||||
* Uploads dirty records to the server, using a PUT request for each record.
|
||||
* Checks for thread interruption before each request to allow quick sync cancellation.
|
||||
*/
|
||||
override fun uploadDirty(): Boolean {
|
||||
var numUploaded = 0
|
||||
|
||||
// upload dirty contacts
|
||||
for (local in localCollection.findDirty())
|
||||
useLocal(local, {
|
||||
abortIfCancelled()
|
||||
|
||||
if (local.fileName == null) {
|
||||
Logger.log.fine("Generating file name/UID for local record #${local.id}")
|
||||
local.assignNameAndUID()
|
||||
}
|
||||
|
||||
val fileName = local.fileName!!
|
||||
useRemote(DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build()), { remote ->
|
||||
// generate entity to upload (VCard, iCal, whatever)
|
||||
val body = prepareUpload(local)
|
||||
|
||||
try {
|
||||
if (local.eTag == null) {
|
||||
Logger.log.info("Uploading new record $fileName")
|
||||
remote.put(body, null, true)
|
||||
} else {
|
||||
Logger.log.info("Uploading locally modified record $fileName")
|
||||
remote.put(body, local.eTag, false)
|
||||
}
|
||||
numUploaded++
|
||||
} catch(e: ConflictException) {
|
||||
// we can't interact with the user to resolve the conflict, so we treat 409 like 412
|
||||
Logger.log.log(Level.INFO, "Edit conflict, ignoring", e)
|
||||
} catch(e: PreconditionFailedException) {
|
||||
Logger.log.log(Level.INFO, "Resource has been modified on the server before upload, ignoring", e)
|
||||
}
|
||||
|
||||
val newETag = remote.properties[GetETag::class.java]
|
||||
val eTag: String?
|
||||
if (newETag != null) {
|
||||
eTag = newETag.eTag
|
||||
Logger.log.fine("Received new ETag=$eTag after uploading")
|
||||
} else {
|
||||
Logger.log.fine("Didn't receive new ETag after uploading, setting to null")
|
||||
eTag = null
|
||||
}
|
||||
|
||||
local.clearDirty(eTag)
|
||||
})
|
||||
})
|
||||
Logger.log.info("Sent $numUploaded record(s) to server")
|
||||
return numUploaded > 0
|
||||
}
|
||||
|
||||
override fun syncRequired(): Boolean {
|
||||
val localState = localCollection.lastSyncState
|
||||
val remoteState = syncState(false)
|
||||
Logger.log.info("Local sync state = $localState, remote sync state = $remoteState")
|
||||
return when {
|
||||
remoteState?.type == SyncState.Type.SYNC_TOKEN -> {
|
||||
val lastKnownToken = localState?.takeIf { it.type == SyncState.Type.SYNC_TOKEN }?.value
|
||||
lastKnownToken != remoteState.value
|
||||
}
|
||||
remoteState?.type == SyncState.Type.CTAG -> {
|
||||
val lastKnownCTag = localState?.takeIf { it.type == SyncState.Type.CTAG }?.value
|
||||
lastKnownCTag != remoteState.value
|
||||
}
|
||||
else ->
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun syncState(forceRefresh: Boolean) = useRemoteCollection { remote ->
|
||||
if (forceRefresh)
|
||||
remote.propfind(0, GetCTag.NAME, SyncToken.NAME)
|
||||
|
||||
remote.properties[SyncToken::class.java]?.token?.let {
|
||||
SyncState(SyncState.Type.SYNC_TOKEN, it)
|
||||
} ?:
|
||||
remote.properties[GetCTag::class.java]?.cTag?.let {
|
||||
SyncState(SyncState.Type.CTAG, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resetPresentRemotely() {
|
||||
val number = localCollection.markNotDirty(0)
|
||||
Logger.log.info("Number of local non-dirty entries: $number")
|
||||
}
|
||||
|
||||
override fun compareLocalRemote(syncState: SyncState?, remoteResources: Map<String, DavResource>): RemoteChanges {
|
||||
/* check which resources are
|
||||
1. updated remotely -> update
|
||||
2. added remotely -> update
|
||||
3. not present remotely anymore -> ignore (because they will be deleted by deleteObsolete()
|
||||
*/
|
||||
|
||||
val changes = RemoteChanges(syncState, false)
|
||||
|
||||
for ((name, remote) in remoteResources)
|
||||
useLocal(localCollection.findByName(name), { local ->
|
||||
if (local == null) {
|
||||
Logger.log.info("$name has been added remotely")
|
||||
changes.updated += remote
|
||||
} else {
|
||||
val localETag = local.eTag
|
||||
val remoteETag = remote.properties[GetETag::class.java]?.eTag ?: throw DavException("Server didn't provide ETag")
|
||||
if (localETag == remoteETag)
|
||||
Logger.log.fine("$name has not been changed on server (ETag still $remoteETag)")
|
||||
else {
|
||||
Logger.log.info("$name has been changed on server (current ETag=$remoteETag, last known ETag=$localETag)")
|
||||
changes.updated += remote
|
||||
}
|
||||
|
||||
// mark as remotely present, so that this resource won't be deleted at the end
|
||||
local.updateFlags(LocalResource.FLAG_REMOTELY_PRESENT)
|
||||
}
|
||||
})
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
override fun listRemoteChanges(state: SyncState?): RemoteChanges {
|
||||
throw UnsupportedOperationException("Collection sync not implemented yet")
|
||||
|
||||
/* TODO
|
||||
try {
|
||||
davCollection.reportChanges(
|
||||
state?.takeIf { state.type == SyncState.Type.SYNC_TOKEN }?.value,
|
||||
false, COLLECTION_SYNC_PAGE_SIZE,
|
||||
GetETag.NAME)
|
||||
} catch(e: HttpException) {
|
||||
if (e.status in arrayOf(500,507))
|
||||
// some servers don't like the limit, try again without
|
||||
davCollection.reportChanges(
|
||||
state?.takeIf { state.type == SyncState.Type.SYNC_TOKEN }?.value,
|
||||
false, null,
|
||||
GetETag.NAME, GetContentType.NAME)
|
||||
}
|
||||
|
||||
var syncToken: String? = null
|
||||
davCollection.properties[SyncToken::class.java]?.let {
|
||||
syncToken = it.token
|
||||
}
|
||||
|
||||
val changes = RemoteChanges(syncToken?.let { SyncState(SyncState.Type.SYNC_TOKEN, it) }, davCollection.furtherResults)
|
||||
for (member in davCollection.members)
|
||||
changes.updated += member
|
||||
|
||||
for (member in davCollection.removedMembers)
|
||||
changes.deleted += member.fileName()
|
||||
|
||||
Logger.log.log(Level.INFO, "Received list of changed/removed resources", changes)
|
||||
return changes
|
||||
*/
|
||||
}
|
||||
|
||||
override fun deleteNotPresentRemotely() {
|
||||
val removed = localCollection.removeNotDirtyMarked(0)
|
||||
Logger.log.info("Removed $removed local resources which are not present on the server anymore")
|
||||
}
|
||||
|
||||
override fun postProcess() {
|
||||
}
|
||||
|
||||
|
||||
protected fun<T: LocalResource<*>?, R> useLocal(local: T, body: (T) -> R): R {
|
||||
local?.let { currentLocalResource.push(it) }
|
||||
val result = body(local)
|
||||
local?.let { currentLocalResource.pop() }
|
||||
return result
|
||||
}
|
||||
|
||||
protected fun<T: DavResource, R> useRemote(remote: T, body: (T) -> R): R {
|
||||
currentRemoteResource.push(remote)
|
||||
val result = body(remote)
|
||||
currentRemoteResource.pop()
|
||||
return result
|
||||
}
|
||||
|
||||
protected fun<R> useRemoteCollection(body: (RemoteType) -> R) =
|
||||
useRemote(davCollection, body)
|
||||
|
||||
}
|
||||
@@ -9,18 +9,17 @@
|
||||
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.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.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalCalendar
|
||||
import at.bitfire.davdroid.resource.LocalEvent
|
||||
@@ -30,7 +29,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 +36,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 +46,47 @@ 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}") {
|
||||
localCalendar: LocalCalendar
|
||||
): BaseDavSyncManager<LocalEvent, LocalCalendar, DavCalendar>(context, settings, account, accountSettings, extras, authority, syncResult, localCalendar) {
|
||||
|
||||
companion object {
|
||||
private val MAX_MULTIGET = 20
|
||||
const val MULTIGET_MAX_RESOURCES = 30
|
||||
}
|
||||
|
||||
init {
|
||||
localCollection = localCalendar
|
||||
}
|
||||
|
||||
override fun notificationId() = Constants.NOTIFICATION_CALENDAR_SYNC
|
||||
|
||||
override fun getSyncErrorTitle() = context.getString(R.string.sync_error_calendar, account.name)!!
|
||||
|
||||
|
||||
override fun prepare(): Boolean {
|
||||
collectionURL = HttpUrl.parse(localCalendar.name ?: return false) ?: return false
|
||||
if (!super.prepare())
|
||||
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)
|
||||
useRemoteCollection { it.propfind(0, GetCTag.NAME, SyncToken.NAME) }
|
||||
}
|
||||
|
||||
override fun prepareDirty() {
|
||||
super.prepareDirty()
|
||||
localCalendar.processDirtyExceptions()
|
||||
}
|
||||
override fun syncAlgorithm() = SyncAlgorithm.PROPFIND_REPORT
|
||||
|
||||
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)
|
||||
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)
|
||||
val os = ByteArrayOutputStream()
|
||||
event.write(os)
|
||||
|
||||
return RequestBody.create(
|
||||
DavCalendar.MIME_ICALENDAR,
|
||||
os.toByteArray()
|
||||
)
|
||||
} else
|
||||
throw IllegalArgumentException("resource must be a LocalEvent")
|
||||
}
|
||||
RequestBody.create(
|
||||
DavCalendar.MIME_ICALENDAR_UTF8,
|
||||
os.toByteArray()
|
||||
)
|
||||
})
|
||||
|
||||
override fun listRemote() {
|
||||
override fun listAllRemote(): Map<String, DavResource> {
|
||||
// calculate time range limits
|
||||
var limitStart: Date? = null
|
||||
accountSettings.getTimeRangePastDays()?.let { pastDays ->
|
||||
@@ -105,75 +95,71 @@ 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)
|
||||
return useRemoteCollection { remote ->
|
||||
// fetch list of remote VEVENTs and build hash table to index file name
|
||||
Logger.log.info("Querying events since $limitStart")
|
||||
remote.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
|
||||
val result = LinkedHashMap<String, DavResource>(remote.members.size)
|
||||
for (iCal in remote.members) {
|
||||
val fileName = iCal.fileName()
|
||||
Logger.log.fine("Found remote VEVENT: $fileName")
|
||||
result[fileName] = iCal
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
currentDavResource = null
|
||||
}
|
||||
|
||||
override fun downloadRemote() {
|
||||
Logger.log.info("Downloading ${toDownload.size} events ($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 ->
|
||||
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))
|
||||
}
|
||||
override fun processRemoteChanges(changes: RemoteChanges) {
|
||||
for (name in changes.deleted)
|
||||
localCollection.findByName(name)?.let {
|
||||
Logger.log.info("Deleting local event $name")
|
||||
useLocal(it, { local -> local.delete() })
|
||||
syncResult.stats.numDeletes++
|
||||
}
|
||||
|
||||
currentDavResource = null
|
||||
val toDownload = changes.updated.map { it.location }
|
||||
Logger.log.info("Downloading ${toDownload.size} resources ($MULTIGET_MAX_RESOURCES at once)")
|
||||
|
||||
for (bunch in toDownload.chunked(MULTIGET_MAX_RESOURCES)) {
|
||||
if (bunch.size == 1)
|
||||
// only one contact, use GET
|
||||
useRemote(DavResource(httpClient.okHttpClient, bunch.first()), { remote ->
|
||||
val body = remote.get(DavCalendar.MIME_ICALENDAR.toString())
|
||||
|
||||
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
|
||||
val eTag = remote.properties[GetETag::class.java]?.eTag
|
||||
?: throw DavException("Received CalDAV GET response without ETag for ${remote.location}")
|
||||
|
||||
body.charStream().use { reader ->
|
||||
processVEvent(remote.fileName(), eTag, reader)
|
||||
}
|
||||
})
|
||||
else {
|
||||
// multiple contacts, use multi-get
|
||||
useRemoteCollection { it.multiget(bunch) }
|
||||
|
||||
// process multiget results
|
||||
for (remote in davCollection.members)
|
||||
useRemote(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")
|
||||
|
||||
processVEvent(remote.fileName(), eTag, StringReader(iCalendar))
|
||||
})
|
||||
}
|
||||
|
||||
abortIfCancelled()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
private fun davCalendar() = davCollection as DavCalendar
|
||||
|
||||
private fun processVEvent(fileName: String, eTag: String, reader: Reader) {
|
||||
val events: List<Event>
|
||||
try {
|
||||
@@ -187,24 +173,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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -51,7 +51,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -12,29 +12,26 @@ 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.exception.DavException
|
||||
import at.bitfire.dav4android.property.*
|
||||
import at.bitfire.davdroid.*
|
||||
import at.bitfire.davdroid.AccountSettings
|
||||
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.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 +42,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 +80,93 @@ class ContactsSyncManager(
|
||||
authority: String,
|
||||
syncResult: SyncResult,
|
||||
val provider: ContentProviderClient,
|
||||
private val localAddressBook: LocalAddressBook
|
||||
): SyncManager(context, settings, account, accountSettings, extras, authority, syncResult, "addressBook") {
|
||||
localAddressBook: LocalAddressBook
|
||||
): BaseDavSyncManager<LocalAddress, LocalAddressBook, DavAddressBook>(context, settings, account, accountSettings, extras, authority, syncResult, localAddressBook) {
|
||||
|
||||
companion object {
|
||||
private val MAX_MULTIGET = 10
|
||||
private const val MULTIGET_MAX_RESOURCES = 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)!!
|
||||
|
||||
|
||||
override fun prepare(): Boolean {
|
||||
if (!super.prepare())
|
||||
return false
|
||||
|
||||
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)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun queryCapabilities() {
|
||||
// prepare remote address book
|
||||
davCollection.propfind(0, SupportedAddressData.NAME, GetCTag.NAME)
|
||||
useRemoteCollection { dav ->
|
||||
dav.propfind(0, SupportedAddressData.NAME, GetCTag.NAME, SyncToken.NAME)
|
||||
|
||||
val properties = davCollection.properties
|
||||
properties[SupportedAddressData::class.java]?.let {
|
||||
hasVCard4 = it.hasVCard4()
|
||||
val properties = dav.properties
|
||||
properties[SupportedAddressData::class.java]?.let {
|
||||
hasVCard4 = it.hasVCard4()
|
||||
}
|
||||
Logger.log.info("Server advertises VCard/4 support: $hasVCard4")
|
||||
|
||||
Logger.log.info("Contact group method: $groupMethod")
|
||||
// in case of GROUP_VCARDs, treat groups as contacts in the local address book
|
||||
localCollection.includeGroups = groupMethod == GroupMethod.GROUP_VCARDS
|
||||
}
|
||||
Logger.log.info("Server advertises VCard/4 support: $hasVCard4")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
override fun processLocallyDeleted() {
|
||||
override fun syncAlgorithm() = 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 +178,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 +206,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 +216,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,47 +243,40 @@ 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
|
||||
})
|
||||
|
||||
override fun listAllRemote() = useRemoteCollection { dav ->
|
||||
// fetch list of remote VCards and build hash table to index file name
|
||||
addressBook.propfind(1, ResourceType.NAME, GetETag.NAME)
|
||||
dav.propfind(1, ResourceType.NAME, GetETag.NAME)
|
||||
|
||||
remoteResources = HashMap(davCollection.members.size)
|
||||
for (vCard in davCollection.members) {
|
||||
val result = LinkedHashMap<String, DavResource>(dav.members.size)
|
||||
for (vCard in dav.members) {
|
||||
// ignore member collections
|
||||
var ignore = false
|
||||
vCard.properties[ResourceType::class.java]?.let { type ->
|
||||
@@ -297,61 +288,60 @@ class ContactsSyncManager(
|
||||
|
||||
val fileName = vCard.fileName()
|
||||
Logger.log.fine("Found remote VCard: $fileName")
|
||||
remoteResources[fileName] = vCard
|
||||
result[fileName] = vCard
|
||||
}
|
||||
|
||||
currentDavResource = null
|
||||
result
|
||||
}
|
||||
|
||||
override fun downloadRemote() {
|
||||
Logger.log.info("Downloading ${toDownload.size} contacts ($MAX_MULTIGET at once)")
|
||||
override fun processRemoteChanges(changes: RemoteChanges) {
|
||||
for (name in changes.deleted)
|
||||
localCollection.findByName(name)?.let {
|
||||
Logger.log.info("Deleting local address $name")
|
||||
useLocal(it, { it.delete() })
|
||||
syncResult.stats.numDeletes++
|
||||
}
|
||||
|
||||
val toDownload = changes.updated.map { it.location }
|
||||
Logger.log.info("Downloading ${toDownload.size} resources ($MULTIGET_MAX_RESOURCES 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) {
|
||||
for (bunch in toDownload.chunked(CalendarSyncManager.MULTIGET_MAX_RESOURCES)) {
|
||||
if (bunch.size == 1)
|
||||
// only one contact, use GET
|
||||
val remote = bunch.first()
|
||||
currentDavResource = remote
|
||||
useRemote(DavResource(httpClient.okHttpClient, bunch.first()), { remote ->
|
||||
val body = remote.get("text/vcard;version=4.0, text/vcard;charset=utf-8;q=0.8, text/vcard;q=0.5")
|
||||
|
||||
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]?.eTag
|
||||
?: throw DavException("Received CardDAV GET response without ETag for ${remote.location}")
|
||||
|
||||
// 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, reader, downloader)
|
||||
}
|
||||
})
|
||||
|
||||
body.charStream().use { reader ->
|
||||
processVCard(remote.fileName(), eTag.eTag!!, reader, downloader)
|
||||
}
|
||||
|
||||
} else {
|
||||
else {
|
||||
// multiple contacts, use multi-get
|
||||
val addressBook = davAddressBook()
|
||||
currentDavResource = addressBook
|
||||
addressBook.multiget(bunch.map { it.location }, hasVCard4)
|
||||
useRemoteCollection { it.multiget(bunch, hasVCard4) }
|
||||
|
||||
// process multi-get results
|
||||
for (remote in davCollection.members) {
|
||||
currentDavResource = remote
|
||||
for (remote in davCollection.members)
|
||||
useRemote(remote, {
|
||||
val eTag = remote.properties[GetETag::class.java]?.eTag
|
||||
?: throw DavException("Received multi-get response without ETag")
|
||||
|
||||
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")
|
||||
|
||||
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)
|
||||
}
|
||||
processVCard(remote.fileName(), eTag, StringReader(vCard), downloader)
|
||||
})
|
||||
}
|
||||
|
||||
currentDavResource = null
|
||||
abortIfCancelled()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,20 +351,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 +380,67 @@ 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.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.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)
|
||||
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
||||
local.updateHashCode(null)
|
||||
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)
|
||||
|
||||
currentLocalResource = null
|
||||
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 as? LocalContact)?.updateHashCode(null)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,23 +8,27 @@
|
||||
|
||||
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.util.*
|
||||
import java.util.logging.Level
|
||||
@@ -32,10 +36,10 @@ import java.util.logging.Level
|
||||
abstract class SyncAdapterService: Service() {
|
||||
|
||||
companion object {
|
||||
val runningSyncs = Collections.synchronizedSet(mutableSetOf<Pair<String, Account>>())!!
|
||||
val runningSyncs: MutableSet<Pair<String, Account>> = Collections.synchronizedSet(mutableSetOf<Pair<String, Account>>())
|
||||
}
|
||||
|
||||
abstract protected fun syncAdapter(): AbstractThreadedSyncAdapter
|
||||
protected abstract fun syncAdapter(): AbstractThreadedSyncAdapter
|
||||
|
||||
override fun onBind(intent: Intent?) = syncAdapter().syncAdapterBinder!!
|
||||
|
||||
@@ -81,8 +85,10 @@ 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) }
|
||||
}
|
||||
@@ -96,19 +102,11 @@ abstract class SyncAdapterService: Service() {
|
||||
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 +119,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 +141,18 @@ abstract class SyncAdapterService: Service() {
|
||||
return true
|
||||
}
|
||||
|
||||
private 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,34 +5,43 @@
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.app.PendingIntent
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SyncResult
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.RemoteException
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.ContactsContract
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.app.NotificationManagerCompat
|
||||
import at.bitfire.dav4android.DavResource
|
||||
import at.bitfire.dav4android.exception.*
|
||||
import at.bitfire.dav4android.property.GetCTag
|
||||
import at.bitfire.dav4android.property.GetETag
|
||||
import at.bitfire.davdroid.*
|
||||
import at.bitfire.dav4android.exception.DavException
|
||||
import at.bitfire.dav4android.exception.HttpException
|
||||
import at.bitfire.dav4android.exception.ServiceUnavailableException
|
||||
import at.bitfire.dav4android.exception.UnauthorizedException
|
||||
import at.bitfire.davdroid.AccountSettings
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.DavService
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalCollection
|
||||
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.AccountSettingsActivity
|
||||
import at.bitfire.davdroid.ui.DebugInfoActivity
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.ical4android.MiscUtils
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.RequestBody
|
||||
import java.io.Closeable
|
||||
import org.dmfs.tasks.contract.TaskContract
|
||||
import java.io.IOException
|
||||
import java.io.InterruptedIOException
|
||||
import java.security.cert.CertificateException
|
||||
@@ -40,7 +49,7 @@ import java.util.*
|
||||
import java.util.logging.Level
|
||||
import javax.net.ssl.SSLHandshakeException
|
||||
|
||||
abstract class SyncManager(
|
||||
abstract class SyncManager<out ResourceType: LocalResource<*>, out CollectionType: LocalCollection<ResourceType>>(
|
||||
val context: Context,
|
||||
val settings: ISettings,
|
||||
val account: Account,
|
||||
@@ -48,67 +57,36 @@ abstract class SyncManager(
|
||||
val extras: Bundle,
|
||||
val authority: String,
|
||||
val syncResult: SyncResult,
|
||||
val uniqueCollectionId: String
|
||||
): Closeable {
|
||||
val localCollection: CollectionType
|
||||
): AutoCloseable {
|
||||
|
||||
companion object {
|
||||
|
||||
val SYNC_PHASE_PREPARE = 0
|
||||
val SYNC_PHASE_QUERY_CAPABILITIES = 1
|
||||
val SYNC_PHASE_PROCESS_LOCALLY_DELETED = 2
|
||||
val SYNC_PHASE_PREPARE_DIRTY = 3
|
||||
val SYNC_PHASE_UPLOAD_DIRTY = 4
|
||||
val SYNC_PHASE_CHECK_SYNC_STATE = 5
|
||||
val SYNC_PHASE_LIST_LOCAL = 6
|
||||
val SYNC_PHASE_LIST_REMOTE = 7
|
||||
val SYNC_PHASE_COMPARE_LOCAL_REMOTE = 8
|
||||
val SYNC_PHASE_DOWNLOAD_REMOTE = 9
|
||||
val SYNC_PHASE_POST_PROCESSING = 10
|
||||
val SYNC_PHASE_SAVE_SYNC_STATE = 11
|
||||
fun cancelNotifications(manager: NotificationManagerCompat, authority: String, account: Account) =
|
||||
manager.cancel(notificationTag(authority, account), NotificationUtils.NOTIFY_SYNC_ERROR)
|
||||
|
||||
infix fun <T> Set<T>.disjunct(other: Set<T>) = (this - other) union (other - this)
|
||||
private fun notificationTag(authority: String, account: Account) =
|
||||
"$authority-${account.name}".hashCode().toString()
|
||||
|
||||
}
|
||||
|
||||
protected val notificationManager = NotificationUtils.createChannels(context)
|
||||
private val mainAccount = if (localCollection is LocalAddressBook)
|
||||
localCollection.mainAccount
|
||||
else
|
||||
account
|
||||
|
||||
protected lateinit var localCollection: LocalCollection<*>
|
||||
protected val notificationManager = NotificationManagerCompat.from(context)
|
||||
protected val notificationTag = Companion.notificationTag(authority, mainAccount)
|
||||
|
||||
protected val httpClient = HttpClient.Builder(context, settings, accountSettings).build()
|
||||
protected lateinit var collectionURL: HttpUrl
|
||||
protected lateinit var davCollection: DavResource
|
||||
/** Local resource we're currently operating on. Used for error notifications. **/
|
||||
protected val currentLocalResource = LinkedList<LocalResource<*>>()
|
||||
/** Remote resource we're currently operating on. Used for error notifications. **/
|
||||
protected val currentRemoteResource = LinkedList<DavResource>()
|
||||
|
||||
|
||||
/** current sync phase */
|
||||
private var syncPhase: Int = SYNC_PHASE_PREPARE
|
||||
|
||||
/** state information for debug info (local resource) */
|
||||
protected var currentLocalResource: LocalResource? = null
|
||||
|
||||
/** state information for debug info (remote resource) */
|
||||
protected var currentDavResource: DavResource? = null
|
||||
|
||||
|
||||
/** remote CTag at the time of {@link #listRemote()} */
|
||||
protected var remoteCTag: String? = null
|
||||
|
||||
/** sync-able resources in the local collection, as enumerated by {@link #listLocal()} */
|
||||
protected lateinit var localResources: MutableMap<String, LocalResource>
|
||||
|
||||
/** sync-able resources in the remote collection, as enumerated by {@link #listRemote()} */
|
||||
protected lateinit var remoteResources: MutableMap<String, DavResource>
|
||||
|
||||
/** resources which have changed on the server, as determined by {@link #compareLocalRemote()} */
|
||||
protected val toDownload = mutableSetOf<DavResource>()
|
||||
|
||||
|
||||
protected abstract fun notificationId(): Int
|
||||
protected abstract fun getSyncErrorTitle(): String
|
||||
|
||||
@Suppress("UNUSED_VALUE")
|
||||
fun performSync() {
|
||||
// dismiss previous error notifications
|
||||
notificationManager.cancel(uniqueCollectionId, notificationId())
|
||||
notificationManager.cancel(notificationTag, NotificationUtils.NOTIFY_SYNC_ERROR)
|
||||
|
||||
try {
|
||||
Logger.log.info("Preparing synchronization")
|
||||
@@ -116,143 +94,209 @@ abstract class SyncManager(
|
||||
Logger.log.info("No reason to synchronize, aborting")
|
||||
return
|
||||
}
|
||||
|
||||
abortIfCancelled()
|
||||
syncPhase = SYNC_PHASE_QUERY_CAPABILITIES
|
||||
Logger.log.info("Querying capabilities")
|
||||
|
||||
Logger.log.info("Querying server capabilities")
|
||||
queryCapabilities()
|
||||
|
||||
syncPhase = SYNC_PHASE_PROCESS_LOCALLY_DELETED
|
||||
Logger.log.info("Processing locally deleted entries")
|
||||
processLocallyDeleted()
|
||||
|
||||
abortIfCancelled()
|
||||
syncPhase = SYNC_PHASE_PREPARE_DIRTY
|
||||
Logger.log.info("Locally preparing dirty entries")
|
||||
prepareDirty()
|
||||
|
||||
syncPhase = SYNC_PHASE_UPLOAD_DIRTY
|
||||
Logger.log.info("Uploading dirty entries")
|
||||
uploadDirty()
|
||||
Logger.log.info("Sending local deletes/updates to server")
|
||||
val modificationsSent =
|
||||
processLocallyDeleted() ||
|
||||
uploadDirty()
|
||||
abortIfCancelled()
|
||||
|
||||
syncPhase = SYNC_PHASE_CHECK_SYNC_STATE
|
||||
Logger.log.info("Checking sync state")
|
||||
if (checkSyncState()) {
|
||||
syncPhase = SYNC_PHASE_LIST_LOCAL
|
||||
Logger.log.info("Listing local resources")
|
||||
listLocal()
|
||||
if (modificationsSent || syncRequired())
|
||||
when (syncAlgorithm()) {
|
||||
SyncAlgorithm.PROPFIND_REPORT -> {
|
||||
Logger.log.info("Sync algorithm: full listing as one result (PROPFIND/REPORT)")
|
||||
resetPresentRemotely()
|
||||
|
||||
abortIfCancelled()
|
||||
syncPhase = SYNC_PHASE_LIST_REMOTE
|
||||
Logger.log.info("Listing remote resources")
|
||||
listRemote()
|
||||
// get current sync state
|
||||
val syncState = syncState(modificationsSent)
|
||||
|
||||
abortIfCancelled()
|
||||
syncPhase = SYNC_PHASE_COMPARE_LOCAL_REMOTE
|
||||
Logger.log.info("Comparing local/remote entries")
|
||||
compareLocalRemote()
|
||||
// list all entries at now current sync state (which may be the same as or newer than lastSyncState)
|
||||
Logger.log.info("Listing remote entries")
|
||||
val remote = listAllRemote()
|
||||
abortIfCancelled()
|
||||
|
||||
syncPhase = SYNC_PHASE_DOWNLOAD_REMOTE
|
||||
Logger.log.info("Downloading remote entries")
|
||||
downloadRemote()
|
||||
Logger.log.info("Comparing local/remote entries")
|
||||
val changes = compareLocalRemote(syncState, remote)
|
||||
|
||||
syncPhase = SYNC_PHASE_POST_PROCESSING
|
||||
Logger.log.info("Post-processing")
|
||||
postProcess()
|
||||
Logger.log.info("Processing remote changes")
|
||||
processRemoteChanges(changes)
|
||||
|
||||
syncPhase = SYNC_PHASE_SAVE_SYNC_STATE
|
||||
Logger.log.info("Saving sync state")
|
||||
saveSyncState()
|
||||
} else
|
||||
Logger.log.info("Remote collection didn't change, skipping remote sync")
|
||||
Logger.log.info("Deleting entries which are not present remotely anymore")
|
||||
deleteNotPresentRemotely()
|
||||
|
||||
} catch (e: InterruptedException) {
|
||||
// re-throw to SyncAdapterService
|
||||
throw e
|
||||
} catch (e: InterruptedIOException) {
|
||||
throw e
|
||||
Logger.log.info("Post-processing")
|
||||
postProcess()
|
||||
|
||||
} catch (e: SSLHandshakeException) {
|
||||
Logger.log.info("Saving sync state")
|
||||
localCollection.lastSyncState = changes.state
|
||||
}
|
||||
SyncAlgorithm.COLLECTION_SYNC -> {
|
||||
throw UnsupportedOperationException("Collection sync not supported yet")
|
||||
|
||||
/*val lastSyncState = localCollection.getLastSyncState()?.takeIf { it.type == SyncState.Type.SYNC_TOKEN }
|
||||
val initialSync = lastSyncState == null
|
||||
if (initialSync)
|
||||
resetPresentRemotely()
|
||||
|
||||
var changes = listRemoteChanges(lastSyncState)
|
||||
do {
|
||||
processRemoteChanges(changes)
|
||||
localCollection.setLastSyncState(changes.state)
|
||||
|
||||
changes = listRemoteChanges(changes.state)
|
||||
} while(changes.furtherChanges)
|
||||
|
||||
if (initialSync)
|
||||
deleteNotPresentRemotely()
|
||||
|
||||
postProcess()*/
|
||||
}
|
||||
}
|
||||
else
|
||||
Logger.log.info("Remote collection didn't change, no reason to sync")
|
||||
|
||||
}
|
||||
// sync was cancelled: re-throw to SyncAdapterService
|
||||
catch (e: InterruptedException) { throw e }
|
||||
|
||||
// specific I/O errors
|
||||
catch (e: SSLHandshakeException) {
|
||||
Logger.log.log(Level.WARNING, "SSL handshake failed", e)
|
||||
|
||||
// when a certificate is rejected by cert4android, the cause will be a CertificateException
|
||||
if (!BuildConfig.customCerts || e.cause !is CertificateException)
|
||||
notifyException(e)
|
||||
} catch (e: IOException) {
|
||||
Logger.log.log(Level.WARNING, "I/O exception during sync, trying again later", e)
|
||||
syncResult.stats.numIoExceptions++
|
||||
} catch (e: ServiceUnavailableException) {
|
||||
}
|
||||
|
||||
// specific HTTP errors
|
||||
catch (e: ServiceUnavailableException) {
|
||||
Logger.log.log(Level.WARNING, "Got 503 Service unavailable, trying again later", e)
|
||||
syncResult.stats.numIoExceptions++
|
||||
e.retryAfter?.let { retryAfter ->
|
||||
// how many seconds to wait? getTime() returns ms, so divide by 1000
|
||||
syncResult.delayUntil = (retryAfter.time - Date().time) / 1000
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
notifyException(e)
|
||||
}
|
||||
|
||||
// all others
|
||||
catch (e: Throwable) { notifyException(e) }
|
||||
}
|
||||
|
||||
private fun notifyException(e: Throwable) {
|
||||
val messageString: Int
|
||||
|
||||
when (e) {
|
||||
is UnauthorizedException -> {
|
||||
Logger.log.log(Level.SEVERE, "Not authorized anymore", e)
|
||||
messageString = R.string.sync_error_unauthorized
|
||||
syncResult.stats.numAuthExceptions++
|
||||
}
|
||||
is HttpException, is DavException -> {
|
||||
Logger.log.log(Level.SEVERE, "HTTP/DAV Exception during sync", e)
|
||||
messageString = R.string.sync_error_http_dav
|
||||
syncResult.stats.numParseExceptions++
|
||||
}
|
||||
is CalendarStorageException, is ContactsStorageException -> {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't access local storage", e)
|
||||
messageString = R.string.sync_error_local_storage
|
||||
syncResult.databaseError = true
|
||||
}
|
||||
else -> {
|
||||
Logger.log.log(Level.SEVERE, "Unknown sync error", e)
|
||||
messageString = R.string.sync_error
|
||||
syncResult.stats.numParseExceptions++
|
||||
}
|
||||
}
|
||||
protected abstract fun prepare(): Boolean
|
||||
|
||||
val detailsIntent: Intent
|
||||
if (e is UnauthorizedException) {
|
||||
detailsIntent = Intent(context, AccountSettingsActivity::class.java)
|
||||
detailsIntent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account)
|
||||
} else {
|
||||
detailsIntent = Intent(context, DebugInfoActivity::class.java)
|
||||
detailsIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e)
|
||||
detailsIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account)
|
||||
detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority)
|
||||
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase)
|
||||
currentLocalResource?.let { detailsIntent.putExtra(DebugInfoActivity.KEY_LOCAL_RESOURCE, it.toString()) }
|
||||
currentDavResource?.let { detailsIntent.putExtra(DebugInfoActivity.KEY_REMOTE_RESOURCE, it.toString()) }
|
||||
}
|
||||
/**
|
||||
* Queries the server for synchronization capabilities like specific report types,
|
||||
* data formats etc.
|
||||
*/
|
||||
protected abstract fun queryCapabilities()
|
||||
|
||||
// to make the PendingIntent unique
|
||||
detailsIntent.data = Uri.parse("uri://${javaClass.name}/$uniqueCollectionId")
|
||||
protected abstract fun processLocallyDeleted(): Boolean
|
||||
protected abstract fun uploadDirty(): Boolean
|
||||
|
||||
val builder = NotificationCompat.Builder(context, NotificationUtils.CHANNEL_SYNC_PROBLEMS)
|
||||
builder .setSmallIcon(R.drawable.ic_sync_error_notification)
|
||||
.setLargeIcon(App.getLauncherBitmap(context))
|
||||
.setContentTitle(getSyncErrorTitle())
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT))
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
/**
|
||||
* Determines whether a sync is required because there were changes on the server.
|
||||
* For instance, this method can check the collection's CTag/sync-token.
|
||||
*
|
||||
* When local changes have been uploaded ([processLocallyDeleted] and/or
|
||||
* [uploadDirty] were true), a sync is always required and this method
|
||||
* will not be evaluated.
|
||||
*
|
||||
* @return whether data has been changed on the server = whether running the
|
||||
* sync algorithm is required
|
||||
*/
|
||||
protected abstract fun syncRequired(): Boolean
|
||||
|
||||
try {
|
||||
val phases = context.resources.getStringArray(R.array.sync_error_phases)
|
||||
val message = context.getString(messageString, phases[syncPhase])
|
||||
builder.setContentText(message)
|
||||
} catch (ex: IndexOutOfBoundsException) {
|
||||
// should never happen
|
||||
}
|
||||
/**
|
||||
* Determines which sync algorithm to use.
|
||||
* @return
|
||||
* - [SyncAlgorithm.PROPFIND_REPORT]: list all resources (with plain WebDAV
|
||||
* PROPFIND or specific REPORT requests), then compare and synchronize
|
||||
* - [SyncAlgorithm.COLLECTION_SYNC]: use incremental collection synchronization (RFC 6578)
|
||||
*/
|
||||
protected abstract fun syncAlgorithm(): SyncAlgorithm
|
||||
|
||||
/**
|
||||
* Returns the current sync state of the remote resource. Keep in mind that
|
||||
* WebDAV operations are atomic and the sync state might already be obsolete when used.
|
||||
*
|
||||
* @param forceRefresh false: result may be taken from a previous request, for instance
|
||||
* from the [prepare] phase; true: sends a request to determine the current sync state
|
||||
*/
|
||||
protected abstract fun syncState(forceRefresh: Boolean): SyncState?
|
||||
|
||||
/**
|
||||
* Marks all local resources which shall be taken into consideration for this
|
||||
* sync as "synchronizing". Purpose of marking is that resources which have been marked
|
||||
* and are not present remotely anymore can be deleted.
|
||||
*
|
||||
* Used together with [deleteNotPresentRemotely].
|
||||
*/
|
||||
protected abstract fun resetPresentRemotely()
|
||||
|
||||
/**
|
||||
* Lists all remote resources which should be taken into account for synchronization.
|
||||
* Will be used if incremental synchronization is not available.
|
||||
* @return Map with resource names (like "mycontact.vcf") as keys and the resources
|
||||
*/
|
||||
protected abstract fun listAllRemote(): Map<String, DavResource>
|
||||
|
||||
|
||||
/**
|
||||
* Compares local resources which are marked for synchronization and remote resources by file name and ETag.
|
||||
* Remote resources
|
||||
* + which are not present locally
|
||||
* + whose ETag has changed since the last sync (i.e. remote ETag != locally known last remote ETag)
|
||||
* will be saved as "updated" in the result.
|
||||
*
|
||||
* Must mark all found remote resources as "present remotely", so that a later execution of
|
||||
* [deleteNotPresentRemotely] doesn't (locally) delete any currently available remote resources.
|
||||
*
|
||||
* @param remoteResources Map of remote resource names and resources
|
||||
* @return List of updated resources on the server. The "deleted" list remains empty. The sync
|
||||
* state is taken from [syncState].
|
||||
*/
|
||||
protected abstract fun compareLocalRemote(syncState: SyncState?, remoteResources: Map<String, DavResource>): RemoteChanges
|
||||
|
||||
/**
|
||||
* Lists remote changes (incremental sync).
|
||||
*
|
||||
* Must mark all found remote resources as "present remotely", so that a later execution of
|
||||
* [deleteNotPresentRemotely] doesn't (locally) delete any currently available remote resources.
|
||||
*
|
||||
* @return List of of remote changes together with the sync state after those changes
|
||||
*/
|
||||
protected abstract fun listRemoteChanges(state: SyncState?): RemoteChanges
|
||||
|
||||
/**
|
||||
* Processes remote changes:
|
||||
* + downloads and locally saves remotely updated resources
|
||||
* + locally deletes remotely deleted resources
|
||||
* Should call [abortIfCancelled] from time to time, for instance
|
||||
* after downloading a resource.
|
||||
* @param changes List of remotely updated and deleted resources
|
||||
*/
|
||||
protected abstract fun processRemoteChanges(changes: RemoteChanges)
|
||||
|
||||
/**
|
||||
* Locally deletes entries which are
|
||||
* 1. not dirty and
|
||||
* 2. not marked as [LocalResource.FLAG_REMOTELY_PRESENT].
|
||||
*
|
||||
* Used together with [resetPresentRemotely] when a full listing has been received from
|
||||
* the server to locally delete resources which are not present remotely (anymore).
|
||||
*/
|
||||
protected abstract fun deleteNotPresentRemotely()
|
||||
|
||||
/**
|
||||
* Post-processing of synchronized entries, for instance contact group membership operations.
|
||||
*/
|
||||
protected abstract fun postProcess()
|
||||
|
||||
notificationManager.notify(uniqueCollectionId, notificationId(), builder.build())
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an [InterruptedException] if the current thread has been interrupted,
|
||||
@@ -264,231 +308,158 @@ abstract class SyncManager(
|
||||
throw InterruptedException("Sync was cancelled")
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
httpClient.close()
|
||||
}
|
||||
private fun notifyException(e: Throwable) {
|
||||
val message: String
|
||||
|
||||
|
||||
/** Prepares synchronization (for instance, allocates necessary resources).
|
||||
* @return whether actual synchronization is required / can be made. true = synchronization
|
||||
* shall be continued, false = synchronization can be skipped */
|
||||
abstract protected fun prepare(): Boolean
|
||||
|
||||
abstract protected fun queryCapabilities()
|
||||
|
||||
/**
|
||||
* Process locally deleted entries (DELETE them on the server as well).
|
||||
* Checks Thread.interrupted() before each request to allow quick sync cancellation.
|
||||
*/
|
||||
protected open fun processLocallyDeleted() {
|
||||
// Remove locally deleted entries from server (if they have a name, i.e. if they were uploaded before),
|
||||
// but only if they don't have changed on the server. Then finally remove them from the local address book.
|
||||
val localList = localCollection.getDeleted()
|
||||
for (local in localList) {
|
||||
if (Thread.interrupted())
|
||||
return
|
||||
|
||||
currentLocalResource = local
|
||||
|
||||
val fileName = local.fileName
|
||||
if (fileName != null) {
|
||||
Logger.log.info("$fileName has been deleted locally -> deleting from server")
|
||||
|
||||
val remote = DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build())
|
||||
currentDavResource = remote
|
||||
try {
|
||||
remote.delete(local.eTag)
|
||||
} catch (e: HttpException) {
|
||||
Logger.log.warning("Couldn't delete $fileName from server; ignoring (may be downloaded again)")
|
||||
}
|
||||
} else
|
||||
Logger.log.info("Removing local record #${local.id} which has been deleted locally and was never uploaded")
|
||||
local.delete()
|
||||
syncResult.stats.numDeletes++
|
||||
|
||||
currentLocalResource = null
|
||||
currentDavResource = null
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun prepareDirty() {
|
||||
// assign file names and UIDs to new contacts so that we can use the file name as an index
|
||||
Logger.log.info("Looking for contacts/groups without file name")
|
||||
for (local in localCollection.getWithoutFileName()) {
|
||||
currentLocalResource = local
|
||||
|
||||
Logger.log.fine("Found local record #${local.id} without file name; generating file name/UID if necessary")
|
||||
local.prepareForUpload()
|
||||
|
||||
currentLocalResource = null
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected fun prepareUpload(resource: LocalResource): RequestBody
|
||||
|
||||
/**
|
||||
* Uploads dirty records to the server, using a PUT request for each record.
|
||||
* Checks Thread.interrupted() before each request to allow quick sync cancellation.
|
||||
*/
|
||||
protected open fun uploadDirty() {
|
||||
// upload dirty contacts
|
||||
for (local in localCollection.getDirty()) {
|
||||
if (Thread.interrupted())
|
||||
return
|
||||
|
||||
currentLocalResource = local
|
||||
val fileName = local.fileName
|
||||
|
||||
val remote = DavResource(httpClient.okHttpClient, collectionURL.newBuilder().addPathSegment(fileName).build())
|
||||
currentDavResource = remote
|
||||
|
||||
// generate entity to upload (VCard, iCal, whatever)
|
||||
val body = prepareUpload(local)
|
||||
|
||||
try {
|
||||
if (local.eTag == null) {
|
||||
Logger.log.info("Uploading new record $fileName")
|
||||
remote.put(body, null, true)
|
||||
} else {
|
||||
Logger.log.info("Uploading locally modified record $fileName")
|
||||
remote.put(body, local.eTag, false)
|
||||
}
|
||||
} catch(e: ConflictException) {
|
||||
// we can't interact with the user to resolve the conflict, so we treat 409 like 412
|
||||
Logger.log.log(Level.INFO, "Edit conflict, ignoring", e)
|
||||
} catch(e: PreconditionFailedException) {
|
||||
Logger.log.log(Level.INFO, "Resource has been modified on the server before upload, ignoring", e)
|
||||
when (e) {
|
||||
is IOException,
|
||||
is InterruptedIOException -> {
|
||||
Logger.log.log(Level.WARNING, "I/O error", e)
|
||||
message = context.getString(R.string.sync_error_io, e.localizedMessage)
|
||||
syncResult.stats.numIoExceptions++
|
||||
}
|
||||
|
||||
val newETag = remote.properties[GetETag::class.java]
|
||||
val eTag: String?
|
||||
if (newETag != null) {
|
||||
eTag = newETag.eTag
|
||||
Logger.log.fine("Received new ETag=$eTag after uploading")
|
||||
} else {
|
||||
Logger.log.fine("Didn't receive new ETag after uploading, setting to null")
|
||||
eTag = null
|
||||
is UnauthorizedException -> {
|
||||
Logger.log.log(Level.SEVERE, "Not authorized anymore", e)
|
||||
message = context.getString(R.string.sync_error_authentication_failed)
|
||||
syncResult.stats.numAuthExceptions++
|
||||
}
|
||||
is HttpException, is DavException -> {
|
||||
Logger.log.log(Level.SEVERE, "HTTP/DAV exception", e)
|
||||
message = context.getString(R.string.sync_error_http_dav, e.localizedMessage)
|
||||
syncResult.stats.numParseExceptions++ // numIoExceptions would indicate a soft error
|
||||
}
|
||||
is CalendarStorageException, is ContactsStorageException, is RemoteException -> {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't access local storage", e)
|
||||
message = context.getString(R.string.sync_error_local_storage)
|
||||
syncResult.databaseError = true
|
||||
}
|
||||
else -> {
|
||||
Logger.log.log(Level.SEVERE, "Unclassified sync error", e)
|
||||
message = e.localizedMessage ?: e::class.java.simpleName
|
||||
syncResult.stats.numParseExceptions++
|
||||
}
|
||||
|
||||
local.clearDirty(eTag)
|
||||
|
||||
currentLocalResource = null
|
||||
currentDavResource = null
|
||||
}
|
||||
|
||||
val contentIntent: Intent
|
||||
var viewItemAction: NotificationCompat.Action? = null
|
||||
if (e is UnauthorizedException) {
|
||||
contentIntent = Intent(context, AccountSettingsActivity::class.java)
|
||||
contentIntent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account)
|
||||
} else {
|
||||
contentIntent = Intent(context, DebugInfoActivity::class.java)
|
||||
contentIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e)
|
||||
contentIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account)
|
||||
contentIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority)
|
||||
|
||||
// use current local/remote resource
|
||||
currentLocalResource.firstOrNull()?.let { local ->
|
||||
// pass local resource info to debug info
|
||||
contentIntent.putExtra(DebugInfoActivity.KEY_LOCAL_RESOURCE, local.toString())
|
||||
|
||||
// generate "view item" action
|
||||
viewItemAction = buildViewItemAction(local)
|
||||
}
|
||||
currentRemoteResource.firstOrNull()?.let { remote ->
|
||||
contentIntent.putExtra(DebugInfoActivity.KEY_REMOTE_RESOURCE, remote.location.toString())
|
||||
}
|
||||
}
|
||||
|
||||
// to make the PendingIntent unique
|
||||
contentIntent.data = Uri.parse("davdroid:exception/${e.hashCode()}")
|
||||
|
||||
val channel: String
|
||||
val priority: Int
|
||||
if (e is IOException || e is InterruptedIOException) {
|
||||
channel = NotificationUtils.CHANNEL_SYNC_IO_ERRORS
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
} else {
|
||||
channel = NotificationUtils.CHANNEL_SYNC_ERRORS
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
}
|
||||
|
||||
val builder = NotificationUtils.newBuilder(context, channel)
|
||||
builder .setSmallIcon(R.drawable.ic_sync_error_notification)
|
||||
.setContentTitle(localCollection.title)
|
||||
.setContentText(message)
|
||||
.setStyle(NotificationCompat.BigTextStyle(builder).bigText(message))
|
||||
.setSubText(mainAccount.name)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setPriority(priority)
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
viewItemAction?.let { builder.addAction(it) }
|
||||
builder.addAction(buildRetryAction())
|
||||
|
||||
notificationManager.notify(notificationTag, NotificationUtils.NOTIFY_SYNC_ERROR, builder.build())
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the current sync state (e.g. CTag) and whether synchronization from remote is required.
|
||||
* @return <ul>
|
||||
* <li><code>true</code> if the remote collection has changed, i.e. synchronization from remote is required</li>
|
||||
* <li><code>false</code> if the remote collection hasn't changed</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected open fun checkSyncState(): Boolean {
|
||||
// check CTag (ignore on manual sync)
|
||||
davCollection.properties[GetCTag::class.java]?.let { remoteCTag = it.cTag }
|
||||
private fun buildRetryAction(): NotificationCompat.Action {
|
||||
val retryIntent = Intent(context, DavService::class.java)
|
||||
retryIntent.action = DavService.ACTION_FORCE_SYNC
|
||||
|
||||
val localCTag = if (extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL)) {
|
||||
Logger.log.info("Manual sync, ignoring CTag")
|
||||
val syncAuthority: String
|
||||
val syncAccount: Account
|
||||
if (authority == ContactsContract.AUTHORITY) {
|
||||
// if this is a contacts sync, retry syncing all address books of the main account
|
||||
syncAuthority = context.getString(R.string.address_books_authority)
|
||||
syncAccount = mainAccount
|
||||
} else {
|
||||
syncAuthority = authority
|
||||
syncAccount = account
|
||||
}
|
||||
|
||||
retryIntent.data = Uri.parse("sync://").buildUpon()
|
||||
.authority(syncAuthority)
|
||||
.appendPath(syncAccount.type)
|
||||
.appendPath(syncAccount.name)
|
||||
.build()
|
||||
|
||||
return NotificationCompat.Action(
|
||||
android.R.drawable.ic_menu_rotate, context.getString(R.string.sync_error_retry),
|
||||
PendingIntent.getService(context, 0, retryIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
}
|
||||
|
||||
private fun buildViewItemAction(local: LocalResource<*>): NotificationCompat.Action? {
|
||||
Logger.log.log(Level.FINE, "Adding view action for local resource", local)
|
||||
val intent = local.id?.let { id ->
|
||||
when (local) {
|
||||
is LocalContact ->
|
||||
Intent(Intent.ACTION_VIEW, ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, id))
|
||||
is LocalEvent ->
|
||||
Intent(Intent.ACTION_VIEW, ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id))
|
||||
is LocalTask ->
|
||||
Intent(Intent.ACTION_VIEW, ContentUris.withAppendedId(TaskContract.Tasks.getContentUri(TaskProvider.ProviderName.OpenTasks.authority), id))
|
||||
else ->
|
||||
null
|
||||
}
|
||||
}
|
||||
return if (intent != null && context.packageManager.resolveActivity(intent, 0) != null)
|
||||
NotificationCompat.Action(android.R.drawable.ic_menu_view, context.getString(R.string.sync_error_view_item),
|
||||
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
else
|
||||
null
|
||||
} else
|
||||
localCollection.getCTag()
|
||||
|
||||
return if (remoteCTag != null && remoteCTag == localCTag) {
|
||||
Logger.log.info("Remote collection didn't change (CTag=$remoteCTag), no need to query children")
|
||||
false
|
||||
} else
|
||||
true
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all local resources which should be taken into account for synchronization into {@link #localResources}.
|
||||
*/
|
||||
protected open fun listLocal() {
|
||||
// fetch list of local contacts and build hash table to index file name
|
||||
val localList = localCollection.getAll()
|
||||
val resources = HashMap<String, LocalResource>(localList.size)
|
||||
for (resource in localList) {
|
||||
Logger.log.fine("Found local resource: ${resource.fileName}")
|
||||
resource.fileName?.let { resources[it] = resource }
|
||||
}
|
||||
localResources = resources
|
||||
Logger.log.info("Found ${localResources.size} local resources")
|
||||
|
||||
enum class SyncPhase(val number: Int) {
|
||||
PREPARE(0),
|
||||
QUERY_CAPABILITIES(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all members of the remote collection which should be taken into account for synchronization into {@link #remoteResources}.
|
||||
*/
|
||||
abstract protected fun listRemote()
|
||||
|
||||
/**
|
||||
* Compares {@link #localResources} and {@link #remoteResources} by file name and ETag:
|
||||
* <ul>
|
||||
* <li>Local resources which are not available in the remote collection (anymore) will be removed.</li>
|
||||
* <li>Resources whose remote ETag has changed will be added into {@link #toDownload}</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected open fun compareLocalRemote() {
|
||||
/* check which contacts
|
||||
1. are not present anymore remotely -> delete immediately on local side
|
||||
2. updated remotely -> add to downloadNames
|
||||
3. added remotely -> add to downloadNames
|
||||
*/
|
||||
toDownload.clear()
|
||||
for ((name,local) in localResources) {
|
||||
val remote = remoteResources[name]
|
||||
currentDavResource = remote
|
||||
|
||||
if (remote == null) {
|
||||
Logger.log.info("$name is not on server anymore, deleting")
|
||||
currentLocalResource = local
|
||||
local.delete()
|
||||
syncResult.stats.numDeletes++
|
||||
} else {
|
||||
// contact is still on server, check whether it has been updated remotely
|
||||
val localETag = local.eTag
|
||||
val getETag = remote.properties[GetETag::class.java]
|
||||
val remoteETag = getETag?.eTag ?: throw DavException("Server didn't provide ETag")
|
||||
if (remoteETag == localETag) {
|
||||
Logger.log.fine("$name has not been changed on server (ETag still $remoteETag)")
|
||||
syncResult.stats.numSkippedEntries++
|
||||
} else {
|
||||
Logger.log.info("$name has been changed on server (current ETag=$remoteETag, last known ETag=$localETag)")
|
||||
toDownload.add(remote)
|
||||
}
|
||||
|
||||
// remote entry has been seen, remove from list
|
||||
remoteResources.remove(name)
|
||||
|
||||
currentDavResource = null
|
||||
currentLocalResource = null
|
||||
}
|
||||
}
|
||||
|
||||
// add all unseen (= remotely added) remote contacts
|
||||
if (remoteResources.isNotEmpty()) {
|
||||
Logger.log.info("New resources have been found on the server: ${remoteResources.keys.joinToString(", ")}")
|
||||
toDownload.addAll(remoteResources.values)
|
||||
}
|
||||
enum class SyncAlgorithm {
|
||||
PROPFIND_REPORT,
|
||||
COLLECTION_SYNC
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the remote resources in {@link #toDownload} and stores them locally.
|
||||
* Must check Thread.interrupted() periodically to allow quick sync cancellation.
|
||||
*/
|
||||
abstract protected fun downloadRemote()
|
||||
|
||||
/**
|
||||
* For post-processing of entries, for instance assigning groups.
|
||||
*/
|
||||
protected open fun postProcess() {}
|
||||
class RemoteChanges(
|
||||
val state: SyncState?,
|
||||
val furtherChanges: Boolean
|
||||
) {
|
||||
val deleted = LinkedList<String>()
|
||||
val updated = LinkedList<DavResource>()
|
||||
|
||||
protected open fun saveSyncState() {
|
||||
/* Save sync state (CTag). It doesn't matter if it has changed during the sync process
|
||||
(for instance, because another client has uploaded changes), because this will simply
|
||||
cause all remote entries to be listed at the next sync. */
|
||||
Logger.log.info("Saving CTag=$remoteCTag")
|
||||
localCollection.setCTag(remoteCTag)
|
||||
override fun toString() = MiscUtils.reflectionToString(this)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -60,16 +59,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 +84,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)
|
||||
|
||||
@@ -13,13 +13,13 @@ 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.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.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalResource
|
||||
import at.bitfire.davdroid.resource.LocalTask
|
||||
@@ -27,16 +27,17 @@ 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,98 +46,88 @@ 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}") {
|
||||
localCollection: LocalTaskList
|
||||
): BaseDavSyncManager<LocalTask, LocalTaskList, DavCalendar>(context, settings, account, accountSettings, extras, authority, syncResult, localCollection) {
|
||||
|
||||
val MAX_MULTIGET = 30
|
||||
|
||||
|
||||
init {
|
||||
localCollection = localTaskList
|
||||
companion object {
|
||||
const val MULTIGET_MAX_RESOURCES = 30
|
||||
}
|
||||
|
||||
override fun notificationId() = Constants.NOTIFICATION_TASK_SYNC
|
||||
|
||||
override fun getSyncErrorTitle() = context.getString(R.string.sync_error_tasks, account.name)!!
|
||||
|
||||
|
||||
override fun prepare(): Boolean {
|
||||
collectionURL = HttpUrl.parse(localTaskList.syncId ?: return false) ?: return false
|
||||
if (!super.prepare())
|
||||
return false
|
||||
|
||||
val url = localCollection.syncId ?: return false
|
||||
collectionURL = HttpUrl.parse(url) ?: return false
|
||||
davCollection = DavCalendar(httpClient.okHttpClient, collectionURL)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun queryCapabilities() {
|
||||
davCollection.propfind(0, GetCTag.NAME)
|
||||
useRemoteCollection { it.propfind(0, GetCTag.NAME, SyncToken.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)
|
||||
override fun syncAlgorithm() = SyncAlgorithm.PROPFIND_REPORT
|
||||
|
||||
val os = ByteArrayOutputStream()
|
||||
task.write(os)
|
||||
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)
|
||||
|
||||
return RequestBody.create(
|
||||
DavCalendar.MIME_ICALENDAR,
|
||||
os.toByteArray()
|
||||
)
|
||||
} else
|
||||
throw IllegalArgumentException("resource must be a LocalTask")
|
||||
}
|
||||
val os = ByteArrayOutputStream()
|
||||
task.write(os)
|
||||
|
||||
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)
|
||||
RequestBody.create(
|
||||
DavCalendar.MIME_ICALENDAR_UTF8,
|
||||
os.toByteArray()
|
||||
)
|
||||
})
|
||||
|
||||
remoteResources = HashMap(davCollection.members.size)
|
||||
for (vCard in davCollection.members) {
|
||||
override fun listAllRemote() = useRemoteCollection { remote ->
|
||||
remote.calendarQuery("VTODO", null, null)
|
||||
|
||||
val result = LinkedHashMap<String, DavResource>(remote.members.size)
|
||||
for (vCard in remote.members) {
|
||||
val fileName = vCard.fileName()
|
||||
Logger.log.fine("Found remote VTODO: $fileName")
|
||||
remoteResources[fileName] = vCard
|
||||
result[fileName] = vCard
|
||||
}
|
||||
|
||||
currentDavResource = null
|
||||
result
|
||||
}
|
||||
|
||||
override fun downloadRemote() {
|
||||
Logger.log.info("Downloading ${toDownload.size} tasks ($MAX_MULTIGET at once)")
|
||||
override fun processRemoteChanges(changes: RemoteChanges) {
|
||||
for (name in changes.deleted) {
|
||||
localCollection.findByName(name)?.let {
|
||||
Logger.log.info("Deleting local task $name")
|
||||
useLocal(it, { it.delete() })
|
||||
syncResult.stats.numDeletes++
|
||||
}
|
||||
}
|
||||
|
||||
// download new/updated iCalendars from server
|
||||
for (bunch in ListUtils.partition(toDownload.toList(), MAX_MULTIGET)) {
|
||||
abortIfCancelled()
|
||||
Logger.log.info("Downloading ${bunch.joinToString(", ")}")
|
||||
val toDownload = changes.updated.map { it.location }
|
||||
Logger.log.info("Downloading ${toDownload.size} resources ($MULTIGET_MAX_RESOURCES at once)")
|
||||
|
||||
if (bunch.size == 1) {
|
||||
for (bunch in toDownload.chunked(MULTIGET_MAX_RESOURCES)) {
|
||||
if (bunch.size == 1)
|
||||
// only one contact, use GET
|
||||
val remote = bunch.first()
|
||||
currentDavResource = remote
|
||||
useRemote(DavResource(httpClient.okHttpClient, bunch.first()), { remote ->
|
||||
val body = remote.get(DavCalendar.MIME_ICALENDAR.toString())
|
||||
|
||||
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]?.eTag
|
||||
?: throw DavException("Received CalDAV GET response without ETag for ${remote.location}")
|
||||
|
||||
// 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 {
|
||||
body.charStream().use { reader ->
|
||||
processVTodo(remote.fileName(), eTag, reader)
|
||||
}
|
||||
})
|
||||
else {
|
||||
// multiple contacts, use multi-get
|
||||
val calendar = davCalendar()
|
||||
currentDavResource = calendar
|
||||
calendar.multiget(bunch.map { it.location })
|
||||
davCollection.multiget(bunch)
|
||||
|
||||
// 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")
|
||||
|
||||
@@ -148,15 +139,10 @@ class TasksSyncManager(
|
||||
}
|
||||
}
|
||||
|
||||
currentDavResource = null
|
||||
abortIfCancelled()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
private fun davCalendar() = davCollection as DavCalendar
|
||||
|
||||
private fun processVTodo(fileName: String, eTag: String, reader: Reader) {
|
||||
val tasks: List<Task>
|
||||
try {
|
||||
@@ -170,22 +156,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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -75,9 +75,6 @@ class AboutActivity: AppCompatActivity() {
|
||||
), 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"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -107,8 +104,8 @@ class AboutActivity: AppCompatActivity() {
|
||||
class ComponentFragment: Fragment(), LoaderManager.LoaderCallbacks<Spanned> {
|
||||
|
||||
companion object {
|
||||
val KEY_POSITION = "position"
|
||||
val KEY_FILE_NAME = "fileName"
|
||||
const val KEY_POSITION = "position"
|
||||
const val KEY_FILE_NAME = "fileName"
|
||||
|
||||
fun instantiate(position: Int): ComponentFragment {
|
||||
val frag = ComponentFragment()
|
||||
@@ -168,8 +165,8 @@ class AboutActivity: AppCompatActivity() {
|
||||
return v
|
||||
}
|
||||
|
||||
override fun onCreateLoader(id: Int, args: Bundle) =
|
||||
LicenseLoader(activity!!, args.getString(KEY_FILE_NAME))
|
||||
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 ->
|
||||
|
||||
@@ -18,12 +18,12 @@ import android.content.pm.PackageManager
|
||||
import android.database.DatabaseUtils
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.*
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.ContactsContract
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.test.espresso.IdlingRegistry
|
||||
import android.support.test.espresso.IdlingResource
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v4.content.ContextCompat
|
||||
@@ -32,6 +32,7 @@ import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.*
|
||||
import android.widget.*
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.DavService
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
@@ -42,7 +43,6 @@ 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.*
|
||||
@@ -51,7 +51,7 @@ 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 var isActiveIdlingResource: IsActiveIdlingResource? = null
|
||||
|
||||
|
||||
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)
|
||||
@@ -104,6 +108,18 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
|
||||
// load CardDAV/CalDAV collections
|
||||
loaderManager.initLoader(0, null, this)
|
||||
|
||||
// register Espresso idling resource
|
||||
if (BuildConfig.DEBUG) {
|
||||
isActiveIdlingResource = IsActiveIdlingResource()
|
||||
IdlingRegistry.getInstance().register(isActiveIdlingResource)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (BuildConfig.DEBUG)
|
||||
IdlingRegistry.getInstance().unregister(isActiveIdlingResource)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
@@ -206,9 +222,6 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
|
||||
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 ->
|
||||
@@ -364,6 +377,12 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
View.GONE
|
||||
} ?: View.GONE
|
||||
|
||||
// set idle state for UI tests
|
||||
if (BuildConfig.DEBUG && isActiveIdlingResource!!.isIdleNow)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
isActiveIdlingResource!!.callback?.onTransitionToIdle()
|
||||
}
|
||||
|
||||
// ask for permissions
|
||||
val requiredPermissions = mutableSetOf<String>()
|
||||
info?.carddav?.let { carddav ->
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,8 +711,8 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
try {
|
||||
if (provider != null) {
|
||||
val addressBook = LocalAddressBook(activity!!, addrBookAccount, provider)
|
||||
if (oldAccount == addressBook.getMainAccount())
|
||||
addressBook.setMainAccount(Account(newName, oldAccount.type))
|
||||
if (oldAccount == addressBook.mainAccount)
|
||||
addressBook.mainAccount = Account(newName, oldAccount.type)
|
||||
}
|
||||
} finally {
|
||||
if (Build.VERSION.SDK_INT >= 24)
|
||||
@@ -703,7 +722,7 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
}
|
||||
|
||||
}
|
||||
} catch(e: ContactsStorageException) {
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't update address book accounts", e)
|
||||
}
|
||||
|
||||
@@ -758,4 +777,25 @@ class AccountActivity: AppCompatActivity(), Toolbar.OnMenuItemClickListener, Pop
|
||||
Snackbar.make(findViewById(R.id.parent), R.string.account_synchronizing_now, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* For Espresso tests. Is idle when the CalDAV/CardDAV cards are either invisible or
|
||||
* there's no more progress bar.
|
||||
*/
|
||||
inner class IsActiveIdlingResource: IdlingResource {
|
||||
|
||||
var callback: IdlingResource.ResourceCallback? = null
|
||||
|
||||
override fun getName() = "CalDAV/CardDAV activity (progress bar)"
|
||||
|
||||
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
|
||||
this.callback = callback
|
||||
}
|
||||
|
||||
override fun isIdleNow() =
|
||||
(carddav.visibility == View.GONE || carddav_refreshing.visibility == View.GONE) &&
|
||||
(caldav.visibility == View.GONE || caldav_refreshing.visibility == View.GONE)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,23 +8,28 @@
|
||||
|
||||
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
|
||||
@@ -38,7 +43,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 +58,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 +73,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 +87,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
|
||||
@@ -211,6 +216,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,7 +234,7 @@ 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)
|
||||
@@ -258,11 +270,10 @@ 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)
|
||||
loaderManager.restartLoader(0, arguments, this)
|
||||
@@ -296,7 +307,7 @@ 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)
|
||||
@@ -313,7 +324,7 @@ class AccountSettingsActivity: AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoaderReset(loader: Loader<Pair<ISettings, AccountSettings>?>) {
|
||||
override fun onLoaderReset(loader: Loader<Pair<ISettings, AccountSettings>>) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -330,7 +341,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() {
|
||||
|
||||
@@ -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
|
||||
@@ -47,7 +51,7 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
fab.setOnClickListener({
|
||||
startActivity(Intent(this@AccountsActivity, LoginActivity::class.java))
|
||||
startActivity(Intent(this, LoginActivity::class.java))
|
||||
})
|
||||
|
||||
val toggle = ActionBarDrawerToggle(
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,8 +37,8 @@ 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()
|
||||
@@ -57,8 +57,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.getSerializable(ARG_COLLECTION_INFO) as CollectionInfo
|
||||
|
||||
loaderManager.initLoader(0, null, this)
|
||||
}
|
||||
@@ -74,16 +75,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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -253,10 +256,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")
|
||||
|
||||
@@ -31,8 +31,8 @@ import okhttp3.HttpUrl
|
||||
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
|
||||
@@ -62,12 +62,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()
|
||||
}
|
||||
|
||||
@@ -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,7 +45,7 @@ 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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.StringUtils
|
||||
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,20 +124,38 @@ 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(getString(R.string.startup_autostart_permission, StringUtils.capitalize(Build.MANUFACTURER)))
|
||||
.setMessage(R.string.startup_autostart_permission_message)
|
||||
.setPositiveButton(R.string.startup_more_info, { _, _ ->
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.homepage_url)).buildUpon()
|
||||
.appendPath("faq").appendPath("automatic-synchronization-is-not-run-as-expected").build())
|
||||
if (intent.resolveActivity(activity.packageManager) != null)
|
||||
activity.startActivity(intent)
|
||||
})
|
||||
.setNeutralButton(android.R.string.ok, { _, _ -> })
|
||||
.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)
|
||||
})
|
||||
.setNeutralButton(android.R.string.ok, { _, _ -> })
|
||||
.setNegativeButton(R.string.startup_dont_show_again, { _: DialogInterface, _: Int ->
|
||||
settings?.putBoolean(HINT_BATTERY_OPTIMIZATIONS, false)
|
||||
})
|
||||
@@ -144,12 +172,13 @@ 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, { _, _ ->
|
||||
.setPositiveButton(R.string.startup_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)
|
||||
if (intent.resolveActivity(activity.packageManager) != null)
|
||||
activity.startActivity(intent)
|
||||
})
|
||||
.setNeutralButton(android.R.string.ok, { _, _ -> })
|
||||
.setNegativeButton(R.string.startup_dont_show_again, { _, _ ->
|
||||
settings?.putBoolean(HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED, false)
|
||||
})
|
||||
@@ -164,14 +193,14 @@ 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")
|
||||
})
|
||||
.setNeutralButton(android.R.string.ok, { _, _ -> })
|
||||
.setNegativeButton(R.string.startup_dont_show_again, { _: DialogInterface, _: Int ->
|
||||
settings?.putBoolean(HINT_OPENTASKS_NOT_INSTALLED, false)
|
||||
})
|
||||
|
||||
@@ -11,13 +11,17 @@ package at.bitfire.davdroid.ui.setup
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.app.Activity
|
||||
import android.app.Fragment
|
||||
import android.app.LoaderManager
|
||||
import android.content.*
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.LoaderManager
|
||||
import android.support.v4.content.Loader
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -36,8 +40,7 @@ import java.util.logging.Level
|
||||
class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSettings> {
|
||||
|
||||
companion object {
|
||||
|
||||
val KEY_CONFIG = "config"
|
||||
const val KEY_CONFIG = "config"
|
||||
|
||||
fun newInstance(config: DavResourceFinder.Configuration): AccountDetailsFragment {
|
||||
val frag = AccountDetailsFragment()
|
||||
@@ -46,7 +49,6 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSe
|
||||
frag.arguments = args
|
||||
return frag
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var groupMethod: GroupMethod? = null
|
||||
@@ -57,10 +59,11 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSe
|
||||
val v = inflater.inflate(R.layout.login_account_details, container, false)
|
||||
|
||||
v.back.setOnClickListener({ _ ->
|
||||
fragmentManager.popBackStack()
|
||||
requireFragmentManager().popBackStack()
|
||||
})
|
||||
|
||||
val config = arguments.getSerializable(KEY_CONFIG) as DavResourceFinder.Configuration
|
||||
val args = requireNotNull(arguments)
|
||||
val config = args.getSerializable(KEY_CONFIG) as DavResourceFinder.Configuration
|
||||
|
||||
v.account_name.setText(config.calDAV?.email ?:
|
||||
config.credentials.userName ?:
|
||||
@@ -78,9 +81,9 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSe
|
||||
if (name.isEmpty())
|
||||
v.account_name.error = getString(R.string.login_account_name_required)
|
||||
else {
|
||||
if (createAccount(name, arguments.getSerializable(KEY_CONFIG) as DavResourceFinder.Configuration)) {
|
||||
activity.setResult(Activity.RESULT_OK)
|
||||
activity.finish()
|
||||
if (createAccount(name, args.getSerializable(KEY_CONFIG) as DavResourceFinder.Configuration)) {
|
||||
requireActivity().setResult(Activity.RESULT_OK)
|
||||
requireActivity().finish()
|
||||
} else
|
||||
Snackbar.make(v, R.string.login_account_not_created, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
@@ -91,28 +94,30 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSe
|
||||
return v
|
||||
}
|
||||
|
||||
override fun onCreateLoader(code: Int, args: Bundle?): Loader<CreateSettings?> =
|
||||
GroupMethodLoader(activity)
|
||||
override fun onCreateLoader(code: Int, args: Bundle?) =
|
||||
GroupMethodLoader(requireActivity())
|
||||
|
||||
override fun onLoadFinished(loader: Loader<CreateSettings?>?, result: CreateSettings?) {
|
||||
override fun onLoadFinished(loader: Loader<CreateSettings>, result: CreateSettings?) {
|
||||
settings = (result ?: return).settings
|
||||
groupMethod = result.groupMethod
|
||||
|
||||
if (result.groupMethod != null) {
|
||||
view.contact_group_method.isEnabled = false
|
||||
for ((i, method) in resources.getStringArray(R.array.settings_contact_group_method_values).withIndex()) {
|
||||
if (method == result.groupMethod.name) {
|
||||
view.contact_group_method.setSelection(i)
|
||||
break
|
||||
view?.let { view ->
|
||||
if (result.groupMethod != null) {
|
||||
view.contact_group_method.isEnabled = false
|
||||
for ((i, method) in resources.getStringArray(R.array.settings_contact_group_method_values).withIndex()) {
|
||||
if (method == result.groupMethod.name) {
|
||||
view.contact_group_method.setSelection(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else
|
||||
view.contact_group_method.isEnabled = true
|
||||
} else
|
||||
view.contact_group_method.isEnabled = true
|
||||
|
||||
view.create_account.isEnabled = true
|
||||
view.create_account.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoaderReset(loader: Loader<CreateSettings?>?) {
|
||||
override fun onLoaderReset(loader: Loader<CreateSettings>) {
|
||||
settings = null
|
||||
groupMethod = null
|
||||
view?.create_account?.isEnabled = false
|
||||
@@ -133,10 +138,10 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSe
|
||||
|
||||
// add entries for account to service DB
|
||||
Logger.log.log(Level.INFO, "Writing account configuration to database", config)
|
||||
OpenHelper(activity).use { dbHelper ->
|
||||
OpenHelper(requireActivity()).use { dbHelper ->
|
||||
val db = dbHelper.writableDatabase
|
||||
try {
|
||||
val accountSettings = AccountSettings(activity, settings, account)
|
||||
val accountSettings = AccountSettings(requireActivity(), settings, account)
|
||||
|
||||
val refreshIntent = Intent(activity, DavService::class.java)
|
||||
refreshIntent.action = DavService.ACTION_REFRESH_COLLECTIONS
|
||||
@@ -147,10 +152,10 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSe
|
||||
|
||||
// start CardDAV service detection (refresh collections)
|
||||
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id)
|
||||
activity.startService(refreshIntent)
|
||||
requireActivity().startService(refreshIntent)
|
||||
|
||||
// initial CardDAV account settings
|
||||
val idx = view.contact_group_method.selectedItemPosition
|
||||
val idx = view!!.contact_group_method.selectedItemPosition
|
||||
val groupMethodName = resources.getStringArray(R.array.settings_contact_group_method_values)[idx]
|
||||
accountSettings.setGroupMethod(GroupMethod.valueOf(groupMethodName))
|
||||
|
||||
@@ -165,14 +170,14 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSe
|
||||
|
||||
// start CalDAV service detection (refresh collections)
|
||||
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id)
|
||||
activity.startService(refreshIntent)
|
||||
requireActivity().startService(refreshIntent)
|
||||
|
||||
// calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
|
||||
accountSettings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL)
|
||||
|
||||
// enable task sync if OpenTasks is installed
|
||||
// further changes will be handled by PackageChangedReceiver
|
||||
if (LocalTaskList.tasksProviderAvailable(activity)) {
|
||||
if (LocalTaskList.tasksProviderAvailable(requireActivity())) {
|
||||
ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 1)
|
||||
accountSettings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.authority, Constants.DEFAULT_SYNC_INTERVAL)
|
||||
}
|
||||
@@ -221,7 +226,7 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks<CreateSe
|
||||
|
||||
class GroupMethodLoader(
|
||||
context: Context
|
||||
): SettingsLoader<CreateSettings?>(context) {
|
||||
): SettingsLoader<CreateSettings>(context) {
|
||||
|
||||
override fun loadInBackground(): CreateSettings? {
|
||||
settings?.let { settings ->
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.apache.commons.lang3.builder.ReflectionToStringBuilder
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder
|
||||
import org.xbill.DNS.Lookup
|
||||
import org.xbill.DNS.Type
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.io.Serializable
|
||||
import java.net.URI
|
||||
@@ -36,7 +35,7 @@ import java.util.logging.Logger
|
||||
class DavResourceFinder(
|
||||
val context: Context,
|
||||
private val loginInfo: LoginInfo
|
||||
): Closeable {
|
||||
): AutoCloseable {
|
||||
|
||||
enum class Service(val wellKnownName: String) {
|
||||
CALDAV("caldav"),
|
||||
|
||||
@@ -8,9 +8,17 @@
|
||||
|
||||
package at.bitfire.davdroid.ui.setup
|
||||
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.app.Dialog
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v4.app.LoaderManager
|
||||
import android.support.v4.content.AsyncTaskLoader
|
||||
import android.support.v4.content.Loader
|
||||
import android.support.v7.app.AlertDialog
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.ui.DebugInfoActivity
|
||||
@@ -21,7 +29,7 @@ import kotlin.concurrent.thread
|
||||
class DetectConfigurationFragment: DialogFragment(), LoaderManager.LoaderCallbacks<Configuration> {
|
||||
|
||||
companion object {
|
||||
val ARG_LOGIN_CREDENTIALS = "credentials"
|
||||
const val ARG_LOGIN_CREDENTIALS = "credentials"
|
||||
|
||||
fun newInstance(credentials: LoginInfo): DetectConfigurationFragment {
|
||||
val frag = DetectConfigurationFragment()
|
||||
@@ -53,25 +61,25 @@ class DetectConfigurationFragment: DialogFragment(), LoaderManager.LoaderCallbac
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateLoader(id: Int, args: Bundle) =
|
||||
ServerConfigurationLoader(activity, args.getParcelable(ARG_LOGIN_CREDENTIALS))
|
||||
override fun onCreateLoader(id: Int, args: Bundle?) =
|
||||
ServerConfigurationLoader(requireActivity(), args!!.getParcelable(ARG_LOGIN_CREDENTIALS))
|
||||
|
||||
override fun onLoadFinished(loader: Loader<Configuration>, data: Configuration?) {
|
||||
data?.let {
|
||||
if (it.calDAV == null && it.cardDAV == null)
|
||||
// no service found: show error message
|
||||
fragmentManager.beginTransaction()
|
||||
requireFragmentManager().beginTransaction()
|
||||
.add(NothingDetectedFragment.newInstance(it.logs), null)
|
||||
.commitAllowingStateLoss()
|
||||
.commit()
|
||||
else
|
||||
// service found: continue
|
||||
fragmentManager.beginTransaction()
|
||||
requireFragmentManager().beginTransaction()
|
||||
.replace(android.R.id.content, AccountDetailsFragment.newInstance(data))
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss()
|
||||
.commit()
|
||||
}
|
||||
|
||||
dismissAllowingStateLoss()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
override fun onLoaderReset(loader: Loader<Configuration>) {}
|
||||
@@ -80,7 +88,7 @@ class DetectConfigurationFragment: DialogFragment(), LoaderManager.LoaderCallbac
|
||||
class NothingDetectedFragment: DialogFragment() {
|
||||
|
||||
companion object {
|
||||
val KEY_LOGS = "logs"
|
||||
const val KEY_LOGS = "logs"
|
||||
|
||||
fun newInstance(logs: String): NothingDetectedFragment {
|
||||
val args = Bundle()
|
||||
@@ -92,13 +100,13 @@ class DetectConfigurationFragment: DialogFragment(), LoaderManager.LoaderCallbac
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?) =
|
||||
AlertDialog.Builder(activity)
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.login_configuration_detection)
|
||||
.setIcon(R.drawable.ic_error_dark)
|
||||
.setMessage(R.string.login_no_caldav_carddav)
|
||||
.setNeutralButton(R.string.login_view_logs, { _, _ ->
|
||||
val intent = Intent(activity, DebugInfoActivity::class.java)
|
||||
intent.putExtra(DebugInfoActivity.KEY_LOGS, arguments.getString(KEY_LOGS))
|
||||
intent.putExtra(DebugInfoActivity.KEY_LOGS, arguments!!.getString(KEY_LOGS))
|
||||
startActivity(intent)
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok, { _, _ ->
|
||||
@@ -114,7 +122,7 @@ class DetectConfigurationFragment: DialogFragment(), LoaderManager.LoaderCallbac
|
||||
private val credentials: LoginInfo
|
||||
): AsyncTaskLoader<Configuration>(context) {
|
||||
|
||||
var resourceFinder: DavResourceFinder? = null
|
||||
private var resourceFinder: DavResourceFinder? = null
|
||||
|
||||
override fun onStartLoading() = forceLoad()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
package at.bitfire.davdroid.ui.setup
|
||||
|
||||
import android.app.Fragment
|
||||
import android.support.v4.app.Fragment
|
||||
|
||||
interface ILoginCredentialsFragment {
|
||||
|
||||
|
||||
@@ -28,21 +28,18 @@ class LoginActivity: AppCompatActivity() {
|
||||
* When set, "login by URL" will be activated by default, and the URL field will be set to this value.
|
||||
* When not set, "login by email" will be activated by default.
|
||||
*/
|
||||
@JvmField
|
||||
val EXTRA_URL = "url"
|
||||
const val EXTRA_URL = "url"
|
||||
|
||||
/**
|
||||
* When set, and {@link #EXTRA_PASSWORD} is set too, the user name field will be set to this value.
|
||||
* When set, and {@link #EXTRA_URL} is not set, the email address field will be set to this value.
|
||||
*/
|
||||
@JvmField
|
||||
val EXTRA_USERNAME = "username"
|
||||
const val EXTRA_USERNAME = "username"
|
||||
|
||||
/**
|
||||
* When set, the password field will be set to this value.
|
||||
*/
|
||||
@JvmField
|
||||
val EXTRA_PASSWORD = "password"
|
||||
const val EXTRA_PASSWORD = "password"
|
||||
}
|
||||
|
||||
private val loginFragmentLoader = ServiceLoader.load(ILoginCredentialsFragment::class.java)!!
|
||||
@@ -53,7 +50,7 @@ class LoginActivity: AppCompatActivity() {
|
||||
|
||||
if (savedInstanceState == null)
|
||||
// first call, add first login fragment
|
||||
fragmentManager.beginTransaction()
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(android.R.id.content, loginFragmentLoader.first().getFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import at.bitfire.davdroid.model.Credentials
|
||||
import java.net.URI
|
||||
|
||||
data class LoginInfo(
|
||||
@JvmField val uri: URI,
|
||||
@JvmField val credentials: Credentials
|
||||
val uri: URI,
|
||||
val credentials: Credentials
|
||||
): Parcelable {
|
||||
|
||||
constructor(uri: URI, userName: String? = null, password: String? = null, certificateAlias: String? = null):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright © Ricki Hirner (bitfire web engineering).
|
||||
~ All rights reserved. This program and the accompanying materials
|
||||
~ are made available under the terms of the GNU Public License v3.0
|
||||
~ which accompanies this distribution, and is available at
|
||||
~ http://www.gnu.org/licenses/gpl.html
|
||||
-->
|
||||
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user