mirror of
https://github.com/etesync/android
synced 2025-01-23 06:01:01 +00:00
Kotlin: more kotlin migration.
This commit is contained in:
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…
Reference in New Issue
Block a user