mirror of
https://github.com/etesync/android
synced 2024-11-21 23:48:11 +00:00
Adjust DAVdroid to use the EteSync protocol (mostly working)
This commit includes the major changes between DAVdroid and EteSync. It adjusts the app to use the EteSync protocol and server. It includes some ugliness still, and it's a squash of many ugly snapshot commits while hacking on the initial DAVdroid code. History should be "clean" from this point onwards.
This commit is contained in:
parent
232eaa1d6d
commit
8b5f87c2d4
3
.gitignore
vendored
3
.gitignore
vendored
@ -90,3 +90,6 @@ gradle-app.setting
|
|||||||
|
|
||||||
# Javadoc
|
# Javadoc
|
||||||
javadoc/
|
javadoc/
|
||||||
|
|
||||||
|
### VIM ###
|
||||||
|
*.swp
|
||||||
|
9
.gitmodules
vendored
9
.gitmodules
vendored
@ -1,12 +1,9 @@
|
|||||||
[submodule "dav4android"]
|
|
||||||
path = dav4android
|
|
||||||
url = ../dav4android.git
|
|
||||||
[submodule "ical4android"]
|
[submodule "ical4android"]
|
||||||
path = ical4android
|
path = ical4android
|
||||||
url = ../ical4android.git
|
url = https://gitlab.com/bitfireAT/ical4android.git
|
||||||
[submodule "vcard4android"]
|
[submodule "vcard4android"]
|
||||||
path = vcard4android
|
path = vcard4android
|
||||||
url = ../vcard4android.git
|
url = https://gitlab.com/bitfireAT/vcard4android.git
|
||||||
[submodule "cert4android"]
|
[submodule "cert4android"]
|
||||||
path = cert4android
|
path = cert4android
|
||||||
url = ../cert4android.git
|
url = https://gitlab.com/bitfireAT/cert4android.git
|
||||||
|
@ -32,11 +32,25 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
minifyEnabled false
|
minifyEnabled true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* To override the server's url (for example when developing),
|
||||||
|
* create file gradle.properties in ~/.gradle/ with this content:
|
||||||
|
*
|
||||||
|
* appDebugRemoteUrl="http://localserver:8080/"
|
||||||
|
*/
|
||||||
|
if (project.hasProperty('appDebugRemoteUrl')) {
|
||||||
|
buildConfigField 'String', 'DEBUG_REMOTE_URL', appDebugRemoteUrl
|
||||||
|
} else {
|
||||||
|
buildConfigField 'String', 'DEBUG_REMOTE_URL', 'null'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||||
|
buildConfigField 'String', 'DEBUG_REMOTE_URL', 'null'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +69,14 @@ android {
|
|||||||
disable 'Typos'
|
disable 'Typos'
|
||||||
disable "RestrictedApi" // https://code.google.com/p/android/issues/detail?id=230387
|
disable "RestrictedApi" // https://code.google.com/p/android/issues/detail?id=230387
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dexOptions {
|
||||||
|
preDexLibraries = true
|
||||||
|
// dexInProcess requires much RAM, which is not available on all dev systems
|
||||||
|
dexInProcess = false
|
||||||
|
javaMaxHeapSize "2g"
|
||||||
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
exclude 'LICENSE'
|
exclude 'LICENSE'
|
||||||
exclude 'META-INF/LICENSE.txt'
|
exclude 'META-INF/LICENSE.txt'
|
||||||
@ -68,7 +90,6 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':cert4android')
|
compile project(':cert4android')
|
||||||
compile project(':dav4android')
|
|
||||||
compile project(':ical4android')
|
compile project(':ical4android')
|
||||||
compile project(':vcard4android')
|
compile project(':vcard4android')
|
||||||
|
|
||||||
@ -79,11 +100,10 @@ dependencies {
|
|||||||
|
|
||||||
compile 'com.github.yukuku:ambilwarna:2.0.1'
|
compile 'com.github.yukuku:ambilwarna:2.0.1'
|
||||||
|
|
||||||
|
compile group: 'com.madgag.spongycastle', name: 'core', version: '1.54.0.0'
|
||||||
|
compile group: 'com.madgag.spongycastle', name: 'prov', version: '1.54.0.0'
|
||||||
|
compile group: 'com.google.code.gson', name: 'gson', version: '1.7.2'
|
||||||
compile 'com.squareup.okhttp3:logging-interceptor:3.5.0'
|
compile 'com.squareup.okhttp3:logging-interceptor:3.5.0'
|
||||||
compile 'commons-io:commons-io:2.5'
|
|
||||||
compile 'dnsjava:dnsjava:2.1.7'
|
|
||||||
compile 'org.apache.commons:commons-lang3:3.4'
|
|
||||||
compile 'org.apache.commons:commons-collections4:4.1'
|
|
||||||
provided 'org.projectlombok:lombok:1.16.12'
|
provided 'org.projectlombok:lombok:1.16.12'
|
||||||
|
|
||||||
// for tests
|
// for tests
|
||||||
|
@ -30,8 +30,23 @@
|
|||||||
-dontwarn java.nio.file.** # not available on Android
|
-dontwarn java.nio.file.** # not available on Android
|
||||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||||
|
|
||||||
# dnsjava
|
|
||||||
-dontwarn sun.net.spi.nameservice.** # not available on Android
|
|
||||||
|
|
||||||
# DAVdroid + libs
|
# DAVdroid + libs
|
||||||
-keep class at.bitfire.** { *; } # all DAVdroid code is required
|
-keep class at.bitfire.** { *; } # all DAVdroid code is required
|
||||||
|
|
||||||
|
# Spongcastle
|
||||||
|
-dontwarn org.spongycastle.jce.provider.X509LDAPCertStoreSpi
|
||||||
|
-dontwarn org.spongycastle.x509.util.LDAPStoreHelper
|
||||||
|
-keep class org.spongycastle.crypto.BufferedBlockCipher
|
||||||
|
-keep class org.spongycastle.crypto.CipherParameters
|
||||||
|
-keep class org.spongycastle.crypto.InvalidCipherTextException
|
||||||
|
-keep class org.spongycastle.crypto.digests.SHA256Digest
|
||||||
|
-keep class org.spongycastle.crypto.engines.AESEngine
|
||||||
|
-keep class org.spongycastle.crypto.generators.SCrypt
|
||||||
|
-keep class org.spongycastle.crypto.macs.HMac
|
||||||
|
-keep class org.spongycastle.crypto.modes.CBCBlockCipher
|
||||||
|
-keep class org.spongycastle.crypto.paddings.BlockCipherPadding
|
||||||
|
-keep class org.spongycastle.crypto.paddings.PKCS7Padding
|
||||||
|
-keep class org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher
|
||||||
|
-keep class org.spongycastle.crypto.params.KeyParameter
|
||||||
|
-keep class org.spongycastle.crypto.params.ParametersWithIV
|
||||||
|
-keep class org.spongycastle.util.encoders.Hex
|
||||||
|
@ -9,12 +9,10 @@
|
|||||||
package at.bitfire.davdroid;
|
package at.bitfire.davdroid;
|
||||||
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.support.test.runner.AndroidJUnit4;
|
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
@ -9,84 +9,19 @@
|
|||||||
package at.bitfire.davdroid.model;
|
package at.bitfire.davdroid.model;
|
||||||
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.support.test.runner.AndroidJUnit4;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import at.bitfire.dav4android.DavResource;
|
|
||||||
import at.bitfire.dav4android.exception.DavException;
|
|
||||||
import at.bitfire.dav4android.exception.HttpException;
|
|
||||||
import at.bitfire.dav4android.property.ResourceType;
|
|
||||||
import at.bitfire.davdroid.HttpClient;
|
|
||||||
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
||||||
import okhttp3.mockwebserver.MockResponse;
|
|
||||||
import okhttp3.mockwebserver.MockWebServer;
|
import okhttp3.mockwebserver.MockWebServer;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
|
||||||
import static org.junit.Assert.assertNull;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class CollectionInfoTest {
|
public class CollectionInfoTest {
|
||||||
|
|
||||||
MockWebServer server = new MockWebServer();
|
MockWebServer server = new MockWebServer();
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testFromDavResource() throws IOException, HttpException, DavException {
|
|
||||||
// r/w address book
|
|
||||||
server.enqueue(new MockResponse()
|
|
||||||
.setResponseCode(207)
|
|
||||||
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
|
|
||||||
"<response>" +
|
|
||||||
" <href>/</href>" +
|
|
||||||
" <propstat><prop>" +
|
|
||||||
" <resourcetype><collection/><CARD:addressbook/></resourcetype>" +
|
|
||||||
" <displayname>My Contacts</displayname>" +
|
|
||||||
" <CARD:addressbook-description>My Contacts Description</CARD:addressbook-description>" +
|
|
||||||
" </prop></propstat>" +
|
|
||||||
"</response>" +
|
|
||||||
"</multistatus>"));
|
|
||||||
|
|
||||||
DavResource dav = new DavResource(HttpClient.create(null), server.url("/"));
|
|
||||||
dav.propfind(0, ResourceType.NAME);
|
|
||||||
CollectionInfo info = CollectionInfo.fromDavResource(dav);
|
|
||||||
assertEquals(CollectionInfo.Type.ADDRESS_BOOK, info.type);
|
|
||||||
assertFalse(info.readOnly);
|
|
||||||
assertEquals("My Contacts", info.displayName);
|
|
||||||
assertEquals("My Contacts Description", info.description);
|
|
||||||
|
|
||||||
// read-only calendar, no display name
|
|
||||||
server.enqueue(new MockResponse()
|
|
||||||
.setResponseCode(207)
|
|
||||||
.setBody("<multistatus xmlns='DAV:' xmlns:CAL='urn:ietf:params:xml:ns:caldav' xmlns:ICAL='http://apple.com/ns/ical/'>" +
|
|
||||||
"<response>" +
|
|
||||||
" <href>/</href>" +
|
|
||||||
" <propstat><prop>" +
|
|
||||||
" <resourcetype><collection/><CAL:calendar/></resourcetype>" +
|
|
||||||
" <current-user-privilege-set><privilege><read/></privilege></current-user-privilege-set>" +
|
|
||||||
" <CAL:calendar-description>My Calendar</CAL:calendar-description>" +
|
|
||||||
" <CAL:calendar-timezone>tzdata</CAL:calendar-timezone>" +
|
|
||||||
" <ICAL:calendar-color>#ff0000</ICAL:calendar-color>" +
|
|
||||||
" </prop></propstat>" +
|
|
||||||
"</response>" +
|
|
||||||
"</multistatus>"));
|
|
||||||
|
|
||||||
dav = new DavResource(HttpClient.create(null), server.url("/"));
|
|
||||||
dav.propfind(0, ResourceType.NAME);
|
|
||||||
info = CollectionInfo.fromDavResource(dav);
|
|
||||||
assertEquals(CollectionInfo.Type.CALENDAR, info.type);
|
|
||||||
assertTrue(info.readOnly);
|
|
||||||
assertNull(info.displayName);
|
|
||||||
assertEquals("My Calendar", info.description);
|
|
||||||
assertEquals(0xFFFF0000, (int)info.color);
|
|
||||||
assertEquals("tzdata", info.timeZone);
|
|
||||||
assertTrue(info.supportsVEVENT);
|
|
||||||
assertTrue(info.supportsVTODO);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFromDB() {
|
public void testFromDB() {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
|
@ -8,27 +8,13 @@
|
|||||||
|
|
||||||
package at.bitfire.davdroid.ui.setup;
|
package at.bitfire.davdroid.ui.setup;
|
||||||
|
|
||||||
import android.support.test.runner.AndroidJUnit4;
|
|
||||||
import android.test.InstrumentationTestCase;
|
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
||||||
import at.bitfire.dav4android.DavResource;
|
|
||||||
import at.bitfire.dav4android.exception.DavException;
|
|
||||||
import at.bitfire.dav4android.exception.HttpException;
|
|
||||||
import at.bitfire.dav4android.property.AddressbookHomeSet;
|
|
||||||
import at.bitfire.dav4android.property.ResourceType;
|
|
||||||
import at.bitfire.davdroid.App;
|
import at.bitfire.davdroid.App;
|
||||||
import at.bitfire.davdroid.HttpClient;
|
import at.bitfire.davdroid.HttpClient;
|
||||||
import at.bitfire.davdroid.ui.setup.DavResourceFinder;
|
|
||||||
import at.bitfire.davdroid.ui.setup.DavResourceFinder.Configuration.ServiceInfo;
|
|
||||||
import at.bitfire.davdroid.ui.setup.LoginCredentials;
|
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.mockwebserver.Dispatcher;
|
import okhttp3.mockwebserver.Dispatcher;
|
||||||
import okhttp3.mockwebserver.MockResponse;
|
import okhttp3.mockwebserver.MockResponse;
|
||||||
@ -36,10 +22,6 @@ import okhttp3.mockwebserver.MockWebServer;
|
|||||||
import okhttp3.mockwebserver.RecordedRequest;
|
import okhttp3.mockwebserver.RecordedRequest;
|
||||||
|
|
||||||
import static android.support.test.InstrumentationRegistry.getTargetContext;
|
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 {
|
public class DavResourceFinderTest {
|
||||||
|
|
||||||
@ -69,7 +51,6 @@ public class DavResourceFinderTest {
|
|||||||
finder = new DavResourceFinder(getTargetContext(), credentials);
|
finder = new DavResourceFinder(getTargetContext(), credentials);
|
||||||
|
|
||||||
client = HttpClient.create(null);
|
client = HttpClient.create(null);
|
||||||
client = HttpClient.addAuthentication(client, credentials.userName, credentials.password);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@ -77,66 +58,6 @@ public class DavResourceFinderTest {
|
|||||||
server.shutdown();
|
server.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRememberIfAddressBookOrHomeset() throws IOException, HttpException, DavException {
|
|
||||||
ServiceInfo info;
|
|
||||||
|
|
||||||
// before dav.propfind(), no info is available
|
|
||||||
DavResource dav = new DavResource(client, server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL));
|
|
||||||
finder.rememberIfAddressBookOrHomeset(dav, info = new ServiceInfo());
|
|
||||||
assertEquals(0, info.collections.size());
|
|
||||||
assertEquals(0, info.homeSets.size());
|
|
||||||
|
|
||||||
// recognize home set
|
|
||||||
dav.propfind(0, AddressbookHomeSet.NAME);
|
|
||||||
finder.rememberIfAddressBookOrHomeset(dav, info = new ServiceInfo());
|
|
||||||
assertEquals(0, info.collections.size());
|
|
||||||
assertEquals(1, info.homeSets.size());
|
|
||||||
assertEquals(server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET + "/").uri(), info.homeSets.iterator().next());
|
|
||||||
|
|
||||||
// recognize address book
|
|
||||||
dav = new DavResource(client, server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK));
|
|
||||||
dav.propfind(0, ResourceType.NAME);
|
|
||||||
finder.rememberIfAddressBookOrHomeset(dav, info = new ServiceInfo());
|
|
||||||
assertEquals(1, info.collections.size());
|
|
||||||
assertEquals(server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK + "/").uri(), info.collections.keySet().iterator().next());
|
|
||||||
assertEquals(0, info.homeSets.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testProvidesService() throws IOException {
|
|
||||||
assertFalse(finder.providesService(server.url(PATH_NO_DAV), DavResourceFinder.Service.CALDAV));
|
|
||||||
assertFalse(finder.providesService(server.url(PATH_NO_DAV), DavResourceFinder.Service.CARDDAV));
|
|
||||||
|
|
||||||
assertTrue(finder.providesService(server.url(PATH_CALDAV), DavResourceFinder.Service.CALDAV));
|
|
||||||
assertFalse(finder.providesService(server.url(PATH_CALDAV), DavResourceFinder.Service.CARDDAV));
|
|
||||||
|
|
||||||
assertTrue(finder.providesService(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV));
|
|
||||||
assertFalse(finder.providesService(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV));
|
|
||||||
|
|
||||||
assertTrue(finder.providesService(server.url(PATH_CALDAV_AND_CARDDAV), DavResourceFinder.Service.CALDAV));
|
|
||||||
assertTrue(finder.providesService(server.url(PATH_CALDAV_AND_CARDDAV), DavResourceFinder.Service.CARDDAV));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetCurrentUserPrincipal() throws IOException, HttpException, DavException {
|
|
||||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_NO_DAV), DavResourceFinder.Service.CALDAV));
|
|
||||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_NO_DAV), DavResourceFinder.Service.CARDDAV));
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
server.url(PATH_CALDAV + SUBPATH_PRINCIPAL).uri(),
|
|
||||||
finder.getCurrentUserPrincipal(server.url(PATH_CALDAV), DavResourceFinder.Service.CALDAV)
|
|
||||||
);
|
|
||||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CALDAV), DavResourceFinder.Service.CARDDAV));
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL).uri(),
|
|
||||||
finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV)
|
|
||||||
);
|
|
||||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// mock server
|
// mock server
|
||||||
|
|
||||||
public class TestDispatcher extends Dispatcher {
|
public class TestDispatcher extends Dispatcher {
|
||||||
|
@ -102,26 +102,14 @@
|
|||||||
android:process=":sync"
|
android:process=":sync"
|
||||||
tools:ignore="ExportedService">
|
tools:ignore="ExportedService">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.content.SyncAdapter"/>
|
<action android:name="android.content.SyncAdapter" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.content.SyncAdapter"
|
android:name="android.content.SyncAdapter"
|
||||||
android:resource="@xml/sync_calendars"/>
|
android:resource="@xml/sync_calendars" />
|
||||||
</service>
|
</service>
|
||||||
<service
|
|
||||||
android:name=".syncadapter.TasksSyncAdapterService"
|
|
||||||
android:exported="true"
|
|
||||||
android:process=":sync"
|
|
||||||
tools:ignore="ExportedService">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.content.SyncAdapter"/>
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.content.SyncAdapter"
|
|
||||||
android:resource="@xml/sync_tasks"/>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".DavService"
|
android:name=".DavService"
|
||||||
|
@ -12,55 +12,38 @@ import android.accounts.AccountManager;
|
|||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.PeriodicSync;
|
import android.content.PeriodicSync;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.CalendarContract;
|
|
||||||
import android.provider.ContactsContract;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.HashSet;
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import at.bitfire.davdroid.model.ServiceDB;
|
|
||||||
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
|
||||||
import at.bitfire.davdroid.model.ServiceDB.HomeSets;
|
|
||||||
import at.bitfire.davdroid.model.ServiceDB.Services;
|
|
||||||
import at.bitfire.davdroid.resource.LocalAddressBook;
|
|
||||||
import at.bitfire.davdroid.resource.LocalCalendar;
|
|
||||||
import at.bitfire.davdroid.resource.LocalTaskList;
|
|
||||||
import at.bitfire.ical4android.CalendarStorageException;
|
|
||||||
import at.bitfire.ical4android.TaskProvider;
|
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
|
||||||
import at.bitfire.vcard4android.GroupMethod;
|
import at.bitfire.vcard4android.GroupMethod;
|
||||||
import lombok.Cleanup;
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
|
|
||||||
public class AccountSettings {
|
public class AccountSettings {
|
||||||
private final static int CURRENT_VERSION = 5;
|
private final static int CURRENT_VERSION = 1;
|
||||||
private final static String
|
private final static String
|
||||||
KEY_SETTINGS_VERSION = "version",
|
KEY_SETTINGS_VERSION = "version",
|
||||||
|
KEY_URI = "uri",
|
||||||
KEY_USERNAME = "user_name",
|
KEY_USERNAME = "user_name",
|
||||||
|
KEY_TOKEN = "auth_token",
|
||||||
KEY_WIFI_ONLY = "wifi_only", // sync on WiFi only (default: false)
|
KEY_WIFI_ONLY = "wifi_only", // sync on WiFi only (default: false)
|
||||||
KEY_WIFI_ONLY_SSID = "wifi_only_ssid"; // restrict sync to specific WiFi SSID
|
KEY_WIFI_ONLY_SSID = "wifi_only_ssid"; // restrict sync to specific WiFi SSID
|
||||||
|
|
||||||
/** Time range limitation to the past [in days]
|
/**
|
||||||
value = null default value (DEFAULT_TIME_RANGE_PAST_DAYS)
|
* Time range limitation to the past [in days]
|
||||||
< 0 (-1) no limit
|
* value = null default value (DEFAULT_TIME_RANGE_PAST_DAYS)
|
||||||
>= 0 entries more than n days in the past won't be synchronized
|
* < 0 (-1) no limit
|
||||||
|
* >= 0 entries more than n days in the past won't be synchronized
|
||||||
*/
|
*/
|
||||||
private final static String KEY_TIME_RANGE_PAST_DAYS = "time_range_past_days";
|
private final static String KEY_TIME_RANGE_PAST_DAYS = "time_range_past_days";
|
||||||
private final static int DEFAULT_TIME_RANGE_PAST_DAYS = 90;
|
private final static int DEFAULT_TIME_RANGE_PAST_DAYS = 90;
|
||||||
@ -70,9 +53,10 @@ public class AccountSettings {
|
|||||||
"0" false */
|
"0" false */
|
||||||
private final static String KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors";
|
private final static String KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors";
|
||||||
|
|
||||||
/** Contact group method:
|
/**
|
||||||
value = null (not existing) groups as separate VCards (default)
|
* Contact group method:
|
||||||
"CATEGORIES" groups are per-contact CATEGORIES
|
* value = null (not existing) groups as separate VCards (default)
|
||||||
|
* "CATEGORIES" groups are per-contact CATEGORIES
|
||||||
*/
|
*/
|
||||||
private final static String KEY_CONTACT_GROUP_METHOD = "contact_group_method";
|
private final static String KEY_CONTACT_GROUP_METHOD = "contact_group_method";
|
||||||
|
|
||||||
@ -90,7 +74,7 @@ public class AccountSettings {
|
|||||||
|
|
||||||
accountManager = AccountManager.get(context);
|
accountManager = AccountManager.get(context);
|
||||||
|
|
||||||
synchronized(AccountSettings.class) {
|
synchronized (AccountSettings.class) {
|
||||||
String versionStr = accountManager.getUserData(account, KEY_SETTINGS_VERSION);
|
String versionStr = accountManager.getUserData(account, KEY_SETTINGS_VERSION);
|
||||||
if (versionStr == null)
|
if (versionStr == null)
|
||||||
throw new InvalidAccountException(account);
|
throw new InvalidAccountException(account);
|
||||||
@ -107,21 +91,52 @@ public class AccountSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Bundle initialUserData(String userName) {
|
public static Bundle initialUserData(URI uri, String userName) {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putString(KEY_SETTINGS_VERSION, String.valueOf(CURRENT_VERSION));
|
bundle.putString(KEY_SETTINGS_VERSION, String.valueOf(CURRENT_VERSION));
|
||||||
bundle.putString(KEY_USERNAME, userName);
|
bundle.putString(KEY_USERNAME, userName);
|
||||||
|
bundle.putString(KEY_URI, uri.toString());
|
||||||
return bundle;
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// authentication settings
|
// authentication settings
|
||||||
|
|
||||||
public String username() { return accountManager.getUserData(account, KEY_USERNAME); }
|
public URI getUri() {
|
||||||
public void username(@NonNull String userName) { accountManager.setUserData(account, KEY_USERNAME, userName); }
|
try {
|
||||||
|
return new URI(accountManager.getUserData(account, KEY_URI));
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public String password() { return accountManager.getPassword(account); }
|
public void setUri(@NonNull URI uri) {
|
||||||
public void password(@NonNull String password) { accountManager.setPassword(account, password); }
|
accountManager.setUserData(account, KEY_URI, uri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthToken() {
|
||||||
|
return accountManager.getUserData(account, KEY_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthToken(@NonNull String token) {
|
||||||
|
accountManager.setUserData(account, KEY_TOKEN, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String username() {
|
||||||
|
return accountManager.getUserData(account, KEY_USERNAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void username(@NonNull String userName) {
|
||||||
|
accountManager.setUserData(account, KEY_USERNAME, userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String password() {
|
||||||
|
return accountManager.getPassword(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void password(@NonNull String password) {
|
||||||
|
accountManager.setPassword(account, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// sync. settings
|
// sync. settings
|
||||||
@ -224,175 +239,6 @@ public class AccountSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings({ "Recycle", "unused" })
|
|
||||||
private void update_1_2() throws ContactsStorageException {
|
|
||||||
/* - KEY_ADDRESSBOOK_URL ("addressbook_url"),
|
|
||||||
- KEY_ADDRESSBOOK_CTAG ("addressbook_ctag"),
|
|
||||||
- KEY_ADDRESSBOOK_VCARD_VERSION ("addressbook_vcard_version") are not used anymore (now stored in ContactsContract.SyncState)
|
|
||||||
- KEY_LAST_ANDROID_VERSION ("last_android_version") has been added
|
|
||||||
*/
|
|
||||||
|
|
||||||
// move previous address book info to ContactsContract.SyncState
|
|
||||||
@Cleanup("release") ContentProviderClient provider = context.getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY);
|
|
||||||
if (provider == null)
|
|
||||||
throw new ContactsStorageException("Couldn't access Contacts provider");
|
|
||||||
|
|
||||||
LocalAddressBook addr = new LocalAddressBook(account, provider);
|
|
||||||
|
|
||||||
// until now, ContactsContract.Settings.UNGROUPED_VISIBLE was not set explicitly
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1);
|
|
||||||
addr.updateSettings(values);
|
|
||||||
|
|
||||||
String url = accountManager.getUserData(account, "addressbook_url");
|
|
||||||
if (!TextUtils.isEmpty(url))
|
|
||||||
addr.setURL(url);
|
|
||||||
accountManager.setUserData(account, "addressbook_url", null);
|
|
||||||
|
|
||||||
String cTag = accountManager.getUserData(account, "addressbook_ctag");
|
|
||||||
if (!TextUtils.isEmpty(cTag))
|
|
||||||
addr.setCTag(cTag);
|
|
||||||
accountManager.setUserData(account, "addressbook_ctag", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({ "Recycle", "unused" })
|
|
||||||
private void update_2_3() {
|
|
||||||
// Don't show a warning for Android updates anymore
|
|
||||||
accountManager.setUserData(account, "last_android_version", null);
|
|
||||||
|
|
||||||
Long serviceCardDAV = null, serviceCalDAV = null;
|
|
||||||
|
|
||||||
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(context);
|
|
||||||
try {
|
|
||||||
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
|
||||||
// we have to create the WebDAV Service database only from the old address book, calendar and task list URLs
|
|
||||||
|
|
||||||
// CardDAV: migrate address books
|
|
||||||
ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY);
|
|
||||||
if (client != null)
|
|
||||||
try {
|
|
||||||
LocalAddressBook addrBook = new LocalAddressBook(account, client);
|
|
||||||
String url = addrBook.getURL();
|
|
||||||
if (url != null) {
|
|
||||||
App.log.fine("Migrating address book " + url);
|
|
||||||
|
|
||||||
// insert CardDAV service
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
values.put(Services.ACCOUNT_NAME, account.name);
|
|
||||||
values.put(Services.SERVICE, Services.SERVICE_CARDDAV);
|
|
||||||
serviceCardDAV = db.insert(Services._TABLE, null, values);
|
|
||||||
|
|
||||||
// insert address book
|
|
||||||
values.clear();
|
|
||||||
values.put(Collections.SERVICE_ID, serviceCardDAV);
|
|
||||||
values.put(Collections.URL, url);
|
|
||||||
values.put(Collections.SYNC, 1);
|
|
||||||
db.insert(Collections._TABLE, null, values);
|
|
||||||
|
|
||||||
// insert home set
|
|
||||||
HttpUrl homeSet = HttpUrl.parse(url).resolve("../");
|
|
||||||
values.clear();
|
|
||||||
values.put(HomeSets.SERVICE_ID, serviceCardDAV);
|
|
||||||
values.put(HomeSets.URL, homeSet.toString());
|
|
||||||
db.insert(HomeSets._TABLE, null, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (ContactsStorageException e) {
|
|
||||||
App.log.log(Level.SEVERE, "Couldn't migrate address book", e);
|
|
||||||
} finally {
|
|
||||||
client.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
// CalDAV: migrate calendars + task lists
|
|
||||||
Set<String> collections = new HashSet<>();
|
|
||||||
Set<HttpUrl> homeSets = new HashSet<>();
|
|
||||||
|
|
||||||
client = context.getContentResolver().acquireContentProviderClient(CalendarContract.AUTHORITY);
|
|
||||||
if (client != null)
|
|
||||||
try {
|
|
||||||
LocalCalendar calendars[] = (LocalCalendar[])LocalCalendar.find(account, client, LocalCalendar.Factory.INSTANCE, null, null);
|
|
||||||
for (LocalCalendar calendar : calendars) {
|
|
||||||
String url = calendar.getName();
|
|
||||||
App.log.fine("Migrating calendar " + url);
|
|
||||||
collections.add(url);
|
|
||||||
homeSets.add(HttpUrl.parse(url).resolve("../"));
|
|
||||||
}
|
|
||||||
} catch (CalendarStorageException e) {
|
|
||||||
App.log.log(Level.SEVERE, "Couldn't migrate calendars", e);
|
|
||||||
} finally {
|
|
||||||
client.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskProvider provider = LocalTaskList.acquireTaskProvider(context.getContentResolver());
|
|
||||||
if (provider != null)
|
|
||||||
try {
|
|
||||||
LocalTaskList[] taskLists = (LocalTaskList[])LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, null, null);
|
|
||||||
for (LocalTaskList taskList : taskLists) {
|
|
||||||
String url = taskList.getSyncId();
|
|
||||||
App.log.fine("Migrating task list " + url);
|
|
||||||
collections.add(url);
|
|
||||||
homeSets.add(HttpUrl.parse(url).resolve("../"));
|
|
||||||
}
|
|
||||||
} catch (CalendarStorageException e) {
|
|
||||||
App.log.log(Level.SEVERE, "Couldn't migrate task lists", e);
|
|
||||||
} finally {
|
|
||||||
provider.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!collections.isEmpty()) {
|
|
||||||
// insert CalDAV service
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
values.put(Services.ACCOUNT_NAME, account.name);
|
|
||||||
values.put(Services.SERVICE, Services.SERVICE_CALDAV);
|
|
||||||
serviceCalDAV = db.insert(Services._TABLE, null, values);
|
|
||||||
|
|
||||||
// insert collections
|
|
||||||
for (String url : collections) {
|
|
||||||
values.clear();
|
|
||||||
values.put(Collections.SERVICE_ID, serviceCalDAV);
|
|
||||||
values.put(Collections.URL, url);
|
|
||||||
values.put(Collections.SYNC, 1);
|
|
||||||
db.insert(Collections._TABLE, null, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert home sets
|
|
||||||
for (HttpUrl homeSet : homeSets) {
|
|
||||||
values.clear();
|
|
||||||
values.put(HomeSets.SERVICE_ID, serviceCalDAV);
|
|
||||||
values.put(HomeSets.URL, homeSet.toString());
|
|
||||||
db.insert(HomeSets._TABLE, null, values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
dbHelper.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// initiate service detection (refresh) to get display names, colors etc.
|
|
||||||
Intent refresh = new Intent(context, DavService.class);
|
|
||||||
refresh.setAction(DavService.ACTION_REFRESH_COLLECTIONS);
|
|
||||||
if (serviceCardDAV != null) {
|
|
||||||
refresh.putExtra(DavService.EXTRA_DAV_SERVICE_ID, serviceCardDAV);
|
|
||||||
context.startService(refresh);
|
|
||||||
}
|
|
||||||
if (serviceCalDAV != null) {
|
|
||||||
refresh.putExtra(DavService.EXTRA_DAV_SERVICE_ID, serviceCalDAV);
|
|
||||||
context.startService(refresh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({ "Recycle", "unused" })
|
|
||||||
private void update_3_4() {
|
|
||||||
setGroupMethod(GroupMethod.CATEGORIES);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Android 7.1.1 OpenTasks fix */
|
|
||||||
@SuppressWarnings({ "Recycle", "unused" })
|
|
||||||
private void update_4_5() {
|
|
||||||
// call PackageChangedReceiver which then enables/disables OpenTasks sync when it's (not) available
|
|
||||||
PackageChangedReceiver.updateTaskSync(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static class AppUpdatedReceiver extends BroadcastReceiver {
|
public static class AppUpdatedReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -68,7 +68,6 @@ public class App extends Application {
|
|||||||
|
|
||||||
public final static Logger log = Logger.getLogger("davdroid");
|
public final static Logger log = Logger.getLogger("davdroid");
|
||||||
static {
|
static {
|
||||||
at.bitfire.dav4android.Constants.log = Logger.getLogger("davdroid.dav4android");
|
|
||||||
at.bitfire.cert4android.Constants.log = Logger.getLogger("davdroid.cert4android");
|
at.bitfire.cert4android.Constants.log = Logger.getLogger("davdroid.cert4android");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
|
||||||
* which accompanies this distribution, and is available at
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*/
|
|
||||||
package at.bitfire.davdroid;
|
|
||||||
|
|
||||||
import java.lang.reflect.Array;
|
|
||||||
|
|
||||||
public class ArrayUtils {
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <T> T[][] partition(T[] bigArray, int max) {
|
|
||||||
int nItems = bigArray.length;
|
|
||||||
int nPartArrays = (nItems + max-1)/max;
|
|
||||||
|
|
||||||
T[][] partArrays = (T[][])Array.newInstance(bigArray.getClass().getComponentType(), nPartArrays, 0);
|
|
||||||
|
|
||||||
// nItems is now the number of remaining items
|
|
||||||
for (int i = 0; nItems > 0; i++) {
|
|
||||||
int n = (nItems < max) ? nItems : max;
|
|
||||||
partArrays[i] = (T[])Array.newInstance(bigArray.getClass().getComponentType(), n);
|
|
||||||
System.arraycopy(bigArray, i*max, partArrays[i], 0, n);
|
|
||||||
|
|
||||||
nItems -= n;
|
|
||||||
}
|
|
||||||
|
|
||||||
return partArrays;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -9,6 +9,8 @@ package at.bitfire.davdroid;
|
|||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import static at.bitfire.davdroid.BuildConfig.DEBUG_REMOTE_URL;
|
||||||
|
|
||||||
public class Constants {
|
public class Constants {
|
||||||
|
|
||||||
public static final String
|
public static final String
|
||||||
@ -24,7 +26,15 @@ public class Constants {
|
|||||||
NOTIFICATION_TASK_SYNC = 12,
|
NOTIFICATION_TASK_SYNC = 12,
|
||||||
NOTIFICATION_PERMISSIONS = 20;
|
NOTIFICATION_PERMISSIONS = 20;
|
||||||
|
|
||||||
public static final Uri webUri = Uri.parse("https://davdroid.bitfire.at/?pk_campaign=davdroid-app");
|
public static final Uri webUri = Uri.parse("https://www.etesync.com/");
|
||||||
|
public static final Uri contactUri = Uri.parse("mailto:contact.app@etesync.com");
|
||||||
|
public static final Uri registrationUrl = webUri.buildUpon().appendEncodedPath("accounts/signup/").build();
|
||||||
|
public static final Uri reportIssueUri = Uri.parse("https://github.com/etesync/android/issues");
|
||||||
|
public static final Uri feedbackUri = reportIssueUri;
|
||||||
|
public static final Uri faqUri = webUri.buildUpon().appendEncodedPath("faq/").build();
|
||||||
|
public static final Uri helpUri = faqUri;
|
||||||
|
|
||||||
|
public static final Uri serviceUrl = Uri.parse((DEBUG_REMOTE_URL == null) ? "https://api.etesync.com/" : DEBUG_REMOTE_URL);
|
||||||
|
|
||||||
public static final int DEFAULT_SYNC_INTERVAL = 4 * 3600; // 4 hours
|
public static final int DEFAULT_SYNC_INTERVAL = 4 * 3600; // 4 hours
|
||||||
|
|
||||||
|
@ -11,8 +11,6 @@ package at.bitfire.davdroid;
|
|||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Notification;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@ -22,41 +20,25 @@ import android.database.sqlite.SQLiteDatabase;
|
|||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v4.app.NotificationManagerCompat;
|
|
||||||
import android.support.v7.app.NotificationCompat;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import org.apache.commons.collections4.iterators.IteratorChain;
|
|
||||||
import org.apache.commons.collections4.iterators.SingletonIterator;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import at.bitfire.dav4android.DavResource;
|
import at.bitfire.davdroid.journalmanager.Exceptions;
|
||||||
import at.bitfire.dav4android.UrlUtils;
|
import at.bitfire.davdroid.journalmanager.JournalManager;
|
||||||
import at.bitfire.dav4android.exception.DavException;
|
|
||||||
import at.bitfire.dav4android.exception.HttpException;
|
|
||||||
import at.bitfire.dav4android.property.AddressbookHomeSet;
|
|
||||||
import at.bitfire.dav4android.property.CalendarHomeSet;
|
|
||||||
import at.bitfire.dav4android.property.CalendarProxyReadFor;
|
|
||||||
import at.bitfire.dav4android.property.CalendarProxyWriteFor;
|
|
||||||
import at.bitfire.dav4android.property.GroupMembership;
|
|
||||||
import at.bitfire.davdroid.model.CollectionInfo;
|
import at.bitfire.davdroid.model.CollectionInfo;
|
||||||
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
||||||
import at.bitfire.davdroid.model.ServiceDB.HomeSets;
|
import at.bitfire.davdroid.model.ServiceDB.HomeSets;
|
||||||
import at.bitfire.davdroid.model.ServiceDB.OpenHelper;
|
import at.bitfire.davdroid.model.ServiceDB.OpenHelper;
|
||||||
import at.bitfire.davdroid.model.ServiceDB.Services;
|
import at.bitfire.davdroid.model.ServiceDB.Services;
|
||||||
import at.bitfire.davdroid.ui.DebugInfoActivity;
|
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
@ -165,7 +147,6 @@ public class DavService extends Service {
|
|||||||
private class RefreshCollections implements Runnable {
|
private class RefreshCollections implements Runnable {
|
||||||
final long service;
|
final long service;
|
||||||
final OpenHelper dbHelper;
|
final OpenHelper dbHelper;
|
||||||
SQLiteDatabase db;
|
|
||||||
|
|
||||||
RefreshCollections(long davServiceId) {
|
RefreshCollections(long davServiceId) {
|
||||||
this.service = davServiceId;
|
this.service = davServiceId;
|
||||||
@ -177,148 +158,56 @@ public class DavService extends Service {
|
|||||||
Account account = null;
|
Account account = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db = dbHelper.getWritableDatabase();
|
@Cleanup SQLiteDatabase db = dbHelper.getWritableDatabase();
|
||||||
|
|
||||||
String serviceType = serviceType();
|
String serviceType = dbHelper.getServiceType(db, service);
|
||||||
App.log.info("Refreshing " + serviceType + " collections of service #" + service);
|
App.log.info("Refreshing " + serviceType + " collections of service #" + service);
|
||||||
|
|
||||||
// get account
|
// get account
|
||||||
account = account();
|
account = dbHelper.getServiceAccount(db, service);
|
||||||
|
|
||||||
// create authenticating OkHttpClient (credentials taken from account settings)
|
|
||||||
OkHttpClient httpClient = HttpClient.create(DavService.this, account);
|
OkHttpClient httpClient = HttpClient.create(DavService.this, account);
|
||||||
|
|
||||||
// refresh home sets: principal
|
AccountSettings settings = new AccountSettings(DavService.this, account);
|
||||||
Set<HttpUrl> homeSets = readHomeSets();
|
JournalManager journalsManager = new JournalManager(httpClient, HttpUrl.get(settings.getUri()));
|
||||||
HttpUrl principal = readPrincipal();
|
|
||||||
if (principal != null) {
|
|
||||||
App.log.fine("Querying principal for home sets");
|
|
||||||
DavResource dav = new DavResource(httpClient, principal);
|
|
||||||
queryHomeSets(serviceType, dav, homeSets);
|
|
||||||
|
|
||||||
// refresh home sets: calendar-proxy-read/write-for
|
List<CollectionInfo> collections = new LinkedList<>();
|
||||||
CalendarProxyReadFor proxyRead = (CalendarProxyReadFor)dav.properties.get(CalendarProxyReadFor.NAME);
|
|
||||||
if (proxyRead != null)
|
|
||||||
for (String href : proxyRead.principals) {
|
|
||||||
App.log.fine("Principal is a read-only proxy for " + href + ", checking for home sets");
|
|
||||||
queryHomeSets(serviceType, new DavResource(httpClient, dav.location.resolve(href)), homeSets);
|
|
||||||
}
|
|
||||||
CalendarProxyWriteFor proxyWrite = (CalendarProxyWriteFor)dav.properties.get(CalendarProxyWriteFor.NAME);
|
|
||||||
if (proxyWrite != null)
|
|
||||||
for (String href : proxyWrite.principals) {
|
|
||||||
App.log.fine("Principal is a read-write proxy for " + href + ", checking for home sets");
|
|
||||||
queryHomeSets(serviceType, new DavResource(httpClient, dav.location.resolve(href)), homeSets);
|
|
||||||
}
|
|
||||||
|
|
||||||
// refresh home sets: direct group memberships
|
for (JournalManager.Journal journal : journalsManager.getJournals(settings.password())) {
|
||||||
GroupMembership groupMembership = (GroupMembership)dav.properties.get(GroupMembership.NAME);
|
CollectionInfo info = CollectionInfo.fromJson(journal.getContent(settings.password()));
|
||||||
if (groupMembership != null)
|
info.url = journal.getUuid();
|
||||||
for (String href : groupMembership.hrefs) {
|
if (info.isOfTypeService(serviceType)) {
|
||||||
App.log.fine("Principal is member of group " + href + ", checking for home sets");
|
collections.add(info);
|
||||||
DavResource group = new DavResource(httpClient, dav.location.resolve(href));
|
|
||||||
try {
|
|
||||||
queryHomeSets(serviceType, group, homeSets);
|
|
||||||
} catch(HttpException|DavException e) {
|
|
||||||
App.log.log(Level.WARNING, "Couldn't query member group ", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// now refresh collections (taken from home sets)
|
|
||||||
Map<HttpUrl, CollectionInfo> collections = readCollections();
|
|
||||||
|
|
||||||
// (remember selections before)
|
|
||||||
Set<HttpUrl> selectedCollections = new HashSet<>();
|
|
||||||
for (CollectionInfo info : collections.values())
|
|
||||||
if (info.selected)
|
|
||||||
selectedCollections.add(HttpUrl.parse(info.url));
|
|
||||||
|
|
||||||
for (Iterator<HttpUrl> itHomeSets = homeSets.iterator(); itHomeSets.hasNext(); ) {
|
|
||||||
HttpUrl homeSet = itHomeSets.next();
|
|
||||||
App.log.fine("Listing home set " + homeSet);
|
|
||||||
|
|
||||||
DavResource dav = new DavResource(httpClient, homeSet);
|
|
||||||
try {
|
|
||||||
dav.propfind(1, CollectionInfo.DAV_PROPERTIES);
|
|
||||||
IteratorChain<DavResource> itCollections = new IteratorChain<>(dav.members.iterator(), new SingletonIterator(dav));
|
|
||||||
while (itCollections.hasNext()) {
|
|
||||||
DavResource member = itCollections.next();
|
|
||||||
CollectionInfo info = CollectionInfo.fromDavResource(member);
|
|
||||||
info.confirmed = true;
|
|
||||||
App.log.log(Level.FINE, "Found collection", info);
|
|
||||||
|
|
||||||
if ((serviceType.equals(Services.SERVICE_CARDDAV) && info.type == CollectionInfo.Type.ADDRESS_BOOK) ||
|
|
||||||
(serviceType.equals(Services.SERVICE_CALDAV) && info.type == CollectionInfo.Type.CALENDAR))
|
|
||||||
collections.put(member.location, info);
|
|
||||||
}
|
|
||||||
} catch(HttpException e) {
|
|
||||||
if (e.status == 403 || e.status == 404 || e.status == 410)
|
|
||||||
// delete home set only if it was not accessible (40x)
|
|
||||||
itHomeSets.remove();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check/refresh unconfirmed collections
|
// FIXME: handle deletion from server
|
||||||
for (Iterator<Map.Entry<HttpUrl, CollectionInfo>> iterator = collections.entrySet().iterator(); iterator.hasNext(); ) {
|
|
||||||
Map.Entry<HttpUrl, CollectionInfo> entry = iterator.next();
|
|
||||||
HttpUrl url = entry.getKey();
|
|
||||||
CollectionInfo info = entry.getValue();
|
|
||||||
|
|
||||||
if (!info.confirmed)
|
if (collections.isEmpty()) {
|
||||||
try {
|
CollectionInfo info = CollectionInfo.defaultForService(serviceType);
|
||||||
DavResource dav = new DavResource(httpClient, url);
|
JournalManager.Journal journal = new JournalManager.Journal(settings.password(), info.toJson());
|
||||||
dav.propfind(0, CollectionInfo.DAV_PROPERTIES);
|
journalsManager.putJournal(journal);
|
||||||
info = CollectionInfo.fromDavResource(dav);
|
info.url = journal.getUuid();
|
||||||
info.confirmed = true;
|
collections.add(info);
|
||||||
|
|
||||||
// remove unusable collections
|
|
||||||
if ((serviceType.equals(Services.SERVICE_CARDDAV) && info.type != CollectionInfo.Type.ADDRESS_BOOK) ||
|
|
||||||
(serviceType.equals(Services.SERVICE_CALDAV) && info.type != CollectionInfo.Type.CALENDAR))
|
|
||||||
iterator.remove();
|
|
||||||
} catch(HttpException e) {
|
|
||||||
if (e.status == 403 || e.status == 404 || e.status == 410)
|
|
||||||
// delete collection only if it was not accessible (40x)
|
|
||||||
iterator.remove();
|
|
||||||
else
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// restore selections
|
|
||||||
for (HttpUrl url : selectedCollections) {
|
|
||||||
CollectionInfo info = collections.get(url);
|
|
||||||
if (info != null)
|
|
||||||
info.selected = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
db.beginTransactionNonExclusive();
|
db.beginTransactionNonExclusive();
|
||||||
try {
|
try {
|
||||||
saveHomeSets(homeSets);
|
saveCollections(db, collections);
|
||||||
saveCollections(collections.values());
|
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
} finally {
|
} finally {
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch(InvalidAccountException e) {
|
} catch (InvalidAccountException e) {
|
||||||
App.log.log(Level.SEVERE, "Invalid account", e);
|
// FIXME: Do something
|
||||||
} catch(IOException|HttpException|DavException e) {
|
e.printStackTrace();
|
||||||
App.log.log(Level.SEVERE, "Couldn't refresh collection list", e);
|
} catch (Exceptions.HttpException e) {
|
||||||
|
// FIXME: do something
|
||||||
Intent debugIntent = new Intent(DavService.this, DebugInfoActivity.class);
|
e.printStackTrace();
|
||||||
debugIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e);
|
} catch (Exceptions.IntegrityException e) {
|
||||||
if (account != null)
|
// FIXME: do something
|
||||||
debugIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account);
|
e.printStackTrace();
|
||||||
|
|
||||||
NotificationManagerCompat nm = NotificationManagerCompat.from(DavService.this);
|
|
||||||
Notification notify = new NotificationCompat.Builder(DavService.this)
|
|
||||||
.setSmallIcon(R.drawable.ic_error_light)
|
|
||||||
.setLargeIcon(App.getLauncherBitmap(DavService.this))
|
|
||||||
.setContentTitle(getString(R.string.dav_service_refresh_failed))
|
|
||||||
.setContentText(getString(R.string.dav_service_refresh_couldnt_refresh))
|
|
||||||
.setContentIntent(PendingIntent.getActivity(DavService.this, 0, debugIntent, PendingIntent.FLAG_UPDATE_CURRENT))
|
|
||||||
.build();
|
|
||||||
nm.notify(Constants.NOTIFICATION_REFRESH_COLLECTIONS, notify);
|
|
||||||
} finally {
|
} finally {
|
||||||
dbHelper.close();
|
dbHelper.close();
|
||||||
|
|
||||||
@ -331,91 +220,20 @@ public class DavService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the given URL defines home sets and adds them to the home set list.
|
|
||||||
* @param serviceType CalDAV/CardDAV (calendar home set / addressbook home set)
|
|
||||||
* @param dav DavResource to check
|
|
||||||
* @param homeSets set where found home set URLs will be put into
|
|
||||||
*/
|
|
||||||
private void queryHomeSets(String serviceType, DavResource dav, Set<HttpUrl> homeSets) throws IOException, HttpException, DavException {
|
|
||||||
if (Services.SERVICE_CARDDAV.equals(serviceType)) {
|
|
||||||
dav.propfind(0, AddressbookHomeSet.NAME, GroupMembership.NAME);
|
|
||||||
AddressbookHomeSet addressbookHomeSet = (AddressbookHomeSet)dav.properties.get(AddressbookHomeSet.NAME);
|
|
||||||
if (addressbookHomeSet != null)
|
|
||||||
for (String href : addressbookHomeSet.hrefs)
|
|
||||||
homeSets.add(UrlUtils.withTrailingSlash(dav.location.resolve(href)));
|
|
||||||
} else if (Services.SERVICE_CALDAV.equals(serviceType)) {
|
|
||||||
dav.propfind(0, CalendarHomeSet.NAME, CalendarProxyReadFor.NAME, CalendarProxyWriteFor.NAME, GroupMembership.NAME);
|
|
||||||
CalendarHomeSet calendarHomeSet = (CalendarHomeSet)dav.properties.get(CalendarHomeSet.NAME);
|
|
||||||
if (calendarHomeSet != null)
|
|
||||||
for (String href : calendarHomeSet.hrefs)
|
|
||||||
homeSets.add(UrlUtils.withTrailingSlash(dav.location.resolve(href)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private Account account() {
|
private Map<String, CollectionInfo> readCollections(SQLiteDatabase db) {
|
||||||
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[] { Services.ACCOUNT_NAME }, Services.ID + "=?", new String[] { String.valueOf(service) }, null, null, null);
|
Map<String, CollectionInfo> collections = new LinkedHashMap<>();
|
||||||
if (cursor.moveToNext()) {
|
|
||||||
return new Account(cursor.getString(0), Constants.ACCOUNT_TYPE);
|
|
||||||
} else
|
|
||||||
throw new IllegalArgumentException("Service not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private String serviceType() {
|
|
||||||
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[] { Services.SERVICE }, Services.ID + "=?", new String[] { String.valueOf(service) }, null, null, null);
|
|
||||||
if (cursor.moveToNext())
|
|
||||||
return cursor.getString(0);
|
|
||||||
else
|
|
||||||
throw new IllegalArgumentException("Service not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private HttpUrl readPrincipal() {
|
|
||||||
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[] { Services.PRINCIPAL }, Services.ID + "=?", new String[] { String.valueOf(service) }, null, null, null);
|
|
||||||
if (cursor.moveToNext()) {
|
|
||||||
String principal = cursor.getString(0);
|
|
||||||
if (principal != null)
|
|
||||||
return HttpUrl.parse(cursor.getString(0));
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private Set<HttpUrl> readHomeSets() {
|
|
||||||
Set<HttpUrl> homeSets = new LinkedHashSet<>();
|
|
||||||
@Cleanup Cursor cursor = db.query(HomeSets._TABLE, new String[] { HomeSets.URL }, HomeSets.SERVICE_ID + "=?", new String[] { String.valueOf(service) }, null, null, null);
|
|
||||||
while (cursor.moveToNext())
|
|
||||||
homeSets.add(HttpUrl.parse(cursor.getString(0)));
|
|
||||||
return homeSets;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveHomeSets(Set<HttpUrl> homeSets) {
|
|
||||||
db.delete(HomeSets._TABLE, HomeSets.SERVICE_ID + "=?", new String[] { String.valueOf(service) });
|
|
||||||
for (HttpUrl homeSet : homeSets) {
|
|
||||||
ContentValues values = new ContentValues(1);
|
|
||||||
values.put(HomeSets.SERVICE_ID, service);
|
|
||||||
values.put(HomeSets.URL, homeSet.toString());
|
|
||||||
db.insertOrThrow(HomeSets._TABLE, null, values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private Map<HttpUrl, CollectionInfo> readCollections() {
|
|
||||||
Map<HttpUrl, CollectionInfo> collections = new LinkedHashMap<>();
|
|
||||||
@Cleanup Cursor cursor = db.query(Collections._TABLE, null, Collections.SERVICE_ID + "=?", new String[]{String.valueOf(service)}, null, null, null);
|
@Cleanup Cursor cursor = db.query(Collections._TABLE, null, Collections.SERVICE_ID + "=?", new String[]{String.valueOf(service)}, null, null, null);
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
DatabaseUtils.cursorRowToContentValues(cursor, values);
|
DatabaseUtils.cursorRowToContentValues(cursor, values);
|
||||||
collections.put(HttpUrl.parse(values.getAsString(Collections.URL)), CollectionInfo.fromDB(values));
|
collections.put(values.getAsString(Collections.URL), CollectionInfo.fromDB(values));
|
||||||
}
|
}
|
||||||
return collections;
|
return collections;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveCollections(Iterable<CollectionInfo> collections) {
|
private void saveCollections(SQLiteDatabase db, Iterable<CollectionInfo> collections) {
|
||||||
db.delete(Collections._TABLE, HomeSets.SERVICE_ID + "=?", new String[] { String.valueOf(service) });
|
db.delete(Collections._TABLE, HomeSets.SERVICE_ID + "=?", new String[]{String.valueOf(service)});
|
||||||
for (CollectionInfo collection : collections) {
|
for (CollectionInfo collection : collections) {
|
||||||
ContentValues values = collection.toDB();
|
ContentValues values = collection.toDB();
|
||||||
App.log.log(Level.FINE, "Saving collection", values);
|
App.log.log(Level.FINE, "Saving collection", values);
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
|
||||||
* which accompanies this distribution, and is available at
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
package at.bitfire.davdroid;
|
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
|
|
||||||
public class DavUtils {
|
|
||||||
|
|
||||||
public static String ARGBtoCalDAVColor(int colorWithAlpha) {
|
|
||||||
byte alpha = (byte)(colorWithAlpha >> 24);
|
|
||||||
int color = colorWithAlpha & 0xFFFFFF;
|
|
||||||
return String.format("#%06X%02X", color, alpha);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String lastSegmentOfUrl(@NonNull String url) {
|
|
||||||
// the list returned by HttpUrl.pathSegments() is unmodifiable, so we have to create a copy
|
|
||||||
List<String> segments = new LinkedList<>(HttpUrl.parse(url).pathSegments());
|
|
||||||
Collections.reverse(segments);
|
|
||||||
|
|
||||||
for (String segment : segments)
|
|
||||||
if (!StringUtils.isEmpty(segment))
|
|
||||||
return segment;
|
|
||||||
|
|
||||||
return "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
31
app/src/main/java/at/bitfire/davdroid/GsonHelper.java
Normal file
31
app/src/main/java/at/bitfire/davdroid/GsonHelper.java
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package at.bitfire.davdroid;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonDeserializationContext;
|
||||||
|
import com.google.gson.JsonDeserializer;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import com.google.gson.JsonSerializationContext;
|
||||||
|
import com.google.gson.JsonSerializer;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
|
||||||
|
public class GsonHelper {
|
||||||
|
public static final Gson gson = new GsonBuilder().registerTypeHierarchyAdapter(byte[].class,
|
||||||
|
new ByteArrayToBase64TypeAdapter()).create();
|
||||||
|
|
||||||
|
// Using Android's base64 libraries. This can be replaced with any base64 library.
|
||||||
|
private static class ByteArrayToBase64TypeAdapter implements JsonSerializer<byte[]>, JsonDeserializer<byte[]> {
|
||||||
|
public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
|
return Base64.decode(json.getAsString(), Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
return new JsonPrimitive(Base64.encodeToString(src, Base64.NO_WRAP));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,6 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import at.bitfire.dav4android.BasicDigestAuthHandler;
|
|
||||||
import at.bitfire.davdroid.model.ServiceDB;
|
import at.bitfire.davdroid.model.ServiceDB;
|
||||||
import at.bitfire.davdroid.model.Settings;
|
import at.bitfire.davdroid.model.Settings;
|
||||||
import okhttp3.Interceptor;
|
import okhttp3.Interceptor;
|
||||||
@ -39,9 +38,10 @@ public class HttpClient {
|
|||||||
private static final UserAgentInterceptor userAgentInterceptor = new UserAgentInterceptor();
|
private static final UserAgentInterceptor userAgentInterceptor = new UserAgentInterceptor();
|
||||||
|
|
||||||
private static final String userAgent;
|
private static final String userAgent;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
String date = new SimpleDateFormat("yyyy/MM/dd", Locale.US).format(new Date(BuildConfig.buildTime));
|
String date = new SimpleDateFormat("yyyy/MM/dd", Locale.US).format(new Date(BuildConfig.buildTime));
|
||||||
userAgent = "DAVdroid/" + BuildConfig.VERSION_NAME + " (" + date + "; dav4android; okhttp3) Android/" + Build.VERSION.RELEASE;
|
userAgent = "DAVdroid/" + BuildConfig.VERSION_NAME + " (" + date + "; okhttp3) Android/" + Build.VERSION.RELEASE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpClient() {
|
private HttpClient() {
|
||||||
@ -52,7 +52,7 @@ public class HttpClient {
|
|||||||
|
|
||||||
// use account settings for authentication
|
// use account settings for authentication
|
||||||
AccountSettings settings = new AccountSettings(context, account);
|
AccountSettings settings = new AccountSettings(context, account);
|
||||||
builder = addAuthentication(builder, null, settings.username(), settings.password());
|
builder = addAuthentication(builder, null, settings.getAuthToken());
|
||||||
|
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
@ -75,7 +75,7 @@ public class HttpClient {
|
|||||||
|
|
||||||
// use MemorizingTrustManager to manage self-signed certificates
|
// use MemorizingTrustManager to manage self-signed certificates
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
App app = (App)context.getApplicationContext();
|
App app = (App) context.getApplicationContext();
|
||||||
if (App.getSslSocketFactoryCompat() != null && app.getCertManager() != null)
|
if (App.getSslSocketFactoryCompat() != null && app.getCertManager() != null)
|
||||||
builder.sslSocketFactory(App.getSslSocketFactoryCompat(), app.getCertManager());
|
builder.sslSocketFactory(App.getSslSocketFactoryCompat(), app.getCertManager());
|
||||||
if (App.getHostnameVerifier() != null)
|
if (App.getHostnameVerifier() != null)
|
||||||
@ -87,9 +87,6 @@ public class HttpClient {
|
|||||||
builder.writeTimeout(30, TimeUnit.SECONDS);
|
builder.writeTimeout(30, TimeUnit.SECONDS);
|
||||||
builder.readTimeout(120, TimeUnit.SECONDS);
|
builder.readTimeout(120, TimeUnit.SECONDS);
|
||||||
|
|
||||||
// don't allow redirects, because it would break PROPFIND handling
|
|
||||||
builder.followRedirects(false);
|
|
||||||
|
|
||||||
// custom proxy support
|
// custom proxy support
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
SQLiteOpenHelper dbHelper = new ServiceDB.OpenHelper(context);
|
SQLiteOpenHelper dbHelper = new ServiceDB.OpenHelper(context);
|
||||||
@ -105,7 +102,7 @@ public class HttpClient {
|
|||||||
builder.proxy(proxy);
|
builder.proxy(proxy);
|
||||||
App.log.log(Level.INFO, "Using proxy", proxy);
|
App.log.log(Level.INFO, "Using proxy", proxy);
|
||||||
}
|
}
|
||||||
} catch(IllegalArgumentException|NullPointerException e) {
|
} catch (IllegalArgumentException | NullPointerException e) {
|
||||||
App.log.log(Level.SEVERE, "Can't set proxy, ignoring", e);
|
App.log.log(Level.SEVERE, "Can't set proxy, ignoring", e);
|
||||||
} finally {
|
} finally {
|
||||||
dbHelper.close();
|
dbHelper.close();
|
||||||
@ -115,9 +112,6 @@ public class HttpClient {
|
|||||||
// add User-Agent to every request
|
// add User-Agent to every request
|
||||||
builder.addNetworkInterceptor(userAgentInterceptor);
|
builder.addNetworkInterceptor(userAgentInterceptor);
|
||||||
|
|
||||||
// add cookie store for non-persistent cookies (some services like Horde use cookies for session tracking)
|
|
||||||
builder.cookieJar(new MemoryCookieStore());
|
|
||||||
|
|
||||||
// add network logging, if requested
|
// add network logging, if requested
|
||||||
if (logger.isLoggable(Level.FINEST)) {
|
if (logger.isLoggable(Level.FINEST)) {
|
||||||
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
|
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
|
||||||
@ -126,32 +120,46 @@ public class HttpClient {
|
|||||||
logger.finest(message);
|
logger.finest(message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
|
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
|
||||||
builder.addInterceptor(loggingInterceptor);
|
builder.addInterceptor(loggingInterceptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static OkHttpClient.Builder addAuthentication(@NonNull OkHttpClient.Builder builder, @Nullable String host, @NonNull String username, @NonNull String password) {
|
private static OkHttpClient.Builder addAuthentication(@NonNull OkHttpClient.Builder builder, @Nullable String host, @NonNull String token) {
|
||||||
BasicDigestAuthHandler authHandler = new BasicDigestAuthHandler(host, username, password);
|
TokenAuthenticator authHandler = new TokenAuthenticator(host, token);
|
||||||
return builder
|
|
||||||
.addNetworkInterceptor(authHandler)
|
return builder.addNetworkInterceptor(authHandler);
|
||||||
.authenticator(authHandler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OkHttpClient addAuthentication(@NonNull OkHttpClient client, @NonNull String username, @NonNull String password) {
|
private static class TokenAuthenticator implements Interceptor {
|
||||||
OkHttpClient.Builder builder = client.newBuilder();
|
protected static final String
|
||||||
addAuthentication(builder, null, username, password);
|
HEADER_AUTHORIZATION = "Authorization";
|
||||||
return builder.build();
|
|
||||||
|
// FIXME: Host is not used
|
||||||
|
final String host, token;
|
||||||
|
|
||||||
|
|
||||||
|
private TokenAuthenticator(String host, String token) {
|
||||||
|
this.host = host;
|
||||||
|
this.token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OkHttpClient addAuthentication(@NonNull OkHttpClient client, @NonNull String host, @NonNull String username, @NonNull String password) {
|
@Override
|
||||||
OkHttpClient.Builder builder = client.newBuilder();
|
public Response intercept(Chain chain) throws IOException {
|
||||||
addAuthentication(builder, host, username, password);
|
Request request = chain.request();
|
||||||
return builder.build();
|
|
||||||
|
if ((token != null)
|
||||||
|
&& (request.header(HEADER_AUTHORIZATION) == null)) {
|
||||||
|
request = request.newBuilder()
|
||||||
|
.header(HEADER_AUTHORIZATION, "Token " + token)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return chain.proceed(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static class UserAgentInterceptor implements Interceptor {
|
static class UserAgentInterceptor implements Interceptor {
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
|
||||||
* which accompanies this distribution, and is available at
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
package at.bitfire.davdroid;
|
|
||||||
|
|
||||||
import org.apache.commons.collections4.MapIterator;
|
|
||||||
import org.apache.commons.collections4.keyvalue.MultiKey;
|
|
||||||
import org.apache.commons.collections4.map.HashedMap;
|
|
||||||
import org.apache.commons.collections4.map.MultiKeyMap;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import okhttp3.Cookie;
|
|
||||||
import okhttp3.CookieJar;
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Primitive cookie store that stores cookies in a (volatile) hash map.
|
|
||||||
* Will be sufficient for session cookies.
|
|
||||||
*/
|
|
||||||
public class MemoryCookieStore implements CookieJar {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stored cookies. The multi-key consists of three parts: name, domain, and path.
|
|
||||||
* This ensures that cookies can be overwritten. [RFC 6265 5.3 Storage Model]
|
|
||||||
* Not thread-safe!
|
|
||||||
*/
|
|
||||||
protected final MultiKeyMap<String, Cookie> storage = MultiKeyMap.multiKeyMap(new HashedMap<MultiKey<? extends String>, Cookie>());
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
|
|
||||||
synchronized(storage) {
|
|
||||||
for (Cookie cookie : cookies)
|
|
||||||
storage.put(cookie.name(), cookie.domain(), cookie.path(), cookie);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Cookie> loadForRequest(HttpUrl url) {
|
|
||||||
List<Cookie> cookies = new LinkedList<>();
|
|
||||||
|
|
||||||
synchronized(storage) {
|
|
||||||
MapIterator<MultiKey<? extends String>, Cookie> iter = storage.mapIterator();
|
|
||||||
while (iter.hasNext()) {
|
|
||||||
iter.next();
|
|
||||||
Cookie cookie = iter.getValue();
|
|
||||||
|
|
||||||
// remove expired cookies
|
|
||||||
if (cookie.expiresAt() <= System.currentTimeMillis()) {
|
|
||||||
iter.remove();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add applicable cookies
|
|
||||||
if (cookie.matches(url))
|
|
||||||
cookies.add(cookie);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cookies;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,104 @@
|
|||||||
|
package at.bitfire.davdroid.journalmanager;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.Charsets;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import at.bitfire.davdroid.App;
|
||||||
|
import at.bitfire.davdroid.GsonHelper;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
import static at.bitfire.davdroid.journalmanager.Helpers.hmac;
|
||||||
|
|
||||||
|
abstract class BaseManager {
|
||||||
|
final static protected MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||||
|
|
||||||
|
protected HttpUrl remote;
|
||||||
|
protected OkHttpClient client;
|
||||||
|
|
||||||
|
Response newCall(Request request) throws Exceptions.HttpException {
|
||||||
|
Response response;
|
||||||
|
try {
|
||||||
|
response = client.newCall(request).execute();
|
||||||
|
} catch (IOException e) {
|
||||||
|
App.log.log(Level.SEVERE, "Couldn't download external resource", e);
|
||||||
|
throw new Exceptions.ServiceUnavailableException("Failed downloading");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
switch (response.code()) {
|
||||||
|
case 401:
|
||||||
|
throw new Exceptions.UnauthorizedException("Failed to connect");
|
||||||
|
default:
|
||||||
|
throw new Exceptions.HttpException("Error getting");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Base {
|
||||||
|
@Setter(AccessLevel.PACKAGE)
|
||||||
|
@Getter(AccessLevel.PACKAGE)
|
||||||
|
private byte[] content;
|
||||||
|
@Setter(AccessLevel.PACKAGE)
|
||||||
|
private String uid;
|
||||||
|
|
||||||
|
public String getUuid() {
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContent(String keyBase64) {
|
||||||
|
// FIXME: probably cache encryption object
|
||||||
|
return new String(new Helpers.Cipher().decrypt(keyBase64, content), Charsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setContent(String keyBase64, String content) {
|
||||||
|
// FIXME: probably cache encryption object
|
||||||
|
this.content = new Helpers.Cipher().encrypt(keyBase64, content.getBytes(Charsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] calculateHmac(String keyBase64, String uuid) {
|
||||||
|
ByteArrayOutputStream hashContent = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (uuid != null) {
|
||||||
|
hashContent.write(uuid.getBytes(Charsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
hashContent.write(content);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// FIXME: Do something
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return hmac(keyBase64, hashContent.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Base() {
|
||||||
|
}
|
||||||
|
|
||||||
|
Base(String keyBase64, String content, String uid) {
|
||||||
|
setContent(keyBase64, content);
|
||||||
|
setUid(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getClass().getSimpleName() + "<" + uid + ">";
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() {
|
||||||
|
return GsonHelper.gson.toJson(this, getClass());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package at.bitfire.davdroid.journalmanager;
|
||||||
|
|
||||||
|
public class Exceptions {
|
||||||
|
public static class UnauthorizedException extends HttpException {
|
||||||
|
public UnauthorizedException(String message) {
|
||||||
|
super(401, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ServiceUnavailableException extends HttpException {
|
||||||
|
public ServiceUnavailableException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class HttpException extends Exception {
|
||||||
|
public HttpException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpException(int status, String message) {
|
||||||
|
super(status + " " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class IntegrityException extends Exception {
|
||||||
|
public IntegrityException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
package at.bitfire.davdroid.journalmanager;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.Charsets;
|
||||||
|
import org.spongycastle.crypto.BufferedBlockCipher;
|
||||||
|
import org.spongycastle.crypto.CipherParameters;
|
||||||
|
import org.spongycastle.crypto.InvalidCipherTextException;
|
||||||
|
import org.spongycastle.crypto.digests.SHA256Digest;
|
||||||
|
import org.spongycastle.crypto.engines.AESEngine;
|
||||||
|
import org.spongycastle.crypto.generators.SCrypt;
|
||||||
|
import org.spongycastle.crypto.macs.HMac;
|
||||||
|
import org.spongycastle.crypto.modes.CBCBlockCipher;
|
||||||
|
import org.spongycastle.crypto.paddings.BlockCipherPadding;
|
||||||
|
import org.spongycastle.crypto.paddings.PKCS7Padding;
|
||||||
|
import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher;
|
||||||
|
import org.spongycastle.crypto.params.KeyParameter;
|
||||||
|
import org.spongycastle.crypto.params.ParametersWithIV;
|
||||||
|
import org.spongycastle.util.encoders.Hex;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class Helpers {
|
||||||
|
// FIXME: This should be somewhere else
|
||||||
|
public static String deriveKey(String salt, String password) {
|
||||||
|
final int keySize = 190;
|
||||||
|
|
||||||
|
return Base64.encodeToString(SCrypt.generate(password.getBytes(Charsets.UTF_8), salt.getBytes(Charsets.UTF_8), 16384, 8, 1, keySize), Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] hmac256(byte[] keyByte, byte[] data) {
|
||||||
|
HMac hmac = new HMac(new SHA256Digest());
|
||||||
|
KeyParameter key = new KeyParameter(keyByte);
|
||||||
|
byte[] ret = new byte[hmac.getMacSize()];
|
||||||
|
hmac.init(key);
|
||||||
|
hmac.update(data, 0, data.length);
|
||||||
|
hmac.doFinal(ret, 0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] hmac(String keyBase64, byte[] data) {
|
||||||
|
byte[] derivedKey = hmac256("hmac".getBytes(Charsets.UTF_8), Base64.decode(keyBase64, Base64.NO_WRAP));
|
||||||
|
return hmac256(derivedKey, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Cipher {
|
||||||
|
SecureRandom random;
|
||||||
|
|
||||||
|
Cipher() {
|
||||||
|
random = new SecureRandom();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int blockSize = 16; // AES's block size in bytes
|
||||||
|
|
||||||
|
private BufferedBlockCipher getCipher(String keyBase64, byte[] iv, boolean encrypt) {
|
||||||
|
byte[] derivedKey = hmac256("aes".getBytes(Charsets.UTF_8), Base64.decode(keyBase64, Base64.NO_WRAP));
|
||||||
|
KeyParameter key = new KeyParameter(derivedKey);
|
||||||
|
CipherParameters params = new ParametersWithIV(key, iv);
|
||||||
|
|
||||||
|
BlockCipherPadding padding = new PKCS7Padding();
|
||||||
|
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(
|
||||||
|
new CBCBlockCipher(new AESEngine()), padding);
|
||||||
|
cipher.reset();
|
||||||
|
cipher.init(encrypt, params);
|
||||||
|
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] decrypt(String keyBase64, byte[] _data) {
|
||||||
|
byte[] iv = Arrays.copyOfRange(_data, 0, blockSize);
|
||||||
|
byte[] data = Arrays.copyOfRange(_data, blockSize, _data.length);
|
||||||
|
|
||||||
|
BufferedBlockCipher cipher = getCipher(keyBase64, iv, false);
|
||||||
|
|
||||||
|
byte[] buf = new byte[cipher.getOutputSize(data.length)];
|
||||||
|
int len = cipher.processBytes(data, 0, data.length, buf, 0);
|
||||||
|
try {
|
||||||
|
len += cipher.doFinal(buf, len);
|
||||||
|
} catch (InvalidCipherTextException e) {
|
||||||
|
// FIXME: Fail!
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove padding
|
||||||
|
byte[] out = new byte[len];
|
||||||
|
System.arraycopy(buf, 0, out, 0, len);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] encrypt(String keyBase64, byte[] data) {
|
||||||
|
byte[] iv = new byte[blockSize];
|
||||||
|
random.nextBytes(iv);
|
||||||
|
|
||||||
|
BufferedBlockCipher cipher = getCipher(keyBase64, iv, true);
|
||||||
|
|
||||||
|
byte[] buf = new byte[cipher.getOutputSize(data.length) + blockSize];
|
||||||
|
System.arraycopy(iv, 0, buf, 0, iv.length);
|
||||||
|
int len = iv.length + cipher.processBytes(data, 0, data.length, buf, iv.length);
|
||||||
|
try {
|
||||||
|
cipher.doFinal(buf, len);
|
||||||
|
} catch (InvalidCipherTextException e) {
|
||||||
|
// FIXME: Fail!
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FIXME: Replace with bouncy-castle once available. */
|
||||||
|
static String sha256(String base) {
|
||||||
|
return sha256(base.getBytes(Charsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FIXME: Replace with bouncy-castle once available. */
|
||||||
|
static String sha256(byte[] base) {
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] hash = digest.digest(base);
|
||||||
|
return Hex.toHexString(hash);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package at.bitfire.davdroid.journalmanager;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import at.bitfire.davdroid.App;
|
||||||
|
import at.bitfire.davdroid.GsonHelper;
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class JournalAuthenticator {
|
||||||
|
private HttpUrl remote;
|
||||||
|
private OkHttpClient client;
|
||||||
|
|
||||||
|
public JournalAuthenticator(OkHttpClient client, HttpUrl remote) {
|
||||||
|
this.client = client;
|
||||||
|
this.remote = remote.newBuilder()
|
||||||
|
.addPathSegments("api-token-auth")
|
||||||
|
.addPathSegment("")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AuthResponse {
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
private AuthResponse() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthToken(String username, String password) throws Exceptions.HttpException {
|
||||||
|
FormBody.Builder formBuilder = new FormBody.Builder()
|
||||||
|
.add("username", username)
|
||||||
|
.add("password", password);
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.post(formBuilder.build())
|
||||||
|
.url(remote)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Response response = client.newCall(request).execute();
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
return GsonHelper.gson.fromJson(response.body().charStream(), AuthResponse.class).token;
|
||||||
|
} else if (response.code() == 400) {
|
||||||
|
throw new Exceptions.UnauthorizedException("Username or password incorrect");
|
||||||
|
} else {
|
||||||
|
throw new Exceptions.HttpException("Error authenticating");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
App.log.log(Level.SEVERE, "Couldn't download external resource", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,112 @@
|
|||||||
|
package at.bitfire.davdroid.journalmanager;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import aQute.lib.hex.Hex;
|
||||||
|
import at.bitfire.davdroid.App;
|
||||||
|
import at.bitfire.davdroid.GsonHelper;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
|
public class JournalEntryManager extends BaseManager {
|
||||||
|
final static private Type entryType = new TypeToken<List<Entry>>() {
|
||||||
|
}.getType();
|
||||||
|
|
||||||
|
|
||||||
|
public JournalEntryManager(OkHttpClient httpClient, HttpUrl remote, String journal) {
|
||||||
|
this.remote = remote.newBuilder()
|
||||||
|
.addPathSegments("api/v1/journal")
|
||||||
|
.addPathSegments(journal)
|
||||||
|
.addPathSegment("")
|
||||||
|
.build();
|
||||||
|
App.log.info("Created for: " + this.remote.toString());
|
||||||
|
|
||||||
|
this.client = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Entry> getEntries(String keyBase64, String last) throws Exceptions.HttpException, Exceptions.IntegrityException {
|
||||||
|
Entry previousEntry = null;
|
||||||
|
HttpUrl.Builder urlBuilder = this.remote.newBuilder();
|
||||||
|
if (last != null) {
|
||||||
|
urlBuilder.addQueryParameter("last", last);
|
||||||
|
previousEntry = Entry.getFakeWithUid(last);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpUrl remote = urlBuilder.build();
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.get()
|
||||||
|
.url(remote)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Response response = newCall(request);
|
||||||
|
ResponseBody body = response.body();
|
||||||
|
List<Entry> ret = GsonHelper.gson.fromJson(body.charStream(), entryType);
|
||||||
|
|
||||||
|
for (Entry entry : ret) {
|
||||||
|
entry.verify(keyBase64, previousEntry);
|
||||||
|
previousEntry = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putEntries(List<Entry> entries, String last) throws Exceptions.HttpException {
|
||||||
|
HttpUrl.Builder urlBuilder = this.remote.newBuilder();
|
||||||
|
if (last != null) {
|
||||||
|
urlBuilder.addQueryParameter("last", last);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpUrl remote = urlBuilder.build();
|
||||||
|
|
||||||
|
RequestBody body = RequestBody.create(JSON, GsonHelper.gson.toJson(entries, entryType));
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.post(body)
|
||||||
|
.url(remote)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
newCall(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Entry extends Base {
|
||||||
|
public Entry() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(String keyBase64, String content, Entry previous) {
|
||||||
|
setContent(keyBase64, content);
|
||||||
|
setUid(calculateHmac(keyBase64, previous));
|
||||||
|
}
|
||||||
|
|
||||||
|
void verify(String keyBase64, Entry previous) throws Exceptions.IntegrityException {
|
||||||
|
String correctHash = calculateHmac(keyBase64, previous);
|
||||||
|
if (!getUuid().equals(correctHash)) {
|
||||||
|
throw new Exceptions.IntegrityException("Bad HMAC. " + getUuid() + " != " + correctHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Entry getFakeWithUid(String uid) {
|
||||||
|
Entry ret = new Entry();
|
||||||
|
ret.setUid(uid);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String calculateHmac(String keyBase64, Entry previous) {
|
||||||
|
String uuid = null;
|
||||||
|
if (previous != null) {
|
||||||
|
uuid = previous.getUuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Hex.toHexString(calculateHmac(keyBase64, uuid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,124 @@
|
|||||||
|
package at.bitfire.davdroid.journalmanager;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.spongycastle.util.Arrays;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import aQute.lib.hex.Hex;
|
||||||
|
import at.bitfire.davdroid.App;
|
||||||
|
import at.bitfire.davdroid.GsonHelper;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
|
import static at.bitfire.davdroid.journalmanager.Helpers.sha256;
|
||||||
|
|
||||||
|
public class JournalManager extends BaseManager {
|
||||||
|
final static private Type journalType = new TypeToken<List<Journal>>() {
|
||||||
|
}.getType();
|
||||||
|
|
||||||
|
|
||||||
|
public JournalManager(OkHttpClient httpClient, HttpUrl remote) {
|
||||||
|
this.remote = remote.newBuilder()
|
||||||
|
.addPathSegments("api/v1/journals")
|
||||||
|
.addPathSegment("")
|
||||||
|
.build();
|
||||||
|
App.log.info("Created for: " + this.remote.toString());
|
||||||
|
|
||||||
|
this.client = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Journal> getJournals(String keyBase64) throws Exceptions.HttpException, Exceptions.IntegrityException {
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.get()
|
||||||
|
.url(remote)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Response response = newCall(request);
|
||||||
|
ResponseBody body = response.body();
|
||||||
|
List<Journal> ret = GsonHelper.gson.fromJson(body.charStream(), journalType);
|
||||||
|
|
||||||
|
for (Journal journal : ret) {
|
||||||
|
journal.processFromJson();
|
||||||
|
journal.verify(keyBase64);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteJournal(Journal journal) throws Exceptions.HttpException {
|
||||||
|
HttpUrl remote = this.remote.resolve(journal.getUuid() + "/");
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.delete()
|
||||||
|
.url(remote)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
newCall(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putJournal(Journal journal) throws Exceptions.HttpException {
|
||||||
|
RequestBody body = RequestBody.create(JSON, journal.toJson());
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.post(body)
|
||||||
|
.url(remote)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
newCall(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Journal extends Base {
|
||||||
|
final private transient int hmacSize = 256 / 8; // hmac256 in bytes
|
||||||
|
private transient byte[] hmac = null;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private Journal() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Journal(String keyBase64, String content) {
|
||||||
|
this(keyBase64, content, sha256(UUID.randomUUID().toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Journal(String keyBase64, String content, String uid) {
|
||||||
|
super(keyBase64, content, uid);
|
||||||
|
hmac = calculateHmac(keyBase64);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processFromJson() {
|
||||||
|
hmac = Arrays.copyOfRange(getContent(), 0, hmacSize);
|
||||||
|
setContent(Arrays.copyOfRange(getContent(), hmacSize, getContent().length));
|
||||||
|
}
|
||||||
|
|
||||||
|
void verify(String keyBase64) throws Exceptions.IntegrityException {
|
||||||
|
if (hmac == null) {
|
||||||
|
throw new Exceptions.IntegrityException("HMAC is null!");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] correctHash = calculateHmac(keyBase64);
|
||||||
|
if (!Arrays.areEqual(hmac, correctHash)) {
|
||||||
|
throw new Exceptions.IntegrityException("Bad HMAC. " + Hex.toHexString(hmac) + " != " + Hex.toHexString(correctHash));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] calculateHmac(String keyBase64) {
|
||||||
|
return super.calculateHmac(keyBase64, getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String toJson() {
|
||||||
|
byte[] rawContent = getContent();
|
||||||
|
setContent(Arrays.concatenate(hmac, rawContent));
|
||||||
|
String ret = super.toJson();
|
||||||
|
setContent(rawContent);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,36 +10,25 @@ package at.bitfire.davdroid.model;
|
|||||||
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
import at.bitfire.dav4android.DavResource;
|
import at.bitfire.davdroid.GsonHelper;
|
||||||
import at.bitfire.dav4android.Property;
|
|
||||||
import at.bitfire.dav4android.property.AddressbookDescription;
|
|
||||||
import at.bitfire.dav4android.property.CalendarColor;
|
|
||||||
import at.bitfire.dav4android.property.CalendarDescription;
|
|
||||||
import at.bitfire.dav4android.property.CalendarTimezone;
|
|
||||||
import at.bitfire.dav4android.property.CurrentUserPrivilegeSet;
|
|
||||||
import at.bitfire.dav4android.property.DisplayName;
|
|
||||||
import at.bitfire.dav4android.property.ResourceType;
|
|
||||||
import at.bitfire.dav4android.property.SupportedAddressData;
|
|
||||||
import at.bitfire.dav4android.property.SupportedCalendarComponentSet;
|
|
||||||
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
@ToString
|
@ToString
|
||||||
public class CollectionInfo implements Serializable {
|
public class CollectionInfo implements Serializable {
|
||||||
public long id;
|
public transient long id;
|
||||||
public Long serviceID;
|
public transient Long serviceID;
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
ADDRESS_BOOK,
|
ADDRESS_BOOK,
|
||||||
CALENDAR
|
CALENDAR
|
||||||
}
|
}
|
||||||
|
|
||||||
public Type type;
|
public Type type;
|
||||||
|
|
||||||
public String url;
|
public transient String url; // Essentially the uuid
|
||||||
|
|
||||||
public boolean readOnly;
|
public boolean readOnly;
|
||||||
public String displayName, description;
|
public String displayName, description;
|
||||||
@ -51,68 +40,29 @@ public class CollectionInfo implements Serializable {
|
|||||||
|
|
||||||
public boolean selected;
|
public boolean selected;
|
||||||
|
|
||||||
// non-persistent properties
|
public CollectionInfo() {
|
||||||
public boolean confirmed;
|
}
|
||||||
|
|
||||||
|
public static CollectionInfo defaultForService(String sService) {
|
||||||
public static final Property.Name[] DAV_PROPERTIES = {
|
Type service = Type.valueOf(sService);
|
||||||
ResourceType.NAME,
|
|
||||||
CurrentUserPrivilegeSet.NAME,
|
|
||||||
DisplayName.NAME,
|
|
||||||
AddressbookDescription.NAME, SupportedAddressData.NAME,
|
|
||||||
CalendarDescription.NAME, CalendarColor.NAME, SupportedCalendarComponentSet.NAME
|
|
||||||
};
|
|
||||||
|
|
||||||
public static CollectionInfo fromDavResource(DavResource dav) {
|
|
||||||
CollectionInfo info = new CollectionInfo();
|
CollectionInfo info = new CollectionInfo();
|
||||||
info.url = dav.location.toString();
|
info.displayName = "Default";
|
||||||
|
info.selected = true;
|
||||||
ResourceType type = (ResourceType)dav.properties.get(ResourceType.NAME);
|
|
||||||
if (type != null) {
|
|
||||||
if (type.types.contains(ResourceType.ADDRESSBOOK))
|
|
||||||
info.type = Type.ADDRESS_BOOK;
|
|
||||||
else if (type.types.contains(ResourceType.CALENDAR))
|
|
||||||
info.type = Type.CALENDAR;
|
|
||||||
}
|
|
||||||
|
|
||||||
info.readOnly = false;
|
info.readOnly = false;
|
||||||
CurrentUserPrivilegeSet privilegeSet = (CurrentUserPrivilegeSet)dav.properties.get(CurrentUserPrivilegeSet.NAME);
|
info.type = service;
|
||||||
if (privilegeSet != null)
|
|
||||||
info.readOnly = !privilegeSet.mayWriteContent;
|
|
||||||
|
|
||||||
DisplayName displayName = (DisplayName)dav.properties.get(DisplayName.NAME);
|
if (service.equals(Type.CALENDAR)) {
|
||||||
if (displayName != null && !StringUtils.isEmpty(displayName.displayName))
|
info.supportsVEVENT = true;
|
||||||
info.displayName = displayName.displayName;
|
// info.supportsVTODO = true;
|
||||||
|
} else {
|
||||||
if (info.type == Type.ADDRESS_BOOK) {
|
// Carddav
|
||||||
AddressbookDescription addressbookDescription = (AddressbookDescription)dav.properties.get(AddressbookDescription.NAME);
|
|
||||||
if (addressbookDescription != null)
|
|
||||||
info.description = addressbookDescription.description;
|
|
||||||
|
|
||||||
} else if (info.type == Type.CALENDAR) {
|
|
||||||
CalendarDescription calendarDescription = (CalendarDescription)dav.properties.get(CalendarDescription.NAME);
|
|
||||||
if (calendarDescription != null)
|
|
||||||
info.description = calendarDescription.description;
|
|
||||||
|
|
||||||
CalendarColor calendarColor = (CalendarColor)dav.properties.get(CalendarColor.NAME);
|
|
||||||
if (calendarColor != null)
|
|
||||||
info.color = calendarColor.color;
|
|
||||||
|
|
||||||
CalendarTimezone timeZone = (CalendarTimezone)dav.properties.get(CalendarTimezone.NAME);
|
|
||||||
if (timeZone != null)
|
|
||||||
info.timeZone = timeZone.vTimeZone;
|
|
||||||
|
|
||||||
info.supportsVEVENT = info.supportsVTODO = true;
|
|
||||||
SupportedCalendarComponentSet supportedCalendarComponentSet = (SupportedCalendarComponentSet)dav.properties.get(SupportedCalendarComponentSet.NAME);
|
|
||||||
if (supportedCalendarComponentSet != null) {
|
|
||||||
info.supportsVEVENT = supportedCalendarComponentSet.supportsEvents;
|
|
||||||
info.supportsVTODO = supportedCalendarComponentSet.supportsTasks;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isOfTypeService(String service) {
|
||||||
|
return service.equals(type.toString());
|
||||||
|
}
|
||||||
|
|
||||||
public static CollectionInfo fromDB(ContentValues values) {
|
public static CollectionInfo fromDB(ContentValues values) {
|
||||||
CollectionInfo info = new CollectionInfo();
|
CollectionInfo info = new CollectionInfo();
|
||||||
@ -154,6 +104,13 @@ public class CollectionInfo implements Serializable {
|
|||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static CollectionInfo fromJson(String json) {
|
||||||
|
return GsonHelper.gson.fromJson(json, CollectionInfo.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toJson() {
|
||||||
|
return GsonHelper.gson.toJson(this, CollectionInfo.class);
|
||||||
|
}
|
||||||
|
|
||||||
private static Boolean getAsBooleanOrNull(ContentValues values, String field) {
|
private static Boolean getAsBooleanOrNull(ContentValues values, String field) {
|
||||||
Integer i = values.getAsInteger(field);
|
Integer i = values.getAsInteger(field);
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
package at.bitfire.davdroid.model;
|
package at.bitfire.davdroid.model;
|
||||||
|
|
||||||
|
import android.accounts.Account;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
@ -16,9 +17,11 @@ import android.database.sqlite.SQLiteException;
|
|||||||
import android.database.sqlite.SQLiteOpenHelper;
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.annotation.RequiresApi;
|
import android.support.annotation.RequiresApi;
|
||||||
|
|
||||||
import at.bitfire.davdroid.App;
|
import at.bitfire.davdroid.App;
|
||||||
|
import at.bitfire.davdroid.Constants;
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
|
|
||||||
public class ServiceDB {
|
public class ServiceDB {
|
||||||
@ -35,13 +38,12 @@ public class ServiceDB {
|
|||||||
_TABLE = "services",
|
_TABLE = "services",
|
||||||
ID = "_id",
|
ID = "_id",
|
||||||
ACCOUNT_NAME = "accountName",
|
ACCOUNT_NAME = "accountName",
|
||||||
SERVICE = "service",
|
SERVICE = "service";
|
||||||
PRINCIPAL = "principal";
|
|
||||||
|
|
||||||
// allowed values for SERVICE column
|
// allowed values for SERVICE column
|
||||||
public static final String
|
public static final String
|
||||||
SERVICE_CALDAV = "caldav",
|
SERVICE_CALDAV = CollectionInfo.Type.CALENDAR.toString(),
|
||||||
SERVICE_CARDDAV = "carddav";
|
SERVICE_CARDDAV = CollectionInfo.Type.ADDRESS_BOOK.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class HomeSets {
|
public static class HomeSets {
|
||||||
@ -103,8 +105,7 @@ public class ServiceDB {
|
|||||||
db.execSQL("CREATE TABLE " + Services._TABLE + "(" +
|
db.execSQL("CREATE TABLE " + Services._TABLE + "(" +
|
||||||
Services.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
|
Services.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||||
Services.ACCOUNT_NAME + " TEXT NOT NULL," +
|
Services.ACCOUNT_NAME + " TEXT NOT NULL," +
|
||||||
Services.SERVICE + " TEXT NOT NULL," +
|
Services.SERVICE + " TEXT NOT NULL" +
|
||||||
Services.PRINCIPAL + " TEXT NULL" +
|
|
||||||
")");
|
")");
|
||||||
db.execSQL("CREATE UNIQUE INDEX services_account ON " + Services._TABLE + " (" + Services.ACCOUNT_NAME + "," + Services.SERVICE + ")");
|
db.execSQL("CREATE UNIQUE INDEX services_account ON " + Services._TABLE + " (" + Services.ACCOUNT_NAME + "," + Services.SERVICE + ")");
|
||||||
|
|
||||||
@ -183,6 +184,34 @@ public class ServiceDB {
|
|||||||
}
|
}
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public Account getServiceAccount(SQLiteDatabase db, long service) {
|
||||||
|
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[]{Services.ACCOUNT_NAME}, Services.ID + "=?", new String[]{String.valueOf(service)}, null, null, null);
|
||||||
|
if (cursor.moveToNext()) {
|
||||||
|
return new Account(cursor.getString(0), Constants.ACCOUNT_TYPE);
|
||||||
|
} else
|
||||||
|
throw new IllegalArgumentException("Service not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public String getServiceType(SQLiteDatabase db, long service) {
|
||||||
|
@Cleanup Cursor cursor = db.query(Services._TABLE, new String[]{Services.SERVICE}, Services.ID + "=?", new String[]{String.valueOf(service)}, null, null, null);
|
||||||
|
if (cursor.moveToNext())
|
||||||
|
return cursor.getString(0);
|
||||||
|
else
|
||||||
|
throw new IllegalArgumentException("Service not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Long getService(@NonNull SQLiteDatabase db, @NonNull Account account, String service) {
|
||||||
|
@Cleanup Cursor c = db.query(Services._TABLE, new String[]{Services.ID},
|
||||||
|
Services.ACCOUNT_NAME + "=? AND " + Services.SERVICE + "=?", new String[]{account.name, service}, null, null, null);
|
||||||
|
if (c.moveToNext())
|
||||||
|
return c.getLong(0);
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
|
|||||||
}
|
}
|
||||||
|
|
||||||
public LocalContact[] getDirtyContacts() throws ContactsStorageException {
|
public LocalContact[] getDirtyContacts() throws ContactsStorageException {
|
||||||
return (LocalContact[])queryContacts(RawContacts.DIRTY + "!= 0", null);
|
return (LocalContact[])queryContacts(RawContacts.DIRTY + "!= 0 AND " + RawContacts.DELETED + "== 0", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocalGroup[] getDeletedGroups() throws ContactsStorageException {
|
public LocalGroup[] getDeletedGroups() throws ContactsStorageException {
|
||||||
@ -131,7 +131,7 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
|
|||||||
}
|
}
|
||||||
|
|
||||||
public LocalGroup[] getDirtyGroups() throws ContactsStorageException {
|
public LocalGroup[] getDirtyGroups() throws ContactsStorageException {
|
||||||
return (LocalGroup[])queryGroups(Groups.DIRTY + "!= 0", null);
|
return (LocalGroup[])queryGroups(Groups.DIRTY + "!= 0 AND " + Groups.DELETED + "== 0", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,7 +32,6 @@ import java.util.LinkedList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import at.bitfire.davdroid.App;
|
import at.bitfire.davdroid.App;
|
||||||
import at.bitfire.davdroid.DavUtils;
|
|
||||||
import at.bitfire.davdroid.model.CollectionInfo;
|
import at.bitfire.davdroid.model.CollectionInfo;
|
||||||
import at.bitfire.ical4android.AndroidCalendar;
|
import at.bitfire.ical4android.AndroidCalendar;
|
||||||
import at.bitfire.ical4android.AndroidCalendarFactory;
|
import at.bitfire.ical4android.AndroidCalendarFactory;
|
||||||
@ -86,7 +85,7 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
|
|||||||
private static ContentValues valuesFromCollectionInfo(CollectionInfo info, boolean withColor) {
|
private static ContentValues valuesFromCollectionInfo(CollectionInfo info, boolean withColor) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(Calendars.NAME, info.url);
|
values.put(Calendars.NAME, info.url);
|
||||||
values.put(Calendars.CALENDAR_DISPLAY_NAME, !TextUtils.isEmpty(info.displayName) ? info.displayName : DavUtils.lastSegmentOfUrl(info.url));
|
values.put(Calendars.CALENDAR_DISPLAY_NAME, info.displayName);
|
||||||
|
|
||||||
if (withColor)
|
if (withColor)
|
||||||
values.put(Calendars.CALENDAR_COLOR, info.color != null ? info.color : defaultColor);
|
values.put(Calendars.CALENDAR_COLOR, info.color != null ? info.color : defaultColor);
|
||||||
@ -131,7 +130,7 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
|
|||||||
List<LocalResource> dirty = new LinkedList<>();
|
List<LocalResource> dirty = new LinkedList<>();
|
||||||
|
|
||||||
// get dirty events which are required to have an increased SEQUENCE value
|
// get dirty events which are required to have an increased SEQUENCE value
|
||||||
for (LocalEvent event : (LocalEvent[])queryEvents(Events.DIRTY + "!=0 AND " + Events.ORIGINAL_ID + " IS NULL", null)) {
|
for (LocalEvent event : (LocalEvent[])queryEvents(Events.DIRTY + "!=0 AND " + Events.DELETED + "==0 AND " + Events.ORIGINAL_ID + " IS NULL", null)) {
|
||||||
if (event.getEvent().sequence == null) // sequence has not been assigned yet (i.e. this event was just locally created)
|
if (event.getEvent().sequence == null) // sequence has not been assigned yet (i.e. this event was just locally created)
|
||||||
event.getEvent().sequence = 0;
|
event.getEvent().sequence = 0;
|
||||||
else if (event.weAreOrganizer)
|
else if (event.weAreOrganizer)
|
||||||
|
@ -17,6 +17,7 @@ public interface LocalCollection {
|
|||||||
|
|
||||||
LocalResource[] getDeleted() throws CalendarStorageException, ContactsStorageException;
|
LocalResource[] getDeleted() throws CalendarStorageException, ContactsStorageException;
|
||||||
LocalResource[] getWithoutFileName() throws CalendarStorageException, ContactsStorageException;
|
LocalResource[] getWithoutFileName() throws CalendarStorageException, ContactsStorageException;
|
||||||
|
/** Dirty *non-deleted* entries */
|
||||||
LocalResource[] getDirty() throws CalendarStorageException, ContactsStorageException, FileNotFoundException;
|
LocalResource[] getDirty() throws CalendarStorageException, ContactsStorageException, FileNotFoundException;
|
||||||
|
|
||||||
LocalResource[] getAll() throws CalendarStorageException, ContactsStorageException;
|
LocalResource[] getAll() throws CalendarStorageException, ContactsStorageException;
|
||||||
|
@ -15,12 +15,16 @@ import android.provider.ContactsContract;
|
|||||||
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
|
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
|
||||||
import android.provider.ContactsContract.RawContacts.Data;
|
import android.provider.ContactsContract.RawContacts.Data;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import at.bitfire.davdroid.BuildConfig;
|
import at.bitfire.davdroid.App;
|
||||||
import at.bitfire.davdroid.model.UnknownProperties;
|
import at.bitfire.davdroid.model.UnknownProperties;
|
||||||
import at.bitfire.vcard4android.AndroidAddressBook;
|
import at.bitfire.vcard4android.AndroidAddressBook;
|
||||||
import at.bitfire.vcard4android.AndroidContact;
|
import at.bitfire.vcard4android.AndroidContact;
|
||||||
@ -29,24 +33,32 @@ import at.bitfire.vcard4android.BatchOperation;
|
|||||||
import at.bitfire.vcard4android.CachedGroupMembership;
|
import at.bitfire.vcard4android.CachedGroupMembership;
|
||||||
import at.bitfire.vcard4android.Contact;
|
import at.bitfire.vcard4android.Contact;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
import ezvcard.Ezvcard;
|
import ezvcard.VCardVersion;
|
||||||
|
|
||||||
|
import static at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS;
|
||||||
|
|
||||||
public class LocalContact extends AndroidContact implements LocalResource {
|
public class LocalContact extends AndroidContact implements LocalResource {
|
||||||
static {
|
|
||||||
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " vcard4android ez-vcard/" + Ezvcard.VERSION;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected final Set<Long>
|
protected final Set<Long>
|
||||||
cachedGroupMemberships = new HashSet<>(),
|
cachedGroupMemberships = new HashSet<>(),
|
||||||
groupMemberships = new HashSet<>();
|
groupMemberships = new HashSet<>();
|
||||||
|
|
||||||
|
|
||||||
protected LocalContact(AndroidAddressBook addressBook, long id, String fileName, String eTag) {
|
protected LocalContact(AndroidAddressBook addressBook, long id, String uuid, String eTag) {
|
||||||
super(addressBook, id, fileName, eTag);
|
super(addressBook, id, uuid, eTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocalContact(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) {
|
public LocalContact(AndroidAddressBook addressBook, Contact contact, String uuid, String eTag) {
|
||||||
super(addressBook, contact, fileName, eTag);
|
super(addressBook, contact, uuid, eTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUuid() {
|
||||||
|
// The same now
|
||||||
|
return getFileName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLocalOnly() {
|
||||||
|
return TextUtils.isEmpty(getETag());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearDirty(String eTag) throws ContactsStorageException {
|
public void clearDirty(String eTag) throws ContactsStorageException {
|
||||||
@ -64,7 +76,7 @@ public class LocalContact extends AndroidContact implements LocalResource {
|
|||||||
|
|
||||||
public void updateFileNameAndUID(String uid) throws ContactsStorageException {
|
public void updateFileNameAndUID(String uid) throws ContactsStorageException {
|
||||||
try {
|
try {
|
||||||
String newFileName = uid + ".vcf";
|
String newFileName = uid;
|
||||||
|
|
||||||
ContentValues values = new ContentValues(2);
|
ContentValues values = new ContentValues(2);
|
||||||
values.put(COLUMN_FILENAME, newFileName);
|
values.put(COLUMN_FILENAME, newFileName);
|
||||||
@ -77,6 +89,18 @@ public class LocalContact extends AndroidContact implements LocalResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContent() throws IOException, ContactsStorageException {
|
||||||
|
final Contact contact;
|
||||||
|
contact = getContact();
|
||||||
|
|
||||||
|
App.log.log(Level.FINE, "Preparing upload of VCard " + getUuid(), contact);
|
||||||
|
|
||||||
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
|
contact.write(VCardVersion.V4_0, GROUP_VCARDS, os);
|
||||||
|
|
||||||
|
return os.toString();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void populateData(String mimeType, ContentValues row) {
|
protected void populateData(String mimeType, ContentValues row) {
|
||||||
@ -181,11 +205,6 @@ public class LocalContact extends AndroidContact implements LocalResource {
|
|||||||
return new LocalContact(addressBook, id, fileName, eTag);
|
return new LocalContact(addressBook, id, fileName, eTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocalContact newInstance(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) {
|
|
||||||
return new LocalContact(addressBook, contact, fileName, eTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LocalContact[] newArray(int size) {
|
public LocalContact[] newArray(int size) {
|
||||||
return new LocalContact[size];
|
return new LocalContact[size];
|
||||||
|
@ -16,30 +16,33 @@ import android.os.RemoteException;
|
|||||||
import android.provider.CalendarContract;
|
import android.provider.CalendarContract;
|
||||||
import android.provider.CalendarContract.Events;
|
import android.provider.CalendarContract.Events;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import net.fortuna.ical4j.model.property.ProdId;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import at.bitfire.davdroid.BuildConfig;
|
import at.bitfire.davdroid.App;
|
||||||
import at.bitfire.ical4android.AndroidCalendar;
|
import at.bitfire.ical4android.AndroidCalendar;
|
||||||
import at.bitfire.ical4android.AndroidEvent;
|
import at.bitfire.ical4android.AndroidEvent;
|
||||||
import at.bitfire.ical4android.AndroidEventFactory;
|
import at.bitfire.ical4android.AndroidEventFactory;
|
||||||
import at.bitfire.ical4android.CalendarStorageException;
|
import at.bitfire.ical4android.CalendarStorageException;
|
||||||
import at.bitfire.ical4android.Event;
|
import at.bitfire.ical4android.Event;
|
||||||
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@TargetApi(17)
|
@TargetApi(17)
|
||||||
public class LocalEvent extends AndroidEvent implements LocalResource {
|
public class LocalEvent extends AndroidEvent implements LocalResource {
|
||||||
static {
|
|
||||||
Event.prodId = new ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4android ical4j/2.x");
|
|
||||||
}
|
|
||||||
|
|
||||||
static final String COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1,
|
static final String COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1,
|
||||||
COLUMN_UID = Build.VERSION.SDK_INT >= 17 ? Events.UID_2445 : Events.SYNC_DATA2,
|
COLUMN_UID = Build.VERSION.SDK_INT >= 17 ? Events.UID_2445 : Events.SYNC_DATA2,
|
||||||
COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3;
|
COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3;
|
||||||
|
|
||||||
@Getter protected String fileName;
|
@Getter
|
||||||
@Getter @Setter protected String eTag;
|
protected String fileName;
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
protected String eTag;
|
||||||
|
|
||||||
public boolean weAreOrganizer = true;
|
public boolean weAreOrganizer = true;
|
||||||
|
|
||||||
@ -57,6 +60,26 @@ public class LocalEvent extends AndroidEvent implements LocalResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContent() throws IOException, ContactsStorageException, CalendarStorageException {
|
||||||
|
App.log.log(Level.FINE, "Preparing upload of event " + getFileName(), getEvent());
|
||||||
|
|
||||||
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
|
getEvent().write(os);
|
||||||
|
|
||||||
|
return os.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLocalOnly() {
|
||||||
|
return TextUtils.isEmpty(getETag());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUuid() {
|
||||||
|
// Now the same
|
||||||
|
return getFileName();
|
||||||
|
}
|
||||||
|
|
||||||
/* process LocalEvent-specific fields */
|
/* process LocalEvent-specific fields */
|
||||||
|
|
||||||
@ -83,7 +106,7 @@ public class LocalEvent extends AndroidEvent implements LocalResource {
|
|||||||
boolean buildException = recurrence != null;
|
boolean buildException = recurrence != null;
|
||||||
Event eventToBuild = buildException ? recurrence : event;
|
Event eventToBuild = buildException ? recurrence : event;
|
||||||
|
|
||||||
builder .withValue(COLUMN_UID, event.uid)
|
builder.withValue(COLUMN_UID, event.uid)
|
||||||
.withValue(COLUMN_SEQUENCE, eventToBuild.sequence)
|
.withValue(COLUMN_SEQUENCE, eventToBuild.sequence)
|
||||||
.withValue(CalendarContract.Events.DIRTY, 0)
|
.withValue(CalendarContract.Events.DIRTY, 0)
|
||||||
.withValue(CalendarContract.Events.DELETED, 0);
|
.withValue(CalendarContract.Events.DELETED, 0);
|
||||||
@ -91,7 +114,7 @@ public class LocalEvent extends AndroidEvent implements LocalResource {
|
|||||||
if (buildException)
|
if (buildException)
|
||||||
builder.withValue(Events.ORIGINAL_SYNC_ID, fileName);
|
builder.withValue(Events.ORIGINAL_SYNC_ID, fileName);
|
||||||
else
|
else
|
||||||
builder .withValue(Events._SYNC_ID, fileName)
|
builder.withValue(Events._SYNC_ID, fileName)
|
||||||
.withValue(COLUMN_ETAG, eTag);
|
.withValue(COLUMN_ETAG, eTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +123,7 @@ public class LocalEvent extends AndroidEvent implements LocalResource {
|
|||||||
|
|
||||||
public void updateFileNameAndUID(String uid) throws CalendarStorageException {
|
public void updateFileNameAndUID(String uid) throws CalendarStorageException {
|
||||||
try {
|
try {
|
||||||
String newFileName = uid + ".ics";
|
String newFileName = uid;
|
||||||
|
|
||||||
ContentValues values = new ContentValues(2);
|
ContentValues values = new ContentValues(2);
|
||||||
values.put(Events._SYNC_ID, newFileName);
|
values.put(Events._SYNC_ID, newFileName);
|
||||||
|
@ -23,11 +23,12 @@ import android.provider.ContactsContract.RawContacts.Data;
|
|||||||
import org.apache.commons.lang3.ArrayUtils;
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import at.bitfire.dav4android.Constants;
|
import at.bitfire.davdroid.App;
|
||||||
import at.bitfire.vcard4android.AndroidAddressBook;
|
import at.bitfire.vcard4android.AndroidAddressBook;
|
||||||
import at.bitfire.vcard4android.AndroidGroup;
|
import at.bitfire.vcard4android.AndroidGroup;
|
||||||
import at.bitfire.vcard4android.AndroidGroupFactory;
|
import at.bitfire.vcard4android.AndroidGroupFactory;
|
||||||
@ -36,10 +37,13 @@ import at.bitfire.vcard4android.CachedGroupMembership;
|
|||||||
import at.bitfire.vcard4android.Contact;
|
import at.bitfire.vcard4android.Contact;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
@ToString(callSuper=true)
|
@ToString(callSuper=true)
|
||||||
public class LocalGroup extends AndroidGroup implements LocalResource {
|
public class LocalGroup extends AndroidGroup implements LocalResource {
|
||||||
|
@Getter
|
||||||
|
protected String uuid;
|
||||||
/** marshalled list of member UIDs, as sent by server */
|
/** marshalled list of member UIDs, as sent by server */
|
||||||
public static final String COLUMN_PENDING_MEMBERS = Groups.SYNC3;
|
public static final String COLUMN_PENDING_MEMBERS = Groups.SYNC3;
|
||||||
|
|
||||||
@ -51,6 +55,15 @@ public class LocalGroup extends AndroidGroup implements LocalResource {
|
|||||||
super(addressBook, contact, fileName, eTag);
|
super(addressBook, contact, fileName, eTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContent() throws IOException, ContactsStorageException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLocalOnly() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearDirty(String eTag) throws ContactsStorageException {
|
public void clearDirty(String eTag) throws ContactsStorageException {
|
||||||
@ -145,7 +158,7 @@ public class LocalGroup extends AndroidGroup implements LocalResource {
|
|||||||
BatchOperation batch = new BatchOperation(addressBook.provider);
|
BatchOperation batch = new BatchOperation(addressBook.provider);
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
long id = cursor.getLong(0);
|
long id = cursor.getLong(0);
|
||||||
Constants.log.fine("Assigning members to group " + id);
|
App.log.fine("Assigning members to group " + id);
|
||||||
|
|
||||||
// delete all memberships and cached memberships for this group
|
// delete all memberships and cached memberships for this group
|
||||||
batch.enqueue(new BatchOperation.Operation(
|
batch.enqueue(new BatchOperation.Operation(
|
||||||
@ -167,12 +180,12 @@ public class LocalGroup extends AndroidGroup implements LocalResource {
|
|||||||
|
|
||||||
// insert memberships
|
// insert memberships
|
||||||
for (String uid : members) {
|
for (String uid : members) {
|
||||||
Constants.log.fine("Assigning member: " + uid);
|
App.log.fine("Assigning member: " + uid);
|
||||||
try {
|
try {
|
||||||
LocalContact member = addressBook.findContactByUID(uid);
|
LocalContact member = addressBook.findContactByUID(uid);
|
||||||
member.addToGroup(batch, id);
|
member.addToGroup(batch, id);
|
||||||
} catch(FileNotFoundException e) {
|
} catch(FileNotFoundException e) {
|
||||||
Constants.log.log(Level.WARNING, "Group member not found: " + uid, e);
|
App.log.log(Level.WARNING, "Group member not found: " + uid, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,15 +8,20 @@
|
|||||||
|
|
||||||
package at.bitfire.davdroid.resource;
|
package at.bitfire.davdroid.resource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
import at.bitfire.ical4android.CalendarStorageException;
|
import at.bitfire.ical4android.CalendarStorageException;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
|
|
||||||
public interface LocalResource {
|
public interface LocalResource {
|
||||||
|
String getUuid();
|
||||||
Long getId();
|
Long getId();
|
||||||
|
|
||||||
String getFileName();
|
/** True if doesn't exist on server yet, false otherwise. */
|
||||||
String getETag();
|
boolean isLocalOnly();
|
||||||
|
|
||||||
|
/** Returns a string of how this should be represented for example: vCard. */
|
||||||
|
String getContent() throws IOException, ContactsStorageException, CalendarStorageException;
|
||||||
|
|
||||||
int delete() throws CalendarStorageException, ContactsStorageException;
|
int delete() throws CalendarStorageException, ContactsStorageException;
|
||||||
|
|
||||||
|
@ -14,26 +14,24 @@ import android.os.RemoteException;
|
|||||||
import android.provider.CalendarContract.Events;
|
import android.provider.CalendarContract.Events;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import net.fortuna.ical4j.model.property.ProdId;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskContract.Tasks;
|
import org.dmfs.provider.tasks.TaskContract.Tasks;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
|
|
||||||
import at.bitfire.davdroid.BuildConfig;
|
|
||||||
import at.bitfire.ical4android.AndroidTask;
|
import at.bitfire.ical4android.AndroidTask;
|
||||||
import at.bitfire.ical4android.AndroidTaskFactory;
|
import at.bitfire.ical4android.AndroidTaskFactory;
|
||||||
import at.bitfire.ical4android.AndroidTaskList;
|
import at.bitfire.ical4android.AndroidTaskList;
|
||||||
import at.bitfire.ical4android.CalendarStorageException;
|
import at.bitfire.ical4android.CalendarStorageException;
|
||||||
import at.bitfire.ical4android.Task;
|
import at.bitfire.ical4android.Task;
|
||||||
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
public class LocalTask extends AndroidTask implements LocalResource {
|
public class LocalTask extends AndroidTask implements LocalResource {
|
||||||
static {
|
@Getter
|
||||||
Task.prodId = new ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4android ical4j/2.x");
|
protected String uuid;
|
||||||
}
|
|
||||||
|
|
||||||
static final String COLUMN_ETAG = Tasks.SYNC1,
|
static final String COLUMN_ETAG = Tasks.SYNC1,
|
||||||
COLUMN_UID = Tasks.SYNC2,
|
COLUMN_UID = Tasks.SYNC2,
|
||||||
@ -56,6 +54,15 @@ public class LocalTask extends AndroidTask implements LocalResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContent() throws IOException, ContactsStorageException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLocalOnly() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/* process LocalTask-specific fields */
|
/* process LocalTask-specific fields */
|
||||||
|
|
||||||
|
@ -18,14 +18,12 @@ import android.net.Uri;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskContract.TaskLists;
|
import org.dmfs.provider.tasks.TaskContract.TaskLists;
|
||||||
import org.dmfs.provider.tasks.TaskContract.Tasks;
|
import org.dmfs.provider.tasks.TaskContract.Tasks;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
|
||||||
import at.bitfire.davdroid.DavUtils;
|
|
||||||
import at.bitfire.davdroid.model.CollectionInfo;
|
import at.bitfire.davdroid.model.CollectionInfo;
|
||||||
import at.bitfire.ical4android.AndroidTaskList;
|
import at.bitfire.ical4android.AndroidTaskList;
|
||||||
import at.bitfire.ical4android.AndroidTaskListFactory;
|
import at.bitfire.ical4android.AndroidTaskListFactory;
|
||||||
@ -71,7 +69,7 @@ public class LocalTaskList extends AndroidTaskList implements LocalCollection {
|
|||||||
private static ContentValues valuesFromCollectionInfo(CollectionInfo info, boolean withColor) {
|
private static ContentValues valuesFromCollectionInfo(CollectionInfo info, boolean withColor) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(TaskLists._SYNC_ID, info.url);
|
values.put(TaskLists._SYNC_ID, info.url);
|
||||||
values.put(TaskLists.LIST_NAME, !TextUtils.isEmpty(info.displayName) ? info.displayName : DavUtils.lastSegmentOfUrl(info.url));
|
values.put(TaskLists.LIST_NAME, info.displayName);
|
||||||
|
|
||||||
if (withColor)
|
if (withColor)
|
||||||
values.put(TaskLists.LIST_COLOR, info.color != null ? info.color : defaultColor);
|
values.put(TaskLists.LIST_COLOR, info.color != null ? info.color : defaultColor);
|
||||||
@ -97,7 +95,7 @@ public class LocalTaskList extends AndroidTaskList implements LocalCollection {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LocalResource[] getDirty() throws CalendarStorageException, FileNotFoundException {
|
public LocalResource[] getDirty() throws CalendarStorageException, FileNotFoundException {
|
||||||
LocalTask[] tasks = (LocalTask[])queryTasks(Tasks._DIRTY + "!=0", null);
|
LocalTask[] tasks = (LocalTask[])queryTasks(Tasks._DIRTY + "!=0 AND " + Tasks._DELETED + "== 0", null);
|
||||||
if (tasks != null)
|
if (tasks != null)
|
||||||
for (LocalTask task : tasks) {
|
for (LocalTask task : tasks) {
|
||||||
if (task.getTask().sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created)
|
if (task.getTask().sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created)
|
||||||
|
@ -14,34 +14,18 @@ import android.content.SyncResult;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import org.apache.commons.codec.Charsets;
|
import org.apache.commons.codec.Charsets;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
|
|
||||||
import at.bitfire.dav4android.DavCalendar;
|
|
||||||
import at.bitfire.dav4android.DavResource;
|
|
||||||
import at.bitfire.dav4android.exception.DavException;
|
|
||||||
import at.bitfire.dav4android.exception.HttpException;
|
|
||||||
import at.bitfire.dav4android.property.CalendarData;
|
|
||||||
import at.bitfire.dav4android.property.GetCTag;
|
|
||||||
import at.bitfire.dav4android.property.GetContentType;
|
|
||||||
import at.bitfire.dav4android.property.GetETag;
|
|
||||||
import at.bitfire.davdroid.AccountSettings;
|
import at.bitfire.davdroid.AccountSettings;
|
||||||
import at.bitfire.davdroid.App;
|
import at.bitfire.davdroid.App;
|
||||||
import at.bitfire.davdroid.ArrayUtils;
|
|
||||||
import at.bitfire.davdroid.Constants;
|
import at.bitfire.davdroid.Constants;
|
||||||
import at.bitfire.davdroid.InvalidAccountException;
|
import at.bitfire.davdroid.InvalidAccountException;
|
||||||
import at.bitfire.davdroid.R;
|
import at.bitfire.davdroid.R;
|
||||||
|
import at.bitfire.davdroid.journalmanager.Exceptions;
|
||||||
|
import at.bitfire.davdroid.journalmanager.JournalEntryManager;
|
||||||
import at.bitfire.davdroid.resource.LocalCalendar;
|
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||||
import at.bitfire.davdroid.resource.LocalEvent;
|
import at.bitfire.davdroid.resource.LocalEvent;
|
||||||
import at.bitfire.davdroid.resource.LocalResource;
|
import at.bitfire.davdroid.resource.LocalResource;
|
||||||
@ -49,23 +33,20 @@ import at.bitfire.ical4android.CalendarStorageException;
|
|||||||
import at.bitfire.ical4android.Event;
|
import at.bitfire.ical4android.Event;
|
||||||
import at.bitfire.ical4android.InvalidCalendarException;
|
import at.bitfire.ical4android.InvalidCalendarException;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
import lombok.Cleanup;
|
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
import okhttp3.MediaType;
|
|
||||||
import okhttp3.RequestBody;
|
|
||||||
import okhttp3.ResponseBody;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronization manager for CalDAV collections; handles events ({@code VEVENT}).
|
* <p>Synchronization manager for CardDAV collections; handles contacts and groups.</p>
|
||||||
*/
|
*/
|
||||||
public class CalendarSyncManager extends SyncManager {
|
public class CalendarSyncManager extends SyncManager {
|
||||||
|
protected static final int MAX_MULTIGET = 10;
|
||||||
|
|
||||||
protected static final int MAX_MULTIGET = 20;
|
final private HttpUrl remote;
|
||||||
|
|
||||||
|
public CalendarSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult result, LocalCalendar calendar, HttpUrl remote) throws InvalidAccountException {
|
||||||
public CalendarSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult result, LocalCalendar calendar) throws InvalidAccountException {
|
|
||||||
super(context, account, settings, extras, authority, result, "calendar/" + calendar.getId());
|
super(context, account, settings, extras, authority, result, "calendar/" + calendar.getId());
|
||||||
localCollection = calendar;
|
localCollection = calendar;
|
||||||
|
this.remote = remote;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -78,16 +59,14 @@ public class CalendarSyncManager extends SyncManager {
|
|||||||
return context.getString(R.string.sync_error_calendar, account.name);
|
return context.getString(R.string.sync_error_calendar, account.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void prepare() {
|
protected void prepare() throws ContactsStorageException {
|
||||||
collectionURL = HttpUrl.parse(localCalendar().getName());
|
journal = new JournalEntryManager(httpClient, remote, localCalendar().getName());
|
||||||
davCollection = new DavCalendar(httpClient, collectionURL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void queryCapabilities() throws DavException, IOException, HttpException {
|
protected void applyLocalEntries() throws IOException, Exceptions.HttpException, ContactsStorageException, CalendarStorageException {
|
||||||
davCollection.propfind(0, GetCTag.NAME);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -97,139 +76,55 @@ public class CalendarSyncManager extends SyncManager {
|
|||||||
localCalendar().processDirtyExceptions();
|
localCalendar().processDirtyExceptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected RequestBody prepareUpload(LocalResource resource) throws IOException, CalendarStorageException {
|
|
||||||
LocalEvent local = (LocalEvent)resource;
|
|
||||||
App.log.log(Level.FINE, "Preparing upload of event " + local.getFileName(), local.getEvent());
|
|
||||||
|
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
|
||||||
local.getEvent().write(os);
|
|
||||||
|
|
||||||
return RequestBody.create(
|
|
||||||
DavCalendar.MIME_ICALENDAR,
|
|
||||||
os.toByteArray()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void listRemote() throws IOException, HttpException, DavException {
|
|
||||||
// calculate time range limits
|
|
||||||
Date limitStart = null;
|
|
||||||
Integer pastDays = settings.getTimeRangePastDays();
|
|
||||||
if (pastDays != null) {
|
|
||||||
Calendar calendar = Calendar.getInstance();
|
|
||||||
calendar.add(Calendar.DAY_OF_MONTH, -pastDays);
|
|
||||||
limitStart = calendar.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch list of remote VEVENTs and build hash table to index file name
|
|
||||||
davCalendar().calendarQuery("VEVENT", limitStart, null);
|
|
||||||
|
|
||||||
remoteResources = new HashMap<>(davCollection.members.size());
|
|
||||||
for (DavResource iCal : davCollection.members) {
|
|
||||||
String fileName = iCal.fileName();
|
|
||||||
App.log.fine("Found remote VEVENT: " + fileName);
|
|
||||||
remoteResources.put(fileName, iCal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void downloadRemote() throws IOException, HttpException, DavException, CalendarStorageException {
|
|
||||||
App.log.info("Downloading " + toDownload.size() + " events (" + MAX_MULTIGET + " at once)");
|
|
||||||
|
|
||||||
// download new/updated iCalendars from server
|
|
||||||
for (DavResource[] bunch : ArrayUtils.partition(toDownload.toArray(new DavResource[toDownload.size()]), MAX_MULTIGET)) {
|
|
||||||
if (Thread.interrupted())
|
|
||||||
return;
|
|
||||||
App.log.info("Downloading " + StringUtils.join(bunch, ", "));
|
|
||||||
|
|
||||||
if (bunch.length == 1) {
|
|
||||||
// only one contact, use GET
|
|
||||||
DavResource remote = bunch[0];
|
|
||||||
|
|
||||||
ResponseBody body = remote.get("text/calendar");
|
|
||||||
|
|
||||||
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
|
|
||||||
GetETag eTag = (GetETag)remote.properties.get(GetETag.NAME);
|
|
||||||
if (eTag == null || StringUtils.isEmpty(eTag.eTag))
|
|
||||||
throw new DavException("Received CalDAV GET response without ETag for " + remote.location);
|
|
||||||
|
|
||||||
Charset charset = Charsets.UTF_8;
|
|
||||||
MediaType contentType = body.contentType();
|
|
||||||
if (contentType != null)
|
|
||||||
charset = contentType.charset(Charsets.UTF_8);
|
|
||||||
|
|
||||||
@Cleanup InputStream stream = body.byteStream();
|
|
||||||
processVEvent(remote.fileName(), eTag.eTag, stream, charset);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// multiple contacts, use multi-get
|
|
||||||
List<HttpUrl> urls = new LinkedList<>();
|
|
||||||
for (DavResource remote : bunch)
|
|
||||||
urls.add(remote.location);
|
|
||||||
davCalendar().multiget(urls.toArray(new HttpUrl[urls.size()]));
|
|
||||||
|
|
||||||
// process multiget results
|
|
||||||
for (DavResource remote : davCollection.members) {
|
|
||||||
String eTag;
|
|
||||||
GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME);
|
|
||||||
if (getETag != null)
|
|
||||||
eTag = getETag.eTag;
|
|
||||||
else
|
|
||||||
throw new DavException("Received multi-get response without ETag");
|
|
||||||
|
|
||||||
Charset charset = Charsets.UTF_8;
|
|
||||||
GetContentType getContentType = (GetContentType)remote.properties.get(GetContentType.NAME);
|
|
||||||
if (getContentType != null && getContentType.type != null) {
|
|
||||||
MediaType type = MediaType.parse(getContentType.type);
|
|
||||||
if (type != null)
|
|
||||||
charset = type.charset(Charsets.UTF_8);
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarData calendarData = (CalendarData)remote.properties.get(CalendarData.NAME);
|
|
||||||
if (calendarData == null || calendarData.iCalendar == null)
|
|
||||||
throw new DavException("Received multi-get response without address data");
|
|
||||||
|
|
||||||
@Cleanup InputStream stream = new ByteArrayInputStream(calendarData.iCalendar.getBytes());
|
|
||||||
processVEvent(remote.fileName(), eTag, stream, charset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
|
|
||||||
private LocalCalendar localCalendar() { return ((LocalCalendar)localCollection); }
|
private LocalCalendar localCalendar() {
|
||||||
private DavCalendar davCalendar() { return (DavCalendar)davCollection; }
|
return (LocalCalendar) localCollection;
|
||||||
|
|
||||||
private void processVEvent(String fileName, String eTag, InputStream stream, Charset charset) throws IOException, CalendarStorageException {
|
|
||||||
Event[] events;
|
|
||||||
try {
|
|
||||||
events = Event.fromStream(stream, charset);
|
|
||||||
} catch (InvalidCalendarException e) {
|
|
||||||
App.log.log(Level.SEVERE, "Received invalid iCalendar, ignoring", e);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (events.length == 1) {
|
protected void processSyncEntry(SyncEntry cEntry) throws IOException, ContactsStorageException, CalendarStorageException, InvalidCalendarException {
|
||||||
Event newData = events[0];
|
InputStream is = new ByteArrayInputStream(cEntry.getContent().getBytes(Charsets.UTF_8));
|
||||||
|
|
||||||
|
Event[] events = Event.fromStream(is, Charsets.UTF_8);
|
||||||
|
if (events.length == 0) {
|
||||||
|
App.log.warning("Received VCard without data, ignoring");
|
||||||
|
return;
|
||||||
|
} else if (events.length > 1)
|
||||||
|
App.log.warning("Received multiple VCALs, using first one");
|
||||||
|
|
||||||
|
Event event = events[0];
|
||||||
|
|
||||||
|
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
|
||||||
|
LocalResource local = processEvent(event);
|
||||||
|
|
||||||
|
if (local != null) {
|
||||||
|
localResources.put(local.getUuid(), local);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
LocalResource local = localResources.get(event.uid);
|
||||||
|
App.log.info("Removing local record #" + local.getId() + " which has been deleted on the server");
|
||||||
|
localResources.remove(local.getUuid());
|
||||||
|
local.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalResource processEvent(final Event newData) throws IOException, ContactsStorageException, CalendarStorageException {
|
||||||
// delete local event, if it exists
|
// delete local event, if it exists
|
||||||
LocalEvent localEvent = (LocalEvent)localResources.get(fileName);
|
LocalEvent localEvent = (LocalEvent) localResources.get(newData.uid);
|
||||||
if (localEvent != null) {
|
if (localEvent != null) {
|
||||||
App.log.info("Updating " + fileName + " in local calendar");
|
App.log.info("Updating " + newData.uid + " in local calendar");
|
||||||
localEvent.setETag(eTag);
|
localEvent.setETag(newData.uid);
|
||||||
localEvent.update(newData);
|
localEvent.update(newData);
|
||||||
syncResult.stats.numUpdates++;
|
syncResult.stats.numUpdates++;
|
||||||
} else {
|
} else {
|
||||||
App.log.info("Adding " + fileName + " to local calendar");
|
App.log.info("Adding " + newData.uid + " to local calendar");
|
||||||
localEvent = new LocalEvent(localCalendar(), newData, fileName, eTag);
|
localEvent = new LocalEvent(localCalendar(), newData, newData.uid, null);
|
||||||
localEvent.add();
|
localEvent.add();
|
||||||
syncResult.stats.numInserts++;
|
syncResult.stats.numInserts++;
|
||||||
}
|
}
|
||||||
} else
|
|
||||||
App.log.severe("Received VCALENDAR with not exactly one VEVENT with UID, but without RECURRENCE-ID; ignoring " + fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return localEvent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,11 +18,9 @@ import android.database.Cursor;
|
|||||||
import android.database.DatabaseUtils;
|
import android.database.DatabaseUtils;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.database.sqlite.SQLiteException;
|
import android.database.sqlite.SQLiteException;
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.CalendarContract;
|
import android.provider.CalendarContract;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -38,6 +36,7 @@ import at.bitfire.davdroid.model.ServiceDB.Services;
|
|||||||
import at.bitfire.davdroid.resource.LocalCalendar;
|
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||||
import at.bitfire.ical4android.CalendarStorageException;
|
import at.bitfire.ical4android.CalendarStorageException;
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
|
||||||
public class CalendarsSyncAdapterService extends SyncAdapterService {
|
public class CalendarsSyncAdapterService extends SyncAdapterService {
|
||||||
|
|
||||||
@ -62,29 +61,33 @@ public class CalendarsSyncAdapterService extends SyncAdapterService {
|
|||||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
|
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
updateLocalCalendars(provider, account, settings);
|
HttpUrl principal = updateLocalCalendars(provider, account, settings);
|
||||||
|
|
||||||
for (LocalCalendar calendar : (LocalCalendar[])LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, CalendarContract.Calendars.SYNC_EVENTS + "!=0", null)) {
|
for (LocalCalendar calendar : (LocalCalendar[]) LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, CalendarContract.Calendars.SYNC_EVENTS + "!=0", null)) {
|
||||||
App.log.info("Synchronizing calendar #" + calendar.getId() + ", URL: " + calendar.getName());
|
App.log.info("Synchronizing calendar #" + calendar.getId() + ", URL: " + calendar.getName());
|
||||||
CalendarSyncManager syncManager = new CalendarSyncManager(getContext(), account, settings, extras, authority, syncResult, calendar);
|
CalendarSyncManager syncManager = new CalendarSyncManager(getContext(), account, settings, extras, authority, syncResult, calendar, principal);
|
||||||
syncManager.performSync();
|
syncManager.performSync();
|
||||||
}
|
}
|
||||||
} catch(CalendarStorageException|SQLiteException e) {
|
} catch (CalendarStorageException | SQLiteException e) {
|
||||||
App.log.log(Level.SEVERE, "Couldn't prepare local calendars", e);
|
App.log.log(Level.SEVERE, "Couldn't prepare local calendars", e);
|
||||||
syncResult.databaseError = true;
|
syncResult.databaseError = true;
|
||||||
} catch(InvalidAccountException e) {
|
} catch (InvalidAccountException e) {
|
||||||
App.log.log(Level.SEVERE, "Couldn't get account settings", e);
|
App.log.log(Level.SEVERE, "Couldn't get account settings", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
App.log.info("Calendar sync complete");
|
App.log.info("Calendar sync complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateLocalCalendars(ContentProviderClient provider, Account account, AccountSettings settings) throws CalendarStorageException {
|
private HttpUrl updateLocalCalendars(ContentProviderClient provider, Account account, AccountSettings settings) throws CalendarStorageException {
|
||||||
SQLiteOpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
|
HttpUrl ret = null;
|
||||||
|
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
|
||||||
try {
|
try {
|
||||||
// enumerate remote and local calendars
|
// enumerate remote and local calendars
|
||||||
SQLiteDatabase db = dbHelper.getReadableDatabase();
|
SQLiteDatabase db = dbHelper.getReadableDatabase();
|
||||||
Long service = getService(db, account);
|
Long service = dbHelper.getService(db, account, Services.SERVICE_CALDAV);
|
||||||
|
|
||||||
|
ret = HttpUrl.get(settings.getUri());
|
||||||
|
|
||||||
Map<String, CollectionInfo> remote = remoteCalendars(db, service);
|
Map<String, CollectionInfo> remote = remoteCalendars(db, service);
|
||||||
|
|
||||||
LocalCalendar[] local = (LocalCalendar[])LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, null, null);
|
LocalCalendar[] local = (LocalCalendar[])LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, null, null);
|
||||||
@ -116,16 +119,8 @@ public class CalendarsSyncAdapterService extends SyncAdapterService {
|
|||||||
} finally {
|
} finally {
|
||||||
dbHelper.close();
|
dbHelper.close();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
return ret;
|
||||||
Long getService(@NonNull SQLiteDatabase db, @NonNull Account account) {
|
|
||||||
@Cleanup Cursor c = db.query(Services._TABLE, new String[] { Services.ID },
|
|
||||||
Services.ACCOUNT_NAME + "=? AND " + Services.SERVICE + "=?", new String[] { account.name, Services.SERVICE_CALDAV }, null, null, null);
|
|
||||||
if (c.moveToNext())
|
|
||||||
return c.getLong(0);
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -134,7 +129,7 @@ public class CalendarsSyncAdapterService extends SyncAdapterService {
|
|||||||
if (service != null) {
|
if (service != null) {
|
||||||
@Cleanup Cursor cursor = db.query(Collections._TABLE, null,
|
@Cleanup Cursor cursor = db.query(Collections._TABLE, null,
|
||||||
Collections.SERVICE_ID + "=? AND " + Collections.SUPPORTS_VEVENT + "!=0 AND " + Collections.SYNC,
|
Collections.SERVICE_ID + "=? AND " + Collections.SUPPORTS_VEVENT + "!=0 AND " + Collections.SYNC,
|
||||||
new String[] { String.valueOf(service) }, null, null, null);
|
new String[]{String.valueOf(service)}, null, null, null);
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
DatabaseUtils.cursorRowToContentValues(cursor, values);
|
DatabaseUtils.cursorRowToContentValues(cursor, values);
|
||||||
|
@ -17,7 +17,6 @@ import android.content.SyncResult;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.DatabaseUtils;
|
import android.database.DatabaseUtils;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
@ -31,6 +30,7 @@ import at.bitfire.davdroid.model.CollectionInfo;
|
|||||||
import at.bitfire.davdroid.model.ServiceDB;
|
import at.bitfire.davdroid.model.ServiceDB;
|
||||||
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
|
||||||
public class ContactsSyncAdapterService extends SyncAdapterService {
|
public class ContactsSyncAdapterService extends SyncAdapterService {
|
||||||
|
|
||||||
@ -50,25 +50,23 @@ public class ContactsSyncAdapterService extends SyncAdapterService {
|
|||||||
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
|
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
|
||||||
super.onPerformSync(account, extras, authority, provider, syncResult);
|
super.onPerformSync(account, extras, authority, provider, syncResult);
|
||||||
|
|
||||||
SQLiteOpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
|
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
|
||||||
try {
|
try {
|
||||||
AccountSettings settings = new AccountSettings(getContext(), account);
|
AccountSettings settings = new AccountSettings(getContext(), account);
|
||||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
|
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
SQLiteDatabase db = dbHelper.getReadableDatabase();
|
SQLiteDatabase db = dbHelper.getReadableDatabase();
|
||||||
Long service = getService(db, account);
|
Long service = dbHelper.getService(db, account, ServiceDB.Services.SERVICE_CARDDAV);
|
||||||
if (service != null) {
|
if (service != null) {
|
||||||
CollectionInfo remote = remoteAddressBook(db, service);
|
HttpUrl principal = HttpUrl.get(settings.getUri());
|
||||||
if (remote != null)
|
CollectionInfo info = remoteAddressBook(db, service);
|
||||||
try {
|
try {
|
||||||
ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, settings, extras, authority, provider, syncResult, remote);
|
ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, settings, extras, authority, provider, syncResult, principal, info);
|
||||||
syncManager.performSync();
|
syncManager.performSync();
|
||||||
} catch(InvalidAccountException e) {
|
} catch (InvalidAccountException e) {
|
||||||
App.log.log(Level.SEVERE, "Couldn't get account settings", e);
|
App.log.log(Level.SEVERE, "Couldn't get account settings", e);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
App.log.info("No address book collection selected for synchronization");
|
|
||||||
} else
|
} else
|
||||||
App.log.info("No CardDAV service found in DB");
|
App.log.info("No CardDAV service found in DB");
|
||||||
} catch (InvalidAccountException e) {
|
} catch (InvalidAccountException e) {
|
||||||
@ -80,20 +78,10 @@ public class ContactsSyncAdapterService extends SyncAdapterService {
|
|||||||
App.log.info("Address book sync complete");
|
App.log.info("Address book sync complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private Long getService(@NonNull SQLiteDatabase db, @NonNull Account account) {
|
|
||||||
@Cleanup Cursor c = db.query(ServiceDB.Services._TABLE, new String[] { ServiceDB.Services.ID },
|
|
||||||
ServiceDB.Services.ACCOUNT_NAME + "=? AND " + ServiceDB.Services.SERVICE + "=?", new String[] { account.name, ServiceDB.Services.SERVICE_CARDDAV }, null, null, null);
|
|
||||||
if (c.moveToNext())
|
|
||||||
return c.getLong(0);
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private CollectionInfo remoteAddressBook(@NonNull SQLiteDatabase db, long service) {
|
private CollectionInfo remoteAddressBook(@NonNull SQLiteDatabase db, long service) {
|
||||||
@Cleanup Cursor c = db.query(Collections._TABLE, null,
|
@Cleanup Cursor c = db.query(Collections._TABLE, null,
|
||||||
Collections.SERVICE_ID + "=? AND " + Collections.SYNC, new String[] { String.valueOf(service) }, null, null, null);
|
Collections.SERVICE_ID + "=? AND " + Collections.SYNC, new String[]{String.valueOf(service)}, null, null, null);
|
||||||
if (c.moveToNext()) {
|
if (c.moveToNext()) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
DatabaseUtils.cursorRowToContentValues(c, values);
|
DatabaseUtils.cursorRowToContentValues(c, values);
|
||||||
@ -101,7 +89,6 @@ public class ContactsSyncAdapterService extends SyncAdapterService {
|
|||||||
} else
|
} else
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,123 +10,58 @@ package at.bitfire.davdroid.syncadapter;
|
|||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
import android.content.ContentProviderOperation;
|
|
||||||
import android.content.ContentUris;
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SyncResult;
|
import android.content.SyncResult;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.provider.ContactsContract;
|
import android.provider.ContactsContract;
|
||||||
import android.provider.ContactsContract.Groups;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import org.apache.commons.codec.Charsets;
|
import org.apache.commons.codec.Charsets;
|
||||||
import org.apache.commons.collections4.SetUtils;
|
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import at.bitfire.dav4android.DavAddressBook;
|
|
||||||
import at.bitfire.dav4android.DavResource;
|
|
||||||
import at.bitfire.dav4android.exception.DavException;
|
|
||||||
import at.bitfire.dav4android.exception.HttpException;
|
|
||||||
import at.bitfire.dav4android.property.AddressData;
|
|
||||||
import at.bitfire.dav4android.property.GetCTag;
|
|
||||||
import at.bitfire.dav4android.property.GetContentType;
|
|
||||||
import at.bitfire.dav4android.property.GetETag;
|
|
||||||
import at.bitfire.dav4android.property.ResourceType;
|
|
||||||
import at.bitfire.dav4android.property.SupportedAddressData;
|
|
||||||
import at.bitfire.davdroid.AccountSettings;
|
import at.bitfire.davdroid.AccountSettings;
|
||||||
import at.bitfire.davdroid.App;
|
import at.bitfire.davdroid.App;
|
||||||
import at.bitfire.davdroid.ArrayUtils;
|
|
||||||
import at.bitfire.davdroid.Constants;
|
import at.bitfire.davdroid.Constants;
|
||||||
import at.bitfire.davdroid.HttpClient;
|
import at.bitfire.davdroid.HttpClient;
|
||||||
import at.bitfire.davdroid.InvalidAccountException;
|
import at.bitfire.davdroid.InvalidAccountException;
|
||||||
import at.bitfire.davdroid.R;
|
import at.bitfire.davdroid.R;
|
||||||
|
import at.bitfire.davdroid.journalmanager.JournalEntryManager;
|
||||||
import at.bitfire.davdroid.model.CollectionInfo;
|
import at.bitfire.davdroid.model.CollectionInfo;
|
||||||
import at.bitfire.davdroid.resource.LocalAddressBook;
|
import at.bitfire.davdroid.resource.LocalAddressBook;
|
||||||
import at.bitfire.davdroid.resource.LocalContact;
|
import at.bitfire.davdroid.resource.LocalContact;
|
||||||
import at.bitfire.davdroid.resource.LocalGroup;
|
import at.bitfire.davdroid.resource.LocalGroup;
|
||||||
import at.bitfire.davdroid.resource.LocalResource;
|
import at.bitfire.davdroid.resource.LocalResource;
|
||||||
import at.bitfire.ical4android.CalendarStorageException;
|
import at.bitfire.ical4android.CalendarStorageException;
|
||||||
import at.bitfire.vcard4android.BatchOperation;
|
|
||||||
import at.bitfire.vcard4android.Contact;
|
import at.bitfire.vcard4android.Contact;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
import at.bitfire.vcard4android.GroupMethod;
|
|
||||||
import ezvcard.VCardVersion;
|
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
import okhttp3.MediaType;
|
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.RequestBody;
|
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
import okhttp3.ResponseBody;
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Synchronization manager for CardDAV collections; handles contacts and groups.</p>
|
* <p>Synchronization manager for CardDAV collections; handles contacts and groups.</p>
|
||||||
*
|
|
||||||
* <p></p>Group handling differs according to the {@link #groupMethod}. There are two basic methods to
|
|
||||||
* handle/manage groups:</p>
|
|
||||||
* <ul>
|
|
||||||
* <li>{@code CATEGORIES}: groups memberships are attached to each contact and represented as
|
|
||||||
* "category". When a group is dirty or has been deleted, all its members have to be set to
|
|
||||||
* dirty, too (because they have to be uploaded without the respective category). This
|
|
||||||
* is done in {@link #prepareDirty()}. Empty groups can be deleted without further processing,
|
|
||||||
* which is done in {@link #postProcess()} because groups may become empty after downloading
|
|
||||||
* updated remoted contacts.</li>
|
|
||||||
* <li>Groups as separate VCards: individual and group contacts (with a list of member UIDs) are
|
|
||||||
* distinguished. When a local group is dirty, its members don't need to be set to dirty.
|
|
||||||
* <ol>
|
|
||||||
* <li>However, when a contact is dirty, it has
|
|
||||||
* to be checked whether its group memberships have changed. In this case, the respective
|
|
||||||
* groups have to be set to dirty. For instance, if contact A is in group G and H, and then
|
|
||||||
* group membership of G is removed, the contact will be set to dirty because of the changed
|
|
||||||
* {@link android.provider.ContactsContract.CommonDataKinds.GroupMembership}. DAVdroid will
|
|
||||||
* then have to check whether the group memberships have actually changed, and if so,
|
|
||||||
* all affected groups have to be set to dirty. To detect changes in group memberships,
|
|
||||||
* DAVdroid always mirrors all {@link android.provider.ContactsContract.CommonDataKinds.GroupMembership}
|
|
||||||
* data rows in respective {@link at.bitfire.vcard4android.CachedGroupMembership} rows.
|
|
||||||
* If the cached group memberships are not the same as the current group member ships, the
|
|
||||||
* difference set (in our example G, because its in the cached memberships, but not in the
|
|
||||||
* actual ones) is marked as dirty. This is done in {@link #prepareDirty()}.</li>
|
|
||||||
* <li>When downloading remote contacts, groups (+ member information) may be received
|
|
||||||
* by the actual members. Thus, the member lists have to be cached until all VCards
|
|
||||||
* are received. This is done by caching the member UIDs of each group in
|
|
||||||
* {@link LocalGroup#COLUMN_PENDING_MEMBERS}. In {@link #postProcess()},
|
|
||||||
* these "pending memberships" are assigned to the actual contacs and then cleaned up.</li>
|
|
||||||
* </ol>
|
|
||||||
* </ul>
|
|
||||||
*/
|
*/
|
||||||
public class ContactsSyncManager extends SyncManager {
|
public class ContactsSyncManager extends SyncManager {
|
||||||
protected static final int MAX_MULTIGET = 10;
|
protected static final int MAX_MULTIGET = 10;
|
||||||
|
|
||||||
final private ContentProviderClient provider;
|
final private ContentProviderClient provider;
|
||||||
final private CollectionInfo remote;
|
final private HttpUrl remote;
|
||||||
|
final private CollectionInfo info;
|
||||||
|
|
||||||
private boolean hasVCard4;
|
public ContactsSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, ContentProviderClient provider, SyncResult result, HttpUrl principal, CollectionInfo info) throws InvalidAccountException {
|
||||||
private GroupMethod groupMethod;
|
|
||||||
|
|
||||||
|
|
||||||
public ContactsSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, ContentProviderClient provider, SyncResult result, CollectionInfo remote) throws InvalidAccountException {
|
|
||||||
super(context, account, settings, extras, authority, result, "addressBook");
|
super(context, account, settings, extras, authority, result, "addressBook");
|
||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
this.remote = remote;
|
this.remote = principal;
|
||||||
|
this.info = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -139,20 +74,12 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
return context.getString(R.string.sync_error_contacts, account.name);
|
return context.getString(R.string.sync_error_contacts, account.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void prepare() throws ContactsStorageException {
|
protected void prepare() throws ContactsStorageException, CalendarStorageException {
|
||||||
// prepare local address book
|
// prepare local address book
|
||||||
localCollection = new LocalAddressBook(account, provider);
|
localCollection = new LocalAddressBook(account, provider);
|
||||||
LocalAddressBook localAddressBook = localAddressBook();
|
LocalAddressBook localAddressBook = localAddressBook();
|
||||||
|
localAddressBook.setURL(info.url);
|
||||||
String url = remote.url;
|
|
||||||
String lastUrl = localAddressBook.getURL();
|
|
||||||
if (!url.equals(lastUrl)) {
|
|
||||||
App.log.info("Selected address book has changed from " + lastUrl + " to " + url + ", deleting all local contacts");
|
|
||||||
localAddressBook.deleteAll();
|
|
||||||
localAddressBook.setURL(remote.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up Contacts Provider Settings
|
// set up Contacts Provider Settings
|
||||||
ContentValues values = new ContentValues(2);
|
ContentValues values = new ContentValues(2);
|
||||||
@ -160,22 +87,12 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1);
|
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1);
|
||||||
localAddressBook.updateSettings(values);
|
localAddressBook.updateSettings(values);
|
||||||
|
|
||||||
collectionURL = HttpUrl.parse(url);
|
journal = new JournalEntryManager(httpClient, remote, info.url);
|
||||||
davCollection = new DavAddressBook(httpClient, collectionURL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void queryCapabilities() throws DavException, IOException, HttpException {
|
protected void applyLocalEntries() throws IOException, ContactsStorageException, CalendarStorageException {
|
||||||
// prepare remote address book
|
|
||||||
davCollection.propfind(0, SupportedAddressData.NAME, GetCTag.NAME);
|
|
||||||
SupportedAddressData supportedAddressData = (SupportedAddressData)davCollection.properties.get(SupportedAddressData.NAME);
|
|
||||||
hasVCard4 = supportedAddressData != null && supportedAddressData.hasVCard4();
|
|
||||||
App.log.info("Server advertises VCard/4 support: " + hasVCard4);
|
|
||||||
|
|
||||||
groupMethod = settings.getGroupMethod();
|
|
||||||
App.log.info("Contact group method: " + groupMethod);
|
|
||||||
|
|
||||||
localAddressBook().includeGroups = groupMethod == GroupMethod.GROUP_VCARDS;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -184,232 +101,75 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
|
|
||||||
LocalAddressBook addressBook = localAddressBook();
|
LocalAddressBook addressBook = localAddressBook();
|
||||||
|
|
||||||
if (groupMethod == GroupMethod.CATEGORIES) {
|
/* groups as separate VCards: thtere are group contacts and individual contacts */
|
||||||
/* groups memberships are represented as contact CATEGORIES */
|
|
||||||
|
|
||||||
// groups with DELETED=1: set all members to dirty, then remove group
|
|
||||||
for (LocalGroup group : addressBook.getDeletedGroups()) {
|
|
||||||
App.log.fine("Finally removing group " + group);
|
|
||||||
// useless because Android deletes group memberships as soon as a group is set to DELETED:
|
|
||||||
// group.markMembersDirty();
|
|
||||||
group.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
// groups with DIRTY=1: mark all memberships as dirty, then clean DIRTY flag of group
|
|
||||||
for (LocalGroup group : addressBook.getDirtyGroups()) {
|
|
||||||
App.log.fine("Marking members of modified group " + group + " as dirty");
|
|
||||||
group.markMembersDirty();
|
|
||||||
group.clearDirty(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/* groups as separate VCards: there are group contacts and individual contacts */
|
|
||||||
|
|
||||||
// mark groups with changed members as dirty
|
// mark groups with changed members as dirty
|
||||||
BatchOperation batch = new BatchOperation(addressBook.provider);
|
|
||||||
for (LocalContact contact : addressBook.getDirtyContacts())
|
|
||||||
try {
|
|
||||||
App.log.fine("Looking for changed group memberships of contact " + contact.getFileName());
|
|
||||||
Set<Long> cachedGroups = contact.getCachedGroupMemberships(),
|
|
||||||
currentGroups = contact.getGroupMemberships();
|
|
||||||
for (Long groupID : SetUtils.disjunction(cachedGroups, currentGroups)) {
|
|
||||||
App.log.fine("Marking group as dirty: " + groupID);
|
|
||||||
batch.enqueue(new BatchOperation.Operation(
|
|
||||||
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)))
|
|
||||||
.withValue(Groups.DIRTY, 1)
|
|
||||||
.withYieldAllowed(true)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} catch(FileNotFoundException ignored) {
|
|
||||||
}
|
|
||||||
batch.commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
// FIXME: add back
|
||||||
protected RequestBody prepareUpload(@NonNull LocalResource resource) throws IOException, ContactsStorageException {
|
|
||||||
final Contact contact;
|
|
||||||
if (resource instanceof LocalContact) {
|
|
||||||
LocalContact local = ((LocalContact)resource);
|
|
||||||
contact = local.getContact();
|
|
||||||
|
|
||||||
if (groupMethod == GroupMethod.CATEGORIES) {
|
|
||||||
// add groups as CATEGORIES
|
|
||||||
for (long groupID : local.getGroupMemberships()) {
|
|
||||||
try {
|
|
||||||
@Cleanup Cursor c = provider.query(
|
|
||||||
localAddressBook().syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, groupID)),
|
|
||||||
new String[] { Groups.TITLE },
|
|
||||||
null, null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
if (c != null && c.moveToNext()) {
|
|
||||||
String title = c.getString(0);
|
|
||||||
if (!TextUtils.isEmpty(title))
|
|
||||||
contact.categories.add(title);
|
|
||||||
}
|
|
||||||
} catch(RemoteException e) {
|
|
||||||
throw new ContactsStorageException("Couldn't find group for adding CATEGORIES", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (resource instanceof LocalGroup)
|
|
||||||
contact = ((LocalGroup)resource).getContact();
|
|
||||||
else
|
|
||||||
throw new IllegalArgumentException("Argument must be LocalContact or LocalGroup");
|
|
||||||
|
|
||||||
App.log.log(Level.FINE, "Preparing upload of VCard " + resource.getFileName(), contact);
|
|
||||||
|
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
|
||||||
contact.write(hasVCard4 ? VCardVersion.V4_0 : VCardVersion.V3_0, groupMethod, os);
|
|
||||||
|
|
||||||
return RequestBody.create(
|
|
||||||
hasVCard4 ? DavAddressBook.MIME_VCARD4 : DavAddressBook.MIME_VCARD3_UTF8,
|
|
||||||
os.toByteArray()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void listRemote() throws IOException, HttpException, DavException {
|
|
||||||
// fetch list of remote VCards and build hash table to index file name
|
|
||||||
davAddressBook().propfind(1, ResourceType.NAME, GetETag.NAME);
|
|
||||||
|
|
||||||
remoteResources = new HashMap<>(davCollection.members.size());
|
|
||||||
for (DavResource vCard : davCollection.members) {
|
|
||||||
// ignore member collections
|
|
||||||
ResourceType type = (ResourceType)vCard.properties.get(ResourceType.NAME);
|
|
||||||
if (type != null && type.types.contains(ResourceType.COLLECTION))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
String fileName = vCard.fileName();
|
|
||||||
App.log.fine("Found remote VCard: " + fileName);
|
|
||||||
remoteResources.put(fileName, vCard);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void downloadRemote() throws IOException, HttpException, DavException, ContactsStorageException {
|
|
||||||
App.log.info("Downloading " + toDownload.size() + " contacts (" + MAX_MULTIGET + " at once)");
|
|
||||||
|
|
||||||
// prepare downloader which may be used to download external resource like contact photos
|
|
||||||
Contact.Downloader downloader = new ResourceDownloader(collectionURL);
|
|
||||||
|
|
||||||
// download new/updated VCards from server
|
|
||||||
for (DavResource[] bunch : ArrayUtils.partition(toDownload.toArray(new DavResource[toDownload.size()]), MAX_MULTIGET)) {
|
|
||||||
if (Thread.interrupted())
|
|
||||||
return;
|
|
||||||
|
|
||||||
App.log.info("Downloading " + StringUtils.join(bunch, ", "));
|
|
||||||
|
|
||||||
if (bunch.length == 1) {
|
|
||||||
// only one contact, use GET
|
|
||||||
DavResource remote = bunch[0];
|
|
||||||
|
|
||||||
ResponseBody 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]
|
|
||||||
GetETag eTag = (GetETag)remote.properties.get(GetETag.NAME);
|
|
||||||
if (eTag == null || StringUtils.isEmpty(eTag.eTag))
|
|
||||||
throw new DavException("Received CardDAV GET response without ETag for " + remote.location);
|
|
||||||
|
|
||||||
Charset charset = Charsets.UTF_8;
|
|
||||||
MediaType contentType = body.contentType();
|
|
||||||
if (contentType != null)
|
|
||||||
charset = contentType.charset(Charsets.UTF_8);
|
|
||||||
|
|
||||||
@Cleanup InputStream stream = body.byteStream();
|
|
||||||
processVCard(remote.fileName(), eTag.eTag, stream, charset, downloader);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// multiple contacts, use multi-get
|
|
||||||
List<HttpUrl> urls = new LinkedList<>();
|
|
||||||
for (DavResource remote : bunch)
|
|
||||||
urls.add(remote.location);
|
|
||||||
davAddressBook().multiget(urls.toArray(new HttpUrl[urls.size()]), hasVCard4);
|
|
||||||
|
|
||||||
// process multiget results
|
|
||||||
for (DavResource remote : davCollection.members) {
|
|
||||||
String eTag;
|
|
||||||
GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME);
|
|
||||||
if (getETag != null)
|
|
||||||
eTag = getETag.eTag;
|
|
||||||
else
|
|
||||||
throw new DavException("Received multi-get response without ETag");
|
|
||||||
|
|
||||||
Charset charset = Charsets.UTF_8;
|
|
||||||
GetContentType getContentType = (GetContentType)remote.properties.get(GetContentType.NAME);
|
|
||||||
if (getContentType != null && getContentType.type != null) {
|
|
||||||
MediaType type = MediaType.parse(getContentType.type);
|
|
||||||
if (type != null)
|
|
||||||
charset = type.charset(Charsets.UTF_8);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddressData addressData = (AddressData)remote.properties.get(AddressData.NAME);
|
|
||||||
if (addressData == null || addressData.vCard == null)
|
|
||||||
throw new DavException("Received multi-get response without address data");
|
|
||||||
|
|
||||||
@Cleanup InputStream stream = new ByteArrayInputStream(addressData.vCard.getBytes());
|
|
||||||
processVCard(remote.fileName(), eTag, stream, charset, downloader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void postProcess() throws CalendarStorageException, ContactsStorageException {
|
protected void postProcess() throws CalendarStorageException, ContactsStorageException {
|
||||||
if (groupMethod == GroupMethod.CATEGORIES) {
|
|
||||||
/* VCard3 group handling: groups memberships are represented as contact CATEGORIES */
|
|
||||||
|
|
||||||
// remove empty groups
|
|
||||||
App.log.info("Removing empty groups");
|
|
||||||
localAddressBook().removeEmptyGroups();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
/* VCard4 group handling: there are group contacts and individual contacts */
|
/* VCard4 group handling: there are group contacts and individual contacts */
|
||||||
App.log.info("Assigning memberships of downloaded contact groups");
|
App.log.info("Assigning memberships of downloaded contact groups");
|
||||||
LocalGroup.applyPendingMemberships(localAddressBook());
|
LocalGroup.applyPendingMemberships(localAddressBook());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
|
|
||||||
private DavAddressBook davAddressBook() { return (DavAddressBook)davCollection; }
|
private LocalAddressBook localAddressBook() {
|
||||||
private LocalAddressBook localAddressBook() { return (LocalAddressBook)localCollection; }
|
return (LocalAddressBook) localCollection;
|
||||||
|
}
|
||||||
|
|
||||||
private void processVCard(String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException {
|
protected void processSyncEntry(SyncEntry cEntry) throws IOException, ContactsStorageException, CalendarStorageException {
|
||||||
App.log.info("Processing CardDAV resource " + fileName);
|
InputStream is = new ByteArrayInputStream(cEntry.getContent().getBytes(Charsets.UTF_8));
|
||||||
Contact[] contacts = Contact.fromStream(stream, charset, downloader);
|
// FIXME: Probably cache this and enable it. prepare downloader which may be used to download external resource like contact photos
|
||||||
|
// Contact.Downloader downloader = new ResourceDownloader(collectionURL);
|
||||||
|
|
||||||
|
Contact[] contacts = Contact.fromStream(is, Charsets.UTF_8, null);
|
||||||
if (contacts.length == 0) {
|
if (contacts.length == 0) {
|
||||||
App.log.warning("Received VCard without data, ignoring");
|
App.log.warning("Received VCard without data, ignoring");
|
||||||
return;
|
return;
|
||||||
} else if (contacts.length > 1)
|
} else if (contacts.length > 1)
|
||||||
App.log.warning("Received multiple VCards, using first one");
|
App.log.warning("Received multiple VCards, using first one");
|
||||||
|
|
||||||
final Contact newData = contacts[0];
|
Contact contact = contacts[0];
|
||||||
|
|
||||||
if (groupMethod == GroupMethod.CATEGORIES && newData.group) {
|
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
|
||||||
groupMethod = GroupMethod.GROUP_VCARDS;
|
LocalResource local = processContact(contact);
|
||||||
App.log.warning("Received group VCard although group method is CATEGORIES. Deleting all groups; new group method: " + groupMethod);
|
|
||||||
localAddressBook().removeGroups();
|
if (local != null) {
|
||||||
settings.setGroupMethod(groupMethod);
|
localResources.put(local.getUuid(), local);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
LocalResource local = localResources.get(contact.uid);
|
||||||
|
App.log.info("Removing local record #" + local.getId() + " which has been deleted on the server");
|
||||||
|
localResources.remove(local.getUuid());
|
||||||
|
local.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalResource processContact(final Contact newData) throws IOException, ContactsStorageException {
|
||||||
|
String uuid = newData.uid;
|
||||||
// update local contact, if it exists
|
// update local contact, if it exists
|
||||||
LocalResource local = localResources.get(fileName);
|
LocalResource local = localResources.get(uuid);
|
||||||
if (local != null) {
|
if (local != null) {
|
||||||
App.log.log(Level.INFO, "Updating " + fileName + " in local address book", newData);
|
App.log.log(Level.INFO, "Updating " + uuid + " in local address book", newData);
|
||||||
|
|
||||||
if (local instanceof LocalGroup && newData.group) {
|
if (local instanceof LocalGroup && newData.group) {
|
||||||
// update group
|
// update group
|
||||||
LocalGroup group = (LocalGroup)local;
|
LocalGroup group = (LocalGroup) local;
|
||||||
group.eTag = eTag;
|
group.eTag = uuid;
|
||||||
group.updateFromServer(newData);
|
group.updateFromServer(newData);
|
||||||
syncResult.stats.numUpdates++;
|
syncResult.stats.numUpdates++;
|
||||||
|
|
||||||
} else if (local instanceof LocalContact && !newData.group) {
|
} else if (local instanceof LocalContact && !newData.group) {
|
||||||
// update contact
|
// update contact
|
||||||
LocalContact contact = (LocalContact)local;
|
LocalContact contact = (LocalContact) local;
|
||||||
contact.eTag = eTag;
|
contact.eTag = uuid;
|
||||||
contact.update(newData);
|
contact.update(newData);
|
||||||
syncResult.stats.numUpdates++;
|
syncResult.stats.numUpdates++;
|
||||||
|
|
||||||
@ -418,7 +178,7 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
try {
|
try {
|
||||||
local.delete();
|
local.delete();
|
||||||
local = null;
|
local = null;
|
||||||
} catch(CalendarStorageException e) {
|
} catch (CalendarStorageException e) {
|
||||||
// CalendarStorageException is not used by LocalGroup and LocalContact
|
// CalendarStorageException is not used by LocalGroup and LocalContact
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -427,13 +187,13 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
if (local == null) {
|
if (local == null) {
|
||||||
if (newData.group) {
|
if (newData.group) {
|
||||||
App.log.log(Level.INFO, "Creating local group", newData);
|
App.log.log(Level.INFO, "Creating local group", newData);
|
||||||
LocalGroup group = new LocalGroup(localAddressBook(), newData, fileName, eTag);
|
LocalGroup group = new LocalGroup(localAddressBook(), newData, uuid, null);
|
||||||
group.create();
|
group.create();
|
||||||
|
|
||||||
local = group;
|
local = group;
|
||||||
} else {
|
} else {
|
||||||
App.log.log(Level.INFO, "Creating local contact", newData);
|
App.log.log(Level.INFO, "Creating local contact", newData);
|
||||||
LocalContact contact = new LocalContact(localAddressBook(), newData, fileName, eTag);
|
LocalContact contact = new LocalContact(localAddressBook(), newData, uuid, null);
|
||||||
contact.create();
|
contact.create();
|
||||||
|
|
||||||
local = contact;
|
local = contact;
|
||||||
@ -441,25 +201,9 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
syncResult.stats.numInserts++;
|
syncResult.stats.numInserts++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupMethod == GroupMethod.CATEGORIES && local instanceof LocalContact) {
|
return local;
|
||||||
// VCard3: update group memberships from CATEGORIES
|
|
||||||
LocalContact contact = (LocalContact)local;
|
|
||||||
|
|
||||||
BatchOperation batch = new BatchOperation(provider);
|
|
||||||
App.log.log(Level.FINE, "Removing contact group memberships");
|
|
||||||
contact.removeGroupMemberships(batch);
|
|
||||||
|
|
||||||
for (String category : contact.getContact().categories) {
|
|
||||||
long groupID = localAddressBook().findOrCreateGroup(category);
|
|
||||||
App.log.log(Level.FINE, "Adding membership in group " + category + " (" + groupID + ")");
|
|
||||||
contact.addToGroup(batch, groupID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
batch.commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// downloader helper class
|
// downloader helper class
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ -484,7 +228,7 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
OkHttpClient resourceClient = HttpClient.create(context);
|
OkHttpClient resourceClient = HttpClient.create(context);
|
||||||
|
|
||||||
// authenticate only against a certain host, and only upon request
|
// authenticate only against a certain host, and only upon request
|
||||||
resourceClient = HttpClient.addAuthentication(resourceClient, baseUrl.host(), settings.username(), settings.password());
|
// resourceClient = HttpClient.addAuthentication(resourceClient, baseUrl.host(), settings.username(), settings.password());
|
||||||
|
|
||||||
// allow redirects
|
// allow redirects
|
||||||
resourceClient = resourceClient.newBuilder()
|
resourceClient = resourceClient.newBuilder()
|
||||||
@ -505,7 +249,7 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
} else
|
} else
|
||||||
App.log.severe("Couldn't download external resource");
|
App.log.severe("Couldn't download external resource");
|
||||||
}
|
}
|
||||||
} catch(IOException e) {
|
} catch (IOException e) {
|
||||||
App.log.log(Level.SEVERE, "Couldn't download external resource", e);
|
App.log.log(Level.SEVERE, "Couldn't download external resource", e);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -85,7 +85,7 @@ public abstract class SyncAdapterService extends Service {
|
|||||||
|
|
||||||
protected boolean checkSyncConditions(@NonNull AccountSettings settings) {
|
protected boolean checkSyncConditions(@NonNull AccountSettings settings) {
|
||||||
if (settings.getSyncWifiOnly()) {
|
if (settings.getSyncWifiOnly()) {
|
||||||
ConnectivityManager cm = (ConnectivityManager)getContext().getSystemService(CONNECTIVITY_SERVICE);
|
ConnectivityManager cm = (ConnectivityManager) getContext().getSystemService(CONNECTIVITY_SERVICE);
|
||||||
NetworkInfo network = cm.getActiveNetworkInfo();
|
NetworkInfo network = cm.getActiveNetworkInfo();
|
||||||
if (network == null) {
|
if (network == null) {
|
||||||
App.log.info("No network available, stopping");
|
App.log.info("No network available, stopping");
|
||||||
@ -99,7 +99,7 @@ public abstract class SyncAdapterService extends Service {
|
|||||||
String onlySSID = settings.getSyncWifiOnlySSID();
|
String onlySSID = settings.getSyncWifiOnlySSID();
|
||||||
if (onlySSID != null) {
|
if (onlySSID != null) {
|
||||||
onlySSID = "\"" + onlySSID + "\"";
|
onlySSID = "\"" + onlySSID + "\"";
|
||||||
WifiManager wifi = (WifiManager)getContext().getApplicationContext().getSystemService(WIFI_SERVICE);
|
WifiManager wifi = (WifiManager) getContext().getApplicationContext().getSystemService(WIFI_SERVICE);
|
||||||
WifiInfo info = wifi.getConnectionInfo();
|
WifiInfo info = wifi.getConnectionInfo();
|
||||||
if (info == null || !onlySSID.equals(info.getSSID())) {
|
if (info == null || !onlySSID.equals(info.getSSID())) {
|
||||||
App.log.info("Connected to wrong WiFi network (" + info.getSSID() + ", required: " + onlySSID + "), ignoring");
|
App.log.info("Connected to wrong WiFi network (" + info.getSSID() + ", required: " + onlySSID + "), ignoring");
|
||||||
@ -109,7 +109,6 @@ public abstract class SyncAdapterService extends Service {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||||
* All rights reserved. This program and the accompanying materials
|
* All rights reserved. This program and the accompanying materials
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
* which accompanies this distribution, and is available at
|
* which accompanies this distribution, and is available at
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
*/
|
*/
|
||||||
package at.bitfire.davdroid.syncadapter;
|
package at.bitfire.davdroid.syncadapter;
|
||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SyncResult;
|
import android.content.SyncResult;
|
||||||
@ -18,55 +17,49 @@ import android.net.Uri;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.NotificationManagerCompat;
|
import android.support.v4.app.NotificationManagerCompat;
|
||||||
import android.support.v7.app.NotificationCompat;
|
import android.support.v7.app.NotificationCompat;
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
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.exception.ServiceUnavailableException;
|
|
||||||
import at.bitfire.dav4android.exception.UnauthorizedException;
|
|
||||||
import at.bitfire.dav4android.property.GetCTag;
|
|
||||||
import at.bitfire.dav4android.property.GetETag;
|
|
||||||
import at.bitfire.davdroid.AccountSettings;
|
import at.bitfire.davdroid.AccountSettings;
|
||||||
import at.bitfire.davdroid.App;
|
import at.bitfire.davdroid.App;
|
||||||
|
import at.bitfire.davdroid.GsonHelper;
|
||||||
import at.bitfire.davdroid.HttpClient;
|
import at.bitfire.davdroid.HttpClient;
|
||||||
import at.bitfire.davdroid.InvalidAccountException;
|
import at.bitfire.davdroid.InvalidAccountException;
|
||||||
import at.bitfire.davdroid.R;
|
import at.bitfire.davdroid.R;
|
||||||
|
import at.bitfire.davdroid.journalmanager.Exceptions;
|
||||||
|
import at.bitfire.davdroid.journalmanager.JournalEntryManager;
|
||||||
import at.bitfire.davdroid.resource.LocalCollection;
|
import at.bitfire.davdroid.resource.LocalCollection;
|
||||||
import at.bitfire.davdroid.resource.LocalResource;
|
import at.bitfire.davdroid.resource.LocalResource;
|
||||||
import at.bitfire.davdroid.ui.AccountSettingsActivity;
|
import at.bitfire.davdroid.ui.AccountSettingsActivity;
|
||||||
import at.bitfire.davdroid.ui.DebugInfoActivity;
|
import at.bitfire.davdroid.ui.DebugInfoActivity;
|
||||||
import at.bitfire.ical4android.CalendarStorageException;
|
import at.bitfire.ical4android.CalendarStorageException;
|
||||||
|
import at.bitfire.ical4android.InvalidCalendarException;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
import okhttp3.HttpUrl;
|
import lombok.Getter;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.RequestBody;
|
|
||||||
|
|
||||||
abstract public class SyncManager {
|
abstract public class SyncManager {
|
||||||
|
|
||||||
protected final int SYNC_PHASE_PREPARE = 0,
|
protected final String SYNC_PHASE_PREPARE = "sync_phase_prepare",
|
||||||
SYNC_PHASE_QUERY_CAPABILITIES = 1,
|
SYNC_PHASE_QUERY_CAPABILITIES = "sync_phase_query_capabilities",
|
||||||
SYNC_PHASE_PROCESS_LOCALLY_DELETED = 2,
|
SYNC_PHASE_PREPARE_LOCAL = "sync_phase_prepare_local",
|
||||||
SYNC_PHASE_PREPARE_DIRTY = 3,
|
SYNC_PHASE_CREATE_LOCAL_ENTRIES = "sync_phase_create_local_entries",
|
||||||
SYNC_PHASE_UPLOAD_DIRTY = 4,
|
SYNC_PHASE_FETCH_ENTRIES = "sync_phase_fetch_entries",
|
||||||
SYNC_PHASE_CHECK_SYNC_STATE = 5,
|
SYNC_PHASE_APPLY_REMOTE_ENTRIES = "sync_phase_apply_remote_entries",
|
||||||
SYNC_PHASE_LIST_LOCAL = 6,
|
SYNC_PHASE_APPLY_LOCAL_ENTRIES = "sync_phase_apply_local_entries",
|
||||||
SYNC_PHASE_LIST_REMOTE = 7,
|
SYNC_PHASE_PUSH_ENTRIES = "sync_phase_push_entries",
|
||||||
SYNC_PHASE_COMPARE_LOCAL_REMOTE = 8,
|
SYNC_PHASE_POST_PROCESSING = "sync_phase_post_processing",
|
||||||
SYNC_PHASE_DOWNLOAD_REMOTE = 9,
|
SYNC_PHASE_SAVE_SYNC_TAG = "sync_phase_save_sync_tag";
|
||||||
SYNC_PHASE_POST_PROCESSING = 10,
|
|
||||||
SYNC_PHASE_SAVE_SYNC_STATE = 11;
|
|
||||||
|
|
||||||
protected final NotificationManagerCompat notificationManager;
|
protected final NotificationManagerCompat notificationManager;
|
||||||
protected final String uniqueCollectionId;
|
protected final String uniqueCollectionId;
|
||||||
@ -81,24 +74,29 @@ abstract public class SyncManager {
|
|||||||
protected LocalCollection localCollection;
|
protected LocalCollection localCollection;
|
||||||
|
|
||||||
protected OkHttpClient httpClient;
|
protected OkHttpClient httpClient;
|
||||||
protected HttpUrl collectionURL;
|
|
||||||
protected DavResource davCollection;
|
|
||||||
|
|
||||||
|
protected JournalEntryManager journal;
|
||||||
|
|
||||||
/** remote CTag at the time of {@link #listRemote()} */
|
/**
|
||||||
|
* remote CTag (uuid of the last entry on the server). We update it when we fetch/push and save when everything works.
|
||||||
|
*/
|
||||||
protected String remoteCTag = null;
|
protected String remoteCTag = null;
|
||||||
|
|
||||||
/** sync-able resources in the local collection, as enumerated by {@link #listLocal()} */
|
/**
|
||||||
|
* Syncable local journal entries.
|
||||||
|
*/
|
||||||
|
protected List<JournalEntryManager.Entry> localEntries;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncable remote journal entries (fetch from server).
|
||||||
|
*/
|
||||||
|
protected List<JournalEntryManager.Entry> remoteEntries;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sync-able resources in the local collection, as enumerated by {@link #prepareLocal()}
|
||||||
|
*/
|
||||||
protected Map<String, LocalResource> localResources;
|
protected Map<String, LocalResource> localResources;
|
||||||
|
|
||||||
/** sync-able resources in the remote collection, as enumerated by {@link #listRemote()} */
|
|
||||||
protected Map<String, DavResource> remoteResources;
|
|
||||||
|
|
||||||
/** resources which have changed on the server, as determined by {@link #compareLocalRemote()} */
|
|
||||||
protected Set<DavResource> toDownload;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public SyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult syncResult, String uniqueCollectionId) throws InvalidAccountException {
|
public SyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult syncResult, String uniqueCollectionId) throws InvalidAccountException {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.account = account;
|
this.account = account;
|
||||||
@ -117,95 +115,100 @@ abstract public class SyncManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected abstract int notificationId();
|
protected abstract int notificationId();
|
||||||
|
|
||||||
protected abstract String getSyncErrorTitle();
|
protected abstract String getSyncErrorTitle();
|
||||||
|
|
||||||
@TargetApi(21)
|
@TargetApi(21)
|
||||||
public void performSync() {
|
public void performSync() {
|
||||||
int syncPhase = SYNC_PHASE_PREPARE;
|
String syncPhase = SYNC_PHASE_PREPARE;
|
||||||
try {
|
try {
|
||||||
App.log.info("Preparing synchronization");
|
App.log.info("Sync phase: " + syncPhase);
|
||||||
prepare();
|
prepare();
|
||||||
|
|
||||||
if (Thread.interrupted())
|
if (Thread.interrupted())
|
||||||
return;
|
return;
|
||||||
syncPhase = SYNC_PHASE_QUERY_CAPABILITIES;
|
syncPhase = SYNC_PHASE_QUERY_CAPABILITIES;
|
||||||
App.log.info("Querying capabilities");
|
App.log.info("Sync phase: " + syncPhase);
|
||||||
queryCapabilities();
|
queryCapabilities();
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_PROCESS_LOCALLY_DELETED;
|
if (Thread.interrupted())
|
||||||
App.log.info("Processing locally deleted entries");
|
return;
|
||||||
processLocallyDeleted();
|
syncPhase = SYNC_PHASE_PREPARE_LOCAL;
|
||||||
|
App.log.info("Sync phase: " + syncPhase);
|
||||||
|
prepareLocal();
|
||||||
|
|
||||||
|
/* Create journal entries out of local changes. */
|
||||||
|
if (Thread.interrupted())
|
||||||
|
return;
|
||||||
|
syncPhase = SYNC_PHASE_CREATE_LOCAL_ENTRIES;
|
||||||
|
App.log.info("Sync phase: " + syncPhase);
|
||||||
|
createLocalEntries();
|
||||||
|
|
||||||
if (Thread.interrupted())
|
if (Thread.interrupted())
|
||||||
return;
|
return;
|
||||||
syncPhase = SYNC_PHASE_PREPARE_DIRTY;
|
syncPhase = SYNC_PHASE_FETCH_ENTRIES;
|
||||||
App.log.info("Locally preparing dirty entries");
|
App.log.info("Sync phase: " + syncPhase);
|
||||||
prepareDirty();
|
fetchEntries();
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_UPLOAD_DIRTY;
|
|
||||||
App.log.info("Uploading dirty entries");
|
|
||||||
uploadDirty();
|
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_CHECK_SYNC_STATE;
|
|
||||||
App.log.info("Checking sync state");
|
|
||||||
if (checkSyncState()) {
|
|
||||||
syncPhase = SYNC_PHASE_LIST_LOCAL;
|
|
||||||
App.log.info("Listing local entries");
|
|
||||||
listLocal();
|
|
||||||
|
|
||||||
if (Thread.interrupted())
|
if (Thread.interrupted())
|
||||||
return;
|
return;
|
||||||
syncPhase = SYNC_PHASE_LIST_REMOTE;
|
syncPhase = SYNC_PHASE_APPLY_REMOTE_ENTRIES;
|
||||||
App.log.info("Listing remote entries");
|
App.log.info("Sync phase: " + syncPhase);
|
||||||
listRemote();
|
applyRemoteEntries();
|
||||||
|
|
||||||
if (Thread.interrupted())
|
if (Thread.interrupted())
|
||||||
return;
|
return;
|
||||||
syncPhase = SYNC_PHASE_COMPARE_LOCAL_REMOTE;
|
syncPhase = SYNC_PHASE_APPLY_LOCAL_ENTRIES;
|
||||||
App.log.info("Comparing local/remote entries");
|
App.log.info("Sync phase: " + syncPhase);
|
||||||
compareLocalRemote();
|
applyLocalEntries();
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_DOWNLOAD_REMOTE;
|
if (Thread.interrupted())
|
||||||
App.log.info("Downloading remote entries");
|
return;
|
||||||
downloadRemote();
|
syncPhase = SYNC_PHASE_PUSH_ENTRIES;
|
||||||
|
App.log.info("Sync phase: " + syncPhase);
|
||||||
|
pushEntries();
|
||||||
|
|
||||||
|
/* Cleanup and finalize changes */
|
||||||
|
if (Thread.interrupted())
|
||||||
|
return;
|
||||||
syncPhase = SYNC_PHASE_POST_PROCESSING;
|
syncPhase = SYNC_PHASE_POST_PROCESSING;
|
||||||
App.log.info("Post-processing");
|
App.log.info("Sync phase: " + syncPhase);
|
||||||
postProcess();
|
postProcess();
|
||||||
|
|
||||||
syncPhase = SYNC_PHASE_SAVE_SYNC_STATE;
|
syncPhase = SYNC_PHASE_SAVE_SYNC_TAG;
|
||||||
App.log.info("Saving sync state");
|
App.log.info("Sync phase: " + syncPhase);
|
||||||
saveSyncState();
|
saveSyncTag();
|
||||||
} else
|
|
||||||
App.log.info("Remote collection didn't change, skipping remote sync");
|
|
||||||
|
|
||||||
} catch (IOException|ServiceUnavailableException e) {
|
} catch (IOException e) {
|
||||||
App.log.log(Level.WARNING, "I/O exception during sync, trying again later", e);
|
App.log.log(Level.WARNING, "I/O exception during sync, trying again later", e);
|
||||||
syncResult.stats.numIoExceptions++;
|
syncResult.stats.numIoExceptions++;
|
||||||
|
|
||||||
if (e instanceof ServiceUnavailableException) {
|
} catch (Exceptions.ServiceUnavailableException e) {
|
||||||
Date retryAfter = ((ServiceUnavailableException) e).retryAfter;
|
Date retryAfter = null; // ((Exceptions.ServiceUnavailableException) e).retryAfter;
|
||||||
if (retryAfter != null) {
|
if (retryAfter != null) {
|
||||||
// how many seconds to wait? getTime() returns ms, so divide by 1000
|
// how many seconds to wait? getTime() returns ms, so divide by 1000
|
||||||
syncResult.delayUntil = (retryAfter.getTime() - new Date().getTime()) / 1000;
|
// syncResult.delayUntil = (retryAfter.getTime() - new Date().getTime()) / 1000;
|
||||||
}
|
}
|
||||||
}
|
} catch (Exception | OutOfMemoryError e) {
|
||||||
|
|
||||||
} catch(Exception|OutOfMemoryError e) {
|
|
||||||
final int messageString;
|
final int messageString;
|
||||||
|
|
||||||
if (e instanceof UnauthorizedException) {
|
if (e instanceof Exceptions.UnauthorizedException) {
|
||||||
App.log.log(Level.SEVERE, "Not authorized anymore", e);
|
App.log.log(Level.SEVERE, "Not authorized anymore", e);
|
||||||
messageString = R.string.sync_error_unauthorized;
|
messageString = R.string.sync_error_unauthorized;
|
||||||
syncResult.stats.numAuthExceptions++;
|
syncResult.stats.numAuthExceptions++;
|
||||||
} else if (e instanceof HttpException || e instanceof DavException) {
|
} else if (e instanceof Exceptions.HttpException) {
|
||||||
App.log.log(Level.SEVERE, "HTTP/DAV Exception during sync", e);
|
App.log.log(Level.SEVERE, "HTTP Exception during sync", e);
|
||||||
messageString = R.string.sync_error_http_dav;
|
messageString = R.string.sync_error_http_dav;
|
||||||
syncResult.stats.numParseExceptions++;
|
syncResult.stats.numParseExceptions++;
|
||||||
} else if (e instanceof CalendarStorageException || e instanceof ContactsStorageException) {
|
} else if (e instanceof CalendarStorageException || e instanceof ContactsStorageException) {
|
||||||
App.log.log(Level.SEVERE, "Couldn't access local storage", e);
|
App.log.log(Level.SEVERE, "Couldn't access local storage", e);
|
||||||
messageString = R.string.sync_error_local_storage;
|
messageString = R.string.sync_error_local_storage;
|
||||||
syncResult.databaseError = true;
|
syncResult.databaseError = true;
|
||||||
|
} else if (e instanceof Exceptions.IntegrityException) {
|
||||||
|
App.log.log(Level.SEVERE, "Integrity error", e);
|
||||||
|
// FIXME: Make a proper error message
|
||||||
|
messageString = R.string.sync_error;
|
||||||
|
syncResult.stats.numParseExceptions++;
|
||||||
} else {
|
} else {
|
||||||
App.log.log(Level.SEVERE, "Unknown sync error", e);
|
App.log.log(Level.SEVERE, "Unknown sync error", e);
|
||||||
messageString = R.string.sync_error;
|
messageString = R.string.sync_error;
|
||||||
@ -213,7 +216,7 @@ abstract public class SyncManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Intent detailsIntent;
|
final Intent detailsIntent;
|
||||||
if (e instanceof UnauthorizedException) {
|
if (e instanceof Exceptions.UnauthorizedException) {
|
||||||
detailsIntent = new Intent(context, AccountSettingsActivity.class);
|
detailsIntent = new Intent(context, AccountSettingsActivity.class);
|
||||||
detailsIntent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account);
|
detailsIntent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account);
|
||||||
} else {
|
} else {
|
||||||
@ -228,55 +231,151 @@ abstract public class SyncManager {
|
|||||||
detailsIntent.setData(Uri.parse("uri://" + getClass().getName() + "/" + uniqueCollectionId));
|
detailsIntent.setData(Uri.parse("uri://" + getClass().getName() + "/" + uniqueCollectionId));
|
||||||
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
|
||||||
builder .setSmallIcon(R.drawable.ic_error_light)
|
builder.setSmallIcon(R.drawable.ic_error_light)
|
||||||
.setLargeIcon(App.getLauncherBitmap(context))
|
.setLargeIcon(App.getLauncherBitmap(context))
|
||||||
.setContentTitle(getSyncErrorTitle())
|
.setContentTitle(getSyncErrorTitle())
|
||||||
.setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT))
|
.setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT))
|
||||||
.setCategory(NotificationCompat.CATEGORY_ERROR);
|
.setCategory(NotificationCompat.CATEGORY_ERROR);
|
||||||
|
|
||||||
try {
|
String message = context.getString(messageString, syncPhase);
|
||||||
String[] phases = context.getResources().getStringArray(R.array.sync_error_phases);
|
|
||||||
String message = context.getString(messageString, phases[syncPhase]);
|
|
||||||
builder.setContentText(message);
|
builder.setContentText(message);
|
||||||
} catch (IndexOutOfBoundsException ex) {
|
|
||||||
// should never happen
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationManager.notify(uniqueCollectionId, notificationId(), builder.build());
|
notificationManager.notify(uniqueCollectionId, notificationId(), builder.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
abstract protected void prepare() throws ContactsStorageException;
|
abstract protected void prepare() throws ContactsStorageException, CalendarStorageException;
|
||||||
|
|
||||||
abstract protected void queryCapabilities() throws IOException, HttpException, DavException, CalendarStorageException, ContactsStorageException;
|
abstract protected void processSyncEntry(SyncEntry cEntry) throws IOException, ContactsStorageException, CalendarStorageException, InvalidCalendarException;
|
||||||
|
|
||||||
/**
|
abstract protected void applyLocalEntries() throws IOException, ContactsStorageException, CalendarStorageException, Exceptions.HttpException;
|
||||||
* Process locally deleted entries (DELETE them on the server as well).
|
|
||||||
* Checks Thread.interrupted() before each request to allow quick sync cancellation.
|
protected void queryCapabilities() throws IOException, CalendarStorageException, ContactsStorageException {
|
||||||
*/
|
}
|
||||||
protected void processLocallyDeleted() throws CalendarStorageException, ContactsStorageException {
|
|
||||||
// Remove locally deleted entries from server (if they have a name, i.e. if they were uploaded before),
|
protected void fetchEntries() throws Exceptions.HttpException, ContactsStorageException, CalendarStorageException, Exceptions.IntegrityException {
|
||||||
// but only if they don't have changed on the server. Then finally remove them from the local address book.
|
remoteEntries = journal.getEntries(settings.password(), remoteCTag);
|
||||||
LocalResource[] localList = localCollection.getDeleted();
|
|
||||||
for (LocalResource local : localList) {
|
if (!remoteEntries.isEmpty()) {
|
||||||
|
remoteCTag = remoteEntries.get(remoteEntries.size() - 1).getUuid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void applyRemoteEntries() throws IOException, ContactsStorageException, CalendarStorageException, InvalidCalendarException {
|
||||||
|
// Process new vcards from server
|
||||||
|
for (JournalEntryManager.Entry entry : remoteEntries) {
|
||||||
if (Thread.interrupted())
|
if (Thread.interrupted())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
final String fileName = local.getFileName();
|
App.log.info("Processing " + entry.toString());
|
||||||
if (!TextUtils.isEmpty(fileName)) {
|
|
||||||
App.log.info(fileName + " has been deleted locally -> deleting from server");
|
SyncEntry cEntry = SyncEntry.fromJournalEntry(settings.password(), entry);
|
||||||
try {
|
App.log.info("Processing resource for journal entry " + entry.getUuid());
|
||||||
new DavResource(httpClient, collectionURL.newBuilder().addPathSegment(fileName).build())
|
processSyncEntry(cEntry);
|
||||||
.delete(local.getETag());
|
|
||||||
} catch (IOException|HttpException e) {
|
|
||||||
App.log.warning("Couldn't delete " + fileName + " from server; ignoring (may be downloaded again)");
|
|
||||||
}
|
}
|
||||||
} else
|
}
|
||||||
|
|
||||||
|
protected void pushEntries() throws Exceptions.HttpException, IOException, ContactsStorageException, CalendarStorageException {
|
||||||
|
// upload dirty contacts
|
||||||
|
// FIXME: Deal with failure
|
||||||
|
if (!localEntries.isEmpty()) {
|
||||||
|
journal.putEntries(localEntries, remoteCTag);
|
||||||
|
|
||||||
|
for (LocalResource local : localCollection.getDirty()) {
|
||||||
|
App.log.info("Added/changed resource with UUID: " + local.getUuid());
|
||||||
|
local.clearDirty(local.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (LocalResource local : localCollection.getDeleted()) {
|
||||||
|
local.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteCTag = localEntries.get(localEntries.size() - 1).getUuid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void createLocalEntries() throws CalendarStorageException, ContactsStorageException, IOException {
|
||||||
|
localEntries = new LinkedList<>();
|
||||||
|
|
||||||
|
// Not saving, just creating a fake one until we load it from a local db
|
||||||
|
JournalEntryManager.Entry previousEntry = (remoteCTag != null) ? JournalEntryManager.Entry.getFakeWithUid(remoteCTag) : null;
|
||||||
|
|
||||||
|
for (LocalResource local : processLocallyDeleted()) {
|
||||||
|
SyncEntry entry = new SyncEntry(local.getContent(), SyncEntry.Actions.DELETE);
|
||||||
|
JournalEntryManager.Entry tmp = new JournalEntryManager.Entry();
|
||||||
|
tmp.update(settings.password(), entry.toJson(), previousEntry);
|
||||||
|
previousEntry = tmp;
|
||||||
|
localEntries.add(previousEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (LocalResource local : localCollection.getDirty()) {
|
||||||
|
SyncEntry.Actions action;
|
||||||
|
if (local.isLocalOnly()) {
|
||||||
|
action = SyncEntry.Actions.ADD;
|
||||||
|
} else {
|
||||||
|
action = SyncEntry.Actions.CHANGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncEntry entry = new SyncEntry(local.getContent(), action);
|
||||||
|
JournalEntryManager.Entry tmp = new JournalEntryManager.Entry();
|
||||||
|
tmp.update(settings.password(), entry.toJson(), previousEntry);
|
||||||
|
previousEntry = tmp;
|
||||||
|
localEntries.add(previousEntry);
|
||||||
|
}
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
// FIXME: Do something
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all local resources which should be taken into account for synchronization into {@link #localResources}.
|
||||||
|
*/
|
||||||
|
protected void prepareLocal() throws CalendarStorageException, ContactsStorageException {
|
||||||
|
prepareDirty();
|
||||||
|
|
||||||
|
// fetch list of local contacts and build hash table to index file name
|
||||||
|
LocalResource[] localList = localCollection.getAll();
|
||||||
|
localResources = new HashMap<>(localList.length);
|
||||||
|
for (LocalResource resource : localList) {
|
||||||
|
App.log.fine("Found local resource: " + resource.getUuid());
|
||||||
|
localResources.put(resource.getUuid(), resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteCTag = localCollection.getCTag();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete unpublished locally deleted, and return the rest.
|
||||||
|
* Checks Thread.interrupted() before each request to allow quick sync cancellation.
|
||||||
|
*/
|
||||||
|
protected List<LocalResource> processLocallyDeleted() throws CalendarStorageException, ContactsStorageException {
|
||||||
|
// FIXME: This needs refactoring and fixing, it's just not true.
|
||||||
|
// Remove locally deleted entries from server (if they have a name, i.e. if they were uploaded before),
|
||||||
|
// but only if they don't have changed on the server. Then finally remove them from the local address book.
|
||||||
|
LocalResource[] localList = localCollection.getDeleted();
|
||||||
|
List<LocalResource> ret = new ArrayList<>(localList.length);
|
||||||
|
|
||||||
|
for (LocalResource local : localList) {
|
||||||
|
if (Thread.interrupted())
|
||||||
|
return ret;
|
||||||
|
|
||||||
|
if (!local.isLocalOnly()) {
|
||||||
|
App.log.info(local.getUuid() + " has been deleted locally -> deleting from server");
|
||||||
|
ret.add(local);
|
||||||
|
} else {
|
||||||
App.log.info("Removing local record #" + local.getId() + " which has been deleted locally and was never uploaded");
|
App.log.info("Removing local record #" + local.getId() + " which has been deleted locally and was never uploaded");
|
||||||
local.delete();
|
local.delete();
|
||||||
|
}
|
||||||
|
|
||||||
syncResult.stats.numDeletes++;
|
syncResult.stats.numDeletes++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
|
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
|
||||||
@ -289,158 +388,60 @@ abstract public class SyncManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract protected RequestBody prepareUpload(LocalResource resource) throws IOException, CalendarStorageException, ContactsStorageException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 void uploadDirty() throws IOException, HttpException, CalendarStorageException, ContactsStorageException {
|
|
||||||
// upload dirty contacts
|
|
||||||
for (LocalResource local : localCollection.getDirty()) {
|
|
||||||
if (Thread.interrupted())
|
|
||||||
return;
|
|
||||||
|
|
||||||
final String fileName = local.getFileName();
|
|
||||||
|
|
||||||
DavResource remote = new DavResource(httpClient, collectionURL.newBuilder().addPathSegment(fileName).build());
|
|
||||||
|
|
||||||
// generate entity to upload (VCard, iCal, whatever)
|
|
||||||
RequestBody body = prepareUpload(local);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (local.getETag() == null) {
|
|
||||||
App.log.info("Uploading new record " + fileName);
|
|
||||||
remote.put(body, null, true);
|
|
||||||
} else {
|
|
||||||
App.log.info("Uploading locally modified record " + fileName);
|
|
||||||
remote.put(body, local.getETag(), false);
|
|
||||||
}
|
|
||||||
} catch (ConflictException|PreconditionFailedException e) {
|
|
||||||
// we can't interact with the user to resolve the conflict, so we treat 409 like 412
|
|
||||||
App.log.log(Level.INFO, "Resource has been modified on the server before upload, ignoring", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
String eTag = null;
|
|
||||||
GetETag newETag = (GetETag) remote.properties.get(GetETag.NAME);
|
|
||||||
if (newETag != null) {
|
|
||||||
eTag = newETag.eTag;
|
|
||||||
App.log.fine("Received new ETag=" + eTag + " after uploading");
|
|
||||||
} else
|
|
||||||
App.log.fine("Didn't receive new ETag after uploading, setting to null");
|
|
||||||
|
|
||||||
local.clearDirty(eTag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 boolean checkSyncState() throws CalendarStorageException, ContactsStorageException {
|
|
||||||
// check CTag (ignore on manual sync)
|
|
||||||
GetCTag getCTag = (GetCTag)davCollection.properties.get(GetCTag.NAME);
|
|
||||||
if (getCTag != null)
|
|
||||||
remoteCTag = getCTag.cTag;
|
|
||||||
|
|
||||||
String localCTag = null;
|
|
||||||
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL))
|
|
||||||
App.log.info("Manual sync, ignoring CTag");
|
|
||||||
else
|
|
||||||
localCTag = localCollection.getCTag();
|
|
||||||
|
|
||||||
if (remoteCTag != null && remoteCTag.equals(localCTag)) {
|
|
||||||
App.log.info("Remote collection didn't change (CTag=" + remoteCTag + "), no need to query children");
|
|
||||||
return false;
|
|
||||||
} else
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists all local resources which should be taken into account for synchronization into {@link #localResources}.
|
|
||||||
*/
|
|
||||||
protected void listLocal() throws CalendarStorageException, ContactsStorageException {
|
|
||||||
// fetch list of local contacts and build hash table to index file name
|
|
||||||
LocalResource[] localList = localCollection.getAll();
|
|
||||||
localResources = new HashMap<>(localList.length);
|
|
||||||
for (LocalResource resource : localList) {
|
|
||||||
App.log.fine("Found local resource: " + resource.getFileName());
|
|
||||||
localResources.put(resource.getFileName(), resource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists all members of the remote collection which should be taken into account for synchronization into {@link #remoteResources}.
|
|
||||||
*/
|
|
||||||
abstract protected void listRemote() throws IOException, HttpException, DavException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 void compareLocalRemote() throws IOException, HttpException, DavException, CalendarStorageException, ContactsStorageException {
|
|
||||||
/* 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 = new HashSet<>();
|
|
||||||
for (String localName : localResources.keySet()) {
|
|
||||||
DavResource remote = remoteResources.get(localName);
|
|
||||||
if (remote == null) {
|
|
||||||
App.log.info(localName + " is not on server anymore, deleting");
|
|
||||||
localResources.get(localName).delete();
|
|
||||||
syncResult.stats.numDeletes++;
|
|
||||||
} else {
|
|
||||||
// contact is still on server, check whether it has been updated remotely
|
|
||||||
GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME);
|
|
||||||
if (getETag == null || getETag.eTag == null)
|
|
||||||
throw new DavException("Server didn't provide ETag");
|
|
||||||
String localETag = localResources.get(localName).getETag(),
|
|
||||||
remoteETag = getETag.eTag;
|
|
||||||
if (remoteETag.equals(localETag))
|
|
||||||
syncResult.stats.numSkippedEntries++;
|
|
||||||
else {
|
|
||||||
App.log.info(localName + " 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(localName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add all unseen (= remotely added) remote contacts
|
|
||||||
if (!remoteResources.isEmpty()) {
|
|
||||||
App.log.info("New resources have been found on the server: " + TextUtils.join(", ", remoteResources.keySet()));
|
|
||||||
toDownload.addAll(remoteResources.values());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads the remote resources in {@link #toDownload} and stores them locally.
|
|
||||||
* Must check Thread.interrupted() periodically to allow quick sync cancellation.
|
|
||||||
*/
|
|
||||||
abstract protected void downloadRemote() throws IOException, HttpException, DavException, ContactsStorageException, CalendarStorageException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For post-processing of entries, for instance assigning groups.
|
* For post-processing of entries, for instance assigning groups.
|
||||||
*/
|
*/
|
||||||
protected void postProcess() throws CalendarStorageException, ContactsStorageException {
|
protected void postProcess() throws CalendarStorageException, ContactsStorageException {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void saveSyncState() throws CalendarStorageException, ContactsStorageException {
|
protected void saveSyncTag() throws CalendarStorageException, ContactsStorageException {
|
||||||
/* Save sync state (CTag). It doesn't matter if it has changed during the sync process
|
|
||||||
(for instance, because another client has uploaded changes), because this will simply
|
|
||||||
cause all remote entries to be listed at the next sync. */
|
|
||||||
App.log.info("Saving CTag=" + remoteCTag);
|
App.log.info("Saving CTag=" + remoteCTag);
|
||||||
localCollection.setCTag(remoteCTag);
|
localCollection.setCTag(remoteCTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class SyncEntry {
|
||||||
|
@Getter
|
||||||
|
private String content;
|
||||||
|
@Getter
|
||||||
|
private Actions action;
|
||||||
|
|
||||||
|
enum Actions {
|
||||||
|
ADD("ADD"),
|
||||||
|
CHANGE("CHANGE"),
|
||||||
|
DELETE("DELETE");
|
||||||
|
|
||||||
|
private final String text;
|
||||||
|
|
||||||
|
Actions(final String text) {
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private SyncEntry() {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SyncEntry(String content, Actions action) {
|
||||||
|
this.content = content;
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isAction(Actions action) {
|
||||||
|
return this.action.equals(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SyncEntry fromJournalEntry(String keyBase64, JournalEntryManager.Entry entry) {
|
||||||
|
return GsonHelper.gson.fromJson(entry.getContent(keyBase64), SyncEntry.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() {
|
||||||
|
return GsonHelper.gson.toJson(this, this.getClass());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,155 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
|
||||||
* which accompanies this distribution, and is available at
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*/
|
|
||||||
package at.bitfire.davdroid.syncadapter;
|
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.content.AbstractThreadedSyncAdapter;
|
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SyncResult;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.DatabaseUtils;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskContract;
|
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
|
|
||||||
import at.bitfire.davdroid.AccountSettings;
|
|
||||||
import at.bitfire.davdroid.App;
|
|
||||||
import at.bitfire.davdroid.InvalidAccountException;
|
|
||||||
import at.bitfire.davdroid.model.CollectionInfo;
|
|
||||||
import at.bitfire.davdroid.model.ServiceDB;
|
|
||||||
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
|
||||||
import at.bitfire.davdroid.model.ServiceDB.Services;
|
|
||||||
import at.bitfire.davdroid.resource.LocalTaskList;
|
|
||||||
import at.bitfire.ical4android.CalendarStorageException;
|
|
||||||
import at.bitfire.ical4android.TaskProvider;
|
|
||||||
import lombok.Cleanup;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronization manager for CalDAV collections; handles tasks ({@code VTODO}).
|
|
||||||
*/
|
|
||||||
public class TasksSyncAdapterService extends SyncAdapterService {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected AbstractThreadedSyncAdapter syncAdapter() {
|
|
||||||
return new SyncAdapter(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static class SyncAdapter extends SyncAdapterService.SyncAdapter {
|
|
||||||
|
|
||||||
public SyncAdapter(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient providerClient, SyncResult syncResult) {
|
|
||||||
super.onPerformSync(account, extras, authority, providerClient, syncResult);
|
|
||||||
|
|
||||||
try {
|
|
||||||
@Cleanup TaskProvider provider = TaskProvider.acquire(getContext().getContentResolver(), TaskProvider.ProviderName.OpenTasks);
|
|
||||||
if (provider == null)
|
|
||||||
throw new CalendarStorageException("Couldn't access OpenTasks provider");
|
|
||||||
|
|
||||||
AccountSettings settings = new AccountSettings(getContext(), account);
|
|
||||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
|
|
||||||
return;
|
|
||||||
|
|
||||||
updateLocalTaskLists(provider, account, settings);
|
|
||||||
|
|
||||||
for (LocalTaskList taskList : (LocalTaskList[])LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, TaskContract.TaskLists.SYNC_ENABLED + "!=0", null)) {
|
|
||||||
App.log.info("Synchronizing task list #" + taskList.getId() + " [" + taskList.getSyncId() + "]");
|
|
||||||
TasksSyncManager syncManager = new TasksSyncManager(getContext(), account, settings, extras, authority, provider, syncResult, taskList);
|
|
||||||
syncManager.performSync();
|
|
||||||
}
|
|
||||||
} catch (CalendarStorageException e) {
|
|
||||||
App.log.log(Level.SEVERE, "Couldn't enumerate local task lists", e);
|
|
||||||
} catch (InvalidAccountException e) {
|
|
||||||
App.log.log(Level.SEVERE, "Couldn't get account settings", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
App.log.info("Task sync complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateLocalTaskLists(TaskProvider provider, Account account, AccountSettings settings) throws CalendarStorageException {
|
|
||||||
SQLiteOpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
|
|
||||||
try {
|
|
||||||
// enumerate remote and local task lists
|
|
||||||
SQLiteDatabase db = dbHelper.getReadableDatabase();
|
|
||||||
Long service = getService(db, account);
|
|
||||||
Map<String, CollectionInfo> remote = remoteTaskLists(db, service);
|
|
||||||
LocalTaskList[] local = (LocalTaskList[])LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, null, null);
|
|
||||||
|
|
||||||
boolean updateColors = settings.getManageCalendarColors();
|
|
||||||
|
|
||||||
// delete obsolete local task lists
|
|
||||||
for (LocalTaskList list : local) {
|
|
||||||
String url = list.getSyncId();
|
|
||||||
if (!remote.containsKey(url)) {
|
|
||||||
App.log.fine("Deleting obsolete local task list" + url);
|
|
||||||
list.delete();
|
|
||||||
} else {
|
|
||||||
// remote CollectionInfo found for this local collection, update data
|
|
||||||
CollectionInfo info = remote.get(url);
|
|
||||||
App.log.fine("Updating local task list " + url + " with " + info);
|
|
||||||
list.update(info, updateColors);
|
|
||||||
// we already have a local task list for this remote collection, don't take into consideration anymore
|
|
||||||
remote.remove(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create new local task lists
|
|
||||||
for (String url : remote.keySet()) {
|
|
||||||
CollectionInfo info = remote.get(url);
|
|
||||||
App.log.info("Adding local task list " + info);
|
|
||||||
LocalTaskList.create(account, provider, info);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
dbHelper.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
Long getService(@NonNull SQLiteDatabase db, @NonNull Account account) {
|
|
||||||
@Cleanup Cursor c = db.query(Services._TABLE, new String[] { Services.ID },
|
|
||||||
Services.ACCOUNT_NAME + "=? AND " + Services.SERVICE + "=?", new String[] { account.name, Services.SERVICE_CALDAV }, null, null, null);
|
|
||||||
if (c.moveToNext())
|
|
||||||
return c.getLong(0);
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private Map<String, CollectionInfo> remoteTaskLists(@NonNull SQLiteDatabase db, Long service) {
|
|
||||||
Map<String, CollectionInfo> collections = new LinkedHashMap<>();
|
|
||||||
if (service != null) {
|
|
||||||
@Cleanup Cursor cursor = db.query(Collections._TABLE, null,
|
|
||||||
Collections.SERVICE_ID + "=? AND " + Collections.SUPPORTS_VTODO + "!=0 AND " + Collections.SYNC,
|
|
||||||
new String[] { String.valueOf(service) }, null, null, null);
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
DatabaseUtils.cursorRowToContentValues(cursor, values);
|
|
||||||
CollectionInfo info = CollectionInfo.fromDB(values);
|
|
||||||
collections.put(info.url, info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return collections;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,217 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
|
||||||
* which accompanies this distribution, and is available at
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
package at.bitfire.davdroid.syncadapter;
|
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SyncResult;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import org.apache.commons.codec.Charsets;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
|
|
||||||
import at.bitfire.dav4android.DavCalendar;
|
|
||||||
import at.bitfire.dav4android.DavResource;
|
|
||||||
import at.bitfire.dav4android.exception.DavException;
|
|
||||||
import at.bitfire.dav4android.exception.HttpException;
|
|
||||||
import at.bitfire.dav4android.property.CalendarData;
|
|
||||||
import at.bitfire.dav4android.property.GetCTag;
|
|
||||||
import at.bitfire.dav4android.property.GetContentType;
|
|
||||||
import at.bitfire.dav4android.property.GetETag;
|
|
||||||
import at.bitfire.davdroid.AccountSettings;
|
|
||||||
import at.bitfire.davdroid.App;
|
|
||||||
import at.bitfire.davdroid.ArrayUtils;
|
|
||||||
import at.bitfire.davdroid.Constants;
|
|
||||||
import at.bitfire.davdroid.InvalidAccountException;
|
|
||||||
import at.bitfire.davdroid.R;
|
|
||||||
import at.bitfire.davdroid.resource.LocalResource;
|
|
||||||
import at.bitfire.davdroid.resource.LocalTask;
|
|
||||||
import at.bitfire.davdroid.resource.LocalTaskList;
|
|
||||||
import at.bitfire.ical4android.CalendarStorageException;
|
|
||||||
import at.bitfire.ical4android.InvalidCalendarException;
|
|
||||||
import at.bitfire.ical4android.Task;
|
|
||||||
import at.bitfire.ical4android.TaskProvider;
|
|
||||||
import lombok.Cleanup;
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
import okhttp3.MediaType;
|
|
||||||
import okhttp3.RequestBody;
|
|
||||||
import okhttp3.ResponseBody;
|
|
||||||
|
|
||||||
public class TasksSyncManager extends SyncManager {
|
|
||||||
|
|
||||||
protected static final int MAX_MULTIGET = 30;
|
|
||||||
|
|
||||||
final protected TaskProvider provider;
|
|
||||||
|
|
||||||
|
|
||||||
public TasksSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, TaskProvider provider, SyncResult result, LocalTaskList taskList) throws InvalidAccountException {
|
|
||||||
super(context, account, settings, extras, authority, result, "taskList/" + taskList.getId());
|
|
||||||
this.provider = provider;
|
|
||||||
localCollection = taskList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected int notificationId() {
|
|
||||||
return Constants.NOTIFICATION_TASK_SYNC;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getSyncErrorTitle() {
|
|
||||||
return context.getString(R.string.sync_error_tasks, account.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void prepare() {
|
|
||||||
collectionURL = HttpUrl.parse(localTaskList().getSyncId());
|
|
||||||
davCollection = new DavCalendar(httpClient, collectionURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void queryCapabilities() throws DavException, IOException, HttpException {
|
|
||||||
davCollection.propfind(0, GetCTag.NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected RequestBody prepareUpload(LocalResource resource) throws IOException, CalendarStorageException {
|
|
||||||
LocalTask local = (LocalTask)resource;
|
|
||||||
App.log.log(Level.FINE, "Preparing upload of task " + local.getFileName(), local.getTask() );
|
|
||||||
|
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
|
||||||
local.getTask().write(os);
|
|
||||||
|
|
||||||
return RequestBody.create(
|
|
||||||
DavCalendar.MIME_ICALENDAR,
|
|
||||||
os.toByteArray()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void listRemote() throws IOException, HttpException, DavException {
|
|
||||||
// fetch list of remote VTODOs and build hash table to index file name
|
|
||||||
davCalendar().calendarQuery("VTODO", null, null);
|
|
||||||
remoteResources = new HashMap<>(davCollection.members.size());
|
|
||||||
for (DavResource vCard : davCollection.members) {
|
|
||||||
String fileName = vCard.fileName();
|
|
||||||
App.log.fine("Found remote VTODO: " + fileName);
|
|
||||||
remoteResources.put(fileName, vCard);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void downloadRemote() throws IOException, HttpException, DavException, CalendarStorageException {
|
|
||||||
App.log.info("Downloading " + toDownload.size() + " tasks (" + MAX_MULTIGET + " at once)");
|
|
||||||
|
|
||||||
// download new/updated iCalendars from server
|
|
||||||
for (DavResource[] bunch : ArrayUtils.partition(toDownload.toArray(new DavResource[toDownload.size()]), MAX_MULTIGET)) {
|
|
||||||
if (Thread.interrupted())
|
|
||||||
return;
|
|
||||||
|
|
||||||
App.log.info("Downloading " + StringUtils.join(bunch, ", "));
|
|
||||||
|
|
||||||
if (bunch.length == 1) {
|
|
||||||
// only one contact, use GET
|
|
||||||
DavResource remote = bunch[0];
|
|
||||||
|
|
||||||
ResponseBody body = remote.get("text/calendar");
|
|
||||||
|
|
||||||
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
|
|
||||||
GetETag eTag = (GetETag)remote.properties.get(GetETag.NAME);
|
|
||||||
if (eTag == null || StringUtils.isEmpty(eTag.eTag))
|
|
||||||
throw new DavException("Received CalDAV GET response without ETag for " + remote.location);
|
|
||||||
|
|
||||||
Charset charset = Charsets.UTF_8;
|
|
||||||
MediaType contentType = body.contentType();
|
|
||||||
if (contentType != null)
|
|
||||||
charset = contentType.charset(Charsets.UTF_8);
|
|
||||||
|
|
||||||
@Cleanup InputStream stream = body.byteStream();
|
|
||||||
processVTodo(remote.fileName(), eTag.eTag, stream, charset);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// multiple contacts, use multi-get
|
|
||||||
List<HttpUrl> urls = new LinkedList<>();
|
|
||||||
for (DavResource remote : bunch)
|
|
||||||
urls.add(remote.location);
|
|
||||||
davCalendar().multiget(urls.toArray(new HttpUrl[urls.size()]));
|
|
||||||
|
|
||||||
// process multiget results
|
|
||||||
for (DavResource remote : davCollection.members) {
|
|
||||||
String eTag;
|
|
||||||
GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME);
|
|
||||||
if (getETag != null)
|
|
||||||
eTag = getETag.eTag;
|
|
||||||
else
|
|
||||||
throw new DavException("Received multi-get response without ETag");
|
|
||||||
|
|
||||||
Charset charset = Charsets.UTF_8;
|
|
||||||
GetContentType getContentType = (GetContentType)remote.properties.get(GetContentType.NAME);
|
|
||||||
if (getContentType != null && getContentType.type != null) {
|
|
||||||
MediaType type = MediaType.parse(getContentType.type);
|
|
||||||
if (type != null)
|
|
||||||
charset = type.charset(Charsets.UTF_8);
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarData calendarData = (CalendarData)remote.properties.get(CalendarData.NAME);
|
|
||||||
if (calendarData == null || calendarData.iCalendar == null)
|
|
||||||
throw new DavException("Received multi-get response without address data");
|
|
||||||
|
|
||||||
@Cleanup InputStream stream = new ByteArrayInputStream(calendarData.iCalendar.getBytes());
|
|
||||||
processVTodo(remote.fileName(), eTag, stream, charset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// helpers
|
|
||||||
|
|
||||||
private LocalTaskList localTaskList() { return ((LocalTaskList)localCollection); }
|
|
||||||
private DavCalendar davCalendar() { return (DavCalendar)davCollection; }
|
|
||||||
|
|
||||||
private void processVTodo(String fileName, String eTag, InputStream stream, Charset charset) throws IOException, CalendarStorageException {
|
|
||||||
Task[] tasks;
|
|
||||||
try {
|
|
||||||
tasks = Task.fromStream(stream, charset);
|
|
||||||
} catch (InvalidCalendarException e) {
|
|
||||||
App.log.log(Level.SEVERE, "Received invalid iCalendar, ignoring", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tasks.length == 1) {
|
|
||||||
Task newData = tasks[0];
|
|
||||||
|
|
||||||
// update local task, if it exists
|
|
||||||
LocalTask localTask = (LocalTask)localResources.get(fileName);
|
|
||||||
if (localTask != null) {
|
|
||||||
App.log.info("Updating " + fileName + " in local tasklist");
|
|
||||||
localTask.setETag(eTag);
|
|
||||||
localTask.update(newData);
|
|
||||||
syncResult.stats.numUpdates++;
|
|
||||||
} else {
|
|
||||||
App.log.info("Adding " + fileName + " to local task list");
|
|
||||||
localTask = new LocalTask(localTaskList(), newData, fileName, eTag);
|
|
||||||
localTask.add();
|
|
||||||
syncResult.stats.numInserts++;
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
App.log.severe("Received VCALENDAR with not exactly one VTODO; ignoring " + fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -57,11 +57,8 @@ import android.widget.EditText;
|
|||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.PopupMenu;
|
import android.widget.PopupMenu;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.RadioButton;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -106,15 +103,14 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
|
|||||||
|
|
||||||
// CardDAV toolbar
|
// CardDAV toolbar
|
||||||
tbCardDAV = (Toolbar)findViewById(R.id.carddav_menu);
|
tbCardDAV = (Toolbar)findViewById(R.id.carddav_menu);
|
||||||
tbCardDAV.setOverflowIcon(icMenu);
|
tbCardDAV.setTitle(R.string.settings_carddav);
|
||||||
tbCardDAV.inflateMenu(R.menu.carddav_actions);
|
|
||||||
tbCardDAV.setOnMenuItemClickListener(this);
|
|
||||||
|
|
||||||
// CalDAV toolbar
|
// CalDAV toolbar
|
||||||
tbCalDAV = (Toolbar)findViewById(R.id.caldav_menu);
|
tbCalDAV = (Toolbar)findViewById(R.id.caldav_menu);
|
||||||
tbCalDAV.setOverflowIcon(icMenu);
|
tbCalDAV.setOverflowIcon(icMenu);
|
||||||
tbCalDAV.inflateMenu(R.menu.caldav_actions);
|
tbCalDAV.inflateMenu(R.menu.caldav_actions);
|
||||||
tbCalDAV.setOnMenuItemClickListener(this);
|
tbCalDAV.setOnMenuItemClickListener(this);
|
||||||
|
tbCalDAV.setTitle(R.string.settings_caldav);
|
||||||
|
|
||||||
// load CardDAV/CalDAV collections
|
// load CardDAV/CalDAV collections
|
||||||
getLoaderManager().initLoader(0, getIntent().getExtras(), this);
|
getLoaderManager().initLoader(0, getIntent().getExtras(), this);
|
||||||
@ -142,14 +138,6 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
|
||||||
MenuItem itemRename = menu.findItem(R.id.rename_account);
|
|
||||||
// renameAccount is available for API level 21+
|
|
||||||
itemRename.setVisible(Build.VERSION.SDK_INT >= 21);
|
|
||||||
return super.onPrepareOptionsMenu(menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
@ -161,9 +149,6 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
|
|||||||
intent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account);
|
intent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
break;
|
break;
|
||||||
case R.id.rename_account:
|
|
||||||
RenameAccountFragment.newInstance(account).show(getSupportFragmentManager(), null);
|
|
||||||
break;
|
|
||||||
case R.id.delete_account:
|
case R.id.delete_account:
|
||||||
new AlertDialog.Builder(AccountActivity.this)
|
new AlertDialog.Builder(AccountActivity.this)
|
||||||
.setIcon(R.drawable.ic_error_dark)
|
.setIcon(R.drawable.ic_error_dark)
|
||||||
@ -297,7 +282,6 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
|
|||||||
long id;
|
long id;
|
||||||
boolean refreshing;
|
boolean refreshing;
|
||||||
|
|
||||||
boolean hasHomeSets;
|
|
||||||
List<CollectionInfo> collections;
|
List<CollectionInfo> collections;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -324,13 +308,9 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
|
|||||||
listCardDAV.setEnabled(!info.carddav.refreshing);
|
listCardDAV.setEnabled(!info.carddav.refreshing);
|
||||||
listCardDAV.setAlpha(info.carddav.refreshing ? 0.5f : 1);
|
listCardDAV.setAlpha(info.carddav.refreshing ? 0.5f : 1);
|
||||||
|
|
||||||
tbCardDAV.getMenu().findItem(R.id.create_address_book).setEnabled(info.carddav.hasHomeSets);
|
|
||||||
|
|
||||||
AddressBookAdapter adapter = new AddressBookAdapter(this);
|
AddressBookAdapter adapter = new AddressBookAdapter(this);
|
||||||
adapter.addAll(info.carddav.collections);
|
adapter.addAll(info.carddav.collections);
|
||||||
listCardDAV.setAdapter(adapter);
|
listCardDAV.setAdapter(adapter);
|
||||||
listCardDAV.setOnItemClickListener(onItemClickListener);
|
|
||||||
listCardDAV.setOnItemLongClickListener(onItemLongClickListener);
|
|
||||||
} else
|
} else
|
||||||
card.setVisibility(View.GONE);
|
card.setVisibility(View.GONE);
|
||||||
|
|
||||||
@ -343,8 +323,6 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
|
|||||||
listCalDAV.setEnabled(!info.caldav.refreshing);
|
listCalDAV.setEnabled(!info.caldav.refreshing);
|
||||||
listCalDAV.setAlpha(info.caldav.refreshing ? 0.5f : 1);
|
listCalDAV.setAlpha(info.caldav.refreshing ? 0.5f : 1);
|
||||||
|
|
||||||
tbCalDAV.getMenu().findItem(R.id.create_calendar).setEnabled(info.caldav.hasHomeSets);
|
|
||||||
|
|
||||||
final CalendarAdapter adapter = new CalendarAdapter(this);
|
final CalendarAdapter adapter = new CalendarAdapter(this);
|
||||||
adapter.addAll(info.caldav.collections);
|
adapter.addAll(info.caldav.collections);
|
||||||
listCalDAV.setAdapter(adapter);
|
listCalDAV.setAdapter(adapter);
|
||||||
@ -433,7 +411,6 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
|
|||||||
info.carddav = new AccountInfo.ServiceInfo();
|
info.carddav = new AccountInfo.ServiceInfo();
|
||||||
info.carddav.id = id;
|
info.carddav.id = id;
|
||||||
info.carddav.refreshing = (davService != null && davService.isRefreshing(id)) || ContentResolver.isSyncActive(account, ContactsContract.AUTHORITY);
|
info.carddav.refreshing = (davService != null && davService.isRefreshing(id)) || ContentResolver.isSyncActive(account, ContactsContract.AUTHORITY);
|
||||||
info.carddav.hasHomeSets = hasHomeSets(db, id);
|
|
||||||
info.carddav.collections = readCollections(db, id);
|
info.carddav.collections = readCollections(db, id);
|
||||||
|
|
||||||
} else if (Services.SERVICE_CALDAV.equals(service)) {
|
} else if (Services.SERVICE_CALDAV.equals(service)) {
|
||||||
@ -442,19 +419,12 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
|
|||||||
info.caldav.refreshing = (davService != null && davService.isRefreshing(id)) ||
|
info.caldav.refreshing = (davService != null && davService.isRefreshing(id)) ||
|
||||||
ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) ||
|
ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) ||
|
||||||
ContentResolver.isSyncActive(account, TaskProvider.ProviderName.OpenTasks.authority);
|
ContentResolver.isSyncActive(account, TaskProvider.ProviderName.OpenTasks.authority);
|
||||||
info.caldav.hasHomeSets = hasHomeSets(db, id);
|
|
||||||
info.caldav.collections = readCollections(db, id);
|
info.caldav.collections = readCollections(db, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasHomeSets(@NonNull SQLiteDatabase db, long service) {
|
|
||||||
@Cleanup Cursor cursor = db.query(ServiceDB.HomeSets._TABLE, null, ServiceDB.HomeSets.SERVICE_ID + "=?",
|
|
||||||
new String[] { String.valueOf(service) }, null, null, null);
|
|
||||||
return cursor.getCount() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<CollectionInfo> readCollections(@NonNull SQLiteDatabase db, long service) {
|
private List<CollectionInfo> readCollections(@NonNull SQLiteDatabase db, long service) {
|
||||||
List<CollectionInfo> collections = new LinkedList<>();
|
List<CollectionInfo> collections = new LinkedList<>();
|
||||||
@Cleanup Cursor cursor = db.query(Collections._TABLE, null, Collections.SERVICE_ID + "=?",
|
@Cleanup Cursor cursor = db.query(Collections._TABLE, null, Collections.SERVICE_ID + "=?",
|
||||||
@ -484,9 +454,6 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
|
|||||||
|
|
||||||
final CollectionInfo info = getItem(position);
|
final CollectionInfo info = getItem(position);
|
||||||
|
|
||||||
RadioButton checked = (RadioButton)v.findViewById(R.id.checked);
|
|
||||||
checked.setChecked(info.selected);
|
|
||||||
|
|
||||||
TextView tv = (TextView)v.findViewById(R.id.title);
|
TextView tv = (TextView)v.findViewById(R.id.title);
|
||||||
tv.setText(TextUtils.isEmpty(info.displayName) ? info.url : info.displayName);
|
tv.setText(TextUtils.isEmpty(info.displayName) ? info.url : info.displayName);
|
||||||
|
|
||||||
@ -541,99 +508,10 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
|
|||||||
tv = (TextView)v.findViewById(R.id.read_only);
|
tv = (TextView)v.findViewById(R.id.read_only);
|
||||||
tv.setVisibility(info.readOnly ? View.VISIBLE : View.GONE);
|
tv.setVisibility(info.readOnly ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
tv = (TextView)v.findViewById(R.id.events);
|
|
||||||
tv.setVisibility(BooleanUtils.isTrue(info.supportsVEVENT) ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
tv = (TextView)v.findViewById(R.id.tasks);
|
|
||||||
tv.setVisibility(BooleanUtils.isTrue(info.supportsVTODO) ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* DIALOG FRAGMENTS */
|
|
||||||
|
|
||||||
public static class RenameAccountFragment extends DialogFragment {
|
|
||||||
|
|
||||||
private final static String ARG_ACCOUNT = "account";
|
|
||||||
|
|
||||||
static RenameAccountFragment newInstance(@NonNull Account account) {
|
|
||||||
RenameAccountFragment fragment = new RenameAccountFragment();
|
|
||||||
Bundle args = new Bundle(1);
|
|
||||||
args.putParcelable(ARG_ACCOUNT, account);
|
|
||||||
fragment.setArguments(args);
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
final Account oldAccount = getArguments().getParcelable(ARG_ACCOUNT);
|
|
||||||
|
|
||||||
final EditText editText = new EditText(getContext());
|
|
||||||
editText.setText(oldAccount.name);
|
|
||||||
|
|
||||||
return new AlertDialog.Builder(getContext())
|
|
||||||
.setTitle(R.string.account_rename)
|
|
||||||
.setMessage(R.string.account_rename_new_name)
|
|
||||||
.setView(editText)
|
|
||||||
.setPositiveButton(R.string.account_rename_rename, new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
final String newName = editText.getText().toString();
|
|
||||||
|
|
||||||
if (newName.equals(oldAccount.name))
|
|
||||||
return;
|
|
||||||
|
|
||||||
final AccountManager accountManager = AccountManager.get(getContext());
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
accountManager.renameAccount(oldAccount, newName,
|
|
||||||
new AccountManagerCallback<Account>() {
|
|
||||||
@Override
|
|
||||||
public void run(AccountManagerFuture<Account> future) {
|
|
||||||
App.log.info("Updating account name references");
|
|
||||||
|
|
||||||
// cancel running synchronization
|
|
||||||
ContentResolver.cancelSync(oldAccount, null);
|
|
||||||
|
|
||||||
// update account name references in database
|
|
||||||
@Cleanup OpenHelper dbHelper = new OpenHelper(getContext());
|
|
||||||
ServiceDB.onRenameAccount(dbHelper.getWritableDatabase(), oldAccount.name, newName);
|
|
||||||
|
|
||||||
// update account_name of local contacts
|
|
||||||
try {
|
|
||||||
LocalAddressBook.onRenameAccount(getContext().getContentResolver(), oldAccount.name, newName);
|
|
||||||
} catch(RemoteException e) {
|
|
||||||
App.log.log(Level.SEVERE, "Couldn't propagate new account name to contacts provider");
|
|
||||||
}
|
|
||||||
|
|
||||||
// calendar provider doesn't allow changing account_name of Events
|
|
||||||
|
|
||||||
// update account_name of local tasks
|
|
||||||
try {
|
|
||||||
LocalTaskList.onRenameAccount(getContext().getContentResolver(), oldAccount.name, newName);
|
|
||||||
} catch(RemoteException e) {
|
|
||||||
App.log.log(Level.SEVERE, "Couldn't propagate new account name to tasks provider");
|
|
||||||
}
|
|
||||||
|
|
||||||
// synchronize again
|
|
||||||
requestSync(new Account(newName, oldAccount.type));
|
|
||||||
}
|
|
||||||
}, null
|
|
||||||
);
|
|
||||||
getActivity().finish();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.create();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* USER ACTIONS */
|
/* USER ACTIONS */
|
||||||
|
|
||||||
private void deleteAccount() {
|
private void deleteAccount() {
|
||||||
|
@ -92,23 +92,11 @@ public class AccountSettingsActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// category: authentication
|
// category: authentication
|
||||||
final EditTextPreference prefUserName = (EditTextPreference)findPreference("username");
|
|
||||||
prefUserName.setSummary(settings.username());
|
|
||||||
prefUserName.setText(settings.username());
|
|
||||||
prefUserName.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
|
||||||
settings.username((String)newValue);
|
|
||||||
refresh();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final EditTextPreference prefPassword = (EditTextPreference)findPreference("password");
|
final EditTextPreference prefPassword = (EditTextPreference)findPreference("password");
|
||||||
prefPassword.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
prefPassword.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||||
settings.password((String)newValue);
|
settings.setAuthToken((String)newValue);
|
||||||
refresh();
|
refresh();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -205,66 +193,6 @@ public class AccountSettingsActivity extends AppCompatActivity {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// category: CardDAV
|
|
||||||
final ListPreference prefGroupMethod = (ListPreference)findPreference("contact_group_method");
|
|
||||||
if (syncIntervalContacts != null) {
|
|
||||||
prefGroupMethod.setValue(settings.getGroupMethod().name());
|
|
||||||
prefGroupMethod.setSummary(prefGroupMethod.getEntry());
|
|
||||||
prefGroupMethod.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object o) {
|
|
||||||
String name = (String)o;
|
|
||||||
settings.setGroupMethod(GroupMethod.valueOf(name));
|
|
||||||
refresh();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else
|
|
||||||
prefGroupMethod.setEnabled(false);
|
|
||||||
|
|
||||||
// category: CalDAV
|
|
||||||
final EditTextPreference prefTimeRangePastDays = (EditTextPreference)findPreference("time_range_past_days");
|
|
||||||
if (syncIntervalCalendars != null) {
|
|
||||||
Integer pastDays = settings.getTimeRangePastDays();
|
|
||||||
if (pastDays != null) {
|
|
||||||
prefTimeRangePastDays.setText(pastDays.toString());
|
|
||||||
prefTimeRangePastDays.setSummary(getResources().getQuantityString(R.plurals.settings_sync_time_range_past_days, pastDays, pastDays));
|
|
||||||
} else {
|
|
||||||
prefTimeRangePastDays.setText(null);
|
|
||||||
prefTimeRangePastDays.setSummary(R.string.settings_sync_time_range_past_none);
|
|
||||||
}
|
|
||||||
prefTimeRangePastDays.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
|
||||||
int days;
|
|
||||||
try {
|
|
||||||
days = Integer.parseInt((String)newValue);
|
|
||||||
} catch(NumberFormatException ignored) {
|
|
||||||
days = -1;
|
|
||||||
}
|
|
||||||
settings.setTimeRangePastDays(days < 0 ? null : days);
|
|
||||||
refresh();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else
|
|
||||||
prefTimeRangePastDays.setEnabled(false);
|
|
||||||
|
|
||||||
final SwitchPreferenceCompat prefManageColors = (SwitchPreferenceCompat)findPreference("manage_calendar_colors");
|
|
||||||
if (syncIntervalCalendars != null || syncIntervalTasks != null) {
|
|
||||||
prefManageColors.setChecked(settings.getManageCalendarColors());
|
|
||||||
prefManageColors.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
|
||||||
settings.setManageCalendarColors((Boolean)newValue);
|
|
||||||
refresh();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else
|
|
||||||
prefManageColors.setEnabled(false);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,11 +10,9 @@ package at.bitfire.davdroid.ui;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.design.widget.FloatingActionButton;
|
import android.support.design.widget.FloatingActionButton;
|
||||||
import android.support.design.widget.NavigationView;
|
import android.support.design.widget.NavigationView;
|
||||||
import android.support.v4.app.FragmentTransaction;
|
|
||||||
import android.support.v4.view.GravityCompat;
|
import android.support.v4.view.GravityCompat;
|
||||||
import android.support.v4.widget.DrawerLayout;
|
import android.support.v4.widget.DrawerLayout;
|
||||||
import android.support.v7.app.ActionBarDrawerToggle;
|
import android.support.v7.app.ActionBarDrawerToggle;
|
||||||
@ -23,8 +21,6 @@ import android.support.v7.widget.Toolbar;
|
|||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import at.bitfire.davdroid.App;
|
|
||||||
import at.bitfire.davdroid.BuildConfig;
|
|
||||||
import at.bitfire.davdroid.Constants;
|
import at.bitfire.davdroid.Constants;
|
||||||
import at.bitfire.davdroid.R;
|
import at.bitfire.davdroid.R;
|
||||||
import at.bitfire.davdroid.ui.setup.LoginActivity;
|
import at.bitfire.davdroid.ui.setup.LoginActivity;
|
||||||
@ -56,13 +52,6 @@ public class AccountsActivity extends AppCompatActivity implements NavigationVie
|
|||||||
NavigationView navigationView = (NavigationView)findViewById(R.id.nav_view);
|
NavigationView navigationView = (NavigationView)findViewById(R.id.nav_view);
|
||||||
navigationView.setNavigationItemSelectedListener(this);
|
navigationView.setNavigationItemSelectedListener(this);
|
||||||
navigationView.setItemIconTintList(null);
|
navigationView.setItemIconTintList(null);
|
||||||
|
|
||||||
if (savedInstanceState == null && !getPackageName().equals(getCallingPackage())) {
|
|
||||||
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
|
|
||||||
for (StartupDialogFragment fragment : StartupDialogFragment.getStartupDialogs(this))
|
|
||||||
ft.add(fragment, null);
|
|
||||||
ft.commit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -83,20 +72,17 @@ public class AccountsActivity extends AppCompatActivity implements NavigationVie
|
|||||||
case R.id.nav_app_settings:
|
case R.id.nav_app_settings:
|
||||||
startActivity(new Intent(this, AppSettingsActivity.class));
|
startActivity(new Intent(this, AppSettingsActivity.class));
|
||||||
break;
|
break;
|
||||||
case R.id.nav_twitter:
|
|
||||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://twitter.com/davdroidapp")));
|
|
||||||
break;
|
|
||||||
case R.id.nav_website:
|
case R.id.nav_website:
|
||||||
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri));
|
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri));
|
||||||
break;
|
break;
|
||||||
case R.id.nav_faq:
|
case R.id.nav_faq:
|
||||||
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("faq/").build()));
|
startActivity(new Intent(Intent.ACTION_VIEW, Constants.faqUri));
|
||||||
break;
|
break;
|
||||||
case R.id.nav_forums:
|
case R.id.nav_report_issue:
|
||||||
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("forums/").build()));
|
startActivity(new Intent(Intent.ACTION_VIEW, Constants.reportIssueUri));
|
||||||
break;
|
break;
|
||||||
case R.id.nav_donate:
|
case R.id.nav_contact:
|
||||||
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("donate/").build()));
|
startActivity(new Intent(Intent.ACTION_VIEW, Constants.contactUri));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,36 +9,21 @@
|
|||||||
package at.bitfire.davdroid.ui;
|
package at.bitfire.davdroid.ui;
|
||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.LoaderManager;
|
|
||||||
import android.support.v4.app.NavUtils;
|
import android.support.v4.app.NavUtils;
|
||||||
import android.support.v4.content.AsyncTaskLoader;
|
|
||||||
import android.support.v4.content.Loader;
|
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.Spinner;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import at.bitfire.davdroid.R;
|
import at.bitfire.davdroid.R;
|
||||||
import at.bitfire.davdroid.model.CollectionInfo;
|
import at.bitfire.davdroid.model.CollectionInfo;
|
||||||
import at.bitfire.davdroid.model.ServiceDB;
|
|
||||||
import lombok.Cleanup;
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
|
|
||||||
public class CreateAddressBookActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<CreateAddressBookActivity.AccountInfo> {
|
public class CreateAddressBookActivity extends AppCompatActivity {
|
||||||
public static final String EXTRA_ACCOUNT = "account";
|
public static final String EXTRA_ACCOUNT = "account";
|
||||||
|
|
||||||
protected Account account;
|
protected Account account;
|
||||||
@ -51,8 +36,6 @@ public class CreateAddressBookActivity extends AppCompatActivity implements Load
|
|||||||
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
setContentView(R.layout.activity_create_address_book);
|
setContentView(R.layout.activity_create_address_book);
|
||||||
|
|
||||||
getSupportLoaderManager().initLoader(0, getIntent().getExtras(), this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -76,9 +59,6 @@ public class CreateAddressBookActivity extends AppCompatActivity implements Load
|
|||||||
boolean ok = true;
|
boolean ok = true;
|
||||||
CollectionInfo info = new CollectionInfo();
|
CollectionInfo info = new CollectionInfo();
|
||||||
|
|
||||||
Spinner spinner = (Spinner)findViewById(R.id.home_sets);
|
|
||||||
String homeSet = (String)spinner.getSelectedItem();
|
|
||||||
|
|
||||||
EditText edit = (EditText)findViewById(R.id.display_name);
|
EditText edit = (EditText)findViewById(R.id.display_name);
|
||||||
info.displayName = edit.getText().toString();
|
info.displayName = edit.getText().toString();
|
||||||
if (TextUtils.isEmpty(info.displayName)) {
|
if (TextUtils.isEmpty(info.displayName)) {
|
||||||
@ -91,71 +71,8 @@ public class CreateAddressBookActivity extends AppCompatActivity implements Load
|
|||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
info.type = CollectionInfo.Type.ADDRESS_BOOK;
|
info.type = CollectionInfo.Type.ADDRESS_BOOK;
|
||||||
info.url = HttpUrl.parse(homeSet).resolve(UUID.randomUUID().toString() + "/").toString();
|
|
||||||
CreateCollectionFragment.newInstance(account, info).show(getSupportFragmentManager(), null);
|
CreateCollectionFragment.newInstance(account, info).show(getSupportFragmentManager(), null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Loader<AccountInfo> onCreateLoader(int id, Bundle args) {
|
|
||||||
return new AccountInfoLoader(this, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadFinished(Loader<AccountInfo> loader, AccountInfo info) {
|
|
||||||
if (info != null) {
|
|
||||||
Spinner spinner = (Spinner)findViewById(R.id.home_sets);
|
|
||||||
spinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, info.homeSets));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoaderReset(Loader<AccountInfo> loader) {
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static class AccountInfo {
|
|
||||||
List<String> homeSets = new LinkedList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static class AccountInfoLoader extends AsyncTaskLoader<AccountInfo> {
|
|
||||||
private final Account account;
|
|
||||||
private final ServiceDB.OpenHelper dbHelper;
|
|
||||||
|
|
||||||
public AccountInfoLoader(Context context, Account account) {
|
|
||||||
super(context);
|
|
||||||
this.account = account;
|
|
||||||
dbHelper = new ServiceDB.OpenHelper(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStartLoading() {
|
|
||||||
forceLoad();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AccountInfo loadInBackground() {
|
|
||||||
final AccountInfo info = new AccountInfo();
|
|
||||||
|
|
||||||
// find DAV service and home sets
|
|
||||||
SQLiteDatabase db = dbHelper.getReadableDatabase();
|
|
||||||
try {
|
|
||||||
@Cleanup Cursor cursorService = db.query(ServiceDB.Services._TABLE, new String[] { ServiceDB.Services.ID },
|
|
||||||
ServiceDB.Services.ACCOUNT_NAME + "=? AND " + ServiceDB.Services.SERVICE + "=?",
|
|
||||||
new String[] { account.name, ServiceDB.Services.SERVICE_CARDDAV }, null, null, null);
|
|
||||||
if (!cursorService.moveToNext())
|
|
||||||
return null;
|
|
||||||
String strServiceID = cursorService.getString(0);
|
|
||||||
|
|
||||||
@Cleanup Cursor cursorHomeSets = db.query(ServiceDB.HomeSets._TABLE, new String[] { ServiceDB.HomeSets.URL },
|
|
||||||
ServiceDB.HomeSets.SERVICE_ID + "=?", new String[] { strServiceID }, null, null, null);
|
|
||||||
while (cursorHomeSets.moveToNext())
|
|
||||||
info.homeSets.add(cursorHomeSets.getString(0));
|
|
||||||
} finally {
|
|
||||||
dbHelper.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -9,22 +9,15 @@
|
|||||||
package at.bitfire.davdroid.ui;
|
package at.bitfire.davdroid.ui;
|
||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.app.LoaderManager;
|
|
||||||
import android.support.v4.app.NavUtils;
|
import android.support.v4.app.NavUtils;
|
||||||
import android.support.v4.content.AsyncTaskLoader;
|
|
||||||
import android.support.v4.content.Loader;
|
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.RadioGroup;
|
import android.widget.RadioGroup;
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
@ -33,20 +26,12 @@ import net.fortuna.ical4j.model.Calendar;
|
|||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import at.bitfire.davdroid.R;
|
import at.bitfire.davdroid.R;
|
||||||
import at.bitfire.davdroid.model.CollectionInfo;
|
import at.bitfire.davdroid.model.CollectionInfo;
|
||||||
import at.bitfire.davdroid.model.ServiceDB;
|
|
||||||
import at.bitfire.ical4android.DateUtils;
|
import at.bitfire.ical4android.DateUtils;
|
||||||
import lombok.Cleanup;
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
import yuku.ambilwarna.AmbilWarnaDialog;
|
import yuku.ambilwarna.AmbilWarnaDialog;
|
||||||
|
|
||||||
public class CreateCalendarActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<CreateCalendarActivity.AccountInfo> {
|
public class CreateCalendarActivity extends AppCompatActivity {
|
||||||
public static final String EXTRA_ACCOUNT = "account";
|
public static final String EXTRA_ACCOUNT = "account";
|
||||||
|
|
||||||
protected Account account;
|
protected Account account;
|
||||||
@ -64,7 +49,7 @@ public class CreateCalendarActivity extends AppCompatActivity implements LoaderM
|
|||||||
colorSquare.setOnClickListener(new View.OnClickListener() {
|
colorSquare.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
new AmbilWarnaDialog(CreateCalendarActivity.this, ((ColorDrawable)colorSquare.getBackground()).getColor(), true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
new AmbilWarnaDialog(CreateCalendarActivity.this, ((ColorDrawable) colorSquare.getBackground()).getColor(), true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onCancel(AmbilWarnaDialog dialog) {
|
public void onCancel(AmbilWarnaDialog dialog) {
|
||||||
}
|
}
|
||||||
@ -76,8 +61,6 @@ public class CreateCalendarActivity extends AppCompatActivity implements LoaderM
|
|||||||
}).show();
|
}).show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
getSupportLoaderManager().initLoader(0, null, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -101,122 +84,33 @@ public class CreateCalendarActivity extends AppCompatActivity implements LoaderM
|
|||||||
boolean ok = true;
|
boolean ok = true;
|
||||||
CollectionInfo info = new CollectionInfo();
|
CollectionInfo info = new CollectionInfo();
|
||||||
|
|
||||||
Spinner spinner = (Spinner)findViewById(R.id.home_sets);
|
Spinner spinner;
|
||||||
String homeSet = (String)spinner.getSelectedItem();
|
|
||||||
|
|
||||||
EditText edit = (EditText)findViewById(R.id.display_name);
|
EditText edit = (EditText) findViewById(R.id.display_name);
|
||||||
info.displayName = edit.getText().toString();
|
info.displayName = edit.getText().toString();
|
||||||
if (TextUtils.isEmpty(info.displayName)) {
|
if (TextUtils.isEmpty(info.displayName)) {
|
||||||
edit.setError(getString(R.string.create_collection_display_name_required));
|
edit.setError(getString(R.string.create_collection_display_name_required));
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
edit = (EditText)findViewById(R.id.description);
|
edit = (EditText) findViewById(R.id.description);
|
||||||
info.description = StringUtils.trimToNull(edit.getText().toString());
|
info.description = StringUtils.trimToNull(edit.getText().toString());
|
||||||
|
|
||||||
View view = findViewById(R.id.color);
|
View view = findViewById(R.id.color);
|
||||||
info.color = ((ColorDrawable)view.getBackground()).getColor();
|
info.color = ((ColorDrawable) view.getBackground()).getColor();
|
||||||
|
|
||||||
spinner = (Spinner)findViewById(R.id.time_zone);
|
spinner = (Spinner) findViewById(R.id.time_zone);
|
||||||
net.fortuna.ical4j.model.TimeZone tz = DateUtils.tzRegistry.getTimeZone((String)spinner.getSelectedItem());
|
net.fortuna.ical4j.model.TimeZone tz = DateUtils.tzRegistry.getTimeZone((String) spinner.getSelectedItem());
|
||||||
if (tz != null) {
|
if (tz != null) {
|
||||||
Calendar cal = new Calendar();
|
Calendar cal = new Calendar();
|
||||||
cal.getComponents().add(tz.getVTimeZone());
|
cal.getComponents().add(tz.getVTimeZone());
|
||||||
info.timeZone = cal.toString();
|
info.timeZone = cal.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
RadioGroup typeGroup = (RadioGroup)findViewById(R.id.type);
|
|
||||||
switch (typeGroup.getCheckedRadioButtonId()) {
|
|
||||||
case R.id.type_events:
|
|
||||||
info.supportsVEVENT = true;
|
|
||||||
break;
|
|
||||||
case R.id.type_tasks:
|
|
||||||
info.supportsVTODO = true;
|
|
||||||
break;
|
|
||||||
case R.id.type_events_and_tasks:
|
|
||||||
info.supportsVEVENT = true;
|
|
||||||
info.supportsVTODO = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
info.type = CollectionInfo.Type.CALENDAR;
|
info.type = CollectionInfo.Type.CALENDAR;
|
||||||
info.url = HttpUrl.parse(homeSet).resolve(UUID.randomUUID().toString() + "/").toString();
|
|
||||||
CreateCollectionFragment.newInstance(account, info).show(getSupportFragmentManager(), null);
|
CreateCollectionFragment.newInstance(account, info).show(getSupportFragmentManager(), null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Loader<AccountInfo> onCreateLoader(int id, Bundle args) {
|
|
||||||
return new AccountInfoLoader(this, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadFinished(Loader<AccountInfo> loader, AccountInfo info) {
|
|
||||||
Spinner spinner = (Spinner)findViewById(R.id.time_zone);
|
|
||||||
String[] timeZones = TimeZone.getAvailableIDs();
|
|
||||||
spinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, timeZones));
|
|
||||||
// select system time zone
|
|
||||||
String defaultTimeZone = TimeZone.getDefault().getID();
|
|
||||||
for (int i = 0; i < timeZones.length; i++)
|
|
||||||
if (timeZones[i].equals(defaultTimeZone)) {
|
|
||||||
spinner.setSelection(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info != null) {
|
|
||||||
spinner = (Spinner)findViewById(R.id.home_sets);
|
|
||||||
spinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, info.homeSets));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoaderReset(Loader<AccountInfo> loader) {
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static class AccountInfo {
|
|
||||||
List<String> homeSets = new LinkedList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static class AccountInfoLoader extends AsyncTaskLoader<AccountInfo> {
|
|
||||||
private final Account account;
|
|
||||||
private final ServiceDB.OpenHelper dbHelper;
|
|
||||||
|
|
||||||
public AccountInfoLoader(Context context, Account account) {
|
|
||||||
super(context);
|
|
||||||
this.account = account;
|
|
||||||
dbHelper = new ServiceDB.OpenHelper(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStartLoading() {
|
|
||||||
forceLoad();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AccountInfo loadInBackground() {
|
|
||||||
final AccountInfo info = new AccountInfo();
|
|
||||||
|
|
||||||
// find DAV service and home sets
|
|
||||||
SQLiteDatabase db = dbHelper.getReadableDatabase();
|
|
||||||
try {
|
|
||||||
@Cleanup Cursor cursorService = db.query(ServiceDB.Services._TABLE, new String[] { ServiceDB.Services.ID },
|
|
||||||
ServiceDB.Services.ACCOUNT_NAME + "=? AND " + ServiceDB.Services.SERVICE + "=?",
|
|
||||||
new String[] { account.name, ServiceDB.Services.SERVICE_CALDAV }, null, null, null);
|
|
||||||
if (!cursorService.moveToNext())
|
|
||||||
return null;
|
|
||||||
String strServiceID = cursorService.getString(0);
|
|
||||||
|
|
||||||
@Cleanup Cursor cursorHomeSets = db.query(ServiceDB.HomeSets._TABLE, new String[] { ServiceDB.HomeSets.URL },
|
|
||||||
ServiceDB.HomeSets.SERVICE_ID + "=?", new String[] { strServiceID }, null, null, null);
|
|
||||||
while (cursorHomeSets.moveToNext())
|
|
||||||
info.homeSets.add(cursorHomeSets.getString(0));
|
|
||||||
} finally {
|
|
||||||
dbHelper.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -23,26 +23,16 @@ import android.support.v4.app.LoaderManager;
|
|||||||
import android.support.v4.content.AsyncTaskLoader;
|
import android.support.v4.content.AsyncTaskLoader;
|
||||||
import android.support.v4.content.Loader;
|
import android.support.v4.content.Loader;
|
||||||
|
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
import at.bitfire.davdroid.AccountSettings;
|
||||||
import org.xmlpull.v1.XmlSerializer;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
|
|
||||||
import at.bitfire.dav4android.DavResource;
|
|
||||||
import at.bitfire.dav4android.XmlUtils;
|
|
||||||
import at.bitfire.dav4android.exception.HttpException;
|
|
||||||
import at.bitfire.davdroid.App;
|
|
||||||
import at.bitfire.davdroid.DavUtils;
|
|
||||||
import at.bitfire.davdroid.HttpClient;
|
import at.bitfire.davdroid.HttpClient;
|
||||||
import at.bitfire.davdroid.InvalidAccountException;
|
import at.bitfire.davdroid.InvalidAccountException;
|
||||||
import at.bitfire.davdroid.R;
|
import at.bitfire.davdroid.R;
|
||||||
|
import at.bitfire.davdroid.journalmanager.Exceptions;
|
||||||
|
import at.bitfire.davdroid.journalmanager.JournalManager;
|
||||||
import at.bitfire.davdroid.model.CollectionInfo;
|
import at.bitfire.davdroid.model.CollectionInfo;
|
||||||
import at.bitfire.davdroid.model.ServiceDB;
|
import at.bitfire.davdroid.model.ServiceDB;
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
|
|
||||||
public class CreateCollectionFragment extends DialogFragment implements LoaderManager.LoaderCallbacks<Exception> {
|
public class CreateCollectionFragment extends DialogFragment implements LoaderManager.LoaderCallbacks<Exception> {
|
||||||
private static final String
|
private static final String
|
||||||
@ -127,95 +117,9 @@ public class CreateCollectionFragment extends DialogFragment implements LoaderMa
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Exception loadInBackground() {
|
public Exception loadInBackground() {
|
||||||
StringWriter writer = new StringWriter();
|
|
||||||
try {
|
|
||||||
XmlSerializer serializer = XmlUtils.newSerializer();
|
|
||||||
serializer.setOutput(writer);
|
|
||||||
serializer.startDocument("UTF-8", null);
|
|
||||||
serializer.setPrefix("", XmlUtils.NS_WEBDAV);
|
|
||||||
serializer.setPrefix("CAL", XmlUtils.NS_CALDAV);
|
|
||||||
serializer.setPrefix("CARD", XmlUtils.NS_CARDDAV);
|
|
||||||
|
|
||||||
serializer.startTag(XmlUtils.NS_WEBDAV, "mkcol");
|
|
||||||
serializer.startTag(XmlUtils.NS_WEBDAV, "set");
|
|
||||||
serializer.startTag(XmlUtils.NS_WEBDAV, "prop");
|
|
||||||
serializer.startTag(XmlUtils.NS_WEBDAV, "resourcetype");
|
|
||||||
serializer.startTag(XmlUtils.NS_WEBDAV, "collection");
|
|
||||||
serializer.endTag(XmlUtils.NS_WEBDAV, "collection");
|
|
||||||
if (info.type == CollectionInfo.Type.ADDRESS_BOOK) {
|
|
||||||
serializer.startTag(XmlUtils.NS_CARDDAV, "addressbook");
|
|
||||||
serializer.endTag(XmlUtils.NS_CARDDAV, "addressbook");
|
|
||||||
} else if (info.type == CollectionInfo.Type.CALENDAR) {
|
|
||||||
serializer.startTag(XmlUtils.NS_CALDAV, "calendar");
|
|
||||||
serializer.endTag(XmlUtils.NS_CALDAV, "calendar");
|
|
||||||
}
|
|
||||||
serializer.endTag(XmlUtils.NS_WEBDAV, "resourcetype");
|
|
||||||
if (info.displayName != null) {
|
|
||||||
serializer.startTag(XmlUtils.NS_WEBDAV, "displayname");
|
|
||||||
serializer.text(info.displayName);
|
|
||||||
serializer.endTag(XmlUtils.NS_WEBDAV, "displayname");
|
|
||||||
}
|
|
||||||
|
|
||||||
// addressbook-specific properties
|
|
||||||
if (info.type == CollectionInfo.Type.ADDRESS_BOOK) {
|
|
||||||
if (info.description != null) {
|
|
||||||
serializer.startTag(XmlUtils.NS_CARDDAV, "addressbook-description");
|
|
||||||
serializer.text(info.description);
|
|
||||||
serializer.endTag(XmlUtils.NS_CARDDAV, "addressbook-description");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// calendar-specific properties
|
|
||||||
if (info.type == CollectionInfo.Type.CALENDAR) {
|
|
||||||
if (info.description != null) {
|
|
||||||
serializer.startTag(XmlUtils.NS_CALDAV, "calendar-description");
|
|
||||||
serializer.text(info.description);
|
|
||||||
serializer.endTag(XmlUtils.NS_CALDAV, "calendar-description");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.color != null) {
|
|
||||||
serializer.startTag(XmlUtils.NS_APPLE_ICAL, "calendar-color");
|
|
||||||
serializer.text(DavUtils.ARGBtoCalDAVColor(info.color));
|
|
||||||
serializer.endTag(XmlUtils.NS_APPLE_ICAL, "calendar-color");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.timeZone != null) {
|
|
||||||
serializer.startTag(XmlUtils.NS_CALDAV, "calendar-timezone");
|
|
||||||
serializer.cdsect(info.timeZone);
|
|
||||||
serializer.endTag(XmlUtils.NS_CALDAV, "calendar-timezone");
|
|
||||||
}
|
|
||||||
|
|
||||||
serializer.startTag(XmlUtils.NS_CALDAV, "supported-calendar-component-set");
|
|
||||||
if (BooleanUtils.isTrue(info.supportsVEVENT)) {
|
|
||||||
serializer.startTag(XmlUtils.NS_CALDAV, "comp");
|
|
||||||
serializer.attribute(null, "name", "VEVENT");
|
|
||||||
serializer.endTag(XmlUtils.NS_CALDAV, "comp");
|
|
||||||
}
|
|
||||||
if (BooleanUtils.isTrue(info.supportsVTODO)) {
|
|
||||||
serializer.startTag(XmlUtils.NS_CALDAV, "comp");
|
|
||||||
serializer.attribute(null, "name", "VTODO");
|
|
||||||
serializer.endTag(XmlUtils.NS_CALDAV, "comp");
|
|
||||||
}
|
|
||||||
serializer.endTag(XmlUtils.NS_CALDAV, "supported-calendar-component-set");
|
|
||||||
}
|
|
||||||
|
|
||||||
serializer.endTag(XmlUtils.NS_WEBDAV, "prop");
|
|
||||||
serializer.endTag(XmlUtils.NS_WEBDAV, "set");
|
|
||||||
serializer.endTag(XmlUtils.NS_WEBDAV, "mkcol");
|
|
||||||
serializer.endDocument();
|
|
||||||
} catch (IOException e) {
|
|
||||||
App.log.log(Level.SEVERE, "Couldn't assemble Extended MKCOL request", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
|
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
OkHttpClient client = HttpClient.create(getContext(), account);
|
|
||||||
DavResource collection = new DavResource(client, HttpUrl.parse(info.url));
|
|
||||||
|
|
||||||
// create collection on remote server
|
|
||||||
collection.mkCol(writer.toString());
|
|
||||||
|
|
||||||
// now insert collection into database:
|
// now insert collection into database:
|
||||||
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
||||||
|
|
||||||
@ -235,11 +139,19 @@ public class CreateCollectionFragment extends DialogFragment implements LoaderMa
|
|||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
long serviceID = c.getLong(0);
|
long serviceID = c.getLong(0);
|
||||||
|
|
||||||
|
AccountSettings settings = new AccountSettings(getContext(), account);
|
||||||
|
HttpUrl principal = HttpUrl.get(settings.getUri());
|
||||||
|
|
||||||
|
JournalManager journalManager = new JournalManager(HttpClient.create(getContext(), account), principal);
|
||||||
|
journalManager.putJournal(new JournalManager.Journal(settings.password(), info.toJson(), info.url));
|
||||||
|
|
||||||
// 2. add collection to service
|
// 2. add collection to service
|
||||||
ContentValues values = info.toDB();
|
ContentValues values = info.toDB();
|
||||||
values.put(ServiceDB.Collections.SERVICE_ID, serviceID);
|
values.put(ServiceDB.Collections.SERVICE_ID, serviceID);
|
||||||
db.insert(ServiceDB.Collections._TABLE, null, values);
|
db.insert(ServiceDB.Collections._TABLE, null, values);
|
||||||
} catch(InvalidAccountException|IOException|HttpException|IllegalStateException e) {
|
} catch(IllegalStateException|Exceptions.HttpException e) {
|
||||||
|
return e;
|
||||||
|
} catch (InvalidAccountException e) {
|
||||||
return e;
|
return e;
|
||||||
} finally {
|
} finally {
|
||||||
dbHelper.close();
|
dbHelper.close();
|
||||||
|
@ -38,13 +38,13 @@ import java.io.IOException;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import at.bitfire.dav4android.exception.HttpException;
|
|
||||||
import at.bitfire.davdroid.AccountSettings;
|
import at.bitfire.davdroid.AccountSettings;
|
||||||
import at.bitfire.davdroid.App;
|
import at.bitfire.davdroid.App;
|
||||||
import at.bitfire.davdroid.BuildConfig;
|
import at.bitfire.davdroid.BuildConfig;
|
||||||
import at.bitfire.davdroid.Constants;
|
import at.bitfire.davdroid.Constants;
|
||||||
import at.bitfire.davdroid.InvalidAccountException;
|
import at.bitfire.davdroid.InvalidAccountException;
|
||||||
import at.bitfire.davdroid.R;
|
import at.bitfire.davdroid.R;
|
||||||
|
import at.bitfire.davdroid.journalmanager.Exceptions.HttpException;
|
||||||
import at.bitfire.davdroid.model.ServiceDB;
|
import at.bitfire.davdroid.model.ServiceDB;
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
|
|
||||||
@ -159,21 +159,21 @@ public class DebugInfoActivity extends AppCompatActivity implements LoaderManage
|
|||||||
String logs = null,
|
String logs = null,
|
||||||
authority = null;
|
authority = null;
|
||||||
Account account = null;
|
Account account = null;
|
||||||
int phase = -1;
|
String phase = null;
|
||||||
|
|
||||||
if (extras != null) {
|
if (extras != null) {
|
||||||
throwable = (Throwable)extras.getSerializable(KEY_THROWABLE);
|
throwable = (Throwable)extras.getSerializable(KEY_THROWABLE);
|
||||||
logs = extras.getString(KEY_LOGS);
|
logs = extras.getString(KEY_LOGS);
|
||||||
account = extras.getParcelable(KEY_ACCOUNT);
|
account = extras.getParcelable(KEY_ACCOUNT);
|
||||||
authority = extras.getString(KEY_AUTHORITY);
|
authority = extras.getString(KEY_AUTHORITY);
|
||||||
phase = extras.getInt(KEY_PHASE, -1);
|
phase = extras.getString(KEY_PHASE, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder report = new StringBuilder();
|
StringBuilder report = new StringBuilder();
|
||||||
|
|
||||||
// begin with most specific information
|
// begin with most specific information
|
||||||
|
|
||||||
if (phase != -1)
|
if (phase != null)
|
||||||
report.append("SYNCHRONIZATION INFO\nSynchronization phase: ").append(phase).append("\n");
|
report.append("SYNCHRONIZATION INFO\nSynchronization phase: ").append(phase).append("\n");
|
||||||
if (account != null)
|
if (account != null)
|
||||||
report.append("Account name: ").append(account.name).append("\n");
|
report.append("Account name: ").append(account.name).append("\n");
|
||||||
@ -181,11 +181,13 @@ public class DebugInfoActivity extends AppCompatActivity implements LoaderManage
|
|||||||
report.append("Authority: ").append(authority).append("\n");
|
report.append("Authority: ").append(authority).append("\n");
|
||||||
|
|
||||||
if (throwable instanceof HttpException) {
|
if (throwable instanceof HttpException) {
|
||||||
|
/* FIXME
|
||||||
HttpException http = (HttpException)throwable;
|
HttpException http = (HttpException)throwable;
|
||||||
if (http.request != null)
|
if (http.request != null)
|
||||||
report.append("\nHTTP REQUEST:\n").append(http.request).append("\n\n");
|
report.append("\nHTTP REQUEST:\n").append(http.request).append("\n\n");
|
||||||
if (http.response != null)
|
if (http.response != null)
|
||||||
report.append("HTTP RESPONSE:\n").append(http.response).append("\n");
|
report.append("HTTP RESPONSE:\n").append(http.response).append("\n");
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
if (throwable != null)
|
if (throwable != null)
|
||||||
|
@ -24,17 +24,15 @@ import android.support.v4.content.Loader;
|
|||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import at.bitfire.davdroid.AccountSettings;
|
||||||
|
|
||||||
import at.bitfire.dav4android.DavResource;
|
|
||||||
import at.bitfire.dav4android.exception.HttpException;
|
|
||||||
import at.bitfire.davdroid.HttpClient;
|
import at.bitfire.davdroid.HttpClient;
|
||||||
import at.bitfire.davdroid.InvalidAccountException;
|
import at.bitfire.davdroid.InvalidAccountException;
|
||||||
import at.bitfire.davdroid.R;
|
import at.bitfire.davdroid.R;
|
||||||
|
import at.bitfire.davdroid.journalmanager.Exceptions;
|
||||||
|
import at.bitfire.davdroid.journalmanager.JournalManager;
|
||||||
import at.bitfire.davdroid.model.CollectionInfo;
|
import at.bitfire.davdroid.model.CollectionInfo;
|
||||||
import at.bitfire.davdroid.model.ServiceDB;
|
import at.bitfire.davdroid.model.ServiceDB;
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
|
|
||||||
public class DeleteCollectionFragment extends DialogFragment implements LoaderManager.LoaderCallbacks<Exception> {
|
public class DeleteCollectionFragment extends DialogFragment implements LoaderManager.LoaderCallbacks<Exception> {
|
||||||
protected static final String
|
protected static final String
|
||||||
@ -67,7 +65,7 @@ public class DeleteCollectionFragment extends DialogFragment implements LoaderMa
|
|||||||
@Override
|
@Override
|
||||||
public Loader<Exception> onCreateLoader(int id, Bundle args) {
|
public Loader<Exception> onCreateLoader(int id, Bundle args) {
|
||||||
account = args.getParcelable(ARG_ACCOUNT);
|
account = args.getParcelable(ARG_ACCOUNT);
|
||||||
collectionInfo = (CollectionInfo)args.getSerializable(ARG_COLLECTION_INFO);
|
collectionInfo = (CollectionInfo) args.getSerializable(ARG_COLLECTION_INFO);
|
||||||
return new DeleteCollectionLoader(getContext(), account, collectionInfo);
|
return new DeleteCollectionLoader(getContext(), account, collectionInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +80,7 @@ public class DeleteCollectionFragment extends DialogFragment implements LoaderMa
|
|||||||
else {
|
else {
|
||||||
Activity activity = getActivity();
|
Activity activity = getActivity();
|
||||||
if (activity instanceof AccountActivity)
|
if (activity instanceof AccountActivity)
|
||||||
((AccountActivity)activity).reload();
|
((AccountActivity) activity).reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,18 +109,21 @@ public class DeleteCollectionFragment extends DialogFragment implements LoaderMa
|
|||||||
@Override
|
@Override
|
||||||
public Exception loadInBackground() {
|
public Exception loadInBackground() {
|
||||||
try {
|
try {
|
||||||
OkHttpClient httpClient = HttpClient.create(getContext(), account);
|
|
||||||
DavResource collection = new DavResource(httpClient, HttpUrl.parse(collectionInfo.url));
|
|
||||||
|
|
||||||
// delete collection from server
|
|
||||||
collection.delete(null);
|
|
||||||
|
|
||||||
// delete collection locally
|
// delete collection locally
|
||||||
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
||||||
db.delete(ServiceDB.Collections._TABLE, ServiceDB.Collections.ID + "=?", new String[] { String.valueOf(collectionInfo.id) });
|
|
||||||
|
AccountSettings settings = new AccountSettings(getContext(), account);
|
||||||
|
HttpUrl principal = HttpUrl.get(settings.getUri());
|
||||||
|
|
||||||
|
JournalManager journalManager = new JournalManager(HttpClient.create(getContext(), account), principal);
|
||||||
|
journalManager.deleteJournal(new JournalManager.Journal(settings.password(), collectionInfo.toJson(), collectionInfo.url));
|
||||||
|
|
||||||
|
db.delete(ServiceDB.Collections._TABLE, ServiceDB.Collections.ID + "=?", new String[]{String.valueOf(collectionInfo.id)});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch (InvalidAccountException|IOException|HttpException e) {
|
} catch (Exceptions.HttpException e) {
|
||||||
|
return e;
|
||||||
|
} catch (InvalidAccountException e) {
|
||||||
return e;
|
return e;
|
||||||
} finally {
|
} finally {
|
||||||
dbHelper.close();
|
dbHelper.close();
|
||||||
@ -145,7 +146,7 @@ public class DeleteCollectionFragment extends DialogFragment implements LoaderMa
|
|||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
CollectionInfo collectionInfo = (CollectionInfo)getArguments().getSerializable(ARG_COLLECTION_INFO);
|
CollectionInfo collectionInfo = (CollectionInfo) getArguments().getSerializable(ARG_COLLECTION_INFO);
|
||||||
String name = TextUtils.isEmpty(collectionInfo.displayName) ? collectionInfo.url : collectionInfo.displayName;
|
String name = TextUtils.isEmpty(collectionInfo.displayName) ? collectionInfo.url : collectionInfo.displayName;
|
||||||
|
|
||||||
return new AlertDialog.Builder(getContext())
|
return new AlertDialog.Builder(getContext())
|
||||||
|
@ -19,8 +19,8 @@ import android.support.v7.app.AlertDialog;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import at.bitfire.dav4android.exception.HttpException;
|
|
||||||
import at.bitfire.davdroid.R;
|
import at.bitfire.davdroid.R;
|
||||||
|
import at.bitfire.davdroid.journalmanager.Exceptions.HttpException;
|
||||||
|
|
||||||
public class ExceptionInfoFragment extends DialogFragment {
|
public class ExceptionInfoFragment extends DialogFragment {
|
||||||
protected static final String
|
protected static final String
|
||||||
|
@ -14,9 +14,6 @@ import android.app.Dialog;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@ -28,7 +25,6 @@ import android.support.v7.app.AlertDialog;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.logging.Level;
|
|
||||||
|
|
||||||
import at.bitfire.davdroid.App;
|
import at.bitfire.davdroid.App;
|
||||||
import at.bitfire.davdroid.BuildConfig;
|
import at.bitfire.davdroid.BuildConfig;
|
||||||
@ -139,7 +135,7 @@ public class StartupDialogFragment extends DialogFragment {
|
|||||||
.setNeutralButton(R.string.startup_development_version_give_feedback, new DialogInterface.OnClickListener() {
|
.setNeutralButton(R.string.startup_development_version_give_feedback, new DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("forums/").build()));
|
startActivity(new Intent(Intent.ACTION_VIEW, Constants.feedbackUri));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.create();
|
.create();
|
||||||
|
@ -10,61 +10,25 @@ package at.bitfire.davdroid.ui.setup;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import org.xbill.DNS.Lookup;
|
|
||||||
import org.xbill.DNS.Record;
|
|
||||||
import org.xbill.DNS.SRVRecord;
|
|
||||||
import org.xbill.DNS.TXTRecord;
|
|
||||||
import org.xbill.DNS.Type;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import at.bitfire.dav4android.Constants;
|
|
||||||
import at.bitfire.dav4android.DavResource;
|
|
||||||
import at.bitfire.dav4android.UrlUtils;
|
|
||||||
import at.bitfire.dav4android.exception.DavException;
|
|
||||||
import at.bitfire.dav4android.exception.HttpException;
|
|
||||||
import at.bitfire.dav4android.exception.NotFoundException;
|
|
||||||
import at.bitfire.dav4android.property.AddressbookDescription;
|
|
||||||
import at.bitfire.dav4android.property.AddressbookHomeSet;
|
|
||||||
import at.bitfire.dav4android.property.CalendarColor;
|
|
||||||
import at.bitfire.dav4android.property.CalendarDescription;
|
|
||||||
import at.bitfire.dav4android.property.CalendarHomeSet;
|
|
||||||
import at.bitfire.dav4android.property.CalendarTimezone;
|
|
||||||
import at.bitfire.dav4android.property.CalendarUserAddressSet;
|
|
||||||
import at.bitfire.dav4android.property.CurrentUserPrincipal;
|
|
||||||
import at.bitfire.dav4android.property.CurrentUserPrivilegeSet;
|
|
||||||
import at.bitfire.dav4android.property.DisplayName;
|
|
||||||
import at.bitfire.dav4android.property.ResourceType;
|
|
||||||
import at.bitfire.dav4android.property.SupportedCalendarComponentSet;
|
|
||||||
import at.bitfire.davdroid.HttpClient;
|
import at.bitfire.davdroid.HttpClient;
|
||||||
|
import at.bitfire.davdroid.journalmanager.Exceptions;
|
||||||
|
import at.bitfire.davdroid.journalmanager.JournalAuthenticator;
|
||||||
import at.bitfire.davdroid.log.StringHandler;
|
import at.bitfire.davdroid.log.StringHandler;
|
||||||
import at.bitfire.davdroid.model.CollectionInfo;
|
import at.bitfire.davdroid.model.CollectionInfo;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
public class DavResourceFinder {
|
public class DavResourceFinder {
|
||||||
public enum Service {
|
|
||||||
CALDAV("caldav"),
|
|
||||||
CARDDAV("carddav");
|
|
||||||
|
|
||||||
final String name;
|
|
||||||
Service(String name) { this.name = name;}
|
|
||||||
@Override public String toString() { return name; }
|
|
||||||
}
|
|
||||||
|
|
||||||
protected final Context context;
|
protected final Context context;
|
||||||
protected final LoginCredentials credentials;
|
protected final LoginCredentials credentials;
|
||||||
|
|
||||||
@ -81,304 +45,42 @@ public class DavResourceFinder {
|
|||||||
log.addHandler(logBuffer);
|
log.addHandler(logBuffer);
|
||||||
|
|
||||||
httpClient = HttpClient.create(context, log);
|
httpClient = HttpClient.create(context, log);
|
||||||
httpClient = HttpClient.addAuthentication(httpClient, credentials.userName, credentials.password);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Configuration findInitialConfiguration() {
|
public Configuration findInitialConfiguration() {
|
||||||
final Configuration.ServiceInfo
|
boolean failed = false;
|
||||||
cardDavConfig = findInitialConfiguration(Service.CARDDAV),
|
Configuration.ServiceInfo
|
||||||
calDavConfig = findInitialConfiguration(Service.CALDAV);
|
cardDavConfig = findInitialConfiguration(CollectionInfo.Type.ADDRESS_BOOK),
|
||||||
|
calDavConfig = findInitialConfiguration(CollectionInfo.Type.CALENDAR);
|
||||||
|
|
||||||
|
JournalAuthenticator authenticator = new JournalAuthenticator(httpClient, HttpUrl.get(credentials.uri));
|
||||||
|
|
||||||
|
String authtoken = null;
|
||||||
|
try {
|
||||||
|
authtoken = authenticator.getAuthToken(credentials.userName, credentials.password);
|
||||||
|
} catch (Exceptions.HttpException e) {
|
||||||
|
log.warning(e.getMessage());
|
||||||
|
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
|
||||||
return new Configuration(
|
return new Configuration(
|
||||||
credentials.userName, credentials.password,
|
credentials.uri,
|
||||||
|
credentials.userName, authtoken,
|
||||||
cardDavConfig, calDavConfig,
|
cardDavConfig, calDavConfig,
|
||||||
logBuffer.toString()
|
logBuffer.toString(), failed
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Configuration.ServiceInfo findInitialConfiguration(@NonNull Service service) {
|
protected Configuration.ServiceInfo findInitialConfiguration(@NonNull CollectionInfo.Type service) {
|
||||||
// user-given base URI (either mailto: URI or http(s):// URL)
|
|
||||||
final URI baseURI = credentials.uri;
|
|
||||||
|
|
||||||
// domain for service discovery
|
|
||||||
String discoveryFQDN = null;
|
|
||||||
|
|
||||||
// put discovered information here
|
// put discovered information here
|
||||||
final Configuration.ServiceInfo config = new Configuration.ServiceInfo();
|
final Configuration.ServiceInfo config = new Configuration.ServiceInfo();
|
||||||
log.info("Finding initial " + service.name + " service configuration");
|
log.info("Finding initial " + service.toString() + " service configuration");
|
||||||
|
|
||||||
if ("http".equalsIgnoreCase(baseURI.getScheme()) || "https".equalsIgnoreCase(baseURI.getScheme())) {
|
return config;
|
||||||
final HttpUrl baseURL = HttpUrl.get(baseURI);
|
|
||||||
|
|
||||||
// remember domain for service discovery
|
|
||||||
// try service discovery only for https:// URLs because only secure service discovery is implemented
|
|
||||||
if ("https".equalsIgnoreCase(baseURL.scheme()))
|
|
||||||
discoveryFQDN = baseURI.getHost();
|
|
||||||
|
|
||||||
checkUserGivenURL(baseURL, service, config);
|
|
||||||
|
|
||||||
if (config.principal == null)
|
|
||||||
try {
|
|
||||||
config.principal = getCurrentUserPrincipal(baseURL.resolve("/.well-known/" + service.name), service);
|
|
||||||
} catch (IOException|HttpException|DavException e) {
|
|
||||||
log.log(Level.FINE, "Well-known URL detection failed", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if ("mailto".equalsIgnoreCase(baseURI.getScheme())) {
|
|
||||||
String mailbox = baseURI.getSchemeSpecificPart();
|
|
||||||
|
|
||||||
int posAt = mailbox.lastIndexOf("@");
|
|
||||||
if (posAt != -1)
|
|
||||||
discoveryFQDN = mailbox.substring(posAt + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: If user-given URL didn't reveal a principal, search for it: SERVICE DISCOVERY
|
|
||||||
if (config.principal == null && discoveryFQDN != null) {
|
|
||||||
log.info("No principal found at user-given URL, trying to discover");
|
|
||||||
try {
|
|
||||||
config.principal = discoverPrincipalUrl(discoveryFQDN, service);
|
|
||||||
} catch (IOException|HttpException|DavException e) {
|
|
||||||
log.log(Level.FINE, service.name + " service discovery failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.principal != null && service == Service.CALDAV) {
|
|
||||||
// query email address (CalDAV scheduling: calendar-user-address-set)
|
|
||||||
DavResource davPrincipal = new DavResource(httpClient, HttpUrl.get(config.principal), log);
|
|
||||||
try {
|
|
||||||
davPrincipal.propfind(0, CalendarUserAddressSet.NAME);
|
|
||||||
CalendarUserAddressSet addressSet = (CalendarUserAddressSet)davPrincipal.properties.get(CalendarUserAddressSet.NAME);
|
|
||||||
if (addressSet != null)
|
|
||||||
for (String href : addressSet.hrefs)
|
|
||||||
try {
|
|
||||||
URI uri = new URI(href);
|
|
||||||
if ("mailto".equals(uri.getScheme()))
|
|
||||||
config.email = uri.getSchemeSpecificPart();
|
|
||||||
} catch(URISyntaxException e) {
|
|
||||||
Constants.log.log(Level.WARNING, "Unparseable user address", e);
|
|
||||||
}
|
|
||||||
} catch(IOException | HttpException | DavException e) {
|
|
||||||
Constants.log.log(Level.WARNING, "Couldn't query user email address", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return config or null if config doesn't contain useful information
|
|
||||||
boolean serviceAvailable = config.principal != null || !config.homeSets.isEmpty() || !config.collections.isEmpty();
|
|
||||||
return serviceAvailable ? config : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void checkUserGivenURL(@NonNull HttpUrl baseURL, @NonNull Service service, @NonNull Configuration.ServiceInfo config) {
|
|
||||||
log.info("Checking user-given URL: " + baseURL.toString());
|
|
||||||
|
|
||||||
HttpUrl principal = null;
|
|
||||||
try {
|
|
||||||
DavResource davBase = new DavResource(httpClient, baseURL, log);
|
|
||||||
|
|
||||||
if (service == Service.CARDDAV) {
|
|
||||||
davBase.propfind(0,
|
|
||||||
ResourceType.NAME, DisplayName.NAME, AddressbookDescription.NAME,
|
|
||||||
AddressbookHomeSet.NAME,
|
|
||||||
CurrentUserPrincipal.NAME
|
|
||||||
);
|
|
||||||
rememberIfAddressBookOrHomeset(davBase, config);
|
|
||||||
|
|
||||||
} else if (service == Service.CALDAV) {
|
|
||||||
davBase.propfind(0,
|
|
||||||
ResourceType.NAME, DisplayName.NAME, CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME, CurrentUserPrivilegeSet.NAME, SupportedCalendarComponentSet.NAME,
|
|
||||||
CalendarHomeSet.NAME,
|
|
||||||
CurrentUserPrincipal.NAME
|
|
||||||
);
|
|
||||||
rememberIfCalendarOrHomeset(davBase, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for current-user-principal
|
|
||||||
CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal)davBase.properties.get(CurrentUserPrincipal.NAME);
|
|
||||||
if (currentUserPrincipal != null && currentUserPrincipal.href != null)
|
|
||||||
principal = davBase.location.resolve(currentUserPrincipal.href);
|
|
||||||
|
|
||||||
// check for resource type "principal"
|
|
||||||
if (principal == null) {
|
|
||||||
ResourceType resourceType = (ResourceType)davBase.properties.get(ResourceType.NAME);
|
|
||||||
if (resourceType != null && resourceType.types.contains(ResourceType.PRINCIPAL))
|
|
||||||
principal = davBase.location;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a principal has been detected successfully, ensure that it provides the required service.
|
|
||||||
if (principal != null && providesService(principal, service))
|
|
||||||
config.principal = principal.uri();
|
|
||||||
|
|
||||||
} catch (IOException|HttpException|DavException e) {
|
|
||||||
log.log(Level.FINE, "PROPFIND/OPTIONS on user-given URL failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If #dav is an address book or an address book home set, it will added to
|
|
||||||
* config.collections or config.homesets. Only evaluates already known properties,
|
|
||||||
* does not call dav.propfind()! URLs will be stored with trailing "/".
|
|
||||||
* @param dav resource whose properties are evaluated
|
|
||||||
* @param config structure where the address book (collection) and/or home set is stored into (if found)
|
|
||||||
*/
|
|
||||||
protected void rememberIfAddressBookOrHomeset(@NonNull DavResource dav, @NonNull Configuration.ServiceInfo config) {
|
|
||||||
// Is the collection an address book?
|
|
||||||
ResourceType resourceType = (ResourceType)dav.properties.get(ResourceType.NAME);
|
|
||||||
if (resourceType != null && resourceType.types.contains(ResourceType.ADDRESSBOOK)) {
|
|
||||||
dav.location = UrlUtils.withTrailingSlash(dav.location);
|
|
||||||
log.info("Found address book at " + dav.location);
|
|
||||||
config.collections.put(dav.location.uri(), CollectionInfo.fromDavResource(dav));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Does the collection refer to address book homesets?
|
|
||||||
AddressbookHomeSet homeSets = (AddressbookHomeSet)dav.properties.get(AddressbookHomeSet.NAME);
|
|
||||||
if (homeSets != null)
|
|
||||||
for (String href : homeSets.hrefs) {
|
|
||||||
HttpUrl location = UrlUtils.withTrailingSlash(dav.location.resolve(href));
|
|
||||||
log.info("Found addressbook home-set at " + location);
|
|
||||||
config.homeSets.add(location.uri());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void rememberIfCalendarOrHomeset(@NonNull DavResource dav, @NonNull Configuration.ServiceInfo config) {
|
|
||||||
// Is the collection a calendar collection?
|
|
||||||
ResourceType resourceType = (ResourceType)dav.properties.get(ResourceType.NAME);
|
|
||||||
if (resourceType != null && resourceType.types.contains(ResourceType.CALENDAR)) {
|
|
||||||
dav.location = UrlUtils.withTrailingSlash(dav.location);
|
|
||||||
log.info("Found calendar collection at " + dav.location);
|
|
||||||
config.collections.put(dav.location.uri(), CollectionInfo.fromDavResource(dav));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Does the collection refer to calendar homesets?
|
|
||||||
CalendarHomeSet homeSets = (CalendarHomeSet)dav.properties.get(CalendarHomeSet.NAME);
|
|
||||||
if (homeSets != null)
|
|
||||||
for (String href : homeSets.hrefs)
|
|
||||||
config.homeSets.add(UrlUtils.withTrailingSlash(dav.location.resolve(href)).uri());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected boolean providesService(HttpUrl url, Service service) throws IOException {
|
|
||||||
DavResource davPrincipal = new DavResource(httpClient, url, log);
|
|
||||||
try {
|
|
||||||
davPrincipal.options();
|
|
||||||
|
|
||||||
if ((service == Service.CARDDAV && davPrincipal.capabilities.contains("addressbook")) ||
|
|
||||||
(service == Service.CALDAV && davPrincipal.capabilities.contains("calendar-access")))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (HttpException|DavException e) {
|
|
||||||
log.log(Level.SEVERE, "Couldn't detect services on " + url, e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to find the principal URL by performing service discovery on a given domain name.
|
|
||||||
* Only secure services (caldavs, carddavs) will be discovered!
|
|
||||||
* @param domain domain name, e.g. "icloud.com"
|
|
||||||
* @param service service to discover (CALDAV or CARDDAV)
|
|
||||||
* @return principal URL, or null if none found
|
|
||||||
*/
|
|
||||||
protected URI discoverPrincipalUrl(@NonNull String domain, @NonNull Service service) throws IOException, HttpException, DavException {
|
|
||||||
String scheme;
|
|
||||||
String fqdn;
|
|
||||||
Integer port = 443;
|
|
||||||
List<String> paths = new LinkedList<>(); // there may be multiple paths to try
|
|
||||||
|
|
||||||
final String query = "_" + service.name + "s._tcp." + domain;
|
|
||||||
log.fine("Looking up SRV records for " + query);
|
|
||||||
Record[] records = new Lookup(query, Type.SRV).run();
|
|
||||||
if (records != null && records.length >= 1) {
|
|
||||||
// choose SRV record to use (query may return multiple SRV records)
|
|
||||||
SRVRecord srv = selectSRVRecord(records);
|
|
||||||
|
|
||||||
scheme = "https";
|
|
||||||
fqdn = srv.getTarget().toString(true);
|
|
||||||
port = srv.getPort();
|
|
||||||
log.info("Found " + service + " service at https://" + fqdn + ":" + port);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// no SRV records, try domain name as FQDN
|
|
||||||
log.info("Didn't find " + service + " service, trying at https://" + domain + ":" + port);
|
|
||||||
|
|
||||||
scheme = "https";
|
|
||||||
fqdn = domain;
|
|
||||||
}
|
|
||||||
|
|
||||||
// look for TXT record too (for initial context path)
|
|
||||||
records = new Lookup(query, Type.TXT).run();
|
|
||||||
if (records != null)
|
|
||||||
for (Record record : records)
|
|
||||||
if (record instanceof TXTRecord)
|
|
||||||
for (String segment : (List<String>)((TXTRecord)record).getStrings())
|
|
||||||
if (segment.startsWith("path=")) {
|
|
||||||
paths.add(segment.substring(5));
|
|
||||||
log.info("Found TXT record; initial context path=" + paths);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there's TXT record and if it it's wrong, try well-known
|
|
||||||
paths.add("/.well-known/" + service.name);
|
|
||||||
// if this fails, too, try "/"
|
|
||||||
paths.add("/");
|
|
||||||
|
|
||||||
for (String path : paths)
|
|
||||||
try {
|
|
||||||
HttpUrl initialContextPath = new HttpUrl.Builder()
|
|
||||||
.scheme(scheme)
|
|
||||||
.host(fqdn).port(port)
|
|
||||||
.encodedPath(path)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
log.info("Trying to determine principal from initial context path=" + initialContextPath);
|
|
||||||
URI principal = getCurrentUserPrincipal(initialContextPath, service);
|
|
||||||
|
|
||||||
if (principal != null)
|
|
||||||
return principal;
|
|
||||||
} catch(NotFoundException|IllegalArgumentException e) {
|
|
||||||
log.log(Level.WARNING, "No resource found", e);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queries a given URL for current-user-principal
|
|
||||||
* @param url URL to query with PROPFIND (Depth: 0)
|
|
||||||
* @param service required service (may be null, in which case no service check is done)
|
|
||||||
* @return current-user-principal URL that provides required service, or null if none
|
|
||||||
*/
|
|
||||||
public URI getCurrentUserPrincipal(HttpUrl url, Service service) throws IOException, HttpException, DavException {
|
|
||||||
DavResource dav = new DavResource(httpClient, url, log);
|
|
||||||
dav.propfind(0, CurrentUserPrincipal.NAME);
|
|
||||||
|
|
||||||
CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal)dav.properties.get(CurrentUserPrincipal.NAME);
|
|
||||||
if (currentUserPrincipal != null && currentUserPrincipal.href != null) {
|
|
||||||
HttpUrl principal = dav.location.resolve(currentUserPrincipal.href);
|
|
||||||
if (principal != null) {
|
|
||||||
log.info("Found current-user-principal: " + principal);
|
|
||||||
|
|
||||||
// service check
|
|
||||||
if (service != null && !providesService(principal, service)) {
|
|
||||||
log.info(principal + " doesn't provide required " + service + " service");
|
|
||||||
principal = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return principal != null ? principal.uri() : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// helpers
|
|
||||||
|
|
||||||
private SRVRecord selectSRVRecord(Record[] records) {
|
|
||||||
if (records.length > 1)
|
|
||||||
log.warning("Multiple SRV records not supported yet; using first one");
|
|
||||||
return (SRVRecord)records[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// data classes
|
// data classes
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ -388,20 +90,22 @@ public class DavResourceFinder {
|
|||||||
|
|
||||||
@ToString
|
@ToString
|
||||||
public static class ServiceInfo implements Serializable {
|
public static class ServiceInfo implements Serializable {
|
||||||
public URI principal;
|
public final Map<String, CollectionInfo> collections = new HashMap<>();
|
||||||
public final Set<URI> homeSets = new HashSet<>();
|
|
||||||
public final Map<URI, CollectionInfo> collections = new HashMap<>();
|
|
||||||
|
|
||||||
public String email;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final String userName, password;
|
public final URI url;
|
||||||
|
|
||||||
|
public final String userName, authtoken;
|
||||||
|
public String rawPassword;
|
||||||
|
public String password;
|
||||||
|
|
||||||
public final ServiceInfo cardDAV;
|
public final ServiceInfo cardDAV;
|
||||||
public final ServiceInfo calDAV;
|
public final ServiceInfo calDAV;
|
||||||
|
|
||||||
public final String logs;
|
public final String logs;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final boolean failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ public class DetectConfigurationFragment extends DialogFragment implements Loade
|
|||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(Loader<Configuration> loader, Configuration data) {
|
public void onLoadFinished(Loader<Configuration> loader, Configuration data) {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
if (data.calDAV == null && data.cardDAV == null)
|
if (data.isFailed())
|
||||||
// no service found: show error message
|
// no service found: show error message
|
||||||
getFragmentManager().beginTransaction()
|
getFragmentManager().beginTransaction()
|
||||||
.add(NothingDetectedFragment.newInstance(data.logs), null)
|
.add(NothingDetectedFragment.newInstance(data.logs), null)
|
||||||
@ -72,7 +72,7 @@ public class DetectConfigurationFragment extends DialogFragment implements Loade
|
|||||||
else
|
else
|
||||||
// service found: continue
|
// service found: continue
|
||||||
getFragmentManager().beginTransaction()
|
getFragmentManager().beginTransaction()
|
||||||
.replace(android.R.id.content, AccountDetailsFragment.newInstance(data))
|
.replace(android.R.id.content, EncryptionDetailsFragment.newInstance(data))
|
||||||
.addToBackStack(null)
|
.addToBackStack(null)
|
||||||
.commitAllowingStateLoss();
|
.commitAllowingStateLoss();
|
||||||
} else
|
} else
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
package at.bitfire.davdroid.ui.setup;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import at.bitfire.davdroid.R;
|
||||||
|
import at.bitfire.davdroid.ui.widget.EditPassword;
|
||||||
|
|
||||||
|
public class EncryptionDetailsFragment extends Fragment {
|
||||||
|
|
||||||
|
private static final String KEY_CONFIG = "config";
|
||||||
|
EditPassword editPassword = null;
|
||||||
|
|
||||||
|
|
||||||
|
public static EncryptionDetailsFragment newInstance(DavResourceFinder.Configuration config) {
|
||||||
|
EncryptionDetailsFragment frag = new EncryptionDetailsFragment();
|
||||||
|
Bundle args = new Bundle(1);
|
||||||
|
args.putSerializable(KEY_CONFIG, config);
|
||||||
|
frag.setArguments(args);
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
final View v = inflater.inflate(R.layout.login_encryption_details, container, false);
|
||||||
|
|
||||||
|
Button btnBack = (Button)v.findViewById(R.id.back);
|
||||||
|
btnBack.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
getFragmentManager().popBackStack();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final DavResourceFinder.Configuration config = (DavResourceFinder.Configuration)getArguments().getSerializable(KEY_CONFIG);
|
||||||
|
|
||||||
|
TextView accountName = (TextView)v.findViewById(R.id.account_name);
|
||||||
|
accountName.setText(getString(R.string.login_encryption_account_label) + " " + config.userName);
|
||||||
|
|
||||||
|
editPassword = (EditPassword) v.findViewById(R.id.encryption_password);
|
||||||
|
|
||||||
|
Button btnCreate = (Button)v.findViewById(R.id.create_account);
|
||||||
|
btnCreate.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
if (validateEncryptionData(config) == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetupEncryptionFragment.newInstance(config).show(getFragmentManager(), null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DavResourceFinder.Configuration validateEncryptionData(DavResourceFinder.Configuration config) {
|
||||||
|
boolean valid = true;
|
||||||
|
String password = editPassword.getText().toString();
|
||||||
|
if (password.isEmpty()) {
|
||||||
|
editPassword.setError(getString(R.string.login_password_required));
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.rawPassword = password;
|
||||||
|
|
||||||
|
return valid ? config : null;
|
||||||
|
}
|
||||||
|
}
|
@ -23,16 +23,8 @@ import at.bitfire.davdroid.R;
|
|||||||
* Fields for server/user data can be pre-filled with extras in the Intent.
|
* Fields for server/user data can be pre-filled with extras in the Intent.
|
||||||
*/
|
*/
|
||||||
public class LoginActivity extends AppCompatActivity {
|
public class LoginActivity extends AppCompatActivity {
|
||||||
|
|
||||||
/**
|
|
||||||
* When set, "login by URL" will be activated by default, and the URL field will be set to this value.
|
|
||||||
* When not set, "login by email" will be activated by default.
|
|
||||||
*/
|
|
||||||
public static final String EXTRA_URL = "url";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When set, and {@link #EXTRA_PASSWORD} is set too, the user name field will be set to this value.
|
* When set, and {@link #EXTRA_PASSWORD} is set too, the user name field will be set to this value.
|
||||||
* When set, and {@link #EXTRA_URL} is not set, the email address field will be set to this value.
|
|
||||||
*/
|
*/
|
||||||
public static final String EXTRA_USERNAME = "username";
|
public static final String EXTRA_USERNAME = "username";
|
||||||
|
|
||||||
@ -79,6 +71,6 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void showHelp(MenuItem item) {
|
public void showHelp(MenuItem item) {
|
||||||
startActivity(new Intent(Intent.ACTION_VIEW, Constants.webUri.buildUpon().appendEncodedPath("configuration/").build()));
|
startActivity(new Intent(Intent.ACTION_VIEW, Constants.helpUri));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,32 +17,24 @@ import android.view.LayoutInflater;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.CompoundButton;
|
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.RadioButton;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.net.IDN;
|
import java.net.IDN;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import at.bitfire.dav4android.Constants;
|
import at.bitfire.davdroid.App;
|
||||||
|
import at.bitfire.davdroid.Constants;
|
||||||
import at.bitfire.davdroid.R;
|
import at.bitfire.davdroid.R;
|
||||||
import at.bitfire.davdroid.ui.widget.EditPassword;
|
import at.bitfire.davdroid.ui.widget.EditPassword;
|
||||||
|
|
||||||
public class LoginCredentialsFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
|
public class LoginCredentialsFragment extends Fragment {
|
||||||
|
EditText editUserName;
|
||||||
RadioButton radioUseEmail;
|
|
||||||
LinearLayout emailDetails;
|
|
||||||
EditText editEmailAddress;
|
|
||||||
EditPassword editEmailPassword;
|
|
||||||
|
|
||||||
RadioButton radioUseURL;
|
|
||||||
LinearLayout urlDetails;
|
|
||||||
EditText editBaseURL, editUserName;
|
|
||||||
EditPassword editUrlPassword;
|
EditPassword editUrlPassword;
|
||||||
|
|
||||||
|
|
||||||
@ -50,47 +42,33 @@ public class LoginCredentialsFragment extends Fragment implements CompoundButton
|
|||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
View v = inflater.inflate(R.layout.login_credentials_fragment, container, false);
|
View v = inflater.inflate(R.layout.login_credentials_fragment, container, false);
|
||||||
|
|
||||||
radioUseEmail = (RadioButton)v.findViewById(R.id.login_type_email);
|
editUserName = (EditText) v.findViewById(R.id.user_name);
|
||||||
emailDetails = (LinearLayout)v.findViewById(R.id.login_type_email_details);
|
editUrlPassword = (EditPassword) v.findViewById(R.id.url_password);
|
||||||
editEmailAddress = (EditText)v.findViewById(R.id.email_address);
|
|
||||||
editEmailPassword = (EditPassword)v.findViewById(R.id.email_password);
|
|
||||||
|
|
||||||
radioUseURL = (RadioButton)v.findViewById(R.id.login_type_url);
|
|
||||||
urlDetails = (LinearLayout)v.findViewById(R.id.login_type_url_details);
|
|
||||||
editBaseURL = (EditText)v.findViewById(R.id.base_url);
|
|
||||||
editUserName = (EditText)v.findViewById(R.id.user_name);
|
|
||||||
editUrlPassword = (EditPassword)v.findViewById(R.id.url_password);
|
|
||||||
|
|
||||||
radioUseEmail.setOnCheckedChangeListener(this);
|
|
||||||
radioUseURL.setOnCheckedChangeListener(this);
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
// first call
|
|
||||||
|
|
||||||
Activity activity = getActivity();
|
Activity activity = getActivity();
|
||||||
Intent intent = (activity != null) ? activity.getIntent() : null;
|
Intent intent = (activity != null) ? activity.getIntent() : null;
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
// we've got initial login data
|
// we've got initial login data
|
||||||
String url = intent.getStringExtra(LoginActivity.EXTRA_URL),
|
String username = intent.getStringExtra(LoginActivity.EXTRA_USERNAME),
|
||||||
username = intent.getStringExtra(LoginActivity.EXTRA_USERNAME),
|
|
||||||
password = intent.getStringExtra(LoginActivity.EXTRA_PASSWORD);
|
password = intent.getStringExtra(LoginActivity.EXTRA_PASSWORD);
|
||||||
|
|
||||||
if (url != null) {
|
|
||||||
radioUseURL.setChecked(true);
|
|
||||||
editBaseURL.setText(url);
|
|
||||||
editUserName.setText(username);
|
editUserName.setText(username);
|
||||||
editUrlPassword.setText(password);
|
editUrlPassword.setText(password);
|
||||||
} else {
|
}
|
||||||
radioUseEmail.setChecked(true);
|
|
||||||
editEmailAddress.setText(username);
|
|
||||||
editEmailPassword.setText(password);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else
|
final Button createAccount = (Button) v.findViewById(R.id.create_account);
|
||||||
radioUseEmail.setChecked(true);
|
createAccount.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
Uri createUri = Constants.registrationUrl.buildUpon().appendQueryParameter("email", editUserName.getText().toString()).build();
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW, createUri);
|
||||||
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
final Button login = (Button)v.findViewById(R.id.login);
|
final Button login = (Button) v.findViewById(R.id.login);
|
||||||
login.setOnClickListener(new View.OnClickListener() {
|
login.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
@ -103,70 +81,14 @@ public class LoginCredentialsFragment extends Fragment implements CompoundButton
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
|
||||||
if (isChecked) {
|
|
||||||
boolean loginByEmail = buttonView == radioUseEmail;
|
|
||||||
emailDetails.setVisibility(loginByEmail ? View.VISIBLE : View.GONE);
|
|
||||||
urlDetails.setVisibility(loginByEmail ? View.GONE : View.VISIBLE);
|
|
||||||
(loginByEmail ? editEmailAddress : editBaseURL).requestFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected LoginCredentials validateLoginData() {
|
protected LoginCredentials validateLoginData() {
|
||||||
if (radioUseEmail.isChecked()) {
|
|
||||||
URI uri = null;
|
|
||||||
boolean valid = true;
|
boolean valid = true;
|
||||||
|
|
||||||
String email = editEmailAddress.getText().toString();
|
|
||||||
if (!email.matches(".+@.+")) {
|
|
||||||
editEmailAddress.setError(getString(R.string.login_email_address_error));
|
|
||||||
valid = false;
|
|
||||||
} else
|
|
||||||
try {
|
|
||||||
uri = new URI("mailto", email, null);
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
editEmailAddress.setError(e.getLocalizedMessage());
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String password = editEmailPassword.getText().toString();
|
|
||||||
if (password.isEmpty()) {
|
|
||||||
editEmailPassword.setError(getString(R.string.login_password_required));
|
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return valid ? new LoginCredentials(uri, email, password) : null;
|
|
||||||
|
|
||||||
} else if (radioUseURL.isChecked()) {
|
|
||||||
URI uri = null;
|
URI uri = null;
|
||||||
boolean valid = true;
|
|
||||||
|
|
||||||
Uri baseUrl = Uri.parse(editBaseURL.getText().toString());
|
|
||||||
String scheme = baseUrl.getScheme();
|
|
||||||
if ("https".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme)) {
|
|
||||||
String host = baseUrl.getHost();
|
|
||||||
if (StringUtils.isEmpty(host)) {
|
|
||||||
editBaseURL.setError(getString(R.string.login_url_host_name_required));
|
|
||||||
valid = false;
|
|
||||||
} else
|
|
||||||
try {
|
try {
|
||||||
host = IDN.toASCII(host);
|
uri = new URI(Constants.serviceUrl.toString());
|
||||||
} catch(IllegalArgumentException e) {
|
|
||||||
Constants.log.log(Level.WARNING, "Host name not conforming to RFC 3490", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
String path = baseUrl.getEncodedPath();
|
|
||||||
int port = baseUrl.getPort();
|
|
||||||
try {
|
|
||||||
uri = new URI(baseUrl.getScheme(), null, host, port, path, null, null);
|
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
editBaseURL.setError(e.getLocalizedMessage());
|
App.log.severe("Should never happen, it's a constant");
|
||||||
valid = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
editBaseURL.setError(getString(R.string.login_url_must_be_http_or_https));
|
|
||||||
valid = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String userName = editUserName.getText().toString();
|
String userName = editUserName.getText().toString();
|
||||||
@ -184,7 +106,4 @@ public class LoginCredentialsFragment extends Fragment implements CompoundButton
|
|||||||
return valid ? new LoginCredentials(uri, userName, password) : null;
|
return valid ? new LoginCredentials(uri, userName, password) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -11,23 +11,22 @@ package at.bitfire.davdroid.ui.setup;
|
|||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.app.ProgressDialog;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.CalendarContract;
|
import android.provider.CalendarContract;
|
||||||
import android.provider.ContactsContract;
|
import android.provider.ContactsContract;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.DialogFragment;
|
||||||
import android.view.LayoutInflater;
|
import android.support.v4.app.LoaderManager;
|
||||||
import android.view.View;
|
import android.support.v4.content.AsyncTaskLoader;
|
||||||
import android.view.ViewGroup;
|
import android.support.v4.content.Loader;
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import at.bitfire.davdroid.AccountSettings;
|
import at.bitfire.davdroid.AccountSettings;
|
||||||
@ -36,78 +35,93 @@ import at.bitfire.davdroid.Constants;
|
|||||||
import at.bitfire.davdroid.DavService;
|
import at.bitfire.davdroid.DavService;
|
||||||
import at.bitfire.davdroid.InvalidAccountException;
|
import at.bitfire.davdroid.InvalidAccountException;
|
||||||
import at.bitfire.davdroid.R;
|
import at.bitfire.davdroid.R;
|
||||||
|
import at.bitfire.davdroid.journalmanager.Helpers;
|
||||||
import at.bitfire.davdroid.model.CollectionInfo;
|
import at.bitfire.davdroid.model.CollectionInfo;
|
||||||
import at.bitfire.davdroid.model.ServiceDB.Collections;
|
import at.bitfire.davdroid.model.ServiceDB;
|
||||||
import at.bitfire.davdroid.model.ServiceDB.HomeSets;
|
|
||||||
import at.bitfire.davdroid.model.ServiceDB.OpenHelper;
|
|
||||||
import at.bitfire.davdroid.model.ServiceDB.Services;
|
|
||||||
import at.bitfire.davdroid.resource.LocalTaskList;
|
import at.bitfire.davdroid.resource.LocalTaskList;
|
||||||
|
import at.bitfire.davdroid.ui.setup.DavResourceFinder.Configuration;
|
||||||
import at.bitfire.ical4android.TaskProvider;
|
import at.bitfire.ical4android.TaskProvider;
|
||||||
import at.bitfire.vcard4android.GroupMethod;
|
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
|
|
||||||
public class AccountDetailsFragment extends Fragment {
|
public class SetupEncryptionFragment extends DialogFragment implements LoaderManager.LoaderCallbacks<Configuration> {
|
||||||
|
|
||||||
private static final String KEY_CONFIG = "config";
|
private static final String KEY_CONFIG = "config";
|
||||||
|
|
||||||
Spinner spnrGroupMethod;
|
public static SetupEncryptionFragment newInstance(DavResourceFinder.Configuration config) {
|
||||||
|
SetupEncryptionFragment frag = new SetupEncryptionFragment();
|
||||||
|
|
||||||
public static AccountDetailsFragment newInstance(DavResourceFinder.Configuration config) {
|
|
||||||
AccountDetailsFragment frag = new AccountDetailsFragment();
|
|
||||||
Bundle args = new Bundle(1);
|
Bundle args = new Bundle(1);
|
||||||
args.putSerializable(KEY_CONFIG, config);
|
args.putSerializable(KEY_CONFIG, config);
|
||||||
frag.setArguments(args);
|
frag.setArguments(args);
|
||||||
return frag;
|
return frag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
final View v = inflater.inflate(R.layout.login_account_details, container, false);
|
ProgressDialog progress = new ProgressDialog(getActivity());
|
||||||
|
progress.setTitle(R.string.login_encryption_setup_title);
|
||||||
Button btnBack = (Button)v.findViewById(R.id.back);
|
progress.setMessage(getString(R.string.login_encryption_setup));
|
||||||
btnBack.setOnClickListener(new View.OnClickListener() {
|
progress.setIndeterminate(true);
|
||||||
@Override
|
progress.setCanceledOnTouchOutside(false);
|
||||||
public void onClick(View v) {
|
setCancelable(false);
|
||||||
getFragmentManager().popBackStack();
|
return progress;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
DavResourceFinder.Configuration config = (DavResourceFinder.Configuration)getArguments().getSerializable(KEY_CONFIG);
|
|
||||||
|
|
||||||
final EditText editName = (EditText)v.findViewById(R.id.account_name);
|
|
||||||
editName.setText((config.calDAV != null && config.calDAV.email != null) ? config.calDAV.email : config.userName);
|
|
||||||
|
|
||||||
// CardDAV-specific
|
|
||||||
v.findViewById(R.id.carddav).setVisibility(config.cardDAV != null ? View.VISIBLE : View.GONE);
|
|
||||||
spnrGroupMethod = (Spinner)v.findViewById(R.id.contact_group_method);
|
|
||||||
|
|
||||||
Button btnCreate = (Button)v.findViewById(R.id.create_account);
|
|
||||||
btnCreate.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
String name = editName.getText().toString();
|
super.onCreate(savedInstanceState);
|
||||||
if (name.isEmpty())
|
|
||||||
editName.setError(getString(R.string.login_account_name_required));
|
getLoaderManager().initLoader(0, getArguments(), this);
|
||||||
else {
|
}
|
||||||
if (createAccount(name, (DavResourceFinder.Configuration)getArguments().getSerializable(KEY_CONFIG))) {
|
|
||||||
|
@Override
|
||||||
|
public Loader<Configuration> onCreateLoader(int id, Bundle args) {
|
||||||
|
return new SetupEncryptionLoader(getContext(), (Configuration)args.getSerializable(KEY_CONFIG));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadFinished(Loader<Configuration> loader, Configuration config) {
|
||||||
|
if (createAccount(config.userName, config)) {
|
||||||
getActivity().setResult(Activity.RESULT_OK);
|
getActivity().setResult(Activity.RESULT_OK);
|
||||||
getActivity().finish();
|
getActivity().finish();
|
||||||
} else
|
} else {
|
||||||
Snackbar.make(v, R.string.login_account_not_created, Snackbar.LENGTH_LONG).show();
|
App.log.severe("Account creation failed!");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return v;
|
dismissAllowingStateLoss();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoaderReset(Loader<Configuration> loader) {
|
||||||
|
}
|
||||||
|
|
||||||
|
static class SetupEncryptionLoader extends AsyncTaskLoader<Configuration> {
|
||||||
|
final Context context;
|
||||||
|
final Configuration config;
|
||||||
|
|
||||||
|
public SetupEncryptionLoader(Context context, Configuration config) {
|
||||||
|
super(context);
|
||||||
|
this.context = context;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStartLoading() {
|
||||||
|
forceLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Configuration loadInBackground() {
|
||||||
|
config.password = Helpers.deriveKey(config.userName, config.rawPassword);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
protected boolean createAccount(String accountName, DavResourceFinder.Configuration config) {
|
protected boolean createAccount(String accountName, DavResourceFinder.Configuration config) {
|
||||||
Account account = new Account(accountName, Constants.ACCOUNT_TYPE);
|
Account account = new Account(accountName, Constants.ACCOUNT_TYPE);
|
||||||
|
|
||||||
// create Android account
|
// create Android account
|
||||||
Bundle userData = AccountSettings.initialUserData(config.userName);
|
Bundle userData = AccountSettings.initialUserData(config.url, config.userName);
|
||||||
App.log.log(Level.INFO, "Creating Android account with initial config", new Object[] { account, userData });
|
App.log.log(Level.INFO, "Creating Android account with initial config", new Object[] { account, userData });
|
||||||
|
|
||||||
AccountManager accountManager = AccountManager.get(getContext());
|
AccountManager accountManager = AccountManager.get(getContext());
|
||||||
@ -116,7 +130,7 @@ public class AccountDetailsFragment extends Fragment {
|
|||||||
|
|
||||||
// add entries for account to service DB
|
// add entries for account to service DB
|
||||||
App.log.log(Level.INFO, "Writing account configuration to database", config);
|
App.log.log(Level.INFO, "Writing account configuration to database", config);
|
||||||
@Cleanup OpenHelper dbHelper = new OpenHelper(getContext());
|
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext());
|
||||||
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
||||||
try {
|
try {
|
||||||
AccountSettings settings = new AccountSettings(getContext(), account);
|
AccountSettings settings = new AccountSettings(getContext(), account);
|
||||||
@ -124,19 +138,16 @@ public class AccountDetailsFragment extends Fragment {
|
|||||||
Intent refreshIntent = new Intent(getActivity(), DavService.class);
|
Intent refreshIntent = new Intent(getActivity(), DavService.class);
|
||||||
refreshIntent.setAction(DavService.ACTION_REFRESH_COLLECTIONS);
|
refreshIntent.setAction(DavService.ACTION_REFRESH_COLLECTIONS);
|
||||||
|
|
||||||
|
settings.setAuthToken(config.authtoken);
|
||||||
|
|
||||||
if (config.cardDAV != null) {
|
if (config.cardDAV != null) {
|
||||||
// insert CardDAV service
|
// insert CardDAV service
|
||||||
long id = insertService(db, accountName, Services.SERVICE_CARDDAV, config.cardDAV);
|
long id = insertService(db, accountName, ServiceDB.Services.SERVICE_CARDDAV, config.cardDAV);
|
||||||
|
|
||||||
// start CardDAV service detection (refresh collections)
|
// start CardDAV service detection (refresh collections)
|
||||||
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id);
|
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id);
|
||||||
getActivity().startService(refreshIntent);
|
getActivity().startService(refreshIntent);
|
||||||
|
|
||||||
// initial CardDAV account settings
|
|
||||||
int idx = spnrGroupMethod.getSelectedItemPosition();
|
|
||||||
String groupMethodName = getResources().getStringArray(R.array.settings_contact_group_method_values)[idx];
|
|
||||||
settings.setGroupMethod(GroupMethod.valueOf(groupMethodName));
|
|
||||||
|
|
||||||
// enable contact sync
|
// enable contact sync
|
||||||
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
|
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
|
||||||
settings.setSyncInterval(ContactsContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL);
|
settings.setSyncInterval(ContactsContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL);
|
||||||
@ -144,7 +155,7 @@ public class AccountDetailsFragment extends Fragment {
|
|||||||
|
|
||||||
if (config.calDAV != null) {
|
if (config.calDAV != null) {
|
||||||
// insert CalDAV service
|
// insert CalDAV service
|
||||||
long id = insertService(db, accountName, Services.SERVICE_CALDAV, config.calDAV);
|
long id = insertService(db, accountName, ServiceDB.Services.SERVICE_CALDAV, config.calDAV);
|
||||||
|
|
||||||
// start CalDAV service detection (refresh collections)
|
// start CalDAV service detection (refresh collections)
|
||||||
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id);
|
refreshIntent.putExtra(DavService.EXTRA_DAV_SERVICE_ID, id);
|
||||||
@ -173,28 +184,17 @@ public class AccountDetailsFragment extends Fragment {
|
|||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
|
|
||||||
// insert service
|
// insert service
|
||||||
values.put(Services.ACCOUNT_NAME, accountName);
|
values.put(ServiceDB.Services.ACCOUNT_NAME, accountName);
|
||||||
values.put(Services.SERVICE, service);
|
values.put(ServiceDB.Services.SERVICE, service);
|
||||||
if (info.principal != null)
|
long serviceID = db.insertWithOnConflict(ServiceDB.Services._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
values.put(Services.PRINCIPAL, info.principal.toString());
|
|
||||||
long serviceID = db.insertWithOnConflict(Services._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
|
||||||
|
|
||||||
// insert home sets
|
|
||||||
for (URI homeSet : info.homeSets) {
|
|
||||||
values.clear();
|
|
||||||
values.put(HomeSets.SERVICE_ID, serviceID);
|
|
||||||
values.put(HomeSets.URL, homeSet.toString());
|
|
||||||
db.insertWithOnConflict(HomeSets._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert collections
|
// insert collections
|
||||||
for (CollectionInfo collection : info.collections.values()) {
|
for (CollectionInfo collection : info.collections.values()) {
|
||||||
values = collection.toDB();
|
values = collection.toDB();
|
||||||
values.put(Collections.SERVICE_ID, serviceID);
|
values.put(ServiceDB.Collections.SERVICE_ID, serviceID);
|
||||||
db.insertWithOnConflict(Collections._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
db.insertWithOnConflict(ServiceDB.Collections._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
return serviceID;
|
return serviceID;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
10
app/src/main/res/drawable/ic_bug_report.xml
Normal file
10
app/src/main/res/drawable/ic_bug_report.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0"
|
||||||
|
android:alpha="0.54" >
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_email_black.xml
Normal file
10
app/src/main/res/drawable/ic_email_black.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0"
|
||||||
|
android:alpha="0.54" >
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z"/>
|
||||||
|
</vector>
|
@ -58,16 +58,4 @@
|
|||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
android:background="@drawable/ic_remove_circle_dark"/>
|
android:background="@drawable/ic_remove_circle_dark"/>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/events"
|
|
||||||
android:layout_width="32dp"
|
|
||||||
android:layout_height="32dp"
|
|
||||||
android:background="@drawable/ic_today_dark"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tasks"
|
|
||||||
android:layout_width="32dp"
|
|
||||||
android:layout_height="32dp"
|
|
||||||
android:background="@drawable/ic_alarm_on_dark"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
@ -14,15 +14,6 @@
|
|||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:gravity="center_vertical">
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginRight="4dp"
|
|
||||||
android:focusable="false"
|
|
||||||
android:focusableInTouchMode="false"
|
|
||||||
android:clickable="false"
|
|
||||||
android:id="@+id/checked"/>
|
|
||||||
|
|
||||||
<LinearLayout android:layout_width="0dp"
|
<LinearLayout android:layout_width="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
android:theme="@style/toolbar_theme"
|
android:theme="@style/toolbar_theme"
|
||||||
style="@style/toolbar_style"
|
style="@style/toolbar_style"
|
||||||
app:navigationIcon="@drawable/ic_people_light"
|
app:navigationIcon="@drawable/ic_people_light"
|
||||||
app:title="CardDAV"
|
app:title="Contacts"
|
||||||
android:elevation="2dp" tools:ignore="UnusedAttribute"/>
|
android:elevation="2dp" tools:ignore="UnusedAttribute"/>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
@ -79,7 +79,7 @@
|
|||||||
android:theme="@style/toolbar_theme"
|
android:theme="@style/toolbar_theme"
|
||||||
style="@style/toolbar_style"
|
style="@style/toolbar_style"
|
||||||
app:navigationIcon="@drawable/ic_event_light"
|
app:navigationIcon="@drawable/ic_event_light"
|
||||||
app:title="CalDAV"
|
app:title="Calendar"
|
||||||
android:elevation="2dp" tools:ignore="UnusedAttribute"/>
|
android:elevation="2dp" tools:ignore="UnusedAttribute"/>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
@ -16,22 +16,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="@dimen/activity_margin">
|
android:padding="@dimen/activity_margin">
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/create_calendar"
|
|
||||||
android:textAppearance="@style/TextView.Heading"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/create_collection_home_set"/>
|
|
||||||
<Spinner
|
|
||||||
android:id="@+id/home_sets"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="16dp"/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -85,38 +69,6 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/create_calendar_type"
|
|
||||||
android:textAppearance="@style/TextView.Heading"/>
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
android:id="@+id/type"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/type_events"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:checked="true"
|
|
||||||
android:text="@string/create_calendar_type_only_events"/>
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/type_tasks"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/create_calendar_type_only_tasks"/>
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/type_events_and_tasks"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/create_calendar_type_events_and_tasks"/>
|
|
||||||
|
|
||||||
</RadioGroup>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
@ -13,75 +13,29 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<!-- We don't want the keyboard up when the user arrives in this initial screen -->
|
|
||||||
<View android:layout_height="0dp"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:contentDescription="@null"
|
|
||||||
android:importantForAccessibility="no" tools:ignore="UnusedAttribute">
|
|
||||||
<requestFocus/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<ScrollView android:layout_width="match_parent"
|
<ScrollView android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_margin="@dimen/activity_margin">
|
android:layout_margin="@dimen/activity_margin">
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:animateLayoutChanges="true">
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/login_type_email"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/login_type_email"
|
|
||||||
android:paddingLeft="14dp" tools:ignore="RtlSymmetry"
|
|
||||||
style="@style/login_type_headline"/>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/login_type_email_details"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/email_address"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/login_email_address"
|
|
||||||
android:inputType="textEmailAddress"/>
|
|
||||||
<at.bitfire.davdroid.ui.widget.EditPassword
|
|
||||||
android:id="@+id/email_password"
|
|
||||||
android:hint="@string/login_password"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"/>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/login_type_url"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/login_type_url"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:paddingLeft="14dp" tools:ignore="RtlSymmetry"
|
|
||||||
style="@style/login_type_headline"/>
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/login_type_url_details"
|
android:id="@+id/login_type_url_details"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<EditText
|
<TextView
|
||||||
android:id="@+id/base_url"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/login_base_url"
|
style="@style/login_type_headline"
|
||||||
android:inputType="textUri"/>
|
android:text="@string/login_enter_service_details"
|
||||||
|
android:layout_marginBottom="14dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/login_service_details_description"
|
||||||
|
android:layout_marginBottom="14dp"/>
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/user_name"
|
android:id="@+id/user_name"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -95,8 +49,6 @@
|
|||||||
android:hint="@string/login_password"/>
|
android:hint="@string/login_password"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</RadioGroup>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@ -104,6 +56,13 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
style="@style/stepper_nav_bar">
|
style="@style/stepper_nav_bar">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/create_account"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/login_signup"
|
||||||
|
style="@style/stepper_nav_button"/>
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
|
@ -27,42 +27,19 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
style="@style/login_type_headline"
|
style="@style/login_type_headline"
|
||||||
android:text="@string/login_create_account"
|
android:text="@string/login_enter_encryption_details"
|
||||||
android:layout_marginBottom="14dp"/>
|
android:layout_marginBottom="14dp"/>
|
||||||
|
|
||||||
<EditText
|
<TextView
|
||||||
android:id="@+id/account_name"
|
android:id="@+id/account_name"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"/>
|
||||||
android:hint="@string/login_account_name"
|
|
||||||
android:inputType="textEmailAddress"/>
|
|
||||||
|
|
||||||
<TextView
|
<at.bitfire.davdroid.ui.widget.EditPassword
|
||||||
android:id="@+id/account_email_hint"
|
android:id="@+id/encryption_password"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:hint="@string/login_encryption_password"/>
|
||||||
android:text="@string/login_account_name_info"/>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/carddav"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:text="@string/login_account_contact_group_method"/>
|
|
||||||
|
|
||||||
<Spinner
|
|
||||||
android:id="@+id/contact_group_method"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:entries="@array/settings_contact_group_method_entries"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@ -85,7 +62,7 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:text="@string/login_create_account"
|
android:text="@string/login_signup"
|
||||||
style="@style/stepper_nav_button"/>
|
style="@style/stepper_nav_button"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
@ -20,10 +20,6 @@
|
|||||||
android:title="@string/account_settings"
|
android:title="@string/account_settings"
|
||||||
app:showAsAction="ifRoom"/>
|
app:showAsAction="ifRoom"/>
|
||||||
|
|
||||||
<item android:id="@+id/rename_account"
|
|
||||||
android:title="@string/account_rename"
|
|
||||||
app:showAsAction="never"/>
|
|
||||||
|
|
||||||
<item android:id="@+id/delete_account"
|
<item android:id="@+id/delete_account"
|
||||||
android:title="@string/account_delete"
|
android:title="@string/account_delete"
|
||||||
app:showAsAction="never"/>
|
app:showAsAction="never"/>
|
||||||
|
@ -19,16 +19,6 @@
|
|||||||
android:icon="@drawable/ic_settings_dark"
|
android:icon="@drawable/ic_settings_dark"
|
||||||
android:title="@string/navigation_drawer_settings"/>
|
android:title="@string/navigation_drawer_settings"/>
|
||||||
|
|
||||||
<item android:title="@string/navigation_drawer_news_updates">
|
|
||||||
<menu>
|
|
||||||
<item
|
|
||||||
android:id="@+id/nav_twitter"
|
|
||||||
android:icon="@drawable/twitter"
|
|
||||||
android:title="\@davdroidapp"
|
|
||||||
tools:ignore="HardcodedText"/>
|
|
||||||
</menu>
|
|
||||||
</item>
|
|
||||||
|
|
||||||
<item android:title="@string/navigation_drawer_external_links">
|
<item android:title="@string/navigation_drawer_external_links">
|
||||||
<menu>
|
<menu>
|
||||||
<item
|
<item
|
||||||
@ -40,13 +30,13 @@
|
|||||||
android:icon="@drawable/ic_help_dark"
|
android:icon="@drawable/ic_help_dark"
|
||||||
android:title="@string/navigation_drawer_faq"/>
|
android:title="@string/navigation_drawer_faq"/>
|
||||||
<item
|
<item
|
||||||
android:id="@+id/nav_forums"
|
android:id="@+id/nav_report_issue"
|
||||||
android:icon="@drawable/ic_forum_dark"
|
android:icon="@drawable/ic_bug_report"
|
||||||
android:title="@string/navigation_drawer_forums"/>
|
android:title="@string/navigation_drawer_report_issue"/>
|
||||||
<item
|
<item
|
||||||
android:id="@+id/nav_donate"
|
android:id="@+id/nav_contact"
|
||||||
android:icon="@drawable/ic_attach_money_dark"
|
android:icon="@drawable/ic_email_black"
|
||||||
android:title="@string/navigation_drawer_donate"/>
|
android:title="@string/navigation_drawer_contact"/>
|
||||||
</menu>
|
</menu>
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources>
|
||||||
<!--common strings-->
|
<!--common strings-->
|
||||||
<string name="app_name">DAVdroid</string>
|
<string name="app_name">DAVdroid</string>
|
||||||
<string name="help">Ajuda</string>
|
<string name="help">Ajuda</string>
|
||||||
|
@ -59,6 +59,8 @@
|
|||||||
<string name="navigation_drawer_faq">FAQ</string>
|
<string name="navigation_drawer_faq">FAQ</string>
|
||||||
<string name="navigation_drawer_forums">Community</string>
|
<string name="navigation_drawer_forums">Community</string>
|
||||||
<string name="navigation_drawer_donate">Donate</string>
|
<string name="navigation_drawer_donate">Donate</string>
|
||||||
|
<string name="navigation_drawer_report_issue">Report issue</string>
|
||||||
|
<string name="navigation_drawer_contact">Contact developer</string>
|
||||||
|
|
||||||
<string name="account_list_empty">Welcome to DAVdroid!\n\nYou can add a CalDAV/CardDAV account now.</string>
|
<string name="account_list_empty">Welcome to DAVdroid!\n\nYou can add a CalDAV/CardDAV account now.</string>
|
||||||
|
|
||||||
@ -125,6 +127,7 @@
|
|||||||
<string name="login_email_address">Email address</string>
|
<string name="login_email_address">Email address</string>
|
||||||
<string name="login_email_address_error">Valid email address required</string>
|
<string name="login_email_address_error">Valid email address required</string>
|
||||||
<string name="login_password">Password</string>
|
<string name="login_password">Password</string>
|
||||||
|
<string name="login_encryption_password">Encryption Password</string>
|
||||||
<string name="login_password_required">Password required</string>
|
<string name="login_password_required">Password required</string>
|
||||||
<string name="login_type_url">Login with URL and user name</string>
|
<string name="login_type_url">Login with URL and user name</string>
|
||||||
<string name="login_url_must_be_http_or_https">URL must begin with http(s)://</string>
|
<string name="login_url_must_be_http_or_https">URL must begin with http(s)://</string>
|
||||||
@ -132,9 +135,13 @@
|
|||||||
<string name="login_user_name">User name</string>
|
<string name="login_user_name">User name</string>
|
||||||
<string name="login_user_name_required">User name required</string>
|
<string name="login_user_name_required">User name required</string>
|
||||||
<string name="login_base_url">Base URL</string>
|
<string name="login_base_url">Base URL</string>
|
||||||
<string name="login_login">Login</string>
|
<string name="login_login">Log In</string>
|
||||||
|
<string name="login_signup">Sign Up</string>
|
||||||
<string name="login_back">Back</string>
|
<string name="login_back">Back</string>
|
||||||
<string name="login_create_account">Create account</string>
|
<string name="login_enter_service_details">Enter Login Details</string>
|
||||||
|
<string name="login_enter_encryption_details">Secret Encryption Password</string>
|
||||||
|
<string name="login_encryption_account_label">Account:</string>
|
||||||
|
<string name="login_service_details_description">This is your login password, *not* your encryption password!</string>
|
||||||
<string name="login_account_name">Account name</string>
|
<string name="login_account_name">Account name</string>
|
||||||
<string name="login_account_name_info">Use your email address as account name because Android will use the account name as ORGANIZER field for events you create. You can\'t have two accounts with the same name.</string>
|
<string name="login_account_name_info">Use your email address as account name because Android will use the account name as ORGANIZER field for events you create. You can\'t have two accounts with the same name.</string>
|
||||||
<string name="login_account_contact_group_method">Contact group method:</string>
|
<string name="login_account_contact_group_method">Contact group method:</string>
|
||||||
@ -146,14 +153,21 @@
|
|||||||
<string name="login_no_caldav_carddav">Couldn\'t find CalDAV or CardDAV service.</string>
|
<string name="login_no_caldav_carddav">Couldn\'t find CalDAV or CardDAV service.</string>
|
||||||
<string name="login_view_logs">View logs</string>
|
<string name="login_view_logs">View logs</string>
|
||||||
|
|
||||||
|
<string name="login_encryption_setup_title">Setting up encryption</string>
|
||||||
|
<string name="login_encryption_setup">Please wait, setting up encryption…</string>
|
||||||
|
|
||||||
<!-- AccountSettingsActivity -->
|
<!-- AccountSettingsActivity -->
|
||||||
<string name="settings_title">Settings: %s</string>
|
<string name="settings_title">Settings: %s</string>
|
||||||
<string name="settings_authentication">Authentication</string>
|
<string name="settings_authentication">Authentication</string>
|
||||||
|
<string name="settings_encryption">Encryption</string>
|
||||||
<string name="settings_username">User name</string>
|
<string name="settings_username">User name</string>
|
||||||
<string name="settings_enter_username">Enter user name:</string>
|
<string name="settings_enter_username">Enter user name:</string>
|
||||||
<string name="settings_password">Password</string>
|
<string name="settings_password">Password</string>
|
||||||
<string name="settings_password_summary">Update the password according to your server.</string>
|
<string name="settings_password_summary">Change your authentication password.</string>
|
||||||
<string name="settings_enter_password">Enter your password:</string>
|
<string name="settings_enter_password">Enter your password:</string>
|
||||||
|
<string name="settings_encryption_password">Encryption Password</string>
|
||||||
|
<string name="settings_encryption_password_summary">Change your encryption password (not implemented)</string>
|
||||||
|
<string name="settings_enter_encryption_password">Enter your encryption password:</string>
|
||||||
<string name="settings_sync">Synchronization</string>
|
<string name="settings_sync">Synchronization</string>
|
||||||
<string name="settings_sync_interval_contacts">Contacts sync. interval</string>
|
<string name="settings_sync_interval_contacts">Contacts sync. interval</string>
|
||||||
<string name="settings_sync_summary_manually">Only manually</string>
|
<string name="settings_sync_summary_manually">Only manually</string>
|
||||||
@ -188,7 +202,7 @@
|
|||||||
<string name="settings_sync_wifi_only_ssid_on">Will only synchronize over %s</string>
|
<string name="settings_sync_wifi_only_ssid_on">Will only synchronize over %s</string>
|
||||||
<string name="settings_sync_wifi_only_ssid_off">All WiFi connections may be used</string>
|
<string name="settings_sync_wifi_only_ssid_off">All WiFi connections may be used</string>
|
||||||
<string name="settings_sync_wifi_only_ssid_message">Enter the name of a WiFi network (SSID) to restrict synchronization to this network, or leave blank for all WiFi connections.</string>
|
<string name="settings_sync_wifi_only_ssid_message">Enter the name of a WiFi network (SSID) to restrict synchronization to this network, or leave blank for all WiFi connections.</string>
|
||||||
<string name="settings_carddav">CardDAV</string>
|
<string name="settings_carddav">Contacts</string>
|
||||||
<string name="settings_contact_group_method">Contact group method</string>
|
<string name="settings_contact_group_method">Contact group method</string>
|
||||||
<string-array name="settings_contact_group_method_values">
|
<string-array name="settings_contact_group_method_values">
|
||||||
<item>GROUP_VCARDS</item>
|
<item>GROUP_VCARDS</item>
|
||||||
@ -198,7 +212,7 @@
|
|||||||
<item>Groups are separate VCards</item>
|
<item>Groups are separate VCards</item>
|
||||||
<item>Groups are per-contact categories</item>
|
<item>Groups are per-contact categories</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string name="settings_caldav">CalDAV</string>
|
<string name="settings_caldav">Calendar</string>
|
||||||
<string name="settings_sync_time_range_past">Past event time limit</string>
|
<string name="settings_sync_time_range_past">Past event time limit</string>
|
||||||
<string name="settings_sync_time_range_past_none">All events will be synchronized</string>
|
<string name="settings_sync_time_range_past_none">All events will be synchronized</string>
|
||||||
<plurals name="settings_sync_time_range_past_days">
|
<plurals name="settings_sync_time_range_past_days">
|
||||||
@ -220,7 +234,7 @@
|
|||||||
<string name="create_calendar_type_only_events">Calendar (only events)</string>
|
<string name="create_calendar_type_only_events">Calendar (only events)</string>
|
||||||
<string name="create_calendar_type_only_tasks">Task list (only tasks)</string>
|
<string name="create_calendar_type_only_tasks">Task list (only tasks)</string>
|
||||||
<string name="create_calendar_type_events_and_tasks">Combined (events and tasks)</string>
|
<string name="create_calendar_type_events_and_tasks">Combined (events and tasks)</string>
|
||||||
<string name="create_collection_color">Set a collection color</string>
|
<string name="create_collection_color">Set the calendar\'s color</string>
|
||||||
<string name="create_collection_creating">Creating collection</string>
|
<string name="create_collection_creating">Creating collection</string>
|
||||||
<string name="create_collection_display_name">Display name (title) of this collection:</string>
|
<string name="create_collection_display_name">Display name (title) of this collection:</string>
|
||||||
<string name="create_collection_display_name_required">Title is required</string>
|
<string name="create_collection_display_name_required">Title is required</string>
|
||||||
|
@ -10,14 +10,15 @@
|
|||||||
|
|
||||||
<!-- colors -->
|
<!-- colors -->
|
||||||
|
|
||||||
<color name="green700">#388e3c</color>
|
<!-- FIXME: Colours are no longer correctly named. -->
|
||||||
|
<color name="green700">#ffa100</color>
|
||||||
|
|
||||||
<color name="light_green300">#aed581</color>
|
<color name="light_green300">#ffd54f</color>
|
||||||
<color name="light_green500">#8bc34a</color>
|
<color name="light_green500">#ffc107</color>
|
||||||
<color name="light_green700">#689f38</color>
|
<color name="light_green700">#ffa100</color>
|
||||||
|
|
||||||
<color name="orange400">#ffa726</color>
|
<color name="orange400">#29b6f6</color>
|
||||||
<color name="orangeA700">#ff6d00</color>
|
<color name="orangeA700">#0288d1</color>
|
||||||
|
|
||||||
<color name="grey200">#eeeeee</color>
|
<color name="grey200">#eeeeee</color>
|
||||||
<color name="grey700">#616161</color>
|
<color name="grey700">#616161</color>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
~ http://www.gnu.org/licenses/gpl.html
|
~ http://www.gnu.org/licenses/gpl.html
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android" >
|
<ContactsAccountType>
|
||||||
|
|
||||||
<EditSchema>
|
<EditSchema>
|
||||||
<DataKind
|
<DataKind
|
||||||
|
@ -11,12 +11,6 @@
|
|||||||
|
|
||||||
<PreferenceCategory android:title="@string/settings_authentication">
|
<PreferenceCategory android:title="@string/settings_authentication">
|
||||||
|
|
||||||
<EditTextPreference
|
|
||||||
android:key="username"
|
|
||||||
android:title="@string/settings_username"
|
|
||||||
android:persistent="false"
|
|
||||||
android:dialogTitle="@string/settings_enter_username" />
|
|
||||||
|
|
||||||
<EditTextPreference
|
<EditTextPreference
|
||||||
android:key="password"
|
android:key="password"
|
||||||
android:title="@string/settings_password"
|
android:title="@string/settings_password"
|
||||||
@ -27,6 +21,19 @@
|
|||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory android:title="@string/settings_encryption">
|
||||||
|
|
||||||
|
<EditTextPreference
|
||||||
|
android:key="encryption_password"
|
||||||
|
android:title="@string/settings_encryption_password"
|
||||||
|
android:persistent="false"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:enabled="false"
|
||||||
|
android:summary="@string/settings_encryption_password_summary"
|
||||||
|
android:dialogTitle="@string/settings_enter_encryption_password" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/settings_sync">
|
<PreferenceCategory android:title="@string/settings_sync">
|
||||||
|
|
||||||
<ListPreference
|
<ListPreference
|
||||||
@ -66,34 +73,4 @@
|
|||||||
android:dialogMessage="@string/settings_sync_wifi_only_ssid_message"/>
|
android:dialogMessage="@string/settings_sync_wifi_only_ssid_message"/>
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/settings_carddav">
|
|
||||||
|
|
||||||
<ListPreference
|
|
||||||
android:key="contact_group_method"
|
|
||||||
android:persistent="false"
|
|
||||||
android:title="@string/settings_contact_group_method"
|
|
||||||
android:entries="@array/settings_contact_group_method_entries"
|
|
||||||
android:entryValues="@array/settings_contact_group_method_values"/>
|
|
||||||
|
|
||||||
</PreferenceCategory>
|
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/settings_caldav">
|
|
||||||
|
|
||||||
<EditTextPreference
|
|
||||||
android:key="time_range_past_days"
|
|
||||||
android:persistent="false"
|
|
||||||
android:title="@string/settings_sync_time_range_past"
|
|
||||||
android:dialogMessage="@string/settings_sync_time_range_past_message"
|
|
||||||
android:inputType="number"/>
|
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:key="manage_calendar_colors"
|
|
||||||
android:persistent="false"
|
|
||||||
android:title="@string/settings_manage_calendar_colors"
|
|
||||||
android:summaryOn="@string/settings_manage_calendar_colors_on"
|
|
||||||
android:summaryOff="@string/settings_manage_calendar_colors_off"/>
|
|
||||||
|
|
||||||
</PreferenceCategory>
|
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
|
||||||
* which accompanies this distribution, and is available at
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*/
|
|
||||||
package at.bitfire.davdroid;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
|
|
||||||
|
|
||||||
public class ArrayUtilsTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testPartition() {
|
|
||||||
// n == 0
|
|
||||||
assertTrue(Arrays.deepEquals(
|
|
||||||
new Long[0][0],
|
|
||||||
ArrayUtils.partition(new Long[] {}, 5)));
|
|
||||||
|
|
||||||
// n < max
|
|
||||||
assertTrue(Arrays.deepEquals(
|
|
||||||
new Long[][] { { 1l, 2l } },
|
|
||||||
ArrayUtils.partition(new Long[] { 1l, 2l }, 5)));
|
|
||||||
|
|
||||||
// n == max
|
|
||||||
assertTrue(Arrays.deepEquals(
|
|
||||||
new Long[][] { { 1l, 2l }, { 3l, 4l } },
|
|
||||||
ArrayUtils.partition(new Long[] { 1l, 2l, 3l, 4l }, 2)));
|
|
||||||
|
|
||||||
// n > max
|
|
||||||
assertTrue(Arrays.deepEquals(
|
|
||||||
new Long[][] { { 1l, 2l, 3l, 4l, 5l }, { 6l, 7l, 8l, 9l, 10l }, { 11l } },
|
|
||||||
ArrayUtils.partition(new Long[] { 1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l, 9l, 10l, 11l }, 5)));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
|
||||||
* which accompanies this distribution, and is available at
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
package at.bitfire.davdroid;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
|
|
||||||
public class TestDavUtils {
|
|
||||||
|
|
||||||
private static final String exampleURL = "http://example.com/";
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testLastSegmentOfUrl() {
|
|
||||||
assertEquals("/", DavUtils.lastSegmentOfUrl(exampleURL));
|
|
||||||
assertEquals("dir", DavUtils.lastSegmentOfUrl(exampleURL + "dir"));
|
|
||||||
assertEquals("dir", DavUtils.lastSegmentOfUrl(exampleURL + "dir/"));
|
|
||||||
assertEquals("file.html", DavUtils.lastSegmentOfUrl(exampleURL + "dir/file.html"));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
Subproject commit 62ac4921ec5d5f366d0d79169a4bba94eb65ef48
|
|
1
doc/.gitignore
vendored
1
doc/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
javadoc/
|
|
Binary file not shown.
Binary file not shown.
@ -1,560 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
Calendar Server Extension C. Daboo
|
|
||||||
Apple Computer
|
|
||||||
May 3, 2007
|
|
||||||
|
|
||||||
|
|
||||||
Calendar User Proxy Functionality in CalDAV
|
|
||||||
caldav-cu-proxy-02
|
|
||||||
|
|
||||||
Abstract
|
|
||||||
|
|
||||||
This specification defines an extension to CalDAV that makes it easy
|
|
||||||
for clients to setup and manage calendar user proxies, using the
|
|
||||||
WebDAV Access Control List extension as a basis.
|
|
||||||
|
|
||||||
|
|
||||||
Table of Contents
|
|
||||||
|
|
||||||
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 2
|
|
||||||
2. Conventions Used in This Document . . . . . . . . . . . . . . 2
|
|
||||||
3. Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
|
|
||||||
3.1. Server . . . . . . . . . . . . . . . . . . . . . . . . . . 3
|
|
||||||
3.2. Client . . . . . . . . . . . . . . . . . . . . . . . . . . 3
|
|
||||||
4. Open Issues . . . . . . . . . . . . . . . . . . . . . . . . . 4
|
|
||||||
5. New features in CalDAV . . . . . . . . . . . . . . . . . . . . 4
|
|
||||||
5.1. Proxy Principal Resource . . . . . . . . . . . . . . . . . 4
|
|
||||||
5.2. Privilege Provisioning . . . . . . . . . . . . . . . . . . 8
|
|
||||||
6. Security Considerations . . . . . . . . . . . . . . . . . . . 9
|
|
||||||
7. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 9
|
|
||||||
8. Normative References . . . . . . . . . . . . . . . . . . . . . 9
|
|
||||||
Appendix A. Acknowledgments . . . . . . . . . . . . . . . . . . . 9
|
|
||||||
Appendix B. Change History . . . . . . . . . . . . . . . . . . . 10
|
|
||||||
Author's Address . . . . . . . . . . . . . . . . . . . . . . . . . 10
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo [Page 1]
|
|
||||||
|
|
||||||
CalDAV Proxy May 2007
|
|
||||||
|
|
||||||
|
|
||||||
1. Introduction
|
|
||||||
|
|
||||||
CalDAV [RFC4791] provides a way for calendar users to store calendar
|
|
||||||
data and exchange this data via scheduling operations. Based on the
|
|
||||||
WebDAV protocol [RFC2518], it also includes the ability to manage
|
|
||||||
access to calendar data via the WebDAV ACL extension [RFC3744].
|
|
||||||
|
|
||||||
It is often common for a calendar user to delegate some form of
|
|
||||||
responsibility for their calendar and schedules to another calendar
|
|
||||||
user (e.g., a boss allows an assistant to check a calendar or to send
|
|
||||||
and accept scheduling invites on his behalf). The user handling the
|
|
||||||
calendar data on behalf of someone else is often referred to as a
|
|
||||||
"calendar user proxy".
|
|
||||||
|
|
||||||
Whilst CalDAV does have fine-grained access control features that can
|
|
||||||
be used to setup complex sharing and management of calendars, often
|
|
||||||
the proxy behavior required is an "all-or-nothing" approach - i.e.
|
|
||||||
the proxy has access to all the calendars or to no calendars (in
|
|
||||||
which case they are of course not a proxy). So a simple way to
|
|
||||||
manage access to an entire set of calendars and scheduling ability
|
|
||||||
would be handy.
|
|
||||||
|
|
||||||
In addition, calendar user agents will often want to display to a
|
|
||||||
user who has proxy access to their calendars, or to whom they are
|
|
||||||
acting as a proxy. Again, CalDAV's access control discovery and
|
|
||||||
report features can be used to do that, but with fine-grained control
|
|
||||||
that exists, it can be hard to tell who is a "real" proxy as opposed
|
|
||||||
to someone just granted rights to some subset of calendars. Again, a
|
|
||||||
simple way to discover proxy information would be handy.
|
|
||||||
|
|
||||||
|
|
||||||
2. Conventions Used in This Document
|
|
||||||
|
|
||||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
|
|
||||||
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
|
|
||||||
document are to be interpreted as described in [RFC2119].
|
|
||||||
|
|
||||||
When XML element types in the namespace "DAV:" are referenced in this
|
|
||||||
document outside of the context of an XML fragment, the string "DAV:"
|
|
||||||
will be prefixed to the element type names.
|
|
||||||
|
|
||||||
When XML element types in the namespaces "DAV:" and
|
|
||||||
"urn:ietf:params:xml:ns:caldav" are referenced in this document
|
|
||||||
outside of the context of an XML fragment, the string "DAV:" and
|
|
||||||
"CALDAV:" will be prefixed to the element type names respectively.
|
|
||||||
|
|
||||||
The namespace "http://calendarserver.org/ns/" is used for XML
|
|
||||||
elements defined in this specification. When XML element types in
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo [Page 2]
|
|
||||||
|
|
||||||
CalDAV Proxy May 2007
|
|
||||||
|
|
||||||
|
|
||||||
this namespace are referenced in this document outside of the context
|
|
||||||
of an XML fragment, the string "CS:" will be prefixed to the element
|
|
||||||
type names respectively.
|
|
||||||
|
|
||||||
|
|
||||||
3. Overview
|
|
||||||
|
|
||||||
3.1. Server
|
|
||||||
|
|
||||||
For each calendar user principal on the server, the server will
|
|
||||||
generate two group principals - "proxy groups". One is used to hold
|
|
||||||
the list of principals who have read-only proxy access to the main
|
|
||||||
principal's calendars, the other holds the list of principals who
|
|
||||||
have read-write and scheduling proxy access. NB these new group
|
|
||||||
principals would have no equivalent in Open Directory.
|
|
||||||
|
|
||||||
Privileges on each "proxy group" principal will be set so that the
|
|
||||||
"owner" has the ability to change property values.
|
|
||||||
|
|
||||||
The "proxy group" principals will be child resources of the user
|
|
||||||
principal resource with specific resource types and thus are easy to
|
|
||||||
discover. As a result the user principal resources will also be
|
|
||||||
collection resources.
|
|
||||||
|
|
||||||
When provisioning the calendar user home collection, the server will:
|
|
||||||
|
|
||||||
a. Add an ACE to the calendar home collection giving the read-only
|
|
||||||
"proxy group" inheritable read access.
|
|
||||||
|
|
||||||
b. Add an ACE to the calendar home collection giving the read-write
|
|
||||||
"proxy group" inheritable read-write access.
|
|
||||||
|
|
||||||
c. Add an ACE to each of the calendar Inbox and Outbox collections
|
|
||||||
giving the CALDAV:schedule privilege
|
|
||||||
[I-D.desruisseaux-caldav-sched] to the read-write "proxy group".
|
|
||||||
|
|
||||||
3.2. Client
|
|
||||||
|
|
||||||
A client can see who the proxies are for the current principal by
|
|
||||||
examining the principal resource for the two "proxy group" properties
|
|
||||||
and then looking at the DAV:group-member-set property of each.
|
|
||||||
|
|
||||||
The client can edit the list of proxies for the current principal by
|
|
||||||
editing the DAV:group-member-set property on the relevant "proxy
|
|
||||||
group" principal resource.
|
|
||||||
|
|
||||||
The client can find out who the current principal is a proxy for by
|
|
||||||
running a DAV:principal-match REPORT on the principal collection.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo [Page 3]
|
|
||||||
|
|
||||||
CalDAV Proxy May 2007
|
|
||||||
|
|
||||||
|
|
||||||
Alternatively, the client can find out who the current principal is a
|
|
||||||
proxy for by examining the DAV:group-membership property on the
|
|
||||||
current principal resource looking for membership in other users'
|
|
||||||
"proxy groups".
|
|
||||||
|
|
||||||
|
|
||||||
4. Open Issues
|
|
||||||
|
|
||||||
1. Do we want to separate read-write access to calendars vs the
|
|
||||||
ability to schedule as a proxy?
|
|
||||||
|
|
||||||
2. We may want to restrict changing properties on the proxy group
|
|
||||||
collections to just the DAV:group-member-set property?
|
|
||||||
|
|
||||||
3. There is no way for a proxy to be able to manage the list of
|
|
||||||
proxies. We could allow the main calendar user DAV:write-acl on
|
|
||||||
their "proxy group" principals, in which case they could grant
|
|
||||||
others the right to modify the group membership.
|
|
||||||
|
|
||||||
4. Should the "proxy group" principals also be collections given
|
|
||||||
that the regular principal resources will be?
|
|
||||||
|
|
||||||
|
|
||||||
5. New features in CalDAV
|
|
||||||
|
|
||||||
5.1. Proxy Principal Resource
|
|
||||||
|
|
||||||
Each "regular" principal resource that needs to allow calendar user
|
|
||||||
proxy support MUST be a collection resource. i.e. in addition to
|
|
||||||
including the DAV:principal XML element in the DAV:resourcetype
|
|
||||||
property on the resource, it MUST also include the DAV:collection XML
|
|
||||||
element.
|
|
||||||
|
|
||||||
Each "regular" principal resource MUST contain two child resources
|
|
||||||
with names "calendar-proxy-read" and "calendar-proxy-write" (note
|
|
||||||
that these are only suggested names - the server could choose any
|
|
||||||
unique name for these). These resources are themselves principal
|
|
||||||
resources that are groups contain the list of principals for calendar
|
|
||||||
users who can act as a read-only or read-write proxy respectively.
|
|
||||||
|
|
||||||
The server MUST include the CS:calendar-proxy-read or CS:calendar-
|
|
||||||
proxy-write XML elements in the DAV:resourcetype property of the
|
|
||||||
child resources, respectively. This allows clients to discover the
|
|
||||||
"proxy group" principals by using a PROPFIND, Depth:1 request on the
|
|
||||||
current user's principal resource and requesting the DAV:resourcetype
|
|
||||||
property be returned. The element type declarations are:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo [Page 4]
|
|
||||||
|
|
||||||
CalDAV Proxy May 2007
|
|
||||||
|
|
||||||
|
|
||||||
<!ELEMENT calendar-proxy-read EMPTY>
|
|
||||||
|
|
||||||
<!ELEMENT calendar-proxy-write EMPTY>
|
|
||||||
|
|
||||||
The server MUST allow the "parent" principal to change the DAV:group-
|
|
||||||
member-set property on each of the "child" "proxy group" principal
|
|
||||||
resources. When a principal is listed as a member of the "child"
|
|
||||||
resource, the server MUST include the "child" resource URI in the
|
|
||||||
DAV:group-membership property on the included principal resource.
|
|
||||||
Note that this is just "normal" behavior for a group principal.
|
|
||||||
|
|
||||||
An example principal resource layout might be:
|
|
||||||
|
|
||||||
+ /
|
|
||||||
+ principals/
|
|
||||||
+ users/
|
|
||||||
+ cyrus/
|
|
||||||
calendar-proxy-read
|
|
||||||
calendar-proxy-write
|
|
||||||
+ red/
|
|
||||||
calendar-proxy-read
|
|
||||||
calendar-proxy-write
|
|
||||||
+ wilfredo/
|
|
||||||
calendar-proxy-read
|
|
||||||
calendar-proxy-write
|
|
||||||
|
|
||||||
If the principal "cyrus" wishes to have the principal "red" act as a
|
|
||||||
calendar user proxy on his behalf and have the ability to change
|
|
||||||
items on his calendar or schedule meetings on his behalf, then he
|
|
||||||
would add the principal resource URI for "red" to the DAV:group-
|
|
||||||
member-set property of the principal resource /principals/users/
|
|
||||||
cyrus/calendar-proxy-write, giving:
|
|
||||||
|
|
||||||
<DAV:group-member-set>
|
|
||||||
<DAV:href>/principals/users/red/</DAV:href>
|
|
||||||
</DAV:group-member-set>
|
|
||||||
|
|
||||||
The DAV:group-membership property on the resource /principals/users/
|
|
||||||
red/ would be:
|
|
||||||
|
|
||||||
<DAV:group-membership>
|
|
||||||
<DAV:href>/principals/users/cyrus/calendar-proxy-write</DAV:href>
|
|
||||||
</DAV:group-membership>
|
|
||||||
|
|
||||||
If the principal "red" was also a read-only proxy for the principal
|
|
||||||
"wilfredo", then the DA:group-membership property on the resource
|
|
||||||
/principals/users/red/ would be:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo [Page 5]
|
|
||||||
|
|
||||||
CalDAV Proxy May 2007
|
|
||||||
|
|
||||||
|
|
||||||
<DAV:group-membership>
|
|
||||||
<DAV:href>/principals/users/cyrus/calendar-proxy-write</DAV:href>
|
|
||||||
<DAV:href>/principals/users/wilfredo/calendar-proxy-read</DAV:href>
|
|
||||||
</DAV:group-membership>
|
|
||||||
|
|
||||||
Thus a client can discover to which principals a particular principal
|
|
||||||
is acting as a calendar user proxy for by examining the DAV:group-
|
|
||||||
membership property.
|
|
||||||
|
|
||||||
An alternative to discovering which principals a user can proxy as is
|
|
||||||
to use the WebDAV ACL principal-match report, targeted at the
|
|
||||||
principal collections available on the server.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
>> Request <<
|
|
||||||
|
|
||||||
REPORT /principals/ HTTP/1.1
|
|
||||||
Host: cal.example.com
|
|
||||||
Depth: 0
|
|
||||||
Content-Type: application/xml; charset="utf-8"
|
|
||||||
Content-Length: xxxx
|
|
||||||
Authorization: Digest username="red",
|
|
||||||
realm="cal.example.com", nonce="...",
|
|
||||||
uri="/principals/", response="...", opaque="..."
|
|
||||||
|
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<D:principal-match xmlns:D="DAV:">
|
|
||||||
<D:self/>
|
|
||||||
<D:prop>
|
|
||||||
<D:resourcetype/>
|
|
||||||
</D:prop>
|
|
||||||
</D:principal-match>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo [Page 6]
|
|
||||||
|
|
||||||
CalDAV Proxy May 2007
|
|
||||||
|
|
||||||
|
|
||||||
>> Response <<
|
|
||||||
|
|
||||||
HTTP/1.1 207 Multi-Status
|
|
||||||
Date: Fri, 10 Nov 2006 09:32:12 GMT
|
|
||||||
Content-Type: application/xml; charset="utf-8"
|
|
||||||
Content-Length: xxxx
|
|
||||||
|
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<D:multistatus xmlns:D="DAV:"
|
|
||||||
xmlns:A="http://calendarserver.org/ns/">
|
|
||||||
<D:response>
|
|
||||||
<D:href>/principals/users/red/</D:href>
|
|
||||||
<D:propstat>
|
|
||||||
<D:prop>
|
|
||||||
<D:resourcetype>
|
|
||||||
<D:principal/>
|
|
||||||
<D:collection/>
|
|
||||||
</D:resourcetype>
|
|
||||||
</D:prop>
|
|
||||||
<D:status>HTTP/1.1 200 OK</D:status>
|
|
||||||
</D:propstat>
|
|
||||||
</D:response>
|
|
||||||
<D:response>
|
|
||||||
<D:href>/principals/users/cyrus/calendar-proxy-write</D:href>
|
|
||||||
<D:propstat>
|
|
||||||
<D:prop>
|
|
||||||
<D:resourcetype>
|
|
||||||
<D:principal/>
|
|
||||||
<A:calendar-proxy-write/>
|
|
||||||
</D:resourcetype>
|
|
||||||
</D:prop>
|
|
||||||
<D:status>HTTP/1.1 200 OK</D:status>
|
|
||||||
</D:propstat>
|
|
||||||
</D:response>
|
|
||||||
<D:response>
|
|
||||||
<D:href>/principals/users/wilfredo/calendar-proxy-read</D:href>
|
|
||||||
<D:propstat>
|
|
||||||
<D:prop>
|
|
||||||
<D:resourcetype>
|
|
||||||
<D:principal/>
|
|
||||||
<A:calendar-proxy-read/>
|
|
||||||
</D:resourcetype>
|
|
||||||
</D:prop>
|
|
||||||
<D:status>HTTP/1.1 200 OK</D:status>
|
|
||||||
</D:propstat>
|
|
||||||
</D:response>
|
|
||||||
</D:multistatus>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo [Page 7]
|
|
||||||
|
|
||||||
CalDAV Proxy May 2007
|
|
||||||
|
|
||||||
|
|
||||||
5.2. Privilege Provisioning
|
|
||||||
|
|
||||||
In order for a calendar user proxy to be able to access the calendars
|
|
||||||
of the user they are proxying for the server MUST ensure that the
|
|
||||||
privileges on the relevant calendars are setup accordingly:
|
|
||||||
|
|
||||||
The DAV:read privilege MUST be granted for read-only and read-
|
|
||||||
write calendar user proxy principals
|
|
||||||
|
|
||||||
The DAV:write privilege MUST be granted for read-write calendar
|
|
||||||
user proxy principals.
|
|
||||||
|
|
||||||
Additionally, the CalDAV scheduling Inbox and Outbox calendar
|
|
||||||
collections for the user allowing proxy access, MUST have the CALDAV:
|
|
||||||
schedule privilege [I-D.desruisseaux-caldav-sched] granted for read-
|
|
||||||
write calendar user proxy principals.
|
|
||||||
|
|
||||||
Note that with a suitable repository layout, a server may be able to
|
|
||||||
grant the appropriate privileges on a parent collection and ensure
|
|
||||||
that all the contained collections and resources inherit that. For
|
|
||||||
example, given the following repository layout:
|
|
||||||
|
|
||||||
+ /
|
|
||||||
+ calendars/
|
|
||||||
+ users/
|
|
||||||
+ cyrus/
|
|
||||||
inbox
|
|
||||||
outbox
|
|
||||||
home
|
|
||||||
work
|
|
||||||
+ red/
|
|
||||||
inbox
|
|
||||||
outbox
|
|
||||||
work
|
|
||||||
soccer
|
|
||||||
+ wilfredo/
|
|
||||||
inbox
|
|
||||||
outbox
|
|
||||||
home
|
|
||||||
work
|
|
||||||
flying
|
|
||||||
|
|
||||||
In order for the principal "red" to act as a read-write proxy for the
|
|
||||||
principal "cyrus", the following WebDAV ACE will need to be granted
|
|
||||||
on the resource /calendars/users/cyrus/ and all children of that
|
|
||||||
resource:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo [Page 8]
|
|
||||||
|
|
||||||
CalDAV Proxy May 2007
|
|
||||||
|
|
||||||
|
|
||||||
<DAV:ace>
|
|
||||||
<DAV:principal>
|
|
||||||
<DAV:href>/principals/users/cyrus/calendar-proxy-write</DAV:href>
|
|
||||||
</DAV:principal>
|
|
||||||
<DAV:privileges>
|
|
||||||
<DAV:grant><DAV:read/><DAV:write/></DAV:grant>
|
|
||||||
</DAV:privileges>
|
|
||||||
</DAV:ace>
|
|
||||||
|
|
||||||
|
|
||||||
6. Security Considerations
|
|
||||||
|
|
||||||
TBD
|
|
||||||
|
|
||||||
|
|
||||||
7. IANA Considerations
|
|
||||||
|
|
||||||
This document does not require any actions on the part of IANA.
|
|
||||||
|
|
||||||
|
|
||||||
8. Normative References
|
|
||||||
|
|
||||||
[I-D.desruisseaux-caldav-sched]
|
|
||||||
Desruisseaux, B., "Scheduling Extensions to CalDAV",
|
|
||||||
draft-desruisseaux-caldav-sched-03 (work in progress),
|
|
||||||
January 2007.
|
|
||||||
|
|
||||||
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
|
|
||||||
Requirement Levels", BCP 14, RFC 2119, March 1997.
|
|
||||||
|
|
||||||
[RFC2518] Goland, Y., Whitehead, E., Faizi, A., Carter, S., and D.
|
|
||||||
Jensen, "HTTP Extensions for Distributed Authoring --
|
|
||||||
WEBDAV", RFC 2518, February 1999.
|
|
||||||
|
|
||||||
[RFC3744] Clemm, G., Reschke, J., Sedlar, E., and J. Whitehead, "Web
|
|
||||||
Distributed Authoring and Versioning (WebDAV) Access
|
|
||||||
Control Protocol", RFC 3744, May 2004.
|
|
||||||
|
|
||||||
[RFC4791] Daboo, C., Desruisseaux, B., and L. Dusseault,
|
|
||||||
"Calendaring Extensions to WebDAV (CalDAV)", RFC 4791,
|
|
||||||
March 2007.
|
|
||||||
|
|
||||||
|
|
||||||
Appendix A. Acknowledgments
|
|
||||||
|
|
||||||
This specification is the result of discussions between the Apple
|
|
||||||
calendar server and client teams.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo [Page 9]
|
|
||||||
|
|
||||||
CalDAV Proxy May 2007
|
|
||||||
|
|
||||||
|
|
||||||
Appendix B. Change History
|
|
||||||
|
|
||||||
Changes from -00:
|
|
||||||
|
|
||||||
1. Updated to RFC 4791 reference.
|
|
||||||
|
|
||||||
Changes from -00:
|
|
||||||
|
|
||||||
1. Added more details on actual CalDAV protocol changes.
|
|
||||||
|
|
||||||
2. Changed namespace from http://apple.com/ns/calendarserver/ to
|
|
||||||
http://calendarserver.org/ns/.
|
|
||||||
|
|
||||||
3. Made "proxy group" principals child resources of their "owner"
|
|
||||||
principals.
|
|
||||||
|
|
||||||
4. The "proxy group" principals now have their own resourcetype.
|
|
||||||
|
|
||||||
|
|
||||||
Author's Address
|
|
||||||
|
|
||||||
Cyrus Daboo
|
|
||||||
Apple Computer, Inc.
|
|
||||||
1 Infinite Loop
|
|
||||||
Cupertino, CA 95014
|
|
||||||
USA
|
|
||||||
|
|
||||||
Email: cyrus@daboo.name
|
|
||||||
URI: http://www.apple.com/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo [Page 10]
|
|
||||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,281 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
Network Working Group W. Sanchez
|
|
||||||
Request for Comments: 5397 C. Daboo
|
|
||||||
Category: Standards Track Apple Inc.
|
|
||||||
December 2008
|
|
||||||
|
|
||||||
|
|
||||||
WebDAV Current Principal Extension
|
|
||||||
|
|
||||||
Status of This Memo
|
|
||||||
|
|
||||||
This document specifies an Internet standards track protocol for the
|
|
||||||
Internet community, and requests discussion and suggestions for
|
|
||||||
improvements. Please refer to the current edition of the "Internet
|
|
||||||
Official Protocol Standards" (STD 1) for the standardization state
|
|
||||||
and status of this protocol. Distribution of this memo is unlimited.
|
|
||||||
|
|
||||||
Copyright Notice
|
|
||||||
|
|
||||||
Copyright (c) 2008 IETF Trust and the persons identified as the
|
|
||||||
document authors. All rights reserved.
|
|
||||||
|
|
||||||
This document is subject to BCP 78 and the IETF Trust's Legal
|
|
||||||
Provisions Relating to IETF Documents
|
|
||||||
(http://trustee.ietf.org/license-info) in effect on the date of
|
|
||||||
publication of this document. Please review these documents
|
|
||||||
carefully, as they describe your rights and restrictions with respect
|
|
||||||
to this document.
|
|
||||||
|
|
||||||
Abstract
|
|
||||||
|
|
||||||
This specification defines a new WebDAV property that allows clients
|
|
||||||
to quickly determine the principal corresponding to the current
|
|
||||||
authenticated user.
|
|
||||||
|
|
||||||
Table of Contents
|
|
||||||
|
|
||||||
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 2
|
|
||||||
2. Conventions Used in This Document . . . . . . . . . . . . . . . 2
|
|
||||||
3. DAV:current-user-principal . . . . . . . . . . . . . . . . . . 3
|
|
||||||
4. Security Considerations . . . . . . . . . . . . . . . . . . . . 4
|
|
||||||
5. Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . 4
|
|
||||||
6. Normative References . . . . . . . . . . . . . . . . . . . . . 4
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Sanchez & Daboo Standards Track [Page 1]
|
|
||||||
|
|
||||||
RFC 5397 WebDAV Current Principal December 2008
|
|
||||||
|
|
||||||
|
|
||||||
1. Introduction
|
|
||||||
|
|
||||||
WebDAV [RFC4918] is an extension to HTTP [RFC2616] to support
|
|
||||||
improved document authoring capabilities. The WebDAV Access Control
|
|
||||||
Protocol ("WebDAV ACL") [RFC3744] extension adds access control
|
|
||||||
capabilities to WebDAV. It introduces the concept of a "principal"
|
|
||||||
resource, which is used to represent information about authenticated
|
|
||||||
entities on the system.
|
|
||||||
|
|
||||||
Some clients have a need to determine which [RFC3744] principal a
|
|
||||||
server is associating with the currently authenticated HTTP user.
|
|
||||||
While [RFC3744] defines a DAV:current-user-privilege-set property for
|
|
||||||
retrieving the privileges granted to that principal, there is no
|
|
||||||
recommended way to identify the principal in question, which is
|
|
||||||
necessary to perform other useful operations. For example, a client
|
|
||||||
may wish to determine which groups the current user is a member of,
|
|
||||||
or modify a property of the principal resource associated with the
|
|
||||||
current user.
|
|
||||||
|
|
||||||
The DAV:principal-match REPORT provides some useful functionality,
|
|
||||||
but there are common situations where the results from that query can
|
|
||||||
be ambiguous. For example, not only is an individual user principal
|
|
||||||
returned, but also every group principal that the user is a member
|
|
||||||
of, and there is no clear way to distinguish which is which.
|
|
||||||
|
|
||||||
This specification proposes an extension to WebDAV ACL that adds a
|
|
||||||
DAV:current-user-principal property to resources under access control
|
|
||||||
on the server. This property provides a URL to a principal resource
|
|
||||||
corresponding to the currently authenticated user. This allows a
|
|
||||||
client to "bootstrap" itself by performing additional queries on the
|
|
||||||
principal resource to obtain additional information from that
|
|
||||||
resource, which is the purpose of this extension. Note that while it
|
|
||||||
is possible for multiple URLs to refer to the same principal
|
|
||||||
resource, or for multiple principal resources to correspond to a
|
|
||||||
single principal, this specification only allows for a single http(s)
|
|
||||||
URL in the DAV:current-user-principal property. If a client wishes
|
|
||||||
to obtain alternate URLs for the principal, it can query the
|
|
||||||
principal resource for this information; it is not the purpose of
|
|
||||||
this extension to provide a complete list of such URLs, but simply to
|
|
||||||
provide a means to locate a resource which contains that (and other)
|
|
||||||
information.
|
|
||||||
|
|
||||||
2. Conventions Used in This Document
|
|
||||||
|
|
||||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
|
|
||||||
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
|
|
||||||
document are to be interpreted as described in [RFC2119].
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Sanchez & Daboo Standards Track [Page 2]
|
|
||||||
|
|
||||||
RFC 5397 WebDAV Current Principal December 2008
|
|
||||||
|
|
||||||
|
|
||||||
When XML element types in the namespace "DAV:" are referenced in this
|
|
||||||
document outside of the context of an XML fragment, the string "DAV:"
|
|
||||||
will be prefixed to the element type names.
|
|
||||||
|
|
||||||
Processing of XML by clients and servers MUST follow the rules
|
|
||||||
defined in Section 17 of WebDAV [RFC4918].
|
|
||||||
|
|
||||||
Some of the declarations refer to XML elements defined by WebDAV
|
|
||||||
[RFC4918].
|
|
||||||
|
|
||||||
3. DAV:current-user-principal
|
|
||||||
|
|
||||||
Name: current-user-principal
|
|
||||||
|
|
||||||
Namespace: DAV:
|
|
||||||
|
|
||||||
Purpose: Indicates a URL for the currently authenticated user's
|
|
||||||
principal resource on the server.
|
|
||||||
|
|
||||||
Value: A single DAV:href or DAV:unauthenticated element.
|
|
||||||
|
|
||||||
Protected: This property is computed on a per-request basis, and
|
|
||||||
therefore is protected.
|
|
||||||
|
|
||||||
Description: The DAV:current-user-principal property contains either
|
|
||||||
a DAV:href or DAV:unauthenticated XML element. The DAV:href
|
|
||||||
element contains a URL to a principal resource corresponding to
|
|
||||||
the currently authenticated user. That URL MUST be one of the
|
|
||||||
URLs in the DAV:principal-URL or DAV:alternate-URI-set properties
|
|
||||||
defined on the principal resource and MUST be an http(s) scheme
|
|
||||||
URL. When authentication has not been done or has failed, this
|
|
||||||
property MUST contain the DAV:unauthenticated pseudo-principal.
|
|
||||||
|
|
||||||
In some cases, there may be multiple principal resources
|
|
||||||
corresponding to the same authenticated principal. In that case,
|
|
||||||
the server is free to choose any one of the principal resource
|
|
||||||
URIs for the value of the DAV:current-user-principal property.
|
|
||||||
However, servers SHOULD be consistent and use the same principal
|
|
||||||
resource URI for each authenticated principal.
|
|
||||||
|
|
||||||
COPY/MOVE behavior: This property is computed on a per-request
|
|
||||||
basis, and is thus never copied or moved.
|
|
||||||
|
|
||||||
Definition:
|
|
||||||
|
|
||||||
<!ELEMENT current-user-principal (unauthenticated | href)>
|
|
||||||
<!-- href value: a URL to a principal resource -->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Sanchez & Daboo Standards Track [Page 3]
|
|
||||||
|
|
||||||
RFC 5397 WebDAV Current Principal December 2008
|
|
||||||
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
<D:current-user-principal xmlns:D="DAV:">
|
|
||||||
<D:href>/principals/users/cdaboo</D:href>
|
|
||||||
</D:current-user-principal>
|
|
||||||
|
|
||||||
4. Security Considerations
|
|
||||||
|
|
||||||
This specification does not introduce any additional security issues
|
|
||||||
beyond those defined for HTTP [RFC2616], WebDAV [RFC4918], and WebDAV
|
|
||||||
ACL [RFC3744].
|
|
||||||
|
|
||||||
5. Acknowledgments
|
|
||||||
|
|
||||||
This specification is based on discussions that took place within the
|
|
||||||
Calendaring and Scheduling Consortium's CalDAV Technical Committee.
|
|
||||||
The authors thank the participants of that group for their input.
|
|
||||||
|
|
||||||
The authors thank Julian Reschke for his valuable input via the
|
|
||||||
WebDAV working group mailing list.
|
|
||||||
|
|
||||||
6. Normative References
|
|
||||||
|
|
||||||
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
|
|
||||||
Requirement Levels", BCP 14, RFC 2119, March 1997.
|
|
||||||
|
|
||||||
[RFC2616] Fielding, R., Gettys, J., Mogul, J., Frystyk, H.,
|
|
||||||
Masinter, L., Leach, P., and T. Berners-Lee, "Hypertext
|
|
||||||
Transfer Protocol -- HTTP/1.1", RFC 2616, June 1999.
|
|
||||||
|
|
||||||
[RFC3744] Clemm, G., Reschke, J., Sedlar, E., and J. Whitehead, "Web
|
|
||||||
Distributed Authoring and Versioning (WebDAV)
|
|
||||||
Access Control Protocol", RFC 3744, May 2004.
|
|
||||||
|
|
||||||
[RFC4918] Dusseault, L., "HTTP Extensions for Web Distributed
|
|
||||||
Authoring and Versioning (WebDAV)", RFC 4918, June 2007.
|
|
||||||
|
|
||||||
Authors' Addresses
|
|
||||||
|
|
||||||
Wilfredo Sanchez
|
|
||||||
Apple Inc.
|
|
||||||
1 Infinite Loop
|
|
||||||
Cupertino, CA 95014
|
|
||||||
USA
|
|
||||||
|
|
||||||
EMail: wsanchez@wsanchez.net
|
|
||||||
URI: http://www.apple.com/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Sanchez & Daboo Standards Track [Page 4]
|
|
||||||
|
|
||||||
RFC 5397 WebDAV Current Principal December 2008
|
|
||||||
|
|
||||||
|
|
||||||
Cyrus Daboo
|
|
||||||
Apple Inc.
|
|
||||||
1 Infinite Loop
|
|
||||||
Cupertino, CA 95014
|
|
||||||
USA
|
|
||||||
|
|
||||||
EMail: cyrus@daboo.name
|
|
||||||
URI: http://www.apple.com/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Sanchez & Daboo Standards Track [Page 5]
|
|
||||||
|
|
||||||
|
|
@ -1,451 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Internet Engineering Task Force (IETF) M. Nottingham
|
|
||||||
Request for Comments: 5785 E. Hammer-Lahav
|
|
||||||
Updates: 2616, 2818 April 2010
|
|
||||||
Category: Standards Track
|
|
||||||
ISSN: 2070-1721
|
|
||||||
|
|
||||||
|
|
||||||
Defining Well-Known Uniform Resource Identifiers (URIs)
|
|
||||||
|
|
||||||
Abstract
|
|
||||||
|
|
||||||
This memo defines a path prefix for "well-known locations",
|
|
||||||
"/.well-known/", in selected Uniform Resource Identifier (URI)
|
|
||||||
schemes.
|
|
||||||
|
|
||||||
Status of This Memo
|
|
||||||
|
|
||||||
This is an Internet Standards Track document.
|
|
||||||
|
|
||||||
This document is a product of the Internet Engineering Task Force
|
|
||||||
(IETF). It represents the consensus of the IETF community. It has
|
|
||||||
received public review and has been approved for publication by the
|
|
||||||
Internet Engineering Steering Group (IESG). Further information on
|
|
||||||
Internet Standards is available in Section 2 of RFC 5741.
|
|
||||||
|
|
||||||
Information about the current status of this document, any errata,
|
|
||||||
and how to provide feedback on it may be obtained at
|
|
||||||
http://www.rfc-editor.org/info/rfc5785.
|
|
||||||
|
|
||||||
Copyright Notice
|
|
||||||
|
|
||||||
Copyright (c) 2010 IETF Trust and the persons identified as the
|
|
||||||
document authors. All rights reserved.
|
|
||||||
|
|
||||||
This document is subject to BCP 78 and the IETF Trust's Legal
|
|
||||||
Provisions Relating to IETF Documents
|
|
||||||
(http://trustee.ietf.org/license-info) in effect on the date of
|
|
||||||
publication of this document. Please review these documents
|
|
||||||
carefully, as they describe your rights and restrictions with respect
|
|
||||||
to this document. Code Components extracted from this document must
|
|
||||||
include Simplified BSD License text as described in Section 4.e of
|
|
||||||
the Trust Legal Provisions and are provided without warranty as
|
|
||||||
described in the Simplified BSD License.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Nottingham & Hammer-Lahav Standards Track [Page 1]
|
|
||||||
|
|
||||||
RFC 5785 Defining Well-Known URIs April 2010
|
|
||||||
|
|
||||||
|
|
||||||
Table of Contents
|
|
||||||
|
|
||||||
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 2
|
|
||||||
1.1. Appropriate Use of Well-Known URIs . . . . . . . . . . . . 3
|
|
||||||
2. Notational Conventions . . . . . . . . . . . . . . . . . . . . 3
|
|
||||||
3. Well-Known URIs . . . . . . . . . . . . . . . . . . . . . . . . 3
|
|
||||||
4. Security Considerations . . . . . . . . . . . . . . . . . . . . 4
|
|
||||||
5. IANA Considerations . . . . . . . . . . . . . . . . . . . . . . 4
|
|
||||||
5.1. The Well-Known URI Registry . . . . . . . . . . . . . . . . 4
|
|
||||||
5.1.1. Registration Template . . . . . . . . . . . . . . . . . 5
|
|
||||||
6. References . . . . . . . . . . . . . . . . . . . . . . . . . . 5
|
|
||||||
6.1. Normative References . . . . . . . . . . . . . . . . . . . 5
|
|
||||||
6.2. Informative References . . . . . . . . . . . . . . . . . . 5
|
|
||||||
Appendix A. Acknowledgements . . . . . . . . . . . . . . . . . . . 7
|
|
||||||
Appendix B. Frequently Asked Questions . . . . . . . . . . . . . . 7
|
|
||||||
|
|
||||||
1. Introduction
|
|
||||||
|
|
||||||
It is increasingly common for Web-based protocols to require the
|
|
||||||
discovery of policy or other information about a host ("site-wide
|
|
||||||
metadata") before making a request. For example, the Robots
|
|
||||||
Exclusion Protocol <http://www.robotstxt.org/> specifies a way for
|
|
||||||
automated processes to obtain permission to access resources;
|
|
||||||
likewise, the Platform for Privacy Preferences [W3C.REC-P3P-20020416]
|
|
||||||
tells user-agents how to discover privacy policy beforehand.
|
|
||||||
|
|
||||||
While there are several ways to access per-resource metadata (e.g.,
|
|
||||||
HTTP headers, WebDAV's PROPFIND [RFC4918]), the perceived overhead
|
|
||||||
(either in terms of client-perceived latency and/or deployment
|
|
||||||
difficulties) associated with them often precludes their use in these
|
|
||||||
scenarios.
|
|
||||||
|
|
||||||
When this happens, it is common to designate a "well-known location"
|
|
||||||
for such data, so that it can be easily located. However, this
|
|
||||||
approach has the drawback of risking collisions, both with other such
|
|
||||||
designated "well-known locations" and with pre-existing resources.
|
|
||||||
|
|
||||||
To address this, this memo defines a path prefix in HTTP(S) URIs for
|
|
||||||
these "well-known locations", "/.well-known/". Future specifications
|
|
||||||
that need to define a resource for such site-wide metadata can
|
|
||||||
register their use to avoid collisions and minimise impingement upon
|
|
||||||
sites' URI space.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Nottingham & Hammer-Lahav Standards Track [Page 2]
|
|
||||||
|
|
||||||
RFC 5785 Defining Well-Known URIs April 2010
|
|
||||||
|
|
||||||
|
|
||||||
1.1. Appropriate Use of Well-Known URIs
|
|
||||||
|
|
||||||
There are a number of possible ways that applications could use Well-
|
|
||||||
known URIs. However, in keeping with the Architecture of the World-
|
|
||||||
Wide Web [W3C.REC-webarch-20041215], well-known URIs are not intended
|
|
||||||
for general information retrieval or establishment of large URI
|
|
||||||
namespaces on the Web. Rather, they are designed to facilitate
|
|
||||||
discovery of information on a site when it isn't practical to use
|
|
||||||
other mechanisms; for example, when discovering policy that needs to
|
|
||||||
be evaluated before a resource is accessed, or when using multiple
|
|
||||||
round-trips is judged detrimental to performance.
|
|
||||||
|
|
||||||
As such, the well-known URI space was created with the expectation
|
|
||||||
that it will be used to make site-wide policy information and other
|
|
||||||
metadata available directly (if sufficiently concise), or provide
|
|
||||||
references to other URIs that provide such metadata.
|
|
||||||
|
|
||||||
2. Notational Conventions
|
|
||||||
|
|
||||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
|
|
||||||
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
|
|
||||||
document are to be interpreted as described in RFC 2119 [RFC2119].
|
|
||||||
|
|
||||||
3. Well-Known URIs
|
|
||||||
|
|
||||||
A well-known URI is a URI [RFC3986] whose path component begins with
|
|
||||||
the characters "/.well-known/", and whose scheme is "HTTP", "HTTPS",
|
|
||||||
or another scheme that has explicitly been specified to use well-
|
|
||||||
known URIs.
|
|
||||||
|
|
||||||
Applications that wish to mint new well-known URIs MUST register
|
|
||||||
them, following the procedures in Section 5.1.
|
|
||||||
|
|
||||||
For example, if an application registers the name 'example', the
|
|
||||||
corresponding well-known URI on 'http://www.example.com/' would be
|
|
||||||
'http://www.example.com/.well-known/example'.
|
|
||||||
|
|
||||||
Registered names MUST conform to the segment-nz production in
|
|
||||||
[RFC3986].
|
|
||||||
|
|
||||||
Note that this specification defines neither how to determine the
|
|
||||||
authority to use for a particular context, nor the scope of the
|
|
||||||
metadata discovered by dereferencing the well-known URI; both should
|
|
||||||
be defined by the application itself.
|
|
||||||
|
|
||||||
Typically, a registration will reference a specification that defines
|
|
||||||
the format and associated media type to be obtained by dereferencing
|
|
||||||
the well-known URI.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Nottingham & Hammer-Lahav Standards Track [Page 3]
|
|
||||||
|
|
||||||
RFC 5785 Defining Well-Known URIs April 2010
|
|
||||||
|
|
||||||
|
|
||||||
It MAY also contain additional information, such as the syntax of
|
|
||||||
additional path components, query strings and/or fragment identifiers
|
|
||||||
to be appended to the well-known URI, or protocol-specific details
|
|
||||||
(e.g., HTTP [RFC2616] method handling).
|
|
||||||
|
|
||||||
Note that this specification does not define a format or media-type
|
|
||||||
for the resource located at "/.well-known/" and clients should not
|
|
||||||
expect a resource to exist at that location.
|
|
||||||
|
|
||||||
4. Security Considerations
|
|
||||||
|
|
||||||
This memo does not specify the scope of applicability of metadata or
|
|
||||||
policy obtained from a well-known URI, and does not specify how to
|
|
||||||
discover a well-known URI for a particular application. Individual
|
|
||||||
applications using this mechanism must define both aspects.
|
|
||||||
|
|
||||||
Applications minting new well-known URIs, as well as administrators
|
|
||||||
deploying them, will need to consider several security-related
|
|
||||||
issues, including (but not limited to) exposure of sensitive data,
|
|
||||||
denial-of-service attacks (in addition to normal load issues), server
|
|
||||||
and client authentication, vulnerability to DNS rebinding attacks,
|
|
||||||
and attacks where limited access to a server grants the ability to
|
|
||||||
affect how well-known URIs are served.
|
|
||||||
|
|
||||||
5. IANA Considerations
|
|
||||||
|
|
||||||
5.1. The Well-Known URI Registry
|
|
||||||
|
|
||||||
This document establishes the well-known URI registry.
|
|
||||||
|
|
||||||
Well-known URIs are registered on the advice of one or more
|
|
||||||
Designated Experts (appointed by the IESG or their delegate), with a
|
|
||||||
Specification Required (using terminology from [RFC5226]). However,
|
|
||||||
to allow for the allocation of values prior to publication, the
|
|
||||||
Designated Expert(s) may approve registration once they are satisfied
|
|
||||||
that such a specification will be published.
|
|
||||||
|
|
||||||
Registration requests should be sent to the
|
|
||||||
wellknown-uri-review@ietf.org mailing list for review and comment,
|
|
||||||
with an appropriate subject (e.g., "Request for well-known URI:
|
|
||||||
example").
|
|
||||||
|
|
||||||
Before a period of 14 days has passed, the Designated Expert(s) will
|
|
||||||
either approve or deny the registration request, communicating this
|
|
||||||
decision both to the review list and to IANA. Denials should include
|
|
||||||
an explanation and, if applicable, suggestions as to how to make the
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Nottingham & Hammer-Lahav Standards Track [Page 4]
|
|
||||||
|
|
||||||
RFC 5785 Defining Well-Known URIs April 2010
|
|
||||||
|
|
||||||
|
|
||||||
request successful. Registration requests that are undetermined for
|
|
||||||
a period longer than 21 days can be brought to the IESG's attention
|
|
||||||
(using the iesg@iesg.org mailing list) for resolution.
|
|
||||||
|
|
||||||
5.1.1. Registration Template
|
|
||||||
|
|
||||||
URI suffix: The name requested for the well-known URI, relative to
|
|
||||||
"/.well-known/"; e.g., "example".
|
|
||||||
|
|
||||||
Change controller: For Standards-Track RFCs, state "IETF". For
|
|
||||||
others, give the name of the responsible party. Other details
|
|
||||||
(e.g., postal address, e-mail address, home page URI) may also be
|
|
||||||
included.
|
|
||||||
|
|
||||||
Specification document(s): Reference to the document that specifies
|
|
||||||
the field, preferably including a URI that can be used to retrieve
|
|
||||||
a copy of the document. An indication of the relevant sections
|
|
||||||
may also be included, but is not required.
|
|
||||||
|
|
||||||
Related information: Optionally, citations to additional documents
|
|
||||||
containing further relevant information.
|
|
||||||
|
|
||||||
6. References
|
|
||||||
|
|
||||||
6.1. Normative References
|
|
||||||
|
|
||||||
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
|
|
||||||
Requirement Levels", BCP 14, RFC 2119, March 1997.
|
|
||||||
|
|
||||||
[RFC3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform
|
|
||||||
Resource Identifier (URI): Generic Syntax", STD 66,
|
|
||||||
RFC 3986, January 2005.
|
|
||||||
|
|
||||||
[RFC5226] Narten, T. and H. Alvestrand, "Guidelines for Writing an
|
|
||||||
IANA Considerations Section in RFCs", BCP 26, RFC 5226,
|
|
||||||
May 2008.
|
|
||||||
|
|
||||||
6.2. Informative References
|
|
||||||
|
|
||||||
[RFC2616] Fielding, R., Gettys, J., Mogul, J., Frystyk, H., Masinter,
|
|
||||||
L., Leach, P., and T. Berners-Lee, "Hypertext Transfer
|
|
||||||
Protocol -- HTTP/1.1", RFC 2616, June 1999.
|
|
||||||
|
|
||||||
[RFC4918] Dusseault, L., "HTTP Extensions for Web Distributed
|
|
||||||
Authoring and Versioning (WebDAV)", RFC 4918, June 2007.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Nottingham & Hammer-Lahav Standards Track [Page 5]
|
|
||||||
|
|
||||||
RFC 5785 Defining Well-Known URIs April 2010
|
|
||||||
|
|
||||||
|
|
||||||
[W3C.REC-P3P-20020416]
|
|
||||||
Marchiori, M., "The Platform for Privacy Preferences 1.0
|
|
||||||
(P3P1.0) Specification", World Wide Web Consortium
|
|
||||||
Recommendation REC-P3P-20020416, April 2002,
|
|
||||||
<http://www.w3.org/TR/2002/ REC-P3P-20020416>.
|
|
||||||
|
|
||||||
[W3C.REC-webarch-20041215]
|
|
||||||
Jacobs, I. and N. Walsh, "Architecture of the World Wide
|
|
||||||
Web, Volume One", World Wide Web Consortium
|
|
||||||
Recommendation REC- webarch-20041215, December 2004,
|
|
||||||
<http:// www.w3.org/TR/2004/REC-webarch-20041215>.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Nottingham & Hammer-Lahav Standards Track [Page 6]
|
|
||||||
|
|
||||||
RFC 5785 Defining Well-Known URIs April 2010
|
|
||||||
|
|
||||||
|
|
||||||
Appendix A. Acknowledgements
|
|
||||||
|
|
||||||
We would like to acknowledge the contributions of everyone who
|
|
||||||
provided feedback and use cases for this document; in particular,
|
|
||||||
Phil Archer, Dirk Balfanz, Adam Barth, Tim Bray, Brian Eaton, Brad
|
|
||||||
Fitzpatrick, Joe Gregorio, Paul Hoffman, Barry Leiba, Ashok Malhotra,
|
|
||||||
Breno de Medeiros, John Panzer, and Drummond Reed. However, they are
|
|
||||||
not responsible for errors and omissions.
|
|
||||||
|
|
||||||
Appendix B. Frequently Asked Questions
|
|
||||||
|
|
||||||
1. Aren't well-known locations bad for the Web?
|
|
||||||
|
|
||||||
They are, but for various reasons -- both technical and social --
|
|
||||||
they are commonly used and their use is increasing. This memo
|
|
||||||
defines a "sandbox" for them, to reduce the risks of collision and
|
|
||||||
to minimise the impact upon pre-existing URIs on sites.
|
|
||||||
|
|
||||||
2. Why /.well-known?
|
|
||||||
|
|
||||||
It's short, descriptive, and according to search indices, not
|
|
||||||
widely used.
|
|
||||||
|
|
||||||
3. What impact does this have on existing mechanisms, such as P3P and
|
|
||||||
robots.txt?
|
|
||||||
|
|
||||||
None, until they choose to use this mechanism.
|
|
||||||
|
|
||||||
4. Why aren't per-directory well-known locations defined?
|
|
||||||
|
|
||||||
Allowing every URI path segment to have a well-known location
|
|
||||||
(e.g., "/images/.well-known/") would increase the risks of
|
|
||||||
colliding with a pre-existing URI on a site, and generally these
|
|
||||||
solutions are found not to scale well, because they're too
|
|
||||||
"chatty".
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Nottingham & Hammer-Lahav Standards Track [Page 7]
|
|
||||||
|
|
||||||
RFC 5785 Defining Well-Known URIs April 2010
|
|
||||||
|
|
||||||
|
|
||||||
Authors' Addresses
|
|
||||||
|
|
||||||
Mark Nottingham
|
|
||||||
|
|
||||||
EMail: mnot@mnot.net
|
|
||||||
URI: http://www.mnot.net/
|
|
||||||
|
|
||||||
|
|
||||||
Eran Hammer-Lahav
|
|
||||||
|
|
||||||
EMail: eran@hueniverse.com
|
|
||||||
URI: http://hueniverse.com/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Nottingham & Hammer-Lahav Standards Track [Page 8]
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,787 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Internet Engineering Task Force (IETF) C. Daboo
|
|
||||||
Request for Comments: 6764 Apple Inc.
|
|
||||||
Updates: 4791, 6352 February 2013
|
|
||||||
Category: Standards Track
|
|
||||||
ISSN: 2070-1721
|
|
||||||
|
|
||||||
|
|
||||||
Locating Services for Calendaring Extensions to
|
|
||||||
WebDAV (CalDAV) and vCard Extensions to WebDAV (CardDAV)
|
|
||||||
|
|
||||||
Abstract
|
|
||||||
|
|
||||||
This specification describes how DNS SRV records, DNS TXT records,
|
|
||||||
and well-known URIs can be used together or separately to locate
|
|
||||||
CalDAV (Calendaring Extensions to Web Distributed Authoring and
|
|
||||||
Versioning (WebDAV)) or CardDAV (vCard Extensions to WebDAV)
|
|
||||||
services.
|
|
||||||
|
|
||||||
Status of This Memo
|
|
||||||
|
|
||||||
This is an Internet Standards Track document.
|
|
||||||
|
|
||||||
This document is a product of the Internet Engineering Task Force
|
|
||||||
(IETF). It represents the consensus of the IETF community. It has
|
|
||||||
received public review and has been approved for publication by the
|
|
||||||
Internet Engineering Steering Group (IESG). Further information on
|
|
||||||
Internet Standards is available in Section 2 of RFC 5741.
|
|
||||||
|
|
||||||
Information about the current status of this document, any errata,
|
|
||||||
and how to provide feedback on it may be obtained at
|
|
||||||
http://www.rfc-editor.org/info/rfc6764.
|
|
||||||
|
|
||||||
Copyright Notice
|
|
||||||
|
|
||||||
Copyright (c) 2013 IETF Trust and the persons identified as the
|
|
||||||
document authors. All rights reserved.
|
|
||||||
|
|
||||||
This document is subject to BCP 78 and the IETF Trust's Legal
|
|
||||||
Provisions Relating to IETF Documents
|
|
||||||
(http://trustee.ietf.org/license-info) in effect on the date of
|
|
||||||
publication of this document. Please review these documents
|
|
||||||
carefully, as they describe your rights and restrictions with respect
|
|
||||||
to this document. Code Components extracted from this document must
|
|
||||||
include Simplified BSD License text as described in Section 4.e of
|
|
||||||
the Trust Legal Provisions and are provided without warranty as
|
|
||||||
described in the Simplified BSD License.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo Standards Track [Page 1]
|
|
||||||
|
|
||||||
RFC 6764 SRV for CalDAV & CardDAV February 2013
|
|
||||||
|
|
||||||
|
|
||||||
Table of Contents
|
|
||||||
|
|
||||||
1. Introduction ....................................................2
|
|
||||||
2. Conventions Used in This Document ...............................3
|
|
||||||
3. CalDAV SRV Service Labels .......................................3
|
|
||||||
4. CalDAV and CardDAV Service TXT Records ..........................4
|
|
||||||
5. CalDAV and CardDAV Service Well-Known URI .......................4
|
|
||||||
5.1. Example: Well-Known URI Redirects to Actual
|
|
||||||
"Context Path" .............................................5
|
|
||||||
6. Client "Bootstrapping" Procedures ...............................5
|
|
||||||
7. Guidance for Service Providers ..................................8
|
|
||||||
8. Security Considerations .........................................9
|
|
||||||
9. IANA Considerations .............................................9
|
|
||||||
9.1. Well-Known URI Registrations ...............................9
|
|
||||||
9.1.1. caldav Well-Known URI Registration .................10
|
|
||||||
9.1.2. carddav Well-Known URI Registration ................10
|
|
||||||
9.2. Service Name Registrations ................................10
|
|
||||||
9.2.1. caldav Service Name Registration ...................10
|
|
||||||
9.2.2. caldavs Service Name Registration ..................11
|
|
||||||
9.2.3. carddav Service Name Registration ..................11
|
|
||||||
9.2.4. carddavs Service Name Registration .................12
|
|
||||||
10. Acknowledgments ...............................................12
|
|
||||||
11. References ....................................................12
|
|
||||||
11.1. Normative References .....................................12
|
|
||||||
11.2. Informative References ...................................14
|
|
||||||
|
|
||||||
1. Introduction
|
|
||||||
|
|
||||||
[RFC4791] defines the CalDAV calendar access protocol, based on HTTP
|
|
||||||
[RFC2616], for accessing calendar data stored on a server. CalDAV
|
|
||||||
clients need to be able to discover appropriate CalDAV servers within
|
|
||||||
their local area network and at other domains, e.g., to minimize the
|
|
||||||
need for end users to know specific details such as the fully
|
|
||||||
qualified domain name (FQDN) and port number for their servers.
|
|
||||||
|
|
||||||
[RFC6352] defines the CardDAV address book access protocol based on
|
|
||||||
HTTP [RFC2616], for accessing contact data stored on a server. As
|
|
||||||
with CalDAV, clients also need to be able to discover CardDAV
|
|
||||||
servers.
|
|
||||||
|
|
||||||
[RFC2782] defines a DNS-based service discovery protocol that has
|
|
||||||
been widely adopted as a means of locating particular services within
|
|
||||||
a local area network and beyond, using DNS SRV Resource Records
|
|
||||||
(RRs). This has been enhanced to provide additional service meta-
|
|
||||||
data by use of DNS TXT RRs as per [RFC6763].
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo Standards Track [Page 2]
|
|
||||||
|
|
||||||
RFC 6764 SRV for CalDAV & CardDAV February 2013
|
|
||||||
|
|
||||||
|
|
||||||
This specification defines new SRV service types for the CalDAV
|
|
||||||
protocol and gives an example of how clients can use this together
|
|
||||||
with other protocol features to enable simple client configuration.
|
|
||||||
SRV service types for CardDAV are already defined in Section 11 of
|
|
||||||
[RFC6352].
|
|
||||||
|
|
||||||
Another issue with CalDAV or CardDAV service discovery is that the
|
|
||||||
service might not be located at the "root" URI of the HTTP server
|
|
||||||
hosting it. Thus, a client needs to be able to determine the
|
|
||||||
complete path component of the Request-URI to use in HTTP requests:
|
|
||||||
the "context path". For example, if CalDAV is implemented as a
|
|
||||||
"servlet" in a web server "container", the servlet "context path"
|
|
||||||
might be "/caldav/". So the URI for the CalDAV service would be,
|
|
||||||
e.g., "http://caldav.example.com/caldav/" rather than
|
|
||||||
"http://caldav.example.com/". SRV RRs by themselves only provide an
|
|
||||||
FQDN and port number for the service, not a path. Since the client
|
|
||||||
"bootstrapping" process requires initial access to the "context path"
|
|
||||||
of the service, there needs to be a simple way for clients to also
|
|
||||||
discover what that path is.
|
|
||||||
|
|
||||||
This specification makes use of the "well-known URI" feature
|
|
||||||
[RFC5785] of HTTP servers to provide a well-known URI for CalDAV or
|
|
||||||
CardDAV services that clients can use. The well-known URI will point
|
|
||||||
to a resource on the server that is simply a "stub" resource that
|
|
||||||
provides a redirect to the actual "context path" resource
|
|
||||||
representing the service endpoint.
|
|
||||||
|
|
||||||
2. Conventions Used in This Document
|
|
||||||
|
|
||||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
|
|
||||||
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
|
|
||||||
document are to be interpreted as described in [RFC2119].
|
|
||||||
|
|
||||||
3. CalDAV SRV Service Labels
|
|
||||||
|
|
||||||
This specification adds two SRV service labels for use with CalDAV:
|
|
||||||
|
|
||||||
_caldav: Identifies a CalDAV server that uses HTTP without
|
|
||||||
Transport Layer Security (TLS) [RFC2818].
|
|
||||||
|
|
||||||
_caldavs: Identifies a CalDAV server that uses HTTP with TLS
|
|
||||||
[RFC2818].
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo Standards Track [Page 3]
|
|
||||||
|
|
||||||
RFC 6764 SRV for CalDAV & CardDAV February 2013
|
|
||||||
|
|
||||||
|
|
||||||
Clients MUST honor Priority and Weight values in the SRV RRs, as
|
|
||||||
described by [RFC2782].
|
|
||||||
|
|
||||||
Example: service record for server without TLS
|
|
||||||
|
|
||||||
_caldav._tcp SRV 0 1 80 calendar.example.com.
|
|
||||||
|
|
||||||
Example: service record for server with TLS
|
|
||||||
|
|
||||||
_caldavs._tcp SRV 0 1 443 calendar.example.com.
|
|
||||||
|
|
||||||
4. CalDAV and CardDAV Service TXT Records
|
|
||||||
|
|
||||||
When SRV RRs are used to advertise CalDAV and CardDAV services, it is
|
|
||||||
also convenient to be able to specify a "context path" in the DNS to
|
|
||||||
be retrieved at the same time. To enable that, this specification
|
|
||||||
uses a TXT RR that follows the syntax defined in Section 6 of
|
|
||||||
[RFC6763] and defines a "path" key for use in that record. The value
|
|
||||||
of the key MUST be the actual "context path" to the corresponding
|
|
||||||
service on the server.
|
|
||||||
|
|
||||||
A site might provide TXT records in addition to SRV records for each
|
|
||||||
service. When present, clients MUST use the "path" value as the
|
|
||||||
"context path" for the service in HTTP requests. When not present,
|
|
||||||
clients use the ".well-known" URI approach described next.
|
|
||||||
|
|
||||||
Example: text record for service with TLS
|
|
||||||
|
|
||||||
_caldavs._tcp TXT path=/caldav
|
|
||||||
|
|
||||||
5. CalDAV and CardDAV Service Well-Known URI
|
|
||||||
|
|
||||||
Two ".well-known" URIs are registered by this specification for
|
|
||||||
CalDAV and CardDAV services, "caldav" and "carddav" respectively (see
|
|
||||||
Section 9). These URIs point to a resource that the client can use
|
|
||||||
as the initial "context path" for the service they are trying to
|
|
||||||
connect to. The server MUST redirect HTTP requests for that resource
|
|
||||||
to the actual "context path" using one of the available mechanisms
|
|
||||||
provided by HTTP (e.g., using a 301, 303, or 307 response). Clients
|
|
||||||
MUST handle HTTP redirects on the ".well-known" URI. Servers MUST
|
|
||||||
NOT locate the actual CalDAV or CardDAV service endpoint at the
|
|
||||||
".well-known" URI as per Section 1.1 of [RFC5785].
|
|
||||||
|
|
||||||
Servers SHOULD set an appropriate Cache-Control header value (as per
|
|
||||||
Section 14.9 of [RFC2616]) in the redirect response to ensure caching
|
|
||||||
occurs or does not occur as needed or as required by the type of
|
|
||||||
response generated. For example, if it is anticipated that the
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo Standards Track [Page 4]
|
|
||||||
|
|
||||||
RFC 6764 SRV for CalDAV & CardDAV February 2013
|
|
||||||
|
|
||||||
|
|
||||||
location of the redirect might change over time, then a "no-cache"
|
|
||||||
value would be used.
|
|
||||||
|
|
||||||
To facilitate "context paths" that might differ from user to user,
|
|
||||||
the server MAY require authentication when a client tries to access
|
|
||||||
the ".well-known" URI (i.e., the server would return a 401 status
|
|
||||||
response to the unauthenticated request from the client, then return
|
|
||||||
the redirect response only after a successful authentication by the
|
|
||||||
client).
|
|
||||||
|
|
||||||
5.1. Example: Well-Known URI Redirects to Actual "Context Path"
|
|
||||||
|
|
||||||
A CalDAV server has a "context path" that is "/servlet/caldav". The
|
|
||||||
client will use "/.well-known/caldav" as the path for its
|
|
||||||
"bootstrapping" process after it has first found the FQDN and port
|
|
||||||
number via an SRV lookup or via manual entry of information by the
|
|
||||||
user, from which the client can parse suitable information. When the
|
|
||||||
client makes an HTTP request against "/.well-known/caldav", the
|
|
||||||
server would issue an HTTP redirect response with a Location response
|
|
||||||
header using the path "/servlet/caldav". The client would then
|
|
||||||
"follow" this redirect to the new resource and continue making HTTP
|
|
||||||
requests there to complete its "bootstrapping" process.
|
|
||||||
|
|
||||||
6. Client "Bootstrapping" Procedures
|
|
||||||
|
|
||||||
This section describes a procedure that CalDAV or CardDAV clients
|
|
||||||
SHOULD use to do their initial configuration based on minimal user
|
|
||||||
input. The goal is to determine an http: or https: URI that
|
|
||||||
describes the full path to the user's principal-URL [RFC3744].
|
|
||||||
|
|
||||||
1. Processing user input:
|
|
||||||
|
|
||||||
* For a CalDAV server:
|
|
||||||
|
|
||||||
+ Minimal input from a user would consist of a calendar user
|
|
||||||
address and a password. A calendar user address is defined
|
|
||||||
by iCalendar [RFC5545] to be a URI [RFC3986]. Provided a
|
|
||||||
user identifier and a domain name can be extracted from the
|
|
||||||
URI, this simple "bootstrapping" configuration can be done.
|
|
||||||
|
|
||||||
+ If the calendar user address is a "mailto:" [RFC6068] URI,
|
|
||||||
the "mailbox" portion of the URI is examined, and the
|
|
||||||
"local-part" and "domain" portions are extracted.
|
|
||||||
|
|
||||||
+ If the calendar user address is an "http:" [RFC2616] or
|
|
||||||
"https:" [RFC2818] URI, the "userinfo" and "host" portion
|
|
||||||
of the URI [RFC3986] is extracted.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo Standards Track [Page 5]
|
|
||||||
|
|
||||||
RFC 6764 SRV for CalDAV & CardDAV February 2013
|
|
||||||
|
|
||||||
|
|
||||||
* For a CardDAV server:
|
|
||||||
|
|
||||||
+ Minimal input from a user would consist of their email
|
|
||||||
address [RFC5322] for the domain where the CardDAV service
|
|
||||||
is hosted, and a password. The "mailbox" portion of the
|
|
||||||
email address is examined, and the "local-part" and
|
|
||||||
"domain" portions are extracted.
|
|
||||||
|
|
||||||
2. Determination of service FQDN and port number:
|
|
||||||
|
|
||||||
* An SRV lookup for _caldavs._tcp (for CalDAV) or _carddavs._tcp
|
|
||||||
(for CardDAV) is done with the extracted "domain" as the
|
|
||||||
service domain.
|
|
||||||
|
|
||||||
* If no result is found, the client can try _caldav._tcp (for
|
|
||||||
CalDAV) or _carddav._tcp (for CardDAV) provided non-TLS
|
|
||||||
connections are appropriate.
|
|
||||||
|
|
||||||
* If an SRV record is returned, the client extracts the target
|
|
||||||
FQDN and port number. If multiple SRV records are returned,
|
|
||||||
the client MUST use the Priority and Weight fields in the
|
|
||||||
record to determine which one to pick (as per [RFC2782]).
|
|
||||||
|
|
||||||
* If an SRV record is not found, the client will need to prompt
|
|
||||||
the user to enter the FQDN and port number information
|
|
||||||
directly or use some other heuristic, for example, using the
|
|
||||||
extracted "domain" as the FQDN and default HTTPS or HTTP port
|
|
||||||
numbers. In this situation, clients MUST first attempt an
|
|
||||||
HTTP connection with TLS.
|
|
||||||
|
|
||||||
3. Determination of initial "context path":
|
|
||||||
|
|
||||||
* When an SRV lookup is done and a valid SRV record returned,
|
|
||||||
the client MUST also query for a corresponding TXT record and
|
|
||||||
check for the presence of a "path" key in its response. If
|
|
||||||
present, the value of the "path" key is used for the initial
|
|
||||||
"context path".
|
|
||||||
|
|
||||||
* When an initial "context path" has not been determined from a
|
|
||||||
TXT record, the initial "context path" is taken to be
|
|
||||||
"/.well-known/caldav" (for CalDAV) or "/.well-known/carddav"
|
|
||||||
(for CardDAV).
|
|
||||||
|
|
||||||
* If the initial "context path" derived from a TXT record
|
|
||||||
generates HTTP errors when targeted by requests, the client
|
|
||||||
SHOULD repeat its "bootstrapping" procedure using the
|
|
||||||
appropriate ".well-known" URI instead.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo Standards Track [Page 6]
|
|
||||||
|
|
||||||
RFC 6764 SRV for CalDAV & CardDAV February 2013
|
|
||||||
|
|
||||||
|
|
||||||
4. Determination of user identifier:
|
|
||||||
|
|
||||||
* The client will need to make authenticated HTTP requests to
|
|
||||||
the service. Typically, a "user identifier" is required for
|
|
||||||
some form of user/password authentication. When a user
|
|
||||||
identifier is required, clients MUST first use the "mailbox"
|
|
||||||
portion of the calendar user address provided by the user in
|
|
||||||
the case of a "mailto:" address and, if that results in an
|
|
||||||
authentication failure, SHOULD fall back to using the "local-
|
|
||||||
part" extracted from the "mailto:" address. For an "http:" or
|
|
||||||
"https:" calendar user address, the "userinfo" portion is used
|
|
||||||
as the user identifier for authentication. This is in line
|
|
||||||
with the guidance outlined in Section 7. If these user
|
|
||||||
identifiers result in authentication failure, the client
|
|
||||||
SHOULD prompt the user for a valid identifier.
|
|
||||||
|
|
||||||
5. Connecting to the service:
|
|
||||||
|
|
||||||
* Subsequent to configuration, the client will make HTTP
|
|
||||||
requests to the service. When using "_caldavs" or "_carddavs"
|
|
||||||
services, a TLS negotiation is done immediately upon
|
|
||||||
connection. The client MUST do certificate verification using
|
|
||||||
the procedure outlined in Section 6 of [RFC6125] in regard to
|
|
||||||
verification with an SRV RR as the starting point.
|
|
||||||
|
|
||||||
* The client does a "PROPFIND" [RFC4918] request with the
|
|
||||||
request URI set to the initial "context path". The body of
|
|
||||||
the request SHOULD include the DAV:current-user-principal
|
|
||||||
[RFC5397] property as one of the properties to return. Note
|
|
||||||
that clients MUST properly handle HTTP redirect responses for
|
|
||||||
the request. The server will use the HTTP authentication
|
|
||||||
procedure outlined in [RFC2617] or use some other appropriate
|
|
||||||
authentication schemes to authenticate the user.
|
|
||||||
|
|
||||||
* If the server returns a 404 ("Not Found") HTTP status response
|
|
||||||
to the request on the initial "context path", clients MAY try
|
|
||||||
repeating the request on the "root" URI "/" or prompt the user
|
|
||||||
for a suitable path.
|
|
||||||
|
|
||||||
* If the DAV:current-user-principal property is returned on the
|
|
||||||
request, the client uses that value for the principal-URL of
|
|
||||||
the authenticated user. With that, it can execute a
|
|
||||||
"PROPFIND" request on the principal-URL and discover
|
|
||||||
additional properties for configuration (e.g., calendar or
|
|
||||||
address book "home" collections).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo Standards Track [Page 7]
|
|
||||||
|
|
||||||
RFC 6764 SRV for CalDAV & CardDAV February 2013
|
|
||||||
|
|
||||||
|
|
||||||
* If the DAV:current-user-principal property is not returned,
|
|
||||||
then the client will need to request the principal-URL path
|
|
||||||
from the user in order to continue with configuration.
|
|
||||||
|
|
||||||
Once a successful account discovery step has been done, clients
|
|
||||||
SHOULD cache the service details that were successfully used (user
|
|
||||||
identity, principal-URL with full scheme/host/port details) and reuse
|
|
||||||
those when connecting again at a later time.
|
|
||||||
|
|
||||||
If a subsequent connection attempt fails, or authentication fails
|
|
||||||
persistently, clients SHOULD retry the SRV lookup and account
|
|
||||||
discovery to "refresh" the cached data.
|
|
||||||
|
|
||||||
7. Guidance for Service Providers
|
|
||||||
|
|
||||||
Service providers wanting to offer CalDAV or CardDAV services that
|
|
||||||
can be configured by clients using SRV records need to follow certain
|
|
||||||
procedures to ensure proper operation.
|
|
||||||
|
|
||||||
o CalDAV or CardDAV servers SHOULD be configured to allow
|
|
||||||
authentication with calendar user addresses (just taking the
|
|
||||||
"mailbox" portion of any "mailto:" URI) or email addresses
|
|
||||||
respectively, or with "user identifiers" extracted from them. In
|
|
||||||
the former case, the addresses MUST NOT conflict with other forms
|
|
||||||
of a permitted user login name. In the latter case, the extracted
|
|
||||||
"user identifiers" need to be unique across the server and MUST
|
|
||||||
NOT conflict with any login name on the server.
|
|
||||||
|
|
||||||
o Servers MUST force authentication for "PROPFIND" requests that
|
|
||||||
retrieve the DAV:current-user-principal property to ensure that
|
|
||||||
the value of the DAV:current-user-principal property returned
|
|
||||||
corresponds to the principal-URL of the user making the request.
|
|
||||||
|
|
||||||
o If the service provider uses TLS, the service provider MUST ensure
|
|
||||||
a certificate is installed that can be verified by clients using
|
|
||||||
the procedure outlined in Section 6 of [RFC6125] in regard to
|
|
||||||
verification with an SRV RR as the starting point. In particular,
|
|
||||||
certificates SHOULD include SRV-ID and DNS-ID identifiers as
|
|
||||||
appropriate, as described in Section 8.
|
|
||||||
|
|
||||||
o Service providers should install the appropriate SRV records for
|
|
||||||
the offered services and optionally include TXT records.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo Standards Track [Page 8]
|
|
||||||
|
|
||||||
RFC 6764 SRV for CalDAV & CardDAV February 2013
|
|
||||||
|
|
||||||
|
|
||||||
8. Security Considerations
|
|
||||||
|
|
||||||
Clients that support TLS as defined by [RFC2818] SHOULD try the
|
|
||||||
"_caldavs" or "_carddavs" services first before trying the "_caldav"
|
|
||||||
or "_carddav" services respectively. If a user has explicitly
|
|
||||||
requested a connection with TLS, the client MUST NOT use any service
|
|
||||||
information returned for the "_caldav" or "_carddav" services.
|
|
||||||
Clients MUST follow the certificate-verification process specified in
|
|
||||||
[RFC6125].
|
|
||||||
|
|
||||||
A malicious attacker with access to the DNS server data, or that is
|
|
||||||
able to get spoofed answers cached in a recursive resolver, can
|
|
||||||
potentially cause clients to connect to any server chosen by the
|
|
||||||
attacker. In the absence of a secure DNS option, clients SHOULD
|
|
||||||
check that the target FQDN returned in the SRV record matches the
|
|
||||||
original service domain that was queried. If the target FQDN is not
|
|
||||||
in the queried domain, clients SHOULD verify with the user that the
|
|
||||||
SRV target FQDN is suitable for use before executing any connections
|
|
||||||
to the host. Alternatively, if TLS is being used for the service,
|
|
||||||
clients MUST use the procedure outlined in Section 6 of [RFC6125] to
|
|
||||||
verify the service. When the target FQDN does not match the original
|
|
||||||
service domain that was queried, clients MUST check the SRV-ID
|
|
||||||
identifier in the server's certificate. If the FQDN does match,
|
|
||||||
clients MUST check any SRV-ID identifiers in the server's certificate
|
|
||||||
or, if no SRV-ID identifiers are present, MUST check the DNS-ID
|
|
||||||
identifiers in the server's certificate.
|
|
||||||
|
|
||||||
Implementations of TLS [RFC5246], used as the basis for TLS
|
|
||||||
([RFC2818]), typically support multiple versions of the protocol as
|
|
||||||
well as the older SSL (Secure Sockets Layer) protocol. Because of
|
|
||||||
known security vulnerabilities, clients and servers MUST NOT request,
|
|
||||||
offer, or use SSL 2.0. See Appendix E.2 of [RFC5246] for further
|
|
||||||
details.
|
|
||||||
|
|
||||||
9. IANA Considerations
|
|
||||||
|
|
||||||
9.1. Well-Known URI Registrations
|
|
||||||
|
|
||||||
This document defines two ".well-known" URIs using the registration
|
|
||||||
procedure and template from Section 5.1 of [RFC5785].
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo Standards Track [Page 9]
|
|
||||||
|
|
||||||
RFC 6764 SRV for CalDAV & CardDAV February 2013
|
|
||||||
|
|
||||||
|
|
||||||
9.1.1. caldav Well-Known URI Registration
|
|
||||||
|
|
||||||
URI suffix: caldav
|
|
||||||
|
|
||||||
Change controller: IETF
|
|
||||||
|
|
||||||
Specification document(s): This RFC
|
|
||||||
|
|
||||||
Related information: See also [RFC4791].
|
|
||||||
|
|
||||||
9.1.2. carddav Well-Known URI Registration
|
|
||||||
|
|
||||||
URI suffix: carddav
|
|
||||||
|
|
||||||
Change controller: IETF
|
|
||||||
|
|
||||||
Specification document(s): This RFC
|
|
||||||
|
|
||||||
Related information: See also [RFC6352].
|
|
||||||
|
|
||||||
9.2. Service Name Registrations
|
|
||||||
|
|
||||||
This document registers four new service names as per [RFC6335]. Two
|
|
||||||
are defined in this document, and two are defined in [RFC6352],
|
|
||||||
Section 11.
|
|
||||||
|
|
||||||
9.2.1. caldav Service Name Registration
|
|
||||||
|
|
||||||
Service Name: caldav
|
|
||||||
|
|
||||||
Transport Protocol(s): TCP
|
|
||||||
|
|
||||||
Assignee: IESG <iesg@ietf.org>
|
|
||||||
|
|
||||||
Contact: IETF Chair <chair@ietf.org>
|
|
||||||
|
|
||||||
Description: Calendaring Extensions to WebDAV (CalDAV) - non-TLS
|
|
||||||
|
|
||||||
Reference: [RFC6764]
|
|
||||||
|
|
||||||
Assignment Note: This is an extension of the http service. Defined
|
|
||||||
TXT keys: path=<context path>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo Standards Track [Page 10]
|
|
||||||
|
|
||||||
RFC 6764 SRV for CalDAV & CardDAV February 2013
|
|
||||||
|
|
||||||
|
|
||||||
9.2.2. caldavs Service Name Registration
|
|
||||||
|
|
||||||
Service Name: caldavs
|
|
||||||
|
|
||||||
Transport Protocol(s): TCP
|
|
||||||
|
|
||||||
Assignee: IESG <iesg@ietf.org>
|
|
||||||
|
|
||||||
Contact: IETF Chair <chair@ietf.org>
|
|
||||||
|
|
||||||
Description: Calendaring Extensions to WebDAV (CalDAV) - over TLS
|
|
||||||
|
|
||||||
Reference: [RFC6764]
|
|
||||||
|
|
||||||
Assignment Note: This is an extension of the https service. Defined
|
|
||||||
TXT keys: path=<context path>
|
|
||||||
|
|
||||||
9.2.3. carddav Service Name Registration
|
|
||||||
|
|
||||||
Service Name: carddav
|
|
||||||
|
|
||||||
Transport Protocol(s): TCP
|
|
||||||
|
|
||||||
Assignee: IESG <iesg@ietf.org>
|
|
||||||
|
|
||||||
Contact: IETF Chair <chair@ietf.org>
|
|
||||||
|
|
||||||
Description: vCard Extensions to WebDAV (CardDAV) - non-TLS
|
|
||||||
|
|
||||||
Reference: [RFC6352]
|
|
||||||
|
|
||||||
Assignment Note: This is an extension of the http service. Defined
|
|
||||||
TXT keys: path=<context path>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo Standards Track [Page 11]
|
|
||||||
|
|
||||||
RFC 6764 SRV for CalDAV & CardDAV February 2013
|
|
||||||
|
|
||||||
|
|
||||||
9.2.4. carddavs Service Name Registration
|
|
||||||
|
|
||||||
Service Name: carddavs
|
|
||||||
|
|
||||||
Transport Protocol(s): TCP
|
|
||||||
|
|
||||||
Assignee: IESG <iesg@ietf.org>
|
|
||||||
|
|
||||||
Contact: IETF Chair <chair@ietf.org>
|
|
||||||
|
|
||||||
Description: vCard Extensions to WebDAV (CardDAV) - over TLS
|
|
||||||
|
|
||||||
Reference: [RFC6352]
|
|
||||||
|
|
||||||
Assignment Note: This is an extension of the https service. Defined
|
|
||||||
TXT keys: path=<context path>
|
|
||||||
|
|
||||||
10. Acknowledgments
|
|
||||||
|
|
||||||
This specification was suggested by discussion that took place within
|
|
||||||
the Calendaring and Scheduling Consortium's CalDAV Technical
|
|
||||||
Committee. The author thanks the following for their contributions:
|
|
||||||
Stuart Cheshire, Bernard Desruisseaux, Eran Hammer-Lahav, Helge Hess,
|
|
||||||
Arnaud Quillaud, Wilfredo Sanchez, and Joe Touch.
|
|
||||||
|
|
||||||
11. References
|
|
||||||
|
|
||||||
11.1. Normative References
|
|
||||||
|
|
||||||
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
|
|
||||||
Requirement Levels", BCP 14, RFC 2119, March 1997.
|
|
||||||
|
|
||||||
[RFC2616] Fielding, R., Gettys, J., Mogul, J., Frystyk, H.,
|
|
||||||
Masinter, L., Leach, P., and T. Berners-Lee, "Hypertext
|
|
||||||
Transfer Protocol -- HTTP/1.1", RFC 2616, June 1999.
|
|
||||||
|
|
||||||
[RFC2617] Franks, J., Hallam-Baker, P., Hostetler, J., Lawrence, S.,
|
|
||||||
Leach, P., Luotonen, A., and L. Stewart, "HTTP
|
|
||||||
Authentication: Basic and Digest Access Authentication",
|
|
||||||
RFC 2617, June 1999.
|
|
||||||
|
|
||||||
[RFC2782] Gulbrandsen, A., Vixie, P., and L. Esibov, "A DNS RR for
|
|
||||||
specifying the location of services (DNS SRV)", RFC 2782,
|
|
||||||
February 2000.
|
|
||||||
|
|
||||||
[RFC2818] Rescorla, E., "HTTP Over TLS", RFC 2818, May 2000.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo Standards Track [Page 12]
|
|
||||||
|
|
||||||
RFC 6764 SRV for CalDAV & CardDAV February 2013
|
|
||||||
|
|
||||||
|
|
||||||
[RFC3744] Clemm, G., Reschke, J., Sedlar, E., and J. Whitehead, "Web
|
|
||||||
Distributed Authoring and Versioning (WebDAV)
|
|
||||||
Access Control Protocol", RFC 3744, May 2004.
|
|
||||||
|
|
||||||
[RFC3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform
|
|
||||||
Resource Identifier (URI): Generic Syntax", STD 66,
|
|
||||||
RFC 3986, January 2005.
|
|
||||||
|
|
||||||
[RFC4791] Daboo, C., Desruisseaux, B., and L. Dusseault,
|
|
||||||
"Calendaring Extensions to WebDAV (CalDAV)", RFC 4791,
|
|
||||||
March 2007.
|
|
||||||
|
|
||||||
[RFC4918] Dusseault, L., "HTTP Extensions for Web Distributed
|
|
||||||
Authoring and Versioning (WebDAV)", RFC 4918, June 2007.
|
|
||||||
|
|
||||||
[RFC5246] Dierks, T. and E. Rescorla, "The Transport Layer Security
|
|
||||||
(TLS) Protocol Version 1.2", RFC 5246, August 2008.
|
|
||||||
|
|
||||||
[RFC5322] Resnick, P., Ed., "Internet Message Format", RFC 5322,
|
|
||||||
October 2008.
|
|
||||||
|
|
||||||
[RFC5397] Sanchez, W. and C. Daboo, "WebDAV Current Principal
|
|
||||||
Extension", RFC 5397, December 2008.
|
|
||||||
|
|
||||||
[RFC5785] Nottingham, M. and E. Hammer-Lahav, "Defining Well-Known
|
|
||||||
Uniform Resource Identifiers (URIs)", RFC 5785,
|
|
||||||
April 2010.
|
|
||||||
|
|
||||||
[RFC6068] Duerst, M., Masinter, L., and J. Zawinski, "The 'mailto'
|
|
||||||
URI Scheme", RFC 6068, October 2010.
|
|
||||||
|
|
||||||
[RFC6125] Saint-Andre, P. and J. Hodges, "Representation and
|
|
||||||
Verification of Domain-Based Application Service Identity
|
|
||||||
within Internet Public Key Infrastructure Using X.509
|
|
||||||
(PKIX) Certificates in the Context of Transport Layer
|
|
||||||
Security (TLS)", RFC 6125, March 2011.
|
|
||||||
|
|
||||||
[RFC6335] Cotton, M., Eggert, L., Touch, J., Westerlund, M., and S.
|
|
||||||
Cheshire, "Internet Assigned Numbers Authority (IANA)
|
|
||||||
Procedures for the Management of the Service Name and
|
|
||||||
Transport Protocol Port Number Registry", BCP 165,
|
|
||||||
RFC 6335, August 2011.
|
|
||||||
|
|
||||||
[RFC6352] Daboo, C., "CardDAV: vCard Extensions to Web Distributed
|
|
||||||
Authoring and Versioning (WebDAV)", RFC 6352, August 2011.
|
|
||||||
|
|
||||||
[RFC6763] Cheshire, S. and M. Krochmal, "DNS-Based Service
|
|
||||||
Discovery", RFC 6763, February 2013.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo Standards Track [Page 13]
|
|
||||||
|
|
||||||
RFC 6764 SRV for CalDAV & CardDAV February 2013
|
|
||||||
|
|
||||||
|
|
||||||
11.2. Informative References
|
|
||||||
|
|
||||||
[RFC5545] Desruisseaux, B., "Internet Calendaring and Scheduling
|
|
||||||
Core Object Specification (iCalendar)", RFC 5545,
|
|
||||||
September 2009.
|
|
||||||
|
|
||||||
Author's Address
|
|
||||||
|
|
||||||
Cyrus Daboo
|
|
||||||
Apple Inc.
|
|
||||||
1 Infinite Loop
|
|
||||||
Cupertino, CA 95014
|
|
||||||
USA
|
|
||||||
|
|
||||||
EMail: cyrus@daboo.name
|
|
||||||
URI: http://www.apple.com/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Daboo Standards Track [Page 14]
|
|
||||||
|
|
@ -1 +1 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx2048M
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-3.2.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 70d40e726d6104e90aa0077db8fd5678328b0745
|
Subproject commit 9e6bc81c1a4f953915f03bf02b2c173a58421c17
|
@ -7,7 +7,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
include ':app'
|
include ':app'
|
||||||
include ':dav4android'
|
|
||||||
include ':ical4android'
|
include ':ical4android'
|
||||||
include ':vcard4android'
|
include ':vcard4android'
|
||||||
include ':cert4android'
|
include ':cert4android'
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 628ef73fa72bde2d8e4a3326b9683b58bbe11c8b
|
Subproject commit 3250dc78fac3be16a14a9881a9526ac6ce0eaf58
|
Loading…
Reference in New Issue
Block a user