Kotlin: more kotlin migration.

tmp
Tom Hacohen 5 years ago
parent 959bc4992b
commit 943611a511

@ -171,7 +171,7 @@ object Crypto {
return cipher
}
internal fun decrypt(_data: ByteArray): ByteArray? {
fun decrypt(_data: ByteArray): ByteArray? {
val iv = Arrays.copyOfRange(_data, 0, blockSize)
val data = Arrays.copyOfRange(_data, blockSize, _data.size)
@ -194,7 +194,7 @@ object Crypto {
return out
}
internal fun encrypt(data: ByteArray): ByteArray? {
fun encrypt(data: ByteArray): ByteArray? {
val iv = ByteArray(blockSize)
random.nextBytes(iv)
@ -214,7 +214,7 @@ object Crypto {
return buf
}
internal fun hmac(data: ByteArray): ByteArray {
fun hmac(data: ByteArray): ByteArray {
return if (version.toInt() == 1) {
hmac256(hmacKey, data)
} else {

@ -75,7 +75,7 @@ class JournalEntryManager(httpClient: OkHttpClient, remote: HttpUrl, val uid: St
class Entry : BaseManager.Base() {
fun update(crypto: Crypto.CryptoManager, content: String, previous: Entry) {
fun update(crypto: Crypto.CryptoManager, content: String, previous: Entry?) {
setContent(crypto, content)
uid = calculateHmac(crypto, previous)
}

@ -92,7 +92,7 @@ class UserInfoManager(httpClient: OkHttpClient, remote: HttpUrl) : BaseManager()
return crypto.decrypt(content)
}
internal fun setContent(crypto: Crypto.CryptoManager, rawContent: ByteArray) {
fun setContent(crypto: Crypto.CryptoManager, rawContent: ByteArray) {
val content = crypto.encrypt(rawContent)
this.content = Arrays.concatenate(calculateHmac(crypto, content), content)
}

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

@ -0,0 +1,77 @@
/*
* 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 com.etesync.syncadapter.syncadapter
import android.accounts.AbstractAccountAuthenticator
import android.accounts.Account
import android.accounts.AccountAuthenticatorResponse
import android.accounts.AccountManager
import android.accounts.NetworkErrorException
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import com.etesync.syncadapter.ui.setup.LoginActivity
class AccountAuthenticatorService : Service() {
private var accountAuthenticator: AccountAuthenticator? = null
override fun onCreate() {
accountAuthenticator = AccountAuthenticator(this)
}
override fun onBind(intent: Intent): IBinder? {
return if (intent.action == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT) accountAuthenticator!!.iBinder else null
}
private class AccountAuthenticator(internal val context: Context) : AbstractAccountAuthenticator(context) {
@Throws(NetworkErrorException::class)
override fun addAccount(response: AccountAuthenticatorResponse, accountType: String, authTokenType: String,
requiredFeatures: Array<String>, options: Bundle): Bundle {
val intent = Intent(context, LoginActivity::class.java)
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
val bundle = Bundle()
bundle.putParcelable(AccountManager.KEY_INTENT, intent)
return bundle
}
@Throws(NetworkErrorException::class)
override fun confirmCredentials(response: AccountAuthenticatorResponse, account: Account, options: Bundle): Bundle? {
return null
}
override fun editProperties(response: AccountAuthenticatorResponse, accountType: String): Bundle? {
return null
}
@Throws(NetworkErrorException::class)
override fun getAuthToken(response: AccountAuthenticatorResponse, account: Account, authTokenType: String, options: Bundle): Bundle? {
return null
}
override fun getAuthTokenLabel(authTokenType: String): String? {
return null
}
@Throws(NetworkErrorException::class)
override fun hasFeatures(response: AccountAuthenticatorResponse, account: Account, features: Array<String>): Bundle? {
return null
}
@Throws(NetworkErrorException::class)
override fun updateCredentials(response: AccountAuthenticatorResponse, account: Account, authTokenType: String, options: Bundle): Bundle? {
return null
}
}
}

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

@ -0,0 +1,42 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package com.etesync.syncadapter.syncadapter
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
class AddressBookProvider : ContentProvider() {
override fun onCreate(): Boolean {
return false
}
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
return null
}
override fun getType(uri: Uri): String? {
return null
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
return 0
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
return 0
}
}

@ -1,162 +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 com.etesync.syncadapter.syncadapter;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.database.sqlite.SQLiteException;
import android.os.Bundle;
import android.provider.ContactsContract;
import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.Constants;
import com.etesync.syncadapter.NotificationHelper;
import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Exceptions;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.JournalModel;
import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.resource.LocalAddressBook;
import com.etesync.syncadapter.ui.DebugInfoActivity;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import at.bitfire.vcard4android.ContactsStorageException;
import io.requery.Persistable;
import io.requery.sql.EntityDataStore;
import static com.etesync.syncadapter.Constants.KEY_ACCOUNT;
public class AddressBooksSyncAdapterService extends SyncAdapterService {
@Override
protected AbstractThreadedSyncAdapter syncAdapter() {
return new AddressBooksSyncAdapter(this);
}
private static class AddressBooksSyncAdapter extends SyncAdapter {
public AddressBooksSyncAdapter(Context context) {
super(context);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
super.onPerformSync(account, extras, authority, provider, syncResult);
NotificationHelper notificationManager = new NotificationHelper(getContext(), "journals-contacts", Constants.NOTIFICATION_CONTACTS_SYNC);
notificationManager.cancel();
try {
ContentProviderClient contactsProvider = getContext().getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY);
if (contactsProvider == null) {
App.log.severe("Couldn't access contacts provider");
syncResult.databaseError = true;
return;
}
AccountSettings settings = new AccountSettings(getContext(), account);
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
return;
new RefreshCollections(account, CollectionInfo.Type.ADDRESS_BOOK).run();
updateLocalAddressBooks(contactsProvider, account);
contactsProvider.release();
AccountManager accountManager = AccountManager.get(getContext());
for (Account addressBookAccount : accountManager.getAccountsByType(App.getAddressBookAccountType())) {
App.log.log(Level.INFO, "Running sync for address book", addressBookAccount);
Bundle syncExtras = new Bundle(extras);
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true);
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true);
ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras);
}
} catch (Exceptions.ServiceUnavailableException e) {
syncResult.stats.numIoExceptions++;
syncResult.delayUntil = (e.getRetryAfter() > 0) ? e.getRetryAfter() : Constants.DEFAULT_RETRY_DELAY;
} catch (Exception | OutOfMemoryError e) {
if (e instanceof ContactsStorageException || e instanceof SQLiteException) {
App.log.log(Level.SEVERE, "Couldn't prepare local address books", e);
syncResult.databaseError = true;
}
int syncPhase = R.string.sync_phase_journals;
String title = getContext().getString(R.string.sync_error_contacts, account.name);
notificationManager.setThrowable(e);
final Intent detailsIntent = notificationManager.getDetailsIntent();
detailsIntent.putExtra(KEY_ACCOUNT, account);
if (!(e instanceof Exceptions.UnauthorizedException)) {
detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority);
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase);
}
notificationManager.notify(title, getContext().getString(syncPhase));
}
App.log.info("Address book sync complete");
}
private void updateLocalAddressBooks(ContentProviderClient provider, Account account) throws ContactsStorageException, AuthenticatorException, OperationCanceledException, IOException {
final Context context = getContext();
EntityDataStore<Persistable> data = ((App) getContext().getApplicationContext()).getData();
ServiceEntity service = JournalModel.Service.fetch(data, account.name, CollectionInfo.Type.ADDRESS_BOOK);
Map<String, JournalEntity> remote = new HashMap<>();
List<JournalEntity> remoteJournals = JournalEntity.getJournals(data, service);
for (JournalEntity journalEntity : remoteJournals) {
remote.put(journalEntity.getUid(), journalEntity);
}
LocalAddressBook[] local = LocalAddressBook.find(context, provider, account);
// delete obsolete local address books
for (LocalAddressBook addressBook : local) {
String url = addressBook.getURL();
if (!remote.containsKey(url)) {
App.log.fine("Deleting obsolete local address book " + url);
addressBook.delete();
} else {
// remote CollectionInfo found for this local collection, update data
JournalEntity journalEntity = remote.get(url);
App.log.fine("Updating local address book " + url + " with " + journalEntity);
addressBook.update(journalEntity);
// we already have a local collection for this remote collection, don't take into consideration anymore
remote.remove(url);
}
}
// create new local address books
for (String url : remote.keySet()) {
JournalEntity journalEntity = remote.get(url);
App.log.info("Adding local address book " + journalEntity);
LocalAddressBook.create(context, provider, account, journalEntity);
}
}
}
}

@ -0,0 +1,166 @@
/*
* 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 com.etesync.syncadapter.syncadapter
import android.accounts.Account
import android.accounts.AccountManager
import android.accounts.AuthenticatorException
import android.accounts.OperationCanceledException
import android.content.AbstractThreadedSyncAdapter
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.SyncResult
import android.database.sqlite.SQLiteException
import android.os.Bundle
import android.provider.ContactsContract
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.App
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.NotificationHelper
import com.etesync.syncadapter.R
import com.etesync.syncadapter.journalmanager.Exceptions
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity
import com.etesync.syncadapter.model.JournalModel
import com.etesync.syncadapter.model.ServiceEntity
import com.etesync.syncadapter.resource.LocalAddressBook
import com.etesync.syncadapter.ui.DebugInfoActivity
import java.io.IOException
import java.util.HashMap
import java.util.logging.Level
import at.bitfire.vcard4android.ContactsStorageException
import io.requery.Persistable
import io.requery.sql.EntityDataStore
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
class AddressBooksSyncAdapterService : SyncAdapterService() {
override fun syncAdapter(): AbstractThreadedSyncAdapter {
return AddressBooksSyncAdapter(this)
}
private class AddressBooksSyncAdapter(context: Context) : SyncAdapterService.SyncAdapter(context) {
override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
super.onPerformSync(account, extras, authority, provider, syncResult)
val notificationManager = NotificationHelper(context, "journals-contacts", Constants.NOTIFICATION_CONTACTS_SYNC)
notificationManager.cancel()
try {
val contactsProvider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
if (contactsProvider == null) {
App.log.severe("Couldn't access contacts provider")
syncResult.databaseError = true
return
}
val settings = AccountSettings(context, account)
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
return
RefreshCollections(account, CollectionInfo.Type.ADDRESS_BOOK).run()
updateLocalAddressBooks(contactsProvider, account)
contactsProvider.release()
val accountManager = AccountManager.get(context)
for (addressBookAccount in accountManager.getAccountsByType(App.getAddressBookAccountType())) {
App.log.log(Level.INFO, "Running sync for address book", addressBookAccount)
val syncExtras = Bundle(extras)
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true)
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true)
ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras)
}
} catch (e: Exceptions.ServiceUnavailableException) {
syncResult.stats.numIoExceptions++
syncResult.delayUntil = if (e.retryAfter > 0) e.retryAfter else Constants.DEFAULT_RETRY_DELAY
} catch (e: Exception) {
if (e is ContactsStorageException || e is SQLiteException) {
App.log.log(Level.SEVERE, "Couldn't prepare local address books", e)
syncResult.databaseError = true
}
val syncPhase = R.string.sync_phase_journals
val title = context.getString(R.string.sync_error_contacts, account.name)
notificationManager.setThrowable(e)
val detailsIntent = notificationManager.detailsIntent
detailsIntent.putExtra(KEY_ACCOUNT, account)
if (e !is Exceptions.UnauthorizedException) {
detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority)
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase)
}
notificationManager.notify(title, context.getString(syncPhase))
} catch (e: OutOfMemoryError) {
if (e is ContactsStorageException || e is SQLiteException) {
App.log.log(Level.SEVERE, "Couldn't prepare local address books", e)
syncResult.databaseError = true
}
val syncPhase = R.string.sync_phase_journals
val title = context.getString(R.string.sync_error_contacts, account.name)
notificationManager.setThrowable(e)
val detailsIntent = notificationManager.detailsIntent
detailsIntent.putExtra(KEY_ACCOUNT, account)
notificationManager.notify(title, context.getString(syncPhase))
}
App.log.info("Address book sync complete")
}
@Throws(ContactsStorageException::class, AuthenticatorException::class, OperationCanceledException::class, IOException::class)
private fun updateLocalAddressBooks(provider: ContentProviderClient, account: Account) {
val context = context
val data = (getContext().applicationContext as App).data
val service = JournalModel.Service.fetch(data, account.name, CollectionInfo.Type.ADDRESS_BOOK)
val remote = HashMap<String, JournalEntity>()
val remoteJournals = JournalEntity.getJournals(data, service)
for (journalEntity in remoteJournals) {
remote[journalEntity.uid] = journalEntity
}
val local = LocalAddressBook.find(context, provider, account)
// delete obsolete local address books
for (addressBook in local) {
val url = addressBook.url
val journalEntity = remote[url]
if (journalEntity == null) {
App.log.fine("Deleting obsolete local address book $url")
addressBook.delete()
} else {
// remote CollectionInfo found for this local collection, update data
App.log.fine("Updating local address book $url with $journalEntity")
addressBook.update(journalEntity)
// we already have a local collection for this remote collection, don't take into consideration anymore
remote.remove(url)
}
}
// create new local address books
for (url in remote.keys) {
val journalEntity = remote[url]!!
App.log.info("Adding local address book $journalEntity")
LocalAddressBook.create(context, provider, account, journalEntity)
}
}
}
}

@ -1,270 +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 com.etesync.syncadapter.syncadapter;
import android.accounts.Account;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.Constants;
import com.etesync.syncadapter.NotificationHelper;
import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Exceptions;
import com.etesync.syncadapter.journalmanager.JournalEntryManager;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.SyncEntry;
import com.etesync.syncadapter.resource.LocalCalendar;
import com.etesync.syncadapter.resource.LocalEvent;
import com.etesync.syncadapter.resource.LocalResource;
import net.fortuna.ical4j.model.property.Attendee;
import org.acra.attachment.AcraContentProvider;
import org.acra.util.IOUtils;
import org.apache.commons.codec.Charsets;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.Event;
import at.bitfire.ical4android.InvalidCalendarException;
import at.bitfire.vcard4android.ContactsStorageException;
import okhttp3.HttpUrl;
/**
* <p>Synchronization manager for CardDAV collections; handles contacts and groups.</p>
*/
public class CalendarSyncManager extends SyncManager {
final private HttpUrl remote;
public CalendarSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult result, LocalCalendar calendar, HttpUrl remote) throws Exceptions.IntegrityException, Exceptions.GenericCryptoException {
super(context, account, settings, extras, authority, result, calendar.getName(), CollectionInfo.Type.CALENDAR, account.name);
localCollection = calendar;
this.remote = remote;
}
@Override
protected int notificationId() {
return Constants.NOTIFICATION_CALENDAR_SYNC;
}
@Override
protected String getSyncErrorTitle() {
return context.getString(R.string.sync_error_calendar, account.name);
}
@Override
protected String getSyncSuccessfullyTitle() {
return context.getString(R.string.sync_successfully_calendar, info.displayName,
account.name);
}
@Override
protected boolean prepare() throws ContactsStorageException, CalendarStorageException {
if (!super.prepare())
return false;
journal = new JournalEntryManager(httpClient, remote, localCalendar().getName());
return true;
}
@Override
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
super.prepareDirty();
localCalendar().processDirtyExceptions();
}
// helpers
private LocalCalendar localCalendar() {
return (LocalCalendar) localCollection;
}
protected void processSyncEntry(SyncEntry cEntry) throws IOException, ContactsStorageException, CalendarStorageException, InvalidCalendarException {
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];
LocalEvent local = (LocalEvent) localCollection.getByUid(event.uid);
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
processEvent(event, local);
} else {
if (local != null) {
App.log.info("Removing local record #" + local.getId() + " which has been deleted on the server");
local.delete();
} else {
App.log.warning("Tried deleting a non-existent record: " + event.uid);
}
}
}
protected void createLocalEntries() throws CalendarStorageException, ContactsStorageException, IOException {
super.createLocalEntries();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
createInviteAttendeesNotification();
}
}
private void createInviteAttendeesNotification() throws CalendarStorageException, ContactsStorageException, IOException {
for (LocalResource local : localDirty) {
Event event = ((LocalEvent) local).getEvent();
if (event.attendees.isEmpty()) {
return;
}
createInviteAttendeesNotification(event, local.getContent());
}
}
private void createInviteAttendeesNotification(Event event, String icsContent) {
NotificationHelper notificationHelper = new NotificationHelper(context, event.uid, event.uid.hashCode());
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_EMAIL, getEmailAddresses(event.attendees ,false));
final DateFormat dateFormatDate =
new SimpleDateFormat("EEEE, MMM dd", Locale.US);
intent.putExtra(Intent.EXTRA_SUBJECT,
context.getString(R.string.sync_calendar_attendees_email_subject,
event.summary,
dateFormatDate.format(event.dtStart.getDate())));
intent.putExtra(Intent.EXTRA_TEXT,
context.getString(R.string.sync_calendar_attendees_email_content,
event.summary,
formatEventDates(event),
(event.location != null) ? event.location : "",
formatAttendees(event.attendees)));
Uri uri = createAttachmentFromString(context, event.uid, icsContent);
if (uri == null) {
App.log.severe("Unable to create attachment from calendar event");
return;
}
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra(Intent.EXTRA_STREAM, uri);
notificationHelper.notify(
context.getString(
R.string.sync_calendar_attendees_notification_title, event.summary),
context.getString(R.string.sync_calendar_attendees_notification_content),
null,
intent,
R.drawable.ic_email_black);
}
private LocalResource processEvent(final Event newData, LocalEvent localEvent) throws IOException, ContactsStorageException, CalendarStorageException {
// delete local event, if it exists
if (localEvent != null) {
App.log.info("Updating " + newData.uid + " in local calendar");
localEvent.setETag(newData.uid);
localEvent.update(newData);
syncResult.stats.numUpdates++;
} else {
App.log.info("Adding " + newData.uid + " to local calendar");
localEvent = new LocalEvent(localCalendar(), newData, newData.uid, newData.uid);
localEvent.add();
syncResult.stats.numInserts++;
}
return localEvent;
}
private String[] getEmailAddresses(List<Attendee> attendees,
boolean shouldIncludeAccount) {
List<String> attendeesEmails = new ArrayList<>(attendees.size());
for (Attendee attendee : attendees) {
String attendeeEmail = attendee.getValue().replace("mailto:", "");
if (!shouldIncludeAccount && attendeeEmail.equals(account.name)) {
continue;
}
attendeesEmails.add(attendeeEmail);
}
return attendeesEmails.toArray(new String[0]);
}
private String formatAttendees(List<Attendee> attendeesList) {
StringBuilder stringBuilder = new StringBuilder();
String[] attendees = getEmailAddresses(attendeesList, true);
for (String attendee : attendees) {
stringBuilder.append("\n ").append(attendee);
}
return stringBuilder.toString();
}
private static String formatEventDates(Event event) {
final Locale locale = Locale.getDefault();
final TimeZone timezone = (event.dtStart.getTimeZone() != null) ? event.dtStart.getTimeZone() : TimeZone.getTimeZone("UTC");
final String dateFormatString =
event.isAllDay() ? "EEEE, MMM dd" : "EEEE, MMM dd @ hh:mm a";
final DateFormat longDateFormat =
new SimpleDateFormat(dateFormatString, locale);
longDateFormat.setTimeZone(timezone);
final DateFormat shortDateFormat =
new SimpleDateFormat("hh:mm a", locale);
shortDateFormat.setTimeZone(timezone);
Date startDate = event.dtStart.getDate();
Date endDate = event.getEndDate(true).getDate();
final String tzName = timezone.getDisplayName(timezone.inDaylightTime(startDate), TimeZone.SHORT);
Calendar cal1 = Calendar.getInstance();
Calendar cal2 = Calendar.getInstance();
cal1.setTime(startDate);
cal2.setTime(endDate);
boolean sameDay = cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
if (sameDay && event.isAllDay()) {
return longDateFormat.format(startDate);
}
return sameDay ?
String.format("%s - %s (%s)",
longDateFormat.format(startDate),
shortDateFormat.format(endDate),
tzName) :
String.format("%s - %s (%s)", longDateFormat.format(startDate), longDateFormat.format(endDate), tzName);
}
private Uri createAttachmentFromString(Context context, String name, String content) {
final File parentDir = new File (context.getCacheDir(), name);
parentDir.mkdirs();
final File cache = new File(parentDir, "invite.ics");
try {
IOUtils.writeStringToFile(cache, content);
return AcraContentProvider.getUriForFile(context, cache);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}

@ -0,0 +1,264 @@
/*
* 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 com.etesync.syncadapter.syncadapter
import android.accounts.Account
import android.content.Context
import android.content.Intent
import android.content.SyncResult
import android.net.Uri
import android.os.Build
import android.os.Bundle
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.App
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.NotificationHelper
import com.etesync.syncadapter.R
import com.etesync.syncadapter.journalmanager.Exceptions
import com.etesync.syncadapter.journalmanager.JournalEntryManager
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.SyncEntry
import com.etesync.syncadapter.resource.LocalCalendar
import com.etesync.syncadapter.resource.LocalEvent
import com.etesync.syncadapter.resource.LocalResource
import net.fortuna.ical4j.model.property.Attendee
import org.acra.attachment.AcraContentProvider
import org.acra.util.IOUtils
import org.apache.commons.codec.Charsets
import java.io.ByteArrayInputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.ArrayList
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.InvalidCalendarException
import at.bitfire.vcard4android.ContactsStorageException
import okhttp3.HttpUrl
/**
*
* Synchronization manager for CardDAV collections; handles contacts and groups.
*/
class CalendarSyncManager @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class)
constructor(context: Context, account: Account, settings: AccountSettings, extras: Bundle, authority: String, result: SyncResult, calendar: LocalCalendar, private val remote: HttpUrl) : SyncManager(context, account, settings, extras, authority, result, calendar.name, CollectionInfo.Type.CALENDAR, account.name) {
protected override val syncErrorTitle: String
get() = context.getString(R.string.sync_error_calendar, account.name)
protected override val syncSuccessfullyTitle: String
get() = context.getString(R.string.sync_successfully_calendar, info.displayName,
account.name)
init {
localCollection = calendar
}
override fun notificationId(): Int {
return Constants.NOTIFICATION_CALENDAR_SYNC
}
@Throws(ContactsStorageException::class, CalendarStorageException::class)
override fun prepare(): Boolean {
if (!super.prepare())
return false
journal = JournalEntryManager(httpClient, remote, localCalendar().name)
return true
}
@Throws(CalendarStorageException::class, ContactsStorageException::class)
override fun prepareDirty() {
super.prepareDirty()
localCalendar().processDirtyExceptions()
}
// helpers
private fun localCalendar(): LocalCalendar {
return localCollection as LocalCalendar
}
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, InvalidCalendarException::class)
override fun processSyncEntry(cEntry: SyncEntry) {
val `is` = ByteArrayInputStream(cEntry.content.toByteArray(Charsets.UTF_8))
val events = Event.fromStream(`is`, Charsets.UTF_8)
if (events.size == 0) {
App.log.warning("Received VCard without data, ignoring")
return
} else if (events.size > 1) {
App.log.warning("Received multiple VCALs, using first one")
}
val event = events[0]
val local = localCollection!!.getByUid(event.uid) as LocalEvent
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
processEvent(event, local)
} else {
if (local != null) {
App.log.info("Removing local record #" + local.id + " which has been deleted on the server")
local.delete()
} else {
App.log.warning("Tried deleting a non-existent record: " + event.uid)
}
}
}
@Throws(CalendarStorageException::class, ContactsStorageException::class, IOException::class)
override fun createLocalEntries() {
super.createLocalEntries()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
createInviteAttendeesNotification()
}
}
@Throws(CalendarStorageException::class, ContactsStorageException::class, IOException::class)
private fun createInviteAttendeesNotification() {
for (local in localDirty) {
val event = (local as LocalEvent).event
if (event.attendees.isEmpty()) {
return
}
createInviteAttendeesNotification(event, local.getContent())
}
}
private fun createInviteAttendeesNotification(event: Event, icsContent: String) {
val notificationHelper = NotificationHelper(context, event.uid, event.uid.hashCode())
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_EMAIL, getEmailAddresses(event.attendees, false))
val dateFormatDate = SimpleDateFormat("EEEE, MMM dd", Locale.US)
intent.putExtra(Intent.EXTRA_SUBJECT,
context.getString(R.string.sync_calendar_attendees_email_subject,
event.summary,
dateFormatDate.format(event.dtStart.date)))
intent.putExtra(Intent.EXTRA_TEXT,
context.getString(R.string.sync_calendar_attendees_email_content,
event.summary,
formatEventDates(event),
if (event.location != null) event.location else "",
formatAttendees(event.attendees)))
val uri = createAttachmentFromString(context, event.uid, icsContent)
if (uri == null) {
App.log.severe("Unable to create attachment from calendar event")
return
}
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.putExtra(Intent.EXTRA_STREAM, uri)
notificationHelper.notify(
context.getString(
R.string.sync_calendar_attendees_notification_title, event.summary),
context.getString(R.string.sync_calendar_attendees_notification_content), null,
intent,
R.drawable.ic_email_black)
}
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class)
private fun processEvent(newData: Event, localEvent: LocalEvent?): LocalResource {
var localEvent = localEvent
// delete local event, if it exists
if (localEvent != null) {
App.log.info("Updating " + newData.uid + " in local calendar")
localEvent.eTag = newData.uid
localEvent.update(newData)
syncResult.stats.numUpdates++
} else {
App.log.info("Adding " + newData.uid + " to local calendar")
localEvent = LocalEvent(localCalendar(), newData, newData.uid, newData.uid)
localEvent.add()
syncResult.stats.numInserts++
}
return localEvent
}
private fun getEmailAddresses(attendees: List<Attendee>,
shouldIncludeAccount: Boolean): Array<String> {
val attendeesEmails = ArrayList<String>(attendees.size)
for (attendee in attendees) {
val attendeeEmail = attendee.value.replace("mailto:", "")
if (!shouldIncludeAccount && attendeeEmail == account.name) {
continue
}
attendeesEmails.add(attendeeEmail)
}
return attendeesEmails.toTypedArray()
}
private fun formatAttendees(attendeesList: List<Attendee>): String {
val stringBuilder = StringBuilder()
val attendees = getEmailAddresses(attendeesList, true)
for (attendee in attendees) {
stringBuilder.append("\n ").append(attendee)
}
return stringBuilder.toString()
}
private fun formatEventDates(event: Event): String {
val locale = Locale.getDefault()
val timezone = if (event.dtStart.timeZone != null) event.dtStart.timeZone else TimeZone.getTimeZone("UTC")
val dateFormatString = if (event.isAllDay) "EEEE, MMM dd" else "EEEE, MMM dd @ hh:mm a"
val longDateFormat = SimpleDateFormat(dateFormatString, locale)
longDateFormat.timeZone = timezone
val shortDateFormat = SimpleDateFormat("hh:mm a", locale)
shortDateFormat.timeZone = timezone
val startDate = event.dtStart.date
val endDate = event.getEndDate(true)!!.date
val tzName = timezone.getDisplayName(timezone.inDaylightTime(startDate), TimeZone.SHORT)
val cal1 = Calendar.getInstance()
val cal2 = Calendar.getInstance()
cal1.time = startDate
cal2.time = endDate
val sameDay = cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)
if (sameDay && event.isAllDay) {
return longDateFormat.format(startDate)
}
return if (sameDay)
String.format("%s - %s (%s)",
longDateFormat.format(startDate),
shortDateFormat.format(endDate),
tzName)
else
String.format("%s - %s (%s)", longDateFormat.format(startDate), longDateFormat.format(endDate), tzName)
}
private fun createAttachmentFromString(context: Context, name: String, content: String): Uri? {
val parentDir = File(context.cacheDir, name)
parentDir.mkdirs()
val cache = File(parentDir, "invite.ics")
try {
IOUtils.writeStringToFile(cache, content)
return AcraContentProvider.getUriForFile(context, cache)
} catch (e: IOException) {
e.printStackTrace()
}
return null
}
}

@ -1,149 +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 com.etesync.syncadapter.syncadapter;
import android.accounts.Account;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.database.sqlite.SQLiteException;
import android.os.Bundle;
import android.provider.CalendarContract;
import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.Constants;
import com.etesync.syncadapter.NotificationHelper;
import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Exceptions;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.JournalModel;
import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.resource.LocalCalendar;
import com.etesync.syncadapter.ui.DebugInfoActivity;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import at.bitfire.ical4android.CalendarStorageException;
import io.requery.Persistable;
import io.requery.sql.EntityDataStore;
import okhttp3.HttpUrl;
import static com.etesync.syncadapter.Constants.KEY_ACCOUNT;
public class CalendarsSyncAdapterService 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 provider, SyncResult syncResult) {
super.onPerformSync(account, extras, authority, provider, syncResult);
NotificationHelper notificationManager = new NotificationHelper(getContext(), "journals-calendar", Constants.NOTIFICATION_CALENDAR_SYNC);
notificationManager.cancel();
try {
AccountSettings settings = new AccountSettings(getContext(), account);
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
return;
new RefreshCollections(account, CollectionInfo.Type.CALENDAR).run();
updateLocalCalendars(provider, account, settings);
HttpUrl principal = HttpUrl.get(settings.getUri());
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());
CalendarSyncManager syncManager = new CalendarSyncManager(getContext(), account, settings, extras, authority, syncResult, calendar, principal);
syncManager.performSync();
}
} catch (Exceptions.ServiceUnavailableException e) {
syncResult.stats.numIoExceptions++;
syncResult.delayUntil = (e.getRetryAfter() > 0) ? e.getRetryAfter() : Constants.DEFAULT_RETRY_DELAY;
} catch (Exception | OutOfMemoryError e) {
if (e instanceof CalendarStorageException || e instanceof SQLiteException) {
App.log.log(Level.SEVERE, "Couldn't prepare local calendars", e);
syncResult.databaseError = true;
}
int syncPhase = R.string.sync_phase_journals;
String title = getContext().getString(R.string.sync_error_calendar, account.name);
notificationManager.setThrowable(e);
final Intent detailsIntent = notificationManager.getDetailsIntent();
detailsIntent.putExtra(KEY_ACCOUNT, account);
if (!(e instanceof Exceptions.UnauthorizedException)) {
detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority);
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase);
}
notificationManager.notify(title, getContext().getString(syncPhase));
}
App.log.info("Calendar sync complete");
}
private void updateLocalCalendars(ContentProviderClient provider, Account account, AccountSettings settings) throws CalendarStorageException {
EntityDataStore<Persistable> data = ((App) getContext().getApplicationContext()).getData();
ServiceEntity service = JournalModel.Service.fetch(data, account.name, CollectionInfo.Type.CALENDAR);
Map<String, JournalEntity> remote = new HashMap<>();
List<JournalEntity> remoteJournals = JournalEntity.getJournals(data, service);
for (JournalEntity journalEntity : remoteJournals) {
remote.put(journalEntity.getUid(), journalEntity);
}
LocalCalendar[] local = (LocalCalendar[]) LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, null, null);
boolean updateColors = settings.getManageCalendarColors();
// delete obsolete local calendar
for (LocalCalendar calendar : local) {
String url = calendar.getName();
if (!remote.containsKey(url)) {
App.log.fine("Deleting obsolete local calendar " + url);
calendar.delete();
} else {
// remote CollectionInfo found for this local collection, update data
JournalEntity journalEntity = remote.get(url);
App.log.fine("Updating local calendar " + url + " with " + journalEntity);
calendar.update(journalEntity, updateColors);
// we already have a local calendar for this remote collection, don't take into consideration anymore
remote.remove(url);
}
}
// create new local calendars
for (String url : remote.keySet()) {
JournalEntity journalEntity = remote.get(url);
App.log.info("Adding local calendar list " + journalEntity);
LocalCalendar.create(account, provider, journalEntity);
}
}
}
}

@ -0,0 +1,153 @@
/*
* 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 com.etesync.syncadapter.syncadapter
import android.accounts.Account
import android.content.AbstractThreadedSyncAdapter
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.SyncResult
import android.database.sqlite.SQLiteException
import android.os.Bundle
import android.provider.CalendarContract
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.App
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.NotificationHelper
import com.etesync.syncadapter.R
import com.etesync.syncadapter.journalmanager.Exceptions
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity
import com.etesync.syncadapter.model.JournalModel
import com.etesync.syncadapter.model.ServiceEntity
import com.etesync.syncadapter.resource.LocalCalendar
import com.etesync.syncadapter.ui.DebugInfoActivity
import java.util.HashMap
import java.util.logging.Level
import at.bitfire.ical4android.CalendarStorageException
import io.requery.Persistable
import io.requery.sql.EntityDataStore
import okhttp3.HttpUrl
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
class CalendarsSyncAdapterService : SyncAdapterService() {
override fun syncAdapter(): AbstractThreadedSyncAdapter {
return SyncAdapter(this)
}
private class SyncAdapter(context: Context) : SyncAdapterService.SyncAdapter(context) {
override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
super.onPerformSync(account, extras, authority, provider, syncResult)
val notificationManager = NotificationHelper(context, "journals-calendar", Constants.NOTIFICATION_CALENDAR_SYNC)
notificationManager.cancel()
try {
val settings = AccountSettings(context, account)
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
return
RefreshCollections(account, CollectionInfo.Type.CALENDAR).run()
updateLocalCalendars(provider, account, settings)
val principal = HttpUrl.get(settings.uri!!)!!
for (calendar in LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, CalendarContract.Calendars.SYNC_EVENTS + "!=0", null) as Array<LocalCalendar>) {
App.log.info("Synchronizing calendar #" + calendar.id + ", URL: " + calendar.name)
val syncManager = CalendarSyncManager(context, account, settings, extras, authority, syncResult, calendar, principal)
syncManager.performSync()
}
} catch (e: Exceptions.ServiceUnavailableException) {
syncResult.stats.numIoExceptions++
syncResult.delayUntil = if (e.retryAfter > 0) e.retryAfter else Constants.DEFAULT_RETRY_DELAY
} catch (e: Exception) {
if (e is CalendarStorageException || e is SQLiteException) {
App.log.log(Level.SEVERE, "Couldn't prepare local calendars", e)
syncResult.databaseError = true
}
val syncPhase = R.string.sync_phase_journals
val title = context.getString(R.string.sync_error_calendar, account.name)
notificationManager.setThrowable(e)
val detailsIntent = notificationManager.detailsIntent
detailsIntent.putExtra(KEY_ACCOUNT, account)
if (e !is Exceptions.UnauthorizedException) {
detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority)
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase)
}
notificationManager.notify(title, context.getString(syncPhase))
} catch (e: OutOfMemoryError) {
if (e is CalendarStorageException || e is SQLiteException) {
App.log.log(Level.SEVERE, "Couldn't prepare local calendars", e)
syncResult.databaseError = true
}
val syncPhase = R.string.sync_phase_journals
val title = context.getString(R.string.sync_error_calendar, account.name)
notificationManager.setThrowable(e)
val detailsIntent = notificationManager.detailsIntent
detailsIntent.putExtra(KEY_ACCOUNT, account)
notificationManager.notify(title, context.getString(syncPhase))
}
App.log.info("Calendar sync complete")
}
@Throws(CalendarStorageException::class)
private fun updateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
val data = (context.applicationContext as App).data
val service = JournalModel.Service.fetch(data, account.name, CollectionInfo.Type.CALENDAR)
val remote = HashMap<String, JournalEntity>()
val remoteJournals = JournalEntity.getJournals(data, service)
for (journalEntity in remoteJournals) {
remote[journalEntity.uid] = journalEntity
}
val local = LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, null, null) as Array<LocalCalendar>
val updateColors = settings.manageCalendarColors
// delete obsolete local calendar
for (calendar in local) {
val url = calendar.name
val journalEntity = remote[url]
if (journalEntity == null) {
App.log.fine("Deleting obsolete local calendar $url")
calendar.delete()
} else {
// remote CollectionInfo found for this local collection, update data
App.log.fine("Updating local calendar $url with $journalEntity")
calendar.update(journalEntity, updateColors)
// we already have a local calendar for this remote collection, don't take into consideration anymore
remote.remove(url)
}
}
// create new local calendars
for (url in remote.keys) {
val journalEntity = remote[url]!!
App.log.info("Adding local calendar list $journalEntity")
LocalCalendar.create(account, provider, journalEntity)
}
}
}
}

@ -1,103 +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 com.etesync.syncadapter.syncadapter;
import android.accounts.Account;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.os.Bundle;
import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.Constants;
import com.etesync.syncadapter.InvalidAccountException;
import com.etesync.syncadapter.NotificationHelper;
import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Exceptions;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.JournalModel;
import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.resource.LocalAddressBook;
import com.etesync.syncadapter.ui.DebugInfoActivity;
import java.util.logging.Level;
import at.bitfire.vcard4android.ContactsStorageException;
import io.requery.Persistable;
import io.requery.sql.EntityDataStore;
import okhttp3.HttpUrl;
import static com.etesync.syncadapter.Constants.KEY_ACCOUNT;
public class ContactsSyncAdapterService extends SyncAdapterService {
@Override
protected AbstractThreadedSyncAdapter syncAdapter() {
return new ContactsSyncAdapter(this);
}
private static class ContactsSyncAdapter extends SyncAdapter {
public ContactsSyncAdapter(Context context) {
super(context);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
super.onPerformSync(account, extras, authority, provider, syncResult);
NotificationHelper notificationManager = new NotificationHelper(getContext(), "journals-contacts", Constants.NOTIFICATION_CONTACTS_SYNC);
notificationManager.cancel();
try {
LocalAddressBook addressBook = new LocalAddressBook(getContext(), account, provider);
AccountSettings settings;
try {
settings = new AccountSettings(getContext(), addressBook.getMainAccount());
} catch (InvalidAccountException|ContactsStorageException e) {
App.log.info("Skipping sync due to invalid account.");
App.log.info(e.getLocalizedMessage());
return;
}
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
return;
App.log.info("Synchronizing address book: " + addressBook.getURL());
App.log.info("Taking settings from: " + addressBook.getMainAccount());
HttpUrl principal = HttpUrl.get(settings.getUri());
ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, settings, extras, authority, provider, syncResult, addressBook, principal);
syncManager.performSync();
} catch (Exception | OutOfMemoryError e) {
int syncPhase = R.string.sync_phase_journals;
String title = getContext().getString(R.string.sync_error_contacts, account.name);
notificationManager.setThrowable(e);
final Intent detailsIntent = notificationManager.getDetailsIntent();
detailsIntent.putExtra(KEY_ACCOUNT, account);
if (!(e instanceof Exceptions.UnauthorizedException)) {
detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority);
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase);
}
notificationManager.notify(title, getContext().getString(syncPhase));
}
App.log.info("Contacts sync complete");
}
}
}

@ -0,0 +1,108 @@
/*
* 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 com.etesync.syncadapter.syncadapter
import android.accounts.Account
import android.content.AbstractThreadedSyncAdapter
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.SyncResult
import android.os.Bundle
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.App
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.InvalidAccountException
import com.etesync.syncadapter.NotificationHelper
import com.etesync.syncadapter.R
import com.etesync.syncadapter.journalmanager.Exceptions
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity
import com.etesync.syncadapter.model.JournalModel
import com.etesync.syncadapter.model.ServiceDB
import com.etesync.syncadapter.model.ServiceEntity
import com.etesync.syncadapter.resource.LocalAddressBook
import com.etesync.syncadapter.ui.DebugInfoActivity
import java.util.logging.Level
import at.bitfire.vcard4android.ContactsStorageException
import io.requery.Persistable
import io.requery.sql.EntityDataStore
import okhttp3.HttpUrl
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
class ContactsSyncAdapterService : SyncAdapterService() {
override fun syncAdapter(): AbstractThreadedSyncAdapter {
return ContactsSyncAdapter(this)
}
private class ContactsSyncAdapter(context: Context) : SyncAdapterService.SyncAdapter(context) {
override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
super.onPerformSync(account, extras, authority, provider, syncResult)
val notificationManager = NotificationHelper(context, "journals-contacts", Constants.NOTIFICATION_CONTACTS_SYNC)
notificationManager.cancel()
try {
val addressBook = LocalAddressBook(context, account, provider)
val settings: AccountSettings
try {
settings = AccountSettings(context, addressBook.mainAccount)
} catch (e: InvalidAccountException) {
App.log.info("Skipping sync due to invalid account.")
App.log.info(e.localizedMessage)
return
} catch (e: ContactsStorageException) {
App.log.info("Skipping sync due to invalid account.")
App.log.info(e.localizedMessage)
return
}
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
return
App.log.info("Synchronizing address book: " + addressBook.url)
App.log.info("Taking settings from: " + addressBook.mainAccount)
val principal = HttpUrl.get(settings.uri!!)!!
val syncManager = ContactsSyncManager(context, account, settings, extras, authority, provider, syncResult, addressBook, principal)
syncManager.performSync()
} catch (e: Exception) {
val syncPhase = R.string.sync_phase_journals
val title = context.getString(R.string.sync_error_contacts, account.name)
notificationManager.setThrowable(e)
val detailsIntent = notificationManager.detailsIntent
detailsIntent.putExtra(KEY_ACCOUNT, account)
if (e !is Exceptions.UnauthorizedException) {
detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority)
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase)
}
notificationManager.notify(title, context.getString(syncPhase))
} catch (e: OutOfMemoryError) {
val syncPhase = R.string.sync_phase_journals
val title = context.getString(R.string.sync_error_contacts, account.name)
notificationManager.setThrowable(e)
val detailsIntent = notificationManager.detailsIntent
detailsIntent.putExtra(KEY_ACCOUNT, account)
notificationManager.notify(title, context.getString(syncPhase))
}
App.log.info("Contacts sync complete")
}
}
}

@ -1,303 +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 com.etesync.syncadapter.syncadapter;
import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.SyncResult;
import android.os.Build;
import android.os.Bundle;
import android.provider.ContactsContract;
import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.Constants;
import com.etesync.syncadapter.HttpClient;
import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Exceptions;
import com.etesync.syncadapter.journalmanager.JournalEntryManager;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.SyncEntry;
import com.etesync.syncadapter.resource.LocalAddressBook;
import com.etesync.syncadapter.resource.LocalContact;
import com.etesync.syncadapter.resource.LocalGroup;
import com.etesync.syncadapter.resource.LocalResource;
import org.apache.commons.codec.Charsets;
import org.apache.commons.collections4.SetUtils;
import org.apache.commons.io.IOUtils;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Set;
import java.util.logging.Level;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.BatchOperation;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* <p>Synchronization manager for CardDAV collections; handles contacts and groups.</p>
*/
public class ContactsSyncManager extends SyncManager {
final private ContentProviderClient provider;
final private HttpUrl remote;
public ContactsSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, ContentProviderClient provider, SyncResult result, LocalAddressBook localAddressBook, HttpUrl principal) throws Exceptions.IntegrityException, Exceptions.GenericCryptoException, ContactsStorageException {
super(context, account, settings, extras, authority, result, localAddressBook.getURL(), CollectionInfo.Type.ADDRESS_BOOK, localAddressBook.getMainAccount().name);
this.provider = provider;
this.remote = principal;
localCollection = localAddressBook;
}
@Override
protected int notificationId() {
return Constants.NOTIFICATION_CONTACTS_SYNC;
}
@Override
protected String getSyncErrorTitle() {
return context.getString(R.string.sync_error_contacts, account.name);
}
@Override
protected String getSyncSuccessfullyTitle() {
return context.getString(R.string.sync_successfully_contacts, account.name);
}
@Override
protected boolean prepare() throws ContactsStorageException, CalendarStorageException {
if (!super.prepare())
return false;
LocalAddressBook localAddressBook = localAddressBook();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
int reallyDirty = localAddressBook.verifyDirty(),
deleted = localAddressBook.getDeleted().length;
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_UPLOAD) && reallyDirty == 0 && deleted == 0) {
App.log.info("This sync was called to up-sync dirty/deleted contacts, but no contacts have been changed");
return false;
}
}
// set up Contacts Provider Settings
ContentValues values = new ContentValues(2);
values.put(ContactsContract.Settings.SHOULD_SYNC, 1);
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1);
localAddressBook.updateSettings(values);
journal = new JournalEntryManager(httpClient, remote, localAddressBook.getURL());
localAddressBook.includeGroups = true;
return true;
}
@Override
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
super.prepareDirty();
LocalAddressBook addressBook = localAddressBook();
/* groups as separate VCards: there are group contacts and individual contacts */
// mark groups with changed members as dirty
BatchOperation batch = new BatchOperation(addressBook.provider);
for (LocalContact contact : addressBook.getDirtyContacts()) {
try {
App.log.fine("Looking for changed group memberships of contact " + contact.getFileName());
Set<Long> cachedGroups = contact.getCachedGroupMemberships(),
currentGroups = contact.getGroupMemberships();
for (Long groupID : SetUtils.disjunction(cachedGroups, currentGroups)) {
App.log.fine("Marking group as dirty: " + groupID);
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(ContactsContract.Groups.CONTENT_URI, groupID)))
.withValue(ContactsContract.Groups.DIRTY, 1)
.withYieldAllowed(true)
));
}
} catch (FileNotFoundException ignored) {
}
}
batch.commit();
}
@Override
protected void postProcess() throws CalendarStorageException, ContactsStorageException {
super.postProcess();
/* VCard4 group handling: there are group contacts and individual contacts */
App.log.info("Assigning memberships of downloaded contact groups");
LocalGroup.applyPendingMemberships(localAddressBook());
}
// helpers
private LocalAddressBook localAddressBook() {
return (LocalAddressBook) localCollection;
}
protected void processSyncEntry(SyncEntry cEntry) throws IOException, ContactsStorageException, CalendarStorageException {
InputStream is = new ByteArrayInputStream(cEntry.getContent().getBytes(Charsets.UTF_8));
Contact.Downloader downloader = new ResourceDownloader(context);
Contact[] contacts = Contact.fromStream(is, Charsets.UTF_8, downloader);
if (contacts.length == 0) {
App.log.warning("Received VCard without data, ignoring");
return;
} else if (contacts.length > 1)
App.log.warning("Received multiple VCards, using first one");
Contact contact = contacts[0];
LocalResource local = (LocalResource) localCollection.getByUid(contact.uid);
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
processContact(contact, local);
} else {
if (local != null) {
App.log.info("Removing local record #" + local.getId() + " which has been deleted on the server");
local.delete();
} else {
App.log.warning("Tried deleting a non-existent record: " + contact.uid);
}
}
}
private LocalResource processContact(final Contact newData, LocalResource local) throws IOException, ContactsStorageException {
String uuid = newData.uid;
// update local contact, if it exists
if (local != null) {
App.log.log(Level.INFO, "Updating " + uuid + " in local address book");
if (local instanceof LocalGroup && newData.group) {
// update group
LocalGroup group = (LocalGroup) local;
group.eTag = uuid;
group.updateFromServer(newData);
syncResult.stats.numUpdates++;
} else if (local instanceof LocalContact && !newData.group) {
// update contact
LocalContact contact = (LocalContact) local;
contact.eTag = uuid;
contact.update(newData);
syncResult.stats.numUpdates++;
} else {
// group has become an individual contact or vice versa
try {
local.delete();
local = null;
} catch (CalendarStorageException e) {
// CalendarStorageException is not used by LocalGroup and LocalContact
}
}
}
if (local == null) {
if (newData.group) {
App.log.log(Level.INFO, "Creating local group", newData.uid);
LocalGroup group = new LocalGroup(localAddressBook(), newData, uuid, uuid);
group.create();
local = group;
} else {
App.log.log(Level.INFO, "Creating local contact", newData.uid);
LocalContact contact = new LocalContact(localAddressBook(), newData, uuid, uuid);
contact.create();
local = contact;
}
syncResult.stats.numInserts++;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && local instanceof LocalContact)
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
((LocalContact)local).updateHashCode(null);
return local;
}
// downloader helper class
public static class ResourceDownloader implements Contact.Downloader {
Context context;
public ResourceDownloader(Context context) {
this.context = context;
}
@Override
public byte[] download(String url, String accepts) {
HttpUrl httpUrl = HttpUrl.parse(url);
if (httpUrl == null) {
App.log.log(Level.SEVERE, "Invalid external resource URL", url);
return null;
}
String host = httpUrl.host();
if (host == null) {
App.log.log(Level.SEVERE, "External resource URL doesn't specify a host name", url);
return null;
}
OkHttpClient resourceClient = HttpClient.create(context);
// authenticate only against a certain host, and only upon request
// resourceClient = HttpClient.addAuthentication(resourceClient, baseUrl.host(), settings.username(), settings.password());
// allow redirects
resourceClient = resourceClient.newBuilder()
.followRedirects(true)
.build();
try {
Response response = resourceClient.newCall(new Request.Builder()
.get()
.url(httpUrl)
.build()).execute();
ResponseBody body = response.body();
if (body != null) {
InputStream stream = body.byteStream();
try {
if (response.isSuccessful() && stream != null) {
return IOUtils.toByteArray(stream);
} else
App.log.severe("Couldn't download external resource");
} finally {
if (stream != null)
stream.close();
}
}
} catch (IOException e) {
App.log.log(Level.SEVERE, "Couldn't download external resource", e);
}
return null;
}
}
}

@ -0,0 +1,293 @@
/*
* 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 com.etesync.syncadapter.syncadapter
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentProviderOperation
import android.content.ContentResolver
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.content.SyncResult
import android.os.Build
import android.os.Bundle
import android.provider.ContactsContract
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.App
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.HttpClient
import com.etesync.syncadapter.R
import com.etesync.syncadapter.journalmanager.Exceptions
import com.etesync.syncadapter.journalmanager.JournalEntryManager
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.SyncEntry
import com.etesync.syncadapter.resource.LocalAddressBook
import com.etesync.syncadapter.resource.LocalContact
import com.etesync.syncadapter.resource.LocalGroup
import com.etesync.syncadapter.resource.LocalResource
import org.apache.commons.codec.Charsets
import org.apache.commons.collections4.SetUtils
import org.apache.commons.io.IOUtils
import java.io.ByteArrayInputStream
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.util.logging.Level
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.BatchOperation
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.ContactsStorageException
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
/**
*
* Synchronization manager for CardDAV collections; handles contacts and groups.
*/
class ContactsSyncManager @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class, ContactsStorageException::class)
constructor(context: Context, account: Account, settings: AccountSettings, extras: Bundle, authority: String, private val provider: ContentProviderClient, result: SyncResult, localAddressBook: LocalAddressBook, private val remote: HttpUrl) : SyncManager(context, account, settings, extras, authority, result, localAddressBook.url, CollectionInfo.Type.ADDRESS_BOOK, localAddressBook.mainAccount.name) {
protected override val syncErrorTitle: String
get() = context.getString(R.string.sync_error_contacts, account.name)
protected override val syncSuccessfullyTitle: String
get() = context.getString(R.string.sync_successfully_contacts, account.name)
init {
localCollection = localAddressBook
}
override fun notificationId(): Int {
return Constants.NOTIFICATION_CONTACTS_SYNC
}
@Throws(ContactsStorageException::class, CalendarStorageException::class)
override fun prepare(): Boolean {
if (!super.prepare())
return false
val localAddressBook = localAddressBook()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
val reallyDirty = localAddressBook.verifyDirty()
val deleted = localAddressBook.deleted.size
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_UPLOAD) && reallyDirty == 0 && deleted == 0) {
App.log.info("This sync was called to up-sync dirty/deleted contacts, but no contacts have been changed")
return false
}
}
// set up Contacts Provider Settings
val values = ContentValues(2)
values.put(ContactsContract.Settings.SHOULD_SYNC, 1)
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
localAddressBook.updateSettings(values)
journal = JournalEntryManager(httpClient, remote, localAddressBook.url)
localAddressBook.includeGroups = true
return true
}
@Throws(CalendarStorageException::class, ContactsStorageException::class)
override fun prepareDirty() {
super.prepareDirty()
val addressBook = localAddressBook()
/* groups as separate VCards: there are group contacts and individual contacts */
// mark groups with changed members as dirty
val batch = BatchOperation(addressBook.provider)
for (contact in addressBook.dirtyContacts) {
try {
App.log.fine("Looking for changed group memberships of contact " + contact.fileName)
val cachedGroups = contact.cachedGroupMemberships
val currentGroups = contact.groupMemberships
for (groupID in SetUtils.disjunction(cachedGroups, currentGroups)) {
App.log.fine("Marking group as dirty: " + groupID!!)
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(ContactsContract.Groups.CONTENT_URI, groupID)))
.withValue(ContactsContract.Groups.DIRTY, 1)
.withYieldAllowed(true)
))
}
} catch (ignored: FileNotFoundException) {
}
}
batch.commit()
}
@Throws(CalendarStorageException::class, ContactsStorageException::class)
override fun postProcess() {
super.postProcess()
/* VCard4 group handling: there are group contacts and individual contacts */
App.log.info("Assigning memberships of downloaded contact groups")
LocalGroup.applyPendingMemberships(localAddressBook())
}
// helpers
private fun localAddressBook(): LocalAddressBook {
return localCollection as LocalAddressBook
}
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class)
override fun processSyncEntry(cEntry: SyncEntry) {
val `is` = ByteArrayInputStream(cEntry.content.toByteArray(Charsets.UTF_8))
val downloader = ResourceDownloader(context)
val contacts = Contact.fromStream(`is`, Charsets.UTF_8, downloader)
if (contacts.size == 0) {
App.log.warning("Received VCard without data, ignoring")
return
} else if (contacts.size > 1)
App.log.warning("Received multiple VCards, using first one")
val contact = contacts[0]
val local = localCollection!!.getByUid(contact.uid) as LocalResource
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
processContact(contact, local)
} else {
if (local != null) {
App.log.info("Removing local record #" + local.id + " which has been deleted on the server")
local.delete()
} else {
App.log.warning("Tried deleting a non-existent record: " + contact.uid)
}
}
}
@Throws(IOException::class, ContactsStorageException::class)
private fun processContact(newData: Contact, local: LocalResource?): LocalResource {
var local = local
val uuid = newData.uid
// update local contact, if it exists
if (local != null) {
App.log.log(Level.INFO, "Updating $uuid in local address book")
if (local is LocalGroup && newData.group) {
// update group
val group = local as LocalGroup?
group!!.eTag = uuid
group.updateFromServer(newData)
syncResult.stats.numUpdates++
} else if (local is LocalContact && !newData.group) {
// update contact
val contact = local as LocalContact?
contact!!.eTag = uuid
contact.update(newData)
syncResult.stats.numUpdates++
} else {
// group has become an individual contact or vice versa
try {
local.delete()
local = null
} catch (e: CalendarStorageException) {
// CalendarStorageException is not used by LocalGroup and LocalContact
}
}
}
if (local == null) {
if (newData.group) {
App.log.log(Level.INFO, "Creating local group", newData.uid)
val group = LocalGroup(localAddressBook(), newData, uuid, uuid)
group.create()
local = group
} else {
App.log.log(Level.INFO, "Creating local contact", newData.uid)
val contact = LocalContact(localAddressBook(), newData, uuid, uuid)
contact.create()
local = contact
}
syncResult.stats.numInserts++
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && local is LocalContact)
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
local.updateHashCode(null)
return local
}
// downloader helper class
class ResourceDownloader(internal var context: Context) : Contact.Downloader {
override fun download(url: String, accepts: String): ByteArray? {
val httpUrl = HttpUrl.parse(url)
if (httpUrl == null) {
App.log.log(Level.SEVERE, "Invalid external resource URL", url)
return null
}
val host = httpUrl.host()
if (host == null) {
App.log.log(Level.SEVERE, "External resource URL doesn't specify a host name", url)
return null
}
var resourceClient = HttpClient.create(context)
// authenticate only against a certain host, and only upon request
// resourceClient = HttpClient.addAuthentication(resourceClient, baseUrl.host(), settings.username(), settings.password());
// allow redirects
resourceClient = resourceClient.newBuilder()
.followRedirects(true)
.build()
try {
val response = resourceClient.newCall(Request.Builder()
.get()
.url(httpUrl)
.build()).execute()
val body = response.body()
if (body != null) {
val stream = body.byteStream()
try {
if (response.isSuccessful && stream != null) {
return IOUtils.toByteArray(stream)
} else
App.log.severe("Couldn't download external resource")
} finally {
stream?.close()
}
}
} catch (e: IOException) {
App.log.log(Level.SEVERE, "Couldn't download external resource", e)
}
return null
}
}
}

@ -1,97 +0,0 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package com.etesync.syncadapter.syncadapter;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.NetworkErrorException;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import com.etesync.syncadapter.ui.AccountsActivity;
public class NullAuthenticatorService extends Service {
private AccountAuthenticator accountAuthenticator;
@Override
public void onCreate() {
accountAuthenticator = new NullAuthenticatorService.AccountAuthenticator(this);
}
@Override
public IBinder onBind(Intent intent) {
if (intent.getAction().equals(android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT))
return accountAuthenticator.getIBinder();
return null;
}
private static class AccountAuthenticator extends AbstractAccountAuthenticator {
final Context context;
public AccountAuthenticator(Context context) {
super(context);
this.context = context;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
return null;
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
Intent intent = new Intent(context, AccountsActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public String getAuthTokenLabel(String authTokenType) {
return null;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
return null;
}
@Override
public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response, Account account) {
Bundle result = new Bundle();
boolean allowed = false; // we don't want users to explicitly delete inner accounts
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, allowed);
return result;
}
}
}

@ -0,0 +1,84 @@
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package com.etesync.syncadapter.syncadapter
import android.accounts.AbstractAccountAuthenticator
import android.accounts.Account
import android.accounts.AccountAuthenticatorResponse
import android.accounts.AccountManager
import android.accounts.NetworkErrorException
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import com.etesync.syncadapter.ui.AccountsActivity
class NullAuthenticatorService : Service() {
private var accountAuthenticator: AccountAuthenticator? = null
override fun onCreate() {
accountAuthenticator = NullAuthenticatorService.AccountAuthenticator(this)
}
override fun onBind(intent: Intent): IBinder? {
return if (intent.action == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT) accountAuthenticator!!.iBinder else null
}
private class AccountAuthenticator(internal val context: Context) : AbstractAccountAuthenticator(context) {
override fun editProperties(response: AccountAuthenticatorResponse, accountType: String): Bundle? {
return null
}
@Throws(NetworkErrorException::class)
override fun addAccount(response: AccountAuthenticatorResponse, accountType: String, authTokenType: String, requiredFeatures: Array<String>, options: Bundle): Bundle {
val intent = Intent(context, AccountsActivity::class.java)
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
val bundle = Bundle()
bundle.putParcelable(AccountManager.KEY_INTENT, intent)
return bundle
}
@Throws(NetworkErrorException::class)
override fun confirmCredentials(response: AccountAuthenticatorResponse, account: Account, options: Bundle): Bundle? {
return null
}
@Throws(NetworkErrorException::class)
override fun getAuthToken(response: AccountAuthenticatorResponse, account: Account, authTokenType: String, options: Bundle): Bundle? {
return null
}
override fun getAuthTokenLabel(authTokenType: String): String? {
return null
}
@Throws(NetworkErrorException::class)
override fun updateCredentials(response: AccountAuthenticatorResponse, account: Account, authTokenType: String, options: Bundle): Bundle? {
return null
}
@Throws(NetworkErrorException::class)
override fun hasFeatures(response: AccountAuthenticatorResponse, account: Account, features: Array<String>): Bundle? {
return null
}
override fun getAccountRemovalAllowed(response: AccountAuthenticatorResponse, account: Account): Bundle {
val result = Bundle()
val allowed = false // we don't want users to explicitly delete inner accounts
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, allowed)
return result
}
}
}

@ -1,217 +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 com.etesync.syncadapter.syncadapter;
import android.accounts.Account;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.util.Pair;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.Constants;
import com.etesync.syncadapter.HttpClient;
import com.etesync.syncadapter.InvalidAccountException;
import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Crypto;
import com.etesync.syncadapter.journalmanager.Exceptions;
import com.etesync.syncadapter.journalmanager.JournalManager;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.JournalModel;
import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.ui.PermissionsActivity;
import io.requery.Persistable;
import io.requery.sql.EntityDataStore;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
//import com.android.vending.billing.IInAppBillingService;
public abstract class SyncAdapterService extends Service {
abstract protected AbstractThreadedSyncAdapter syncAdapter();
@Nullable
@Override
public IBinder onBind(Intent intent) {
return syncAdapter().getSyncAdapterBinder();
}
public static abstract class SyncAdapter extends AbstractThreadedSyncAdapter {
public SyncAdapter(Context context) {
super(context, false);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
App.log.log(Level.INFO, authority + " sync of " + account + " has been initiated.", extras.keySet().toArray());
// required for dav4android (ServiceLoader)
Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
}
@Override
public void onSecurityException(Account account, Bundle extras, String authority, SyncResult syncResult) {
App.log.log(Level.WARNING, "Security exception when opening content provider for " + authority);
syncResult.databaseError = true;
Intent intent = new Intent(getContext(), PermissionsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Notification notify = new NotificationCompat.Builder(getContext())
.setSmallIcon(R.drawable.ic_error_light)
.setLargeIcon(App.getLauncherBitmap(getContext()))
.setContentTitle(getContext().getString(R.string.sync_error_permissions))
.setContentText(getContext().getString(R.string.sync_error_permissions_text))
.setContentIntent(PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.build();
NotificationManagerCompat nm = NotificationManagerCompat.from(getContext());
nm.notify(Constants.NOTIFICATION_PERMISSIONS, notify);
}
protected boolean checkSyncConditions(@NonNull AccountSettings settings) {
if (settings.getSyncWifiOnly()) {
ConnectivityManager cm = (ConnectivityManager) getContext().getSystemService(CONNECTIVITY_SERVICE);
NetworkInfo network = cm.getActiveNetworkInfo();
if (network == null) {
App.log.info("No network available, stopping");
return false;
}
if (network.getType() != ConnectivityManager.TYPE_WIFI || !network.isConnected()) {
App.log.info("Not on connected WiFi, stopping");
return false;
}
String onlySSID = settings.getSyncWifiOnlySSID();
if (onlySSID != null) {
onlySSID = "\"" + onlySSID + "\"";
WifiManager wifi = (WifiManager) getContext().getApplicationContext().getSystemService(WIFI_SERVICE);
WifiInfo info = wifi.getConnectionInfo();
if (info == null || !onlySSID.equals(info.getSSID())) {
App.log.info("Connected to wrong WiFi network (" + info.getSSID() + ", required: " + onlySSID + "), ignoring");
return false;
}
}
}
return true;
}
protected class RefreshCollections {
final private Account account;
final private Context context;
final private CollectionInfo.Type serviceType;
RefreshCollections(Account account, CollectionInfo.Type serviceType) {
this.account = account;
this.serviceType = serviceType;
context = getContext();
}
void run() throws Exceptions.HttpException, Exceptions.IntegrityException, InvalidAccountException, Exceptions.GenericCryptoException {
App.log.info("Refreshing " + serviceType + " collections of service #" + serviceType.toString());
AccountSettings settings = new AccountSettings(context, account);
OkHttpClient httpClient = HttpClient.create(context, settings);
JournalManager journalsManager = new JournalManager(httpClient, HttpUrl.get(settings.getUri()));
List<Pair<JournalManager.Journal, CollectionInfo>> journals = new LinkedList<>();
for (JournalManager.Journal journal : journalsManager.list()) {
Crypto.CryptoManager crypto;
if (journal.getKey() != null) {
crypto = new Crypto.CryptoManager(journal.getVersion(), settings.getKeyPair(), journal.getKey());
} else {
crypto = new Crypto.CryptoManager(journal.getVersion(), settings.password(), journal.getUid());
}
journal.verify(crypto);
CollectionInfo info = CollectionInfo.fromJson(journal.getContent(crypto));
info.updateFromJournal(journal);
if (info.type.equals(serviceType)) {
journals.add(new Pair<>(journal, info));
}
}
if (journals.isEmpty()) {
CollectionInfo info = CollectionInfo.defaultForServiceType(serviceType);
info.uid = JournalManager.Journal.genUid();
Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid);
JournalManager.Journal journal = new JournalManager.Journal(crypto, info.toJson(), info.uid);
journalsManager.create(journal);
journals.add(new Pair<>(journal, info));
}
saveCollections(journals);
}
private void saveCollections(Iterable<Pair<JournalManager.Journal, CollectionInfo>> journals) {
EntityDataStore<Persistable> data = ((App) context.getApplicationContext()).getData();
ServiceEntity service = JournalModel.Service.fetch(data, account.name, serviceType);
Map<String, JournalEntity> existing = new HashMap<>();
for (JournalEntity journalEntity : JournalEntity.getJournals(data, service)) {
existing.put(journalEntity.getUid(), journalEntity);
}
for (Pair<JournalManager.Journal, CollectionInfo> pair : journals) {
JournalManager.Journal journal = pair.first;
CollectionInfo collection = pair.second;
App.log.log(Level.FINE, "Saving collection", journal.getUid());
collection.serviceID = service.getId();
JournalEntity journalEntity = JournalEntity.fetchOrCreate(data, collection);
journalEntity.setOwner(journal.getOwner());
journalEntity.setEncryptedKey(journal.getKey());
journalEntity.setReadOnly(journal.isReadOnly());
journalEntity.setDeleted(false);
data.upsert(journalEntity);
existing.remove(collection.uid);
}
for (JournalEntity journalEntity : existing.values()) {
App.log.log(Level.FINE, "Deleting collection", journalEntity.getUid());
journalEntity.setDeleted(true);
data.update(journalEntity);
}
}
}
}
}

@ -0,0 +1,202 @@
/*
* 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 com.etesync.syncadapter.syncadapter
import android.accounts.Account
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.AbstractThreadedSyncAdapter
import android.content.ContentProviderClient
import android.content.Context
import android.content.Intent
import android.content.SyncResult
import android.net.ConnectivityManager
import android.net.NetworkInfo
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.os.Bundle
import android.os.IBinder
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import android.support.v4.util.Pair
import java.util.HashMap
import java.util.LinkedList
import java.util.logging.Level
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.App
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.HttpClient
import com.etesync.syncadapter.InvalidAccountException
import com.etesync.syncadapter.R
import com.etesync.syncadapter.journalmanager.Crypto
import com.etesync.syncadapter.journalmanager.Exceptions
import com.etesync.syncadapter.journalmanager.JournalManager
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity
import com.etesync.syncadapter.model.JournalModel
import com.etesync.syncadapter.model.ServiceEntity
import com.etesync.syncadapter.ui.PermissionsActivity
import io.requery.Persistable
import io.requery.sql.EntityDataStore
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
//import com.android.vending.billing.IInAppBillingService;
abstract class SyncAdapterService : Service() {
protected abstract fun syncAdapter(): AbstractThreadedSyncAdapter
override fun onBind(intent: Intent): IBinder? {
return syncAdapter().syncAdapterBinder
}
abstract class SyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, false) {
override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
App.log.log(Level.INFO, "$authority sync of $account has been initiated.", extras.keySet().toTypedArray())
// required for dav4android (ServiceLoader)
Thread.currentThread().contextClassLoader = context.classLoader
}
override fun onSecurityException(account: Account, extras: Bundle, authority: String, syncResult: SyncResult) {
App.log.log(Level.WARNING, "Security exception when opening content provider for $authority")
syncResult.databaseError = true
val intent = Intent(context, PermissionsActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val notify = NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_error_light)
.setLargeIcon(App.getLauncherBitmap(context))
.setContentTitle(context.getString(R.string.sync_error_permissions))
.setContentText(context.getString(R.string.sync_error_permissions_text))
.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.build()
val nm = NotificationManagerCompat.from(context)
nm.notify(Constants.NOTIFICATION_PERMISSIONS, notify)
}
protected fun checkSyncConditions(settings: AccountSettings): Boolean {
if (settings.syncWifiOnly) {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = cm.activeNetworkInfo
if (network == null) {
App.log.info("No network available, stopping")
return false
}
if (network.type != ConnectivityManager.TYPE_WIFI || !network.isConnected) {
App.log.info("Not on connected WiFi, stopping")
return false
}
var onlySSID = settings.syncWifiOnlySSID
if (onlySSID != null) {
onlySSID = "\"" + onlySSID + "\""
val wifi = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
val info = wifi.connectionInfo
if (info == null || onlySSID != info.ssid) {
App.log.info("Connected to wrong WiFi network (" + info!!.ssid + ", required: " + onlySSID + "), ignoring")
return false
}
}
}
return true
}
inner class RefreshCollections internal constructor(private val account: Account, private val serviceType: CollectionInfo.Type) {
private val context: Context
init {
context = getContext()
}
@Throws(Exceptions.HttpException::class, Exceptions.IntegrityException::class, InvalidAccountException::class, Exceptions.GenericCryptoException::class)
internal fun run() {
App.log.info("Refreshing " + serviceType + " collections of service #" + serviceType.toString())
val settings = AccountSettings(context, account)
val httpClient = HttpClient.create(context, settings)
val journalsManager = JournalManager(httpClient, HttpUrl.get(settings.uri!!)!!)
val journals = LinkedList<Pair<JournalManager.Journal, CollectionInfo>>()
for (journal in journalsManager.list()) {
val crypto: Crypto.CryptoManager
if (journal.key != null) {
crypto = Crypto.CryptoManager(journal.version, settings.keyPair!!, journal.key)
} else {
crypto = Crypto.CryptoManager(journal.version, settings.password(), journal.uid!!)
}
journal.verify(crypto)
val info = CollectionInfo.fromJson(journal.getContent(crypto))
info.updateFromJournal(journal)
if (info.type == serviceType) {
journals.add(Pair(journal, info))
}
}
if (journals.isEmpty()) {
val info = CollectionInfo.defaultForServiceType(serviceType)
info.uid = JournalManager.Journal.genUid()
val crypto = Crypto.CryptoManager(info.version, settings.password(), info.uid)
val journal = JournalManager.Journal(crypto, info.toJson(), info.uid)
journalsManager.create(journal)
journals.add(Pair(journal, info))
}
saveCollections(journals)
}
private fun saveCollections(journals: Iterable<Pair<JournalManager.Journal, CollectionInfo>>) {
val data = (context.applicationContext as App).data
val service = JournalModel.Service.fetch(data, account.name, serviceType)
val existing = HashMap<String, JournalEntity>()
for (journalEntity in JournalEntity.getJournals(data, service)) {
existing[journalEntity.uid] = journalEntity
}
for (pair in journals) {
val journal = pair.first
val collection = pair.second
App.log.log(Level.FINE, "Saving collection", journal!!.uid)
collection!!.serviceID = service.id
val journalEntity = JournalEntity.fetchOrCreate(data, collection)
journalEntity.owner = journal.owner
journalEntity.encryptedKey = journal.key
journalEntity.isReadOnly = journal.isReadOnly
journalEntity.isDeleted = false
data.upsert(journalEntity)
existing.remove(collection.uid)
}
for (journalEntity in existing.values) {
App.log.log(Level.FINE, "Deleting collection", journalEntity.uid)
journalEntity.isDeleted = true
data.update(journalEntity)
}
}
}
}
}

@ -1,522 +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 com.etesync.syncadapter.syncadapter;
import android.accounts.Account;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.content.res.Resources;
import android.os.Bundle;
import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.Constants;
import com.etesync.syncadapter.HttpClient;
import com.etesync.syncadapter.NotificationHelper;
import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Crypto;
import com.etesync.syncadapter.journalmanager.Exceptions;
import com.etesync.syncadapter.journalmanager.JournalEntryManager;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.EntryEntity;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.JournalModel;
import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.model.Settings;
import com.etesync.syncadapter.model.SyncEntry;
import com.etesync.syncadapter.resource.LocalCollection;
import com.etesync.syncadapter.resource.LocalResource;
import com.etesync.syncadapter.ui.DebugInfoActivity;
import com.etesync.syncadapter.ui.ViewCollectionActivity;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.InvalidCalendarException;
import at.bitfire.vcard4android.ContactsStorageException;
import io.requery.Persistable;
import io.requery.sql.EntityDataStore;
import okhttp3.OkHttpClient;
import static com.etesync.syncadapter.Constants.KEY_ACCOUNT;
import static com.etesync.syncadapter.model.SyncEntry.Actions.ADD;
abstract public class SyncManager {
private static final int MAX_FETCH = 50;
private static final int MAX_PUSH = 30;
protected final NotificationHelper notificationManager;
protected final CollectionInfo info;
protected final Context context;
protected final Account account;
protected final Bundle extras;
protected final String authority;
protected final SyncResult syncResult;
protected final CollectionInfo.Type serviceType;
protected final AccountSettings settings;
protected LocalCollection localCollection;
protected OkHttpClient httpClient;
protected JournalEntryManager journal;
private JournalEntity _journalEntity;
private final Crypto.CryptoManager crypto;
private EntityDataStore<Persistable> data;
/**
* remote CTag (uuid of the last entry on the server). We update it when we fetch/push and save when everything works.
*/
private String remoteCTag = null;
/**
* Syncable local journal entries.
*/
private List<JournalEntryManager.Entry> localEntries;
/**
* Syncable remote journal entries (fetch from server).
*/
private List<JournalEntryManager.Entry> remoteEntries;
/**
* Dirty and deleted resources. We need to save them so we safely ignore ones that were added after we started.
*/
private List<LocalResource> localDeleted;
protected LocalResource[] localDirty;
public SyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult syncResult, String journalUid, CollectionInfo.Type serviceType, String accountName) throws Exceptions.IntegrityException, Exceptions.GenericCryptoException {
this.context = context;
this.account = account;
this.settings = settings;
this.extras = extras;
this.authority = authority;
this.syncResult = syncResult;
this.serviceType = serviceType;
// create HttpClient with given logger
httpClient = HttpClient.create(context, settings);
data = ((App) context.getApplicationContext()).getData();
ServiceEntity serviceEntity = JournalModel.Service.fetch(data, accountName, serviceType);
info = JournalEntity.fetch(data, serviceEntity, journalUid).getInfo();
// dismiss previous error notifications
notificationManager = new NotificationHelper(context, journalUid, notificationId());
notificationManager.cancel();
App.log.info(String.format(Locale.getDefault(), "Syncing collection %s (version: %d)", journalUid, info.version));
if (getJournalEntity().getEncryptedKey() != null) {
crypto = new Crypto.CryptoManager(info.version, settings.getKeyPair(), getJournalEntity().getEncryptedKey());
} else {
crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid);
}
}
protected abstract int notificationId();
protected abstract String getSyncErrorTitle();
protected abstract String getSyncSuccessfullyTitle();
@TargetApi(21)
public void performSync() {
int syncPhase = R.string.sync_phase_prepare;
try {
App.log.info("Sync phase: " + context.getString(syncPhase));
if (!prepare()) {
App.log.info("No reason to synchronize, aborting");
return;
}
if (Thread.interrupted())
throw new InterruptedException();
syncPhase = R.string.sync_phase_query_capabilities;
App.log.info("Sync phase: " + context.getString(syncPhase));
queryCapabilities();
if (Thread.interrupted())
throw new InterruptedException();
syncPhase = R.string.sync_phase_prepare_local;
App.log.info("Sync phase: " + context.getString(syncPhase));
prepareLocal();
do {
if (Thread.interrupted())
throw new InterruptedException();
syncPhase = R.string.sync_phase_fetch_entries;
App.log.info("Sync phase: " + context.getString(syncPhase));
fetchEntries();
if (Thread.interrupted())
throw new InterruptedException();
syncPhase = R.string.sync_phase_apply_remote_entries;
App.log.info("Sync phase: " + context.getString(syncPhase));
applyRemoteEntries();
} while (remoteEntries.size() == MAX_FETCH);
do {
/* Create journal entries out of local changes. */
if (Thread.interrupted())
throw new InterruptedException();
syncPhase = R.string.sync_phase_create_local_entries;
App.log.info("Sync phase: " + context.getString(syncPhase));
createLocalEntries();
if (Thread.interrupted())
throw new InterruptedException();
syncPhase = R.string.sync_phase_apply_local_entries;
App.log.info("Sync phase: " + context.getString(syncPhase));
/* FIXME: Skipping this now, because we already override with remote.
applyLocalEntries();
*/
if (Thread.interrupted())
throw new InterruptedException();
syncPhase = R.string.sync_phase_push_entries;
App.log.info("Sync phase: " + context.getString(syncPhase));
pushEntries();
} while (localEntries.size() == MAX_PUSH);
/* Cleanup and finalize changes */
if (Thread.interrupted())
throw new InterruptedException();
syncPhase = R.string.sync_phase_post_processing;
App.log.info("Sync phase: " + context.getString(syncPhase));
postProcess();
notifyUserOnSync();
App.log.info("Finished sync with CTag=" + remoteCTag);
} catch (IOException e) {
App.log.log(Level.WARNING, "I/O exception during sync, trying again later", e);
syncResult.stats.numIoExceptions++;
} catch (Exceptions.ServiceUnavailableException e) {
syncResult.stats.numIoExceptions++;
syncResult.delayUntil = (e.getRetryAfter() > 0) ? e.getRetryAfter() : Constants.DEFAULT_RETRY_DELAY;
} catch (InterruptedException e) {
// Restart sync if interrupted
syncResult.fullSyncRequested = true;
} catch (Exception | OutOfMemoryError e) {
if (e instanceof Exceptions.UnauthorizedException) {
syncResult.stats.numAuthExceptions++;
} else if (e instanceof Exceptions.HttpException) {
syncResult.stats.numParseExceptions++;
} else if (e instanceof CalendarStorageException || e instanceof ContactsStorageException) {
syncResult.databaseError = true;
} else if (e instanceof Exceptions.IntegrityException) {
syncResult.stats.numParseExceptions++;
} else {
syncResult.stats.numParseExceptions++;
}
notificationManager.setThrowable(e);
final Intent detailsIntent = notificationManager.getDetailsIntent();
detailsIntent.putExtra(KEY_ACCOUNT, account);
if (!(e instanceof Exceptions.UnauthorizedException)) {
detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority);
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase);
}
notificationManager.notify(getSyncErrorTitle(), context.getString(syncPhase));
}
}
private void notifyUserOnSync() {
Settings.ChangeNotification changeNotification =
new Settings(new ServiceDB.OpenHelper(context).getReadableDatabase())
.getChangeNotification(App.CHANGE_NOTIFICATION);
if (remoteEntries.isEmpty() ||
changeNotification.equals(Settings.ChangeNotification.NONE)) {
return;
}
NotificationHelper notificationHelper = new NotificationHelper(context,
String.valueOf(System.currentTimeMillis()), notificationId());
int deleted = 0;
int added = 0;
int changed = 0;
for (JournalEntryManager.Entry entry : remoteEntries) {
SyncEntry cEntry = SyncEntry.fromJournalEntry(crypto, entry);
SyncEntry.Actions action = cEntry.getAction();
switch (action) {
case ADD:
added++;
break;
case DELETE:
deleted++;
break;
case CHANGE:
changed++;
break;
}
}
Resources resources = context.getResources();
Intent intent = ViewCollectionActivity.newIntent(context, account, info);
notificationHelper.notify(getSyncSuccessfullyTitle(),
String.format(context.getString(R.string.sync_successfully_modified),
resources.getQuantityString(R.plurals.sync_successfully,
remoteEntries.size(), remoteEntries.size())),
String.format(context.getString(R.string.sync_successfully_modified_full),
resources.getQuantityString(R.plurals.sync_successfully,
added, added),
resources.getQuantityString(R.plurals.sync_successfully,
changed, changed),
resources.getQuantityString(R.plurals.sync_successfully,
deleted, deleted)),
intent);
}
/**
* Prepares synchronization (for instance, allocates necessary resources).
*
* @return whether actual synchronization is required / can be made. true = synchronization
* shall be continued, false = synchronization can be skipped
*/
protected boolean prepare() throws ContactsStorageException, CalendarStorageException {
return true;
}
abstract protected void processSyncEntry(SyncEntry cEntry) throws IOException, ContactsStorageException, CalendarStorageException, InvalidCalendarException;
private JournalEntity getJournalEntity() {
if (_journalEntity == null)
_journalEntity = JournalModel.Journal.fetch(data, info.getServiceEntity(data), info.uid);
return _journalEntity;
}
private void persistSyncEntry(String uid, SyncEntry syncEntry) {
EntryEntity entry = new EntryEntity();
entry.setUid(uid);
entry.setContent(syncEntry);
entry.setJournal(getJournalEntity());
data.insert(entry);
}
protected void applyLocalEntries() throws IOException, ContactsStorageException, CalendarStorageException, Exceptions.HttpException, InvalidCalendarException, InterruptedException {
// FIXME: Need a better strategy
// We re-apply local entries so our changes override whatever was written in the remote.
String strTotal = String.valueOf(localEntries.size());
int i = 0;
for (JournalEntryManager.Entry entry : localEntries) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
i++;
App.log.info("Processing (" + String.valueOf(i) + "/" + strTotal + ") " + entry.toString());
SyncEntry cEntry = SyncEntry.fromJournalEntry(crypto, entry);
if (cEntry.isAction(SyncEntry.Actions.DELETE)) {
continue;
}
App.log.info("Processing resource for journal entry");
processSyncEntry(cEntry);
}
}
protected void queryCapabilities() throws IOException, CalendarStorageException, ContactsStorageException {
}
protected void fetchEntries() throws Exceptions.HttpException, ContactsStorageException, CalendarStorageException, Exceptions.IntegrityException {
int count = data.count(EntryEntity.class).where(EntryEntity.JOURNAL.eq(getJournalEntity())).get().value();
if ((remoteCTag != null) && (count == 0)) {
// If we are updating an existing installation with no saved journal, we need to add
remoteEntries = journal.list(crypto, null, MAX_FETCH);
int i = 0;
for (JournalEntryManager.Entry entry : remoteEntries) {
SyncEntry cEntry = SyncEntry.fromJournalEntry(crypto, entry);
persistSyncEntry(entry.getUid(), cEntry);
i++;
if (remoteCTag.equals(entry.getUid())) {
remoteEntries.subList(0, i).clear();
break;
}
}
} else {
remoteEntries = journal.list(crypto, remoteCTag, MAX_FETCH);
}
App.log.info("Fetched " + String.valueOf(remoteEntries.size()) + " entries");
}
protected void applyRemoteEntries() throws IOException, ContactsStorageException, CalendarStorageException, InvalidCalendarException, InterruptedException {
// Process new vcards from server
String strTotal = String.valueOf(remoteEntries.size());
int i = 0;
for (JournalEntryManager.Entry entry : remoteEntries) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
i++;
App.log.info("Processing (" + String.valueOf(i) + "/" + strTotal + ") " + entry.toString());
SyncEntry cEntry = SyncEntry.fromJournalEntry(crypto, entry);
App.log.info("Processing resource for journal entry");
processSyncEntry(cEntry);
persistSyncEntry(entry.getUid(), cEntry);
remoteCTag = entry.getUid();
}
}
protected void pushEntries() throws Exceptions.HttpException, IOException, ContactsStorageException, CalendarStorageException {
// upload dirty contacts
int pushed = 0;
// FIXME: Deal with failure (someone else uploaded before we go here)
try {
if (!localEntries.isEmpty()) {
List<JournalEntryManager.Entry> entries = localEntries;
journal.create(entries, remoteCTag);
// Persist the entries after they've been pushed
for (JournalEntryManager.Entry entry : entries) {
SyncEntry cEntry = SyncEntry.fromJournalEntry(crypto, entry);
persistSyncEntry(entry.getUid(), cEntry);
}
remoteCTag = entries.get(entries.size() - 1).getUid();
pushed += entries.size();
}
} finally {
// FIXME: A bit fragile, we assume the order in createLocalEntries
int left = pushed;
for (LocalResource local : localDeleted) {
if (pushed-- <= 0) {
break;
}
local.delete();
}
if (left > 0) {
localDeleted.subList(0, Math.min(left, localDeleted.size())).clear();
}
left = pushed;
for (LocalResource local : localDirty) {
if (pushed-- <= 0) {
break;
}
App.log.info("Added/changed resource with UUID: " + local.getUuid());
local.clearDirty(local.getUuid());
}
if (left > 0) {
localDirty = Arrays.copyOfRange(localDirty, left, localDirty.length);
}
if (pushed > 0) {
App.log.severe("Unprocessed localentries left, this should never happen!");
}
}
}
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 : localDeleted) {
SyncEntry entry = new SyncEntry(local.getContent(), SyncEntry.Actions.DELETE);
JournalEntryManager.Entry tmp = new JournalEntryManager.Entry();
tmp.update(crypto, entry.toJson(), previousEntry);
previousEntry = tmp;
localEntries.add(previousEntry);
if (localEntries.size() == MAX_PUSH) {
return;
}
}
for (LocalResource local : localDirty) {
SyncEntry.Actions action;
if (local.isLocalOnly()) {
action = ADD;
} else {
action = SyncEntry.Actions.CHANGE;
}
SyncEntry entry = new SyncEntry(local.getContent(), action);
JournalEntryManager.Entry tmp = new JournalEntryManager.Entry();
tmp.update(crypto, entry.toJson(), previousEntry);
previousEntry = tmp;
localEntries.add(previousEntry);
if (localEntries.size() == MAX_PUSH) {
return;
}
}
}
/**
*/
protected void prepareLocal() throws CalendarStorageException, ContactsStorageException, FileNotFoundException {
remoteCTag = getJournalEntity().getLastUid(data);
localDeleted = processLocallyDeleted();
localDirty = localCollection.getDirty();
// This is done after fetching the local dirty so all the ones we are using will be prepared
prepareDirty();
}
/**
* Delete unpublished locally deleted, and return the rest.
* Checks Thread.interrupted() before each request to allow quick sync cancellation.
*/
private List<LocalResource> processLocallyDeleted() throws CalendarStorageException, ContactsStorageException {
LocalResource[] localList = localCollection.getDeleted();
List<LocalResource> ret = new ArrayList<>(localList.length);
for (LocalResource local : localList) {
if (Thread.interrupted())
return ret;
App.log.info(local.getUuid() + " has been deleted locally -> deleting from server");
ret.add(local);
syncResult.stats.numDeletes++;
}
return ret;
}
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
// assign file names and UIDs to new entries
App.log.info("Looking for local entries without a uuid");
for (LocalResource local : localDirty) {
if (local.getUuid() != null) {
continue;
}
App.log.fine("Found local record #" + local.getId() + " without file name; generating file name/UID if necessary");
local.prepareForUpload();
}
}
/**
* For post-processing of entries, for instance assigning groups.
*/
protected void postProcess() throws CalendarStorageException, ContactsStorageException {
}
}

@ -0,0 +1,523 @@
/*
* 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 com.etesync.syncadapter.syncadapter
import android.accounts.Account
import android.annotation.TargetApi
import android.content.Context
import android.content.Intent
import android.content.SyncResult
import android.content.res.Resources
import android.os.Bundle
import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.App
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.HttpClient
import com.etesync.syncadapter.NotificationHelper
import com.etesync.syncadapter.R
import com.etesync.syncadapter.journalmanager.Crypto
import com.etesync.syncadapter.journalmanager.Exceptions
import com.etesync.syncadapter.journalmanager.JournalEntryManager
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.EntryEntity
import com.etesync.syncadapter.model.JournalEntity
import com.etesync.syncadapter.model.JournalModel
import com.etesync.syncadapter.model.ServiceDB
import com.etesync.syncadapter.model.ServiceEntity
import com.etesync.syncadapter.model.Settings
import com.etesync.syncadapter.model.SyncEntry
import com.etesync.syncadapter.resource.LocalCollection
import com.etesync.syncadapter.resource.LocalResource
import com.etesync.syncadapter.ui.DebugInfoActivity
import com.etesync.syncadapter.ui.ViewCollectionActivity
import java.io.FileNotFoundException
import java.io.IOException
import java.util.ArrayList
import java.util.Arrays
import java.util.LinkedList
import java.util.Locale
import java.util.logging.Level
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.InvalidCalendarException
import at.bitfire.vcard4android.ContactsStorageException
import io.requery.Persistable
import io.requery.sql.EntityDataStore
import okhttp3.OkHttpClient
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
import com.etesync.syncadapter.model.SyncEntry.Actions.ADD
abstract class SyncManager @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class)
constructor(protected val context: Context, protected val account: Account, protected val settings: AccountSettings, protected val extras: Bundle, protected val authority: String, protected val syncResult: SyncResult, journalUid: String, protected val serviceType: CollectionInfo.Type, accountName: String) {
protected val notificationManager: NotificationHelper
protected val info: CollectionInfo
protected var localCollection: LocalCollection? = null
protected var httpClient: OkHttpClient
protected var journal: JournalEntryManager? = null
private var _journalEntity: JournalEntity? = null
private val crypto: Crypto.CryptoManager
private val data: EntityDataStore<Persistable>
/**
* remote CTag (uuid of the last entry on the server). We update it when we fetch/push and save when everything works.
*/
private var remoteCTag: String? = null
/**
* Syncable local journal entries.
*/
private var localEntries: MutableList<JournalEntryManager.Entry>? = null
/**
* Syncable remote journal entries (fetch from server).
*/
private var remoteEntries: List<JournalEntryManager.Entry>? = null
/**
* Dirty and deleted resources. We need to save them so we safely ignore ones that were added after we started.
*/
private var localDeleted: List<LocalResource>? = null
protected var localDirty: Array<LocalResource> = arrayOf()
protected abstract val syncErrorTitle: String
protected abstract val syncSuccessfullyTitle: String
private val journalEntity: JournalEntity
get() = JournalModel.Journal.fetch(data, info.getServiceEntity(data), info.uid)
init {
// create HttpClient with given logger
httpClient = HttpClient.create(context, settings)
data = (context.applicationContext as App).data
val serviceEntity = JournalModel.Service.fetch(data, accountName, serviceType)
info = JournalEntity.fetch(data, serviceEntity, journalUid)!!.info
// dismiss previous error notifications
notificationManager = NotificationHelper(context, journalUid, notificationId())
notificationManager.cancel()
App.log.info(String.format(Locale.getDefault(), "Syncing collection %s (version: %d)", journalUid, info.version))
if (journalEntity.encryptedKey != null) {
crypto = Crypto.CryptoManager(info.version, settings.keyPair!!, journalEntity.encryptedKey)
} else {
crypto = Crypto.CryptoManager(info.version, settings.password(), info.uid)
}
}
protected abstract fun notificationId(): Int
@TargetApi(21)
fun performSync() {
var syncPhase = R.string.sync_phase_prepare
try {
App.log.info("Sync phase: " + context.getString(syncPhase))
if (!prepare()) {
App.log.info("No reason to synchronize, aborting")
return
}
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_query_capabilities
App.log.info("Sync phase: " + context.getString(syncPhase))
queryCapabilities()
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_prepare_local
App.log.info("Sync phase: " + context.getString(syncPhase))
prepareLocal()
do {
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_fetch_entries
App.log.info("Sync phase: " + context.getString(syncPhase))
fetchEntries()
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_apply_remote_entries
App.log.info("Sync phase: " + context.getString(syncPhase))
applyRemoteEntries()
} while (remoteEntries!!.size == MAX_FETCH)
do {
/* Create journal entries out of local changes. */
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_create_local_entries
App.log.info("Sync phase: " + context.getString(syncPhase))
createLocalEntries()
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_apply_local_entries
App.log.info("Sync phase: " + context.getString(syncPhase))
/* FIXME: Skipping this now, because we already override with remote.
applyLocalEntries();
*/
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_push_entries
App.log.info("Sync phase: " + context.getString(syncPhase))
pushEntries()
} while (localEntries!!.size == MAX_PUSH)
/* Cleanup and finalize changes */
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_post_processing
App.log.info("Sync phase: " + context.getString(syncPhase))
postProcess()
notifyUserOnSync()
App.log.info("Finished sync with CTag=" + remoteCTag!!)
} catch (e: IOException) {
App.log.log(Level.WARNING, "I/O exception during sync, trying again later", e)
syncResult.stats.numIoExceptions++
} catch (e: Exceptions.ServiceUnavailableException) {
syncResult.stats.numIoExceptions++
syncResult.delayUntil = if (e.retryAfter > 0) e.retryAfter else Constants.DEFAULT_RETRY_DELAY
} catch (e: InterruptedException) {
// Restart sync if interrupted
syncResult.fullSyncRequested = true
} catch (e: Exception) {
if (e is Exceptions.UnauthorizedException) {
syncResult.stats.numAuthExceptions++
} else if (e is Exceptions.HttpException) {
syncResult.stats.numParseExceptions++
} else if (e is CalendarStorageException || e is ContactsStorageException) {
syncResult.databaseError = true
} else if (e is Exceptions.IntegrityException) {
syncResult.stats.numParseExceptions++
} else {
syncResult.stats.numParseExceptions++
}
notificationManager.setThrowable(e)
val detailsIntent = notificationManager.detailsIntent
detailsIntent.putExtra(KEY_ACCOUNT, account)
if (e !is Exceptions.UnauthorizedException) {
detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority)
detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase)
}
notificationManager.notify(syncErrorTitle, context.getString(syncPhase))
} catch (e: OutOfMemoryError) {
if (e is Exceptions.HttpException) {
syncResult.stats.numParseExceptions++
} else if (e is CalendarStorageException || e is ContactsStorageException) {
syncResult.databaseError = true
} else {
syncResult.stats.numParseExceptions++
}
notificationManager.setThrowable(e)
val detailsIntent = notificationManager.detailsIntent
detailsIntent.putExtra(KEY_ACCOUNT, account)
notificationManager.notify(syncErrorTitle, context.getString(syncPhase))
}
}
private fun notifyUserOnSync() {
val changeNotification = Settings(ServiceDB.OpenHelper(context).readableDatabase)
.getChangeNotification(App.CHANGE_NOTIFICATION)
if (remoteEntries!!.isEmpty() || changeNotification == Settings.ChangeNotification.NONE) {
return
}
val notificationHelper = NotificationHelper(context,
System.currentTimeMillis().toString(), notificationId())
var deleted = 0
var added = 0
var changed = 0
for (entry in remoteEntries!!) {
val cEntry = SyncEntry.fromJournalEntry(crypto, entry)
val action = cEntry.action
when (action) {
ADD -> added++
SyncEntry.Actions.DELETE -> deleted++
SyncEntry.Actions.CHANGE -> changed++
}
}
val resources = context.resources
val intent = ViewCollectionActivity.newIntent(context, account, info)
notificationHelper.notify(syncSuccessfullyTitle,
String.format(context.getString(R.string.sync_successfully_modified),
resources.getQuantityString(R.plurals.sync_successfully,
remoteEntries!!.size, remoteEntries!!.size)),
String.format(context.getString(R.string.sync_successfully_modified_full),
resources.getQuantityString(R.plurals.sync_successfully,
added, added),
resources.getQuantityString(R.plurals.sync_successfully,
changed, changed),
resources.getQuantityString(R.plurals.sync_successfully,
deleted, deleted)),
intent)
}
/**
* Prepares synchronization (for instance, allocates necessary resources).
*
* @return whether actual synchronization is required / can be made. true = synchronization
* shall be continued, false = synchronization can be skipped
*/
@Throws(ContactsStorageException::class, CalendarStorageException::class)
protected open fun prepare(): Boolean {
return true
}
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, InvalidCalendarException::class)
protected abstract fun processSyncEntry(cEntry: SyncEntry)
private fun persistSyncEntry(uid: String?, syncEntry: SyncEntry) {
val entry = EntryEntity()
entry.uid = uid
entry.content = syncEntry
entry.journal = journalEntity
data.insert(entry)
}
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, Exceptions.HttpException::class, InvalidCalendarException::class, InterruptedException::class)
protected fun applyLocalEntries() {
// FIXME: Need a better strategy
// We re-apply local entries so our changes override whatever was written in the remote.
val strTotal = localEntries!!.size.toString()
var i = 0
for (entry in localEntries!!) {
if (Thread.interrupted()) {
throw InterruptedException()
}
i++
App.log.info("Processing (" + i.toString() + "/" + strTotal + ") " + entry.toString())
val cEntry = SyncEntry.fromJournalEntry(crypto, entry)
if (cEntry.isAction(SyncEntry.Actions.DELETE)) {
continue
}
App.log.info("Processing resource for journal entry")
processSyncEntry(cEntry)
}
}
@Throws(IOException::class, CalendarStorageException::class, ContactsStorageException::class)
protected fun queryCapabilities() {
}
@Throws(Exceptions.HttpException::class, ContactsStorageException::class, CalendarStorageException::class, Exceptions.IntegrityException::class)
protected fun fetchEntries() {
val count = data.count(EntryEntity::class.java).where(EntryEntity.JOURNAL.eq(journalEntity)).get().value()
if (remoteCTag != null && count == 0) {
// If we are updating an existing installation with no saved journal, we need to add
remoteEntries = journal!!.list(crypto, null, MAX_FETCH)
var i = 0
for (entry in remoteEntries!!) {
val cEntry = SyncEntry.fromJournalEntry(crypto, entry)
persistSyncEntry(entry.uid, cEntry)
i++
if (remoteCTag == entry.uid) {
remoteEntries = remoteEntries?.drop(i)
break
}
}
} else {
remoteEntries = journal!!.list(crypto, remoteCTag, MAX_FETCH)
}
App.log.info("Fetched " + remoteEntries!!.size.toString() + " entries")
}
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, InvalidCalendarException::class, InterruptedException::class)
protected fun applyRemoteEntries() {
// Process new vcards from server
val strTotal = remoteEntries!!.size.toString()
var i = 0
for (entry in remoteEntries!!) {
if (Thread.interrupted()) {
throw InterruptedException()
}
i++
App.log.info("Processing (" + i.toString() + "/" + strTotal + ") " + entry.toString())
val cEntry = SyncEntry.fromJournalEntry(crypto, entry)
App.log.info("Processing resource for journal entry")
processSyncEntry(cEntry)
persistSyncEntry(entry.uid, cEntry)
remoteCTag = entry.uid
}
}
@Throws(Exceptions.HttpException::class, IOException::class, ContactsStorageException::class, CalendarStorageException::class)
protected fun pushEntries() {
// upload dirty contacts
var pushed = 0
// FIXME: Deal with failure (someone else uploaded before we go here)
try {
if (!localEntries!!.isEmpty()) {
val entries = localEntries
journal!!.create(entries!!, remoteCTag)
// Persist the entries after they've been pushed
for (entry in entries) {
val cEntry = SyncEntry.fromJournalEntry(crypto, entry)
persistSyncEntry(entry.uid, cEntry)
}
remoteCTag = entries[entries.size - 1].uid
pushed += entries.size
}
} finally {
// FIXME: A bit fragile, we assume the order in createLocalEntries
var left = pushed
for (local in localDeleted!!) {
if (pushed-- <= 0) {
break
}
local.delete()
}
if (left > 0) {
localDeleted?.drop(left)
}
left = pushed
for (local in localDirty) {
if (pushed-- <= 0) {
break
}
App.log.info("Added/changed resource with UUID: " + local.uuid)
local.clearDirty(local.uuid)
}
if (left > 0) {
localDirty = Arrays.copyOfRange(localDirty, left, localDirty.size)
}
if (pushed > 0) {
App.log.severe("Unprocessed localentries left, this should never happen!")
}
}
}
@Throws(CalendarStorageException::class, ContactsStorageException::class, IOException::class)
protected open fun createLocalEntries() {
localEntries = LinkedList()
// Not saving, just creating a fake one until we load it from a local db
var previousEntry: JournalEntryManager.Entry? = if (remoteCTag != null) JournalEntryManager.Entry.getFakeWithUid(remoteCTag!!) else null
for (local in localDeleted!!) {
val entry = SyncEntry(local.content, SyncEntry.Actions.DELETE)
val tmp = JournalEntryManager.Entry()
tmp.update(crypto, entry.toJson(), previousEntry!!)
previousEntry = tmp
localEntries!!.add(previousEntry)
if (localEntries!!.size == MAX_PUSH) {
return
}
}
for (local in localDirty) {
val action: SyncEntry.Actions
if (local.isLocalOnly) {
action = ADD
} else {
action = SyncEntry.Actions.CHANGE
}
val entry = SyncEntry(local.content, action)
val tmp = JournalEntryManager.Entry()
tmp.update(crypto, entry.toJson(), previousEntry!!)
previousEntry = tmp
localEntries!!.add(previousEntry)
if (localEntries!!.size == MAX_PUSH) {
return
}
}
}
/**
*/
@Throws(CalendarStorageException::class, ContactsStorageException::class, FileNotFoundException::class)
protected fun prepareLocal() {
remoteCTag = journalEntity.getLastUid(data)
localDeleted = processLocallyDeleted()
localDirty = localCollection!!.dirty
// This is done after fetching the local dirty so all the ones we are using will be prepared
prepareDirty()
}
/**
* Delete unpublished locally deleted, and return the rest.
* Checks Thread.interrupted() before each request to allow quick sync cancellation.
*/
@Throws(CalendarStorageException::class, ContactsStorageException::class)
private fun processLocallyDeleted(): List<LocalResource> {
val localList = localCollection!!.deleted
val ret = ArrayList<LocalResource>(localList.size)
for (local in localList) {
if (Thread.interrupted())
return ret
App.log.info(local.uuid + " has been deleted locally -> deleting from server")
ret.add(local)
syncResult.stats.numDeletes++
}
return ret
}
@Throws(CalendarStorageException::class, ContactsStorageException::class)
protected open fun prepareDirty() {
// assign file names and UIDs to new entries
App.log.info("Looking for local entries without a uuid")
for (local in localDirty) {
if (local.uuid != null) {
continue
}
App.log.fine("Found local record #" + local.id + " without file name; generating file name/UID if necessary")
local.prepareForUpload()
}
}
/**
* For post-processing of entries, for instance assigning groups.
*/
@Throws(CalendarStorageException::class, ContactsStorageException::class)
protected open fun postProcess() {
}
companion object {
private val MAX_FETCH = 50
private val MAX_PUSH = 30
}
}
Loading…
Cancel
Save