mirror of
https://github.com/etesync/android
synced 2025-06-24 17:08:50 +00:00
Kotlin: more kotlin migration.
This commit is contained in:
parent
959bc4992b
commit
943611a511
@ -171,7 +171,7 @@ object Crypto {
|
|||||||
return cipher
|
return cipher
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun decrypt(_data: ByteArray): ByteArray? {
|
fun decrypt(_data: ByteArray): ByteArray? {
|
||||||
val iv = Arrays.copyOfRange(_data, 0, blockSize)
|
val iv = Arrays.copyOfRange(_data, 0, blockSize)
|
||||||
val data = Arrays.copyOfRange(_data, blockSize, _data.size)
|
val data = Arrays.copyOfRange(_data, blockSize, _data.size)
|
||||||
|
|
||||||
@ -194,7 +194,7 @@ object Crypto {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun encrypt(data: ByteArray): ByteArray? {
|
fun encrypt(data: ByteArray): ByteArray? {
|
||||||
val iv = ByteArray(blockSize)
|
val iv = ByteArray(blockSize)
|
||||||
random.nextBytes(iv)
|
random.nextBytes(iv)
|
||||||
|
|
||||||
@ -214,7 +214,7 @@ object Crypto {
|
|||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun hmac(data: ByteArray): ByteArray {
|
fun hmac(data: ByteArray): ByteArray {
|
||||||
return if (version.toInt() == 1) {
|
return if (version.toInt() == 1) {
|
||||||
hmac256(hmacKey, data)
|
hmac256(hmacKey, data)
|
||||||
} else {
|
} else {
|
||||||
|
@ -75,7 +75,7 @@ class JournalEntryManager(httpClient: OkHttpClient, remote: HttpUrl, val uid: St
|
|||||||
|
|
||||||
class Entry : BaseManager.Base() {
|
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)
|
setContent(crypto, content)
|
||||||
uid = calculateHmac(crypto, previous)
|
uid = calculateHmac(crypto, previous)
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ class UserInfoManager(httpClient: OkHttpClient, remote: HttpUrl) : BaseManager()
|
|||||||
return crypto.decrypt(content)
|
return crypto.decrypt(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun setContent(crypto: Crypto.CryptoManager, rawContent: ByteArray) {
|
fun setContent(crypto: Crypto.CryptoManager, rawContent: ByteArray) {
|
||||||
val content = crypto.encrypt(rawContent)
|
val content = crypto.encrypt(rawContent)
|
||||||
this.content = Arrays.concatenate(calculateHmac(crypto, content), content)
|
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