1
0
mirror of https://github.com/etesync/android synced 2025-07-30 02:18:04 +00:00

Merge: add support for multiple address books

Android allows only having one address book per account, so until now
users of EteSync were only able to have one address book. This was
always an annoying limitation, but even more so now that journal sharing
is implemented.

Luckily, DAVdroid recently implemented multiple account support by
creating sub-accounts for address books.

This merge is based on the changes done in DAVdroid but was heavily
adjusted for EteSync.
This commit is contained in:
Tom Hacohen 2017-04-27 11:54:46 +01:00
commit eae4e14b1d
58 changed files with 880 additions and 236 deletions

View File

@ -55,11 +55,11 @@
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<receiver <receiver
android:name=".App$ReinitLoggingReceiver" android:name=".App$ReinitSettingsReceiver"
android:exported="false" android:exported="false"
android:process=":sync"> android:process=":sync">
<intent-filter> <intent-filter>
<action android:name="com.etesync.syncadapter.REINIT_LOGGER"/> <action android:name="at.bitfire.davdroid.REINIT_SETTINGS"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver <receiver
@ -91,6 +91,54 @@
android:name="android.accounts.AccountAuthenticator" android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/account_authenticator"/> android:resource="@xml/account_authenticator"/>
</service> </service>
<!-- Normal account -->
<service
android:name=".syncadapter.CalendarsSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_calendars" />
</service>
<!-- Address book account -->
<service
android:name=".syncadapter.NullAuthenticatorService"
android:exported="false">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/account_authenticator_address_book"/>
</service>
<provider
android:authorities="@string/address_books_authority"
android:exported="false"
android:label="@string/address_books_authority_title"
android:name=".syncadapter.AddressBookProvider"
android:multiprocess="false"/>
<service
android:name=".syncadapter.AddressBooksSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_address_books"/>
</service>
<service <service
android:name=".syncadapter.ContactsSyncAdapterService" android:name=".syncadapter.ContactsSyncAdapterService"
android:exported="true" android:exported="true"
@ -107,19 +155,6 @@
android:name="android.provider.CONTACTS_STRUCTURE" android:name="android.provider.CONTACTS_STRUCTURE"
android:resource="@xml/contacts"/> android:resource="@xml/contacts"/>
</service> </service>
<service
android:name=".syncadapter.CalendarsSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_calendars" />
</service>
<service <service

View File

@ -12,28 +12,36 @@ import android.accounts.AccountManager;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ContentProviderClient;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.PeriodicSync; import android.content.PeriodicSync;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcel;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.etesync.syncadapter.journalmanager.Crypto; import com.etesync.syncadapter.journalmanager.Crypto;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.resource.LocalAddressBook;
import com.etesync.syncadapter.utils.Base64; import com.etesync.syncadapter.utils.Base64;
import java.lang.reflect.Method;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.List; import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
import at.bitfire.vcard4android.ContactsStorageException;
import at.bitfire.vcard4android.GroupMethod; import at.bitfire.vcard4android.GroupMethod;
import lombok.Cleanup;
public class AccountSettings { public class AccountSettings {
private final static int CURRENT_VERSION = 1; private final static int CURRENT_VERSION = 2;
private final static String private final static String
KEY_SETTINGS_VERSION = "version", KEY_SETTINGS_VERSION = "version",
KEY_URI = "uri", KEY_URI = "uri",
@ -96,12 +104,11 @@ public class AccountSettings {
} }
} }
public static Bundle initialUserData(URI uri, String userName) { // XXX: Workaround a bug in Android where passing a bundle to addAccountExplicitly doesn't work.
Bundle bundle = new Bundle(); public static void setUserData(AccountManager accountManager, Account account, URI uri, String userName) {
bundle.putString(KEY_SETTINGS_VERSION, String.valueOf(CURRENT_VERSION)); accountManager.setUserData(account, KEY_SETTINGS_VERSION, String.valueOf(CURRENT_VERSION));
bundle.putString(KEY_USERNAME, userName); accountManager.setUserData(account, KEY_USERNAME, userName);
bundle.putString(KEY_URI, uri.toString()); accountManager.setUserData(account, KEY_URI, uri.toString());
return bundle;
} }
@ -234,16 +241,82 @@ public class AccountSettings {
// update from previous account settings // update from previous account settings
private void update(int fromVersion) { private void update(int fromVersion) {
for (int toVersion = fromVersion + 1; toVersion <= CURRENT_VERSION; toVersion++) { int toVersion = CURRENT_VERSION;
App.log.info("Updating account " + account.name + " from version " + fromVersion + " to " + toVersion); App.log.info("Updating account " + account.name + " from version " + fromVersion + " to " + toVersion);
try {
updateInner(fromVersion);
accountManager.setUserData(account, KEY_SETTINGS_VERSION, String.valueOf(toVersion));
} catch (Exception e) {
App.log.log(Level.SEVERE, "Couldn't update account settings", e);
}
}
private void updateInner(int fromVersion) throws ContactsStorageException {
if (fromVersion < 2) {
@Cleanup("release") ContentProviderClient provider = context.getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY);
if (provider == null)
// no access to contacts provider
return;
// don't run syncs during the migration
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0);
ContentResolver.setIsSyncable(account, App.getAddressBooksAuthority(), 0);
ContentResolver.cancelSync(account, null);
try { try {
Method updateProc = getClass().getDeclaredMethod("update_" + fromVersion + "_" + toVersion); // get previous address book settings (including URL)
updateProc.invoke(this); @Cleanup("recycle") Parcel parcel = Parcel.obtain();
accountManager.setUserData(account, KEY_SETTINGS_VERSION, String.valueOf(toVersion)); byte[] raw = ContactsContract.SyncState.get(provider, account);
} catch (Exception e) { if (raw == null)
App.log.log(Level.SEVERE, "Couldn't update account settings", e); App.log.info("No contacts sync state, ignoring account");
else {
parcel.unmarshall(raw, 0, raw.length);
parcel.setDataPosition(0);
Bundle params = parcel.readBundle();
String url = params.getString("url");
if (url == null)
App.log.info("No address book URL, ignoring account");
else {
// create new address book
CollectionInfo info = new CollectionInfo();
info.type = CollectionInfo.Type.ADDRESS_BOOK;
info.uid = url;
info.displayName = account.name;
App.log.log(Level.INFO, "Creating new address book account", url);
Account addressBookAccount = new Account(LocalAddressBook.accountName(account, info), App.getAddressBookAccountType());
if (!accountManager.addAccountExplicitly(addressBookAccount, null, null))
throw new ContactsStorageException("Couldn't create address book account");
LocalAddressBook.setUserData(accountManager, addressBookAccount, account, info.uid);
LocalAddressBook addressBook = new LocalAddressBook(context, addressBookAccount, provider);
// move contacts to new address book
App.log.info("Moving contacts from " + account + " to " + addressBookAccount);
ContentValues newAccount = new ContentValues(2);
newAccount.put(ContactsContract.RawContacts.ACCOUNT_NAME, addressBookAccount.name);
newAccount.put(ContactsContract.RawContacts.ACCOUNT_TYPE, addressBookAccount.type);
int affected = provider.update(ContactsContract.RawContacts.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type)
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build(),
newAccount,
ContactsContract.RawContacts.ACCOUNT_NAME + "=? AND " + ContactsContract.RawContacts.ACCOUNT_TYPE + "=?",
new String[]{account.name, account.type});
App.log.info(affected + " contacts moved to new address book");
}
ContactsContract.SyncState.set(provider, account, null);
}
} catch (RemoteException e) {
throw new ContactsStorageException("Couldn't migrate contacts to new address book", e);
} }
fromVersion = toVersion;
// update version number so that further syncs don't repeat the migration
accountManager.setUserData(account, KEY_SETTINGS_VERSION, "6");
// request sync of new address book account
ContentResolver.setIsSyncable(account, App.getAddressBooksAuthority(), 1);
setSyncInterval(App.getAddressBooksAuthority(), Constants.DEFAULT_SYNC_INTERVAL);
} }
} }
@ -256,7 +329,7 @@ public class AccountSettings {
// peek into AccountSettings to initiate a possible migration // peek into AccountSettings to initiate a possible migration
AccountManager accountManager = AccountManager.get(context); AccountManager accountManager = AccountManager.get(context);
for (Account account : accountManager.getAccountsByType(Constants.ACCOUNT_TYPE)) for (Account account : accountManager.getAccountsByType(App.getAccountType()))
try { try {
App.log.info("Checking account " + account.name); App.log.info("Checking account " + account.name);
new AccountSettings(context, account); new AccountSettings(context, account);

View File

@ -19,6 +19,7 @@ import android.os.IBinder;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import com.etesync.syncadapter.model.ServiceEntity; import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.resource.LocalAddressBook;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.HashSet; import java.util.HashSet;
@ -26,7 +27,9 @@ import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.logging.Level;
import at.bitfire.vcard4android.ContactsStorageException;
import io.requery.Persistable; import io.requery.Persistable;
import io.requery.sql.EntityDataStore; import io.requery.sql.EntityDataStore;
@ -100,17 +103,30 @@ public class AccountUpdateService extends Service {
void cleanupAccounts() { void cleanupAccounts() {
App.log.info("Cleaning up orphaned accounts"); App.log.info("Cleaning up orphaned accounts");
List<String> sqlAccountNames = new LinkedList<>(); List<String> accountNames = new LinkedList<>();
AccountManager am = AccountManager.get(this); AccountManager am = AccountManager.get(this);
for (Account account : am.getAccountsByType(Constants.ACCOUNT_TYPE)) for (Account account : am.getAccountsByType(App.getAccountType())) {
sqlAccountNames.add(account.name); accountNames.add(account.name);
}
EntityDataStore<Persistable> data = ((App) getApplication()).getData(); EntityDataStore<Persistable> data = ((App) getApplication()).getData();
if (sqlAccountNames.isEmpty()) { // delete orphaned address book accounts
for (Account addrBookAccount : am.getAccountsByType(App.getAddressBookAccountType())) {
LocalAddressBook addressBook = new LocalAddressBook(this, addrBookAccount, null);
try {
if (!accountNames.contains(addressBook.getMainAccount().name))
addressBook.delete();
} catch(ContactsStorageException e) {
App.log.log(Level.SEVERE, "Couldn't get address book main account", e);
}
}
if (accountNames.isEmpty()) {
data.delete(ServiceEntity.class).get().value(); data.delete(ServiceEntity.class).get().value();
} else { } else {
data.delete(ServiceEntity.class).where(ServiceEntity.ACCOUNT.notIn(sqlAccountNames)).get().value(); data.delete(ServiceEntity.class).where(ServiceEntity.ACCOUNT.notIn(accountNames)).get().value();
} }
} }
} }

View File

@ -78,8 +78,6 @@ import okhttp3.internal.tls.OkHostnameVerifier;
public class App extends Application { public class App extends Application {
public static final String FLAVOR_GOOGLE_PLAY = "gplay";
public static final String public static final String
DISTRUST_SYSTEM_CERTIFICATES = "distrustSystemCerts", DISTRUST_SYSTEM_CERTIFICATES = "distrustSystemCerts",
LOG_TO_EXTERNAL_STORAGE = "logToExternalStorage", LOG_TO_EXTERNAL_STORAGE = "logToExternalStorage",
@ -90,6 +88,9 @@ public class App extends Application {
public static final String OVERRIDE_PROXY_HOST_DEFAULT = "localhost"; public static final String OVERRIDE_PROXY_HOST_DEFAULT = "localhost";
public static final int OVERRIDE_PROXY_PORT_DEFAULT = 8118; public static final int OVERRIDE_PROXY_PORT_DEFAULT = 8118;
@Getter
private static String appName;
@Getter @Getter
private CustomCertManager certManager; private CustomCertManager certManager;
@ -107,6 +108,13 @@ public class App extends Application {
at.bitfire.cert4android.Constants.log = Logger.getLogger("syncadapter.cert4android"); at.bitfire.cert4android.Constants.log = Logger.getLogger("syncadapter.cert4android");
} }
@Getter
private static String accountType;
@Getter
private static String addressBookAccountType;
@Getter
private static String addressBooksAuthority;
@Override @Override
@SuppressLint("HardwareIds") @SuppressLint("HardwareIds")
public void onCreate() { public void onCreate() {
@ -117,6 +125,11 @@ public class App extends Application {
initPrefVersion(); initPrefVersion();
uidGenerator = new UidGenerator(null, android.provider.Settings.Secure.getString(getContentResolver(), android.provider.Settings.Secure.ANDROID_ID)); uidGenerator = new UidGenerator(null, android.provider.Settings.Secure.getString(getContentResolver(), android.provider.Settings.Secure.ANDROID_ID));
appName = getString(R.string.app_name);
accountType = getString(R.string.account_type);
addressBookAccountType = getString(R.string.account_type_address_book);
addressBooksAuthority = getString(R.string.address_books_authority);
} }
public void reinitCertManager() { public void reinitCertManager() {
@ -205,11 +218,13 @@ public class App extends Application {
} }
public static class ReinitLoggingReceiver extends BroadcastReceiver { public static class ReinitSettingsReceiver extends BroadcastReceiver {
public static final String ACTION_REINIT_SETTINGS = BuildConfig.APPLICATION_ID + ".REINIT_SETTINGS";
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
log.info("Received broadcast: re-initializing logger"); log.info("Received broadcast: re-initializing settings (logger/cert manager)");
App app = (App)context.getApplicationContext(); App app = (App)context.getApplicationContext();
app.reinitLogger(); app.reinitLogger();
@ -299,18 +314,23 @@ public class App extends Application {
if (fromVersion < 7) { if (fromVersion < 7) {
/* Fix all of the etags to be non-null */ /* Fix all of the etags to be non-null */
AccountManager am = AccountManager.get(this); AccountManager am = AccountManager.get(this);
for (Account account : am.getAccountsByType(Constants.ACCOUNT_TYPE)) { for (Account account : am.getAccountsByType(App.getAccountType())) {
try { try {
// Generate account settings to make sure account is migrated.
new AccountSettings(this, account);
LocalCalendar calendars[] = (LocalCalendar[]) LocalCalendar.find(account, this.getContentResolver().acquireContentProviderClient(CalendarContract.CONTENT_URI), LocalCalendar calendars[] = (LocalCalendar[]) LocalCalendar.find(account, this.getContentResolver().acquireContentProviderClient(CalendarContract.CONTENT_URI),
LocalCalendar.Factory.INSTANCE, null, null); LocalCalendar.Factory.INSTANCE, null, null);
for (LocalCalendar calendar : calendars) { for (LocalCalendar calendar : calendars) {
calendar.fixEtags(); calendar.fixEtags();
} }
} catch (CalendarStorageException e) { } catch (CalendarStorageException|InvalidAccountException e) {
e.printStackTrace(); e.printStackTrace();
} }
}
LocalAddressBook addressBook = new LocalAddressBook(account, this.getContentResolver().acquireContentProviderClient(ContactsContract.Contacts.CONTENT_URI)); for (Account account : am.getAccountsByType(App.getAddressBookAccountType())) {
LocalAddressBook addressBook = new LocalAddressBook(this, account, this.getContentResolver().acquireContentProviderClient(ContactsContract.Contacts.CONTENT_URI));
try { try {
addressBook.fixEtags(); addressBook.fixEtags();
} catch (ContactsStorageException e) { } catch (ContactsStorageException e) {

View File

@ -13,9 +13,6 @@ import static com.etesync.syncadapter.BuildConfig.DEBUG_REMOTE_URL;
public class Constants { public class Constants {
public static final String
ACCOUNT_TYPE = "com.etesync.syncadapter";
// notification IDs // notification IDs
public final static int public final static int
NOTIFICATION_ACCOUNT_SETTINGS_UPDATED = 0, NOTIFICATION_ACCOUNT_SETTINGS_UPDATED = 0,

View File

@ -8,13 +8,15 @@
package com.etesync.syncadapter; package com.etesync.syncadapter;
import android.accounts.Account;
import android.content.Context; import android.content.Context;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.os.Build; import android.os.Build;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.Settings;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Proxy; import java.net.Proxy;
@ -25,8 +27,6 @@ import java.util.concurrent.TimeUnit;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.Settings;
import okhttp3.Interceptor; import okhttp3.Interceptor;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
@ -41,7 +41,7 @@ public class HttpClient {
static { static {
String date = new SimpleDateFormat("yyyy/MM/dd", Locale.US).format(new Date(BuildConfig.buildTime)); String date = new SimpleDateFormat("yyyy/MM/dd", Locale.US).format(new Date(BuildConfig.buildTime));
userAgent = "EteSync/" + BuildConfig.VERSION_NAME + " (" + date + "; okhttp3) Android/" + Build.VERSION.RELEASE; userAgent = App.getAppName() + "/" + BuildConfig.VERSION_NAME + " (" + date + "; okhttp3) Android/" + Build.VERSION.RELEASE;
} }
private HttpClient() { private HttpClient() {
@ -56,9 +56,7 @@ public class HttpClient {
return builder.build(); return builder.build();
} }
public static OkHttpClient create(@Nullable Context context, @NonNull Account account, @NonNull final Logger logger) throws InvalidAccountException { public static OkHttpClient create(@Nullable Context context, @NonNull AccountSettings settings, @NonNull final Logger logger) {
// use account settings for authentication
AccountSettings settings = new AccountSettings(context, account);
return create(context, logger, Constants.serviceUrl.getHost(), settings.getAuthToken()); return create(context, logger, Constants.serviceUrl.getHost(), settings.getAuthToken());
} }
@ -66,8 +64,8 @@ public class HttpClient {
return defaultBuilder(context, logger).build(); return defaultBuilder(context, logger).build();
} }
public static OkHttpClient create(@NonNull Context context, @NonNull Account account) throws InvalidAccountException { public static OkHttpClient create(@NonNull Context context, @NonNull AccountSettings settings) {
return create(context, account, App.log); return create(context, settings, App.log);
} }
public static OkHttpClient create(@Nullable Context context) { public static OkHttpClient create(@Nullable Context context) {

View File

@ -68,19 +68,11 @@ public class JournalModel {
} }
public static List<JournalEntity> getJournals(EntityDataStore<Persistable> data, ServiceEntity serviceEntity) { public static List<JournalEntity> getJournals(EntityDataStore<Persistable> data, ServiceEntity serviceEntity) {
return data.select(JournalEntity.class).where(JournalEntity.SERVICE_MODEL.eq(serviceEntity).and(JournalEntity.DELETED.eq(false))).get().toList(); List<JournalEntity> ret = data.select(JournalEntity.class).where(JournalEntity.SERVICE_MODEL.eq(serviceEntity).and(JournalEntity.DELETED.eq(false))).get().toList();
} for (JournalEntity journal : ret) {
public static List<CollectionInfo> getCollections(EntityDataStore<Persistable> data, ServiceEntity serviceEntity) {
List<CollectionInfo> ret = new LinkedList<>();
List<JournalEntity> journals = getJournals(data, serviceEntity);
for (JournalEntity journal : journals) {
// FIXME: For some reason this isn't always being called, manually do it here. // FIXME: For some reason this isn't always being called, manually do it here.
journal.afterLoad(); journal.afterLoad();
ret.add(journal.getInfo());
} }
return ret; return ret;
} }

View File

@ -8,25 +8,35 @@
package com.etesync.syncadapter.resource; package com.etesync.syncadapter.resource;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.content.ContentProviderClient; import android.content.ContentProviderClient;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcel;
import android.os.RemoteException; import android.os.RemoteException;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership; import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.Groups; import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.RawContacts;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.os.OperationCanceledException;
import com.etesync.syncadapter.App; import com.etesync.syncadapter.App;
import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.utils.AndroidCompat;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
@ -45,9 +55,11 @@ import lombok.Cleanup;
public class LocalAddressBook extends AndroidAddressBook implements LocalCollection { public class LocalAddressBook extends AndroidAddressBook implements LocalCollection {
protected static final String protected static final String
SYNC_STATE_CTAG = "ctag", USER_DATA_MAIN_ACCOUNT_TYPE = "real_account_type",
SYNC_STATE_URL = "url"; USER_DATA_MAIN_ACCOUNT_NAME = "real_account_name",
USER_DATA_URL = "url";
protected final Context context;
private final Bundle syncState = new Bundle(); private final Bundle syncState = new Bundle();
/** /**
@ -58,8 +70,85 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
public boolean includeGroups = true; public boolean includeGroups = true;
public LocalAddressBook(Account account, ContentProviderClient provider) { public static LocalAddressBook[] find(@NonNull Context context, @NonNull ContentProviderClient provider, @Nullable Account mainAccount) throws ContactsStorageException {
AccountManager accountManager = AccountManager.get(context);
List<LocalAddressBook> result = new LinkedList<>();
for (Account account : accountManager.getAccountsByType(App.getAddressBookAccountType())) {
LocalAddressBook addressBook = new LocalAddressBook(context, account, provider);
if (mainAccount == null || addressBook.getMainAccount().equals(mainAccount))
result.add(addressBook);
}
return result.toArray(new LocalAddressBook[result.size()]);
}
public static LocalAddressBook findByUid(@NonNull Context context, @NonNull ContentProviderClient provider, @Nullable Account mainAccount, String uid) throws ContactsStorageException {
AccountManager accountManager = AccountManager.get(context);
for (Account account : accountManager.getAccountsByType(App.getAddressBookAccountType())) {
LocalAddressBook addressBook = new LocalAddressBook(context, account, provider);
if (addressBook.getURL().equals(uid) && (mainAccount == null || addressBook.getMainAccount().equals(mainAccount)))
return addressBook;
}
return null;
}
public static LocalAddressBook create(@NonNull Context context, @NonNull ContentProviderClient provider, @NonNull Account mainAccount, @NonNull JournalEntity journalEntity) throws ContactsStorageException {
CollectionInfo info = journalEntity.getInfo();
AccountManager accountManager = AccountManager.get(context);
Account account = new Account(accountName(mainAccount, info), App.getAddressBookAccountType());
if (!accountManager.addAccountExplicitly(account, null, null))
throw new ContactsStorageException("Couldn't create address book account");
setUserData(accountManager, account, mainAccount, info.uid);
LocalAddressBook addressBook = new LocalAddressBook(context, account, provider);
addressBook.setMainAccount(mainAccount);
addressBook.setURL(info.uid);
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
return addressBook;
}
public void update(@NonNull JournalEntity journalEntity) throws AuthenticatorException, OperationCanceledException, IOException, ContactsStorageException, android.accounts.OperationCanceledException {
CollectionInfo info = journalEntity.getInfo();
final String newAccountName = accountName(getMainAccount(), info);
if (!account.name.equals(newAccountName) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
final AccountManager accountManager = AccountManager.get(context);
AccountManagerFuture<Account> future = accountManager.renameAccount(account, newAccountName, new AccountManagerCallback<Account>() {
@Override
public void run(AccountManagerFuture<Account> future) {
try {
// update raw contacts to new account name
if (provider != null) {
ContentValues values = new ContentValues(1);
values.put(RawContacts.ACCOUNT_NAME, newAccountName);
provider.update(syncAdapterURI(RawContacts.CONTENT_URI), values, RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?",
new String[] { account.name, account.type });
}
} catch(RemoteException e) {
App.log.log(Level.WARNING, "Couldn't re-assign contacts to new account name", e);
}
}
}, null);
account = future.getResult();
}
// make sure it will still be synchronized when contacts are updated
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
}
public void delete() {
AccountManager accountManager = AccountManager.get(context);
AndroidCompat.removeAccount(accountManager, account);
}
public LocalAddressBook(Context context, Account account, ContentProviderClient provider) {
super(account, provider, LocalGroup.Factory.INSTANCE, LocalContact.Factory.INSTANCE); super(account, provider, LocalGroup.Factory.INSTANCE, LocalContact.Factory.INSTANCE);
this.context = context;
} }
@NonNull @NonNull
@ -268,51 +357,52 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
} }
// SYNC STATE // SETTINGS
@SuppressWarnings("ParcelClassLoader,Recycle") // XXX: Workaround a bug in Android where passing a bundle to addAccountExplicitly doesn't work.
protected void readSyncState() throws ContactsStorageException { public static void setUserData(@NonNull AccountManager accountManager, @NonNull Account account, @NonNull Account mainAccount, @NonNull String url) {
@Cleanup("recycle") Parcel parcel = Parcel.obtain(); accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name);
byte[] raw = getSyncState(); accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type);
syncState.clear(); accountManager.setUserData(account, USER_DATA_URL, url);
if (raw != null) {
parcel.unmarshall(raw, 0, raw.length);
parcel.setDataPosition(0);
syncState.putAll(parcel.readBundle());
}
} }
@SuppressWarnings("Recycle") public Account getMainAccount() throws ContactsStorageException {
protected void writeSyncState() throws ContactsStorageException { AccountManager accountManager = AccountManager.get(context);
@Cleanup("recycle") Parcel parcel = Parcel.obtain(); String name = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME),
parcel.writeBundle(syncState); type = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE);
setSyncState(parcel.marshall()); if (name != null && type != null)
return new Account(name, type);
else
throw new ContactsStorageException("Address book doesn't exist anymore");
}
public void setMainAccount(@NonNull Account mainAccount) throws ContactsStorageException {
AccountManager accountManager = AccountManager.get(context);
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name);
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type);
} }
public String getURL() throws ContactsStorageException { public String getURL() throws ContactsStorageException {
synchronized (syncState) { AccountManager accountManager = AccountManager.get(context);
readSyncState(); return accountManager.getUserData(account, USER_DATA_URL);
return syncState.getString(SYNC_STATE_URL);
}
} }
public void setURL(String url) throws ContactsStorageException { public void setURL(String url) throws ContactsStorageException {
synchronized (syncState) { AccountManager accountManager = AccountManager.get(context);
readSyncState(); accountManager.setUserData(account, USER_DATA_URL, url);
syncState.putString(SYNC_STATE_URL, url);
writeSyncState();
}
} }
// HELPERS // HELPERS
public static void onRenameAccount(@NonNull ContentResolver resolver, @NonNull String oldName, @NonNull String newName) throws RemoteException { public static String accountName(@NonNull Account mainAccount, @NonNull CollectionInfo info) {
@Cleanup("release") ContentProviderClient client = resolver.acquireContentProviderClient(ContactsContract.AUTHORITY); String displayName = (info.displayName != null) ? info.displayName : info.uid;
if (client != null) { StringBuilder sb = new StringBuilder(displayName);
ContentValues values = new ContentValues(1); sb .append(" (")
values.put(RawContacts.ACCOUNT_NAME, newName); .append(mainAccount.name)
client.update(RawContacts.CONTENT_URI, values, RawContacts.ACCOUNT_NAME + "=?", new String[]{oldName}); .append(" ")
} .append(info.uid.substring(0, 4))
.append(")");
return sb.toString();
} }
/** Fix all of the etags of all of the non-dirty contacts to be non-null. /** Fix all of the etags of all of the non-dirty contacts to be non-null.

View File

@ -0,0 +1,53 @@
/*
* 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;
}
}

View File

@ -0,0 +1,161 @@
/*
* 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 lombok.Cleanup;
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 {
@Cleanup("release") 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);
AccountManager accountManager = AccountManager.get(getContext());
for (Account addressBookAccount : accountManager.getAccountsByType(getContext().getString(R.string.account_type_address_book))) {
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.retryAfter > 0) ? e.retryAfter : 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);
}
}
}
}

View File

@ -13,16 +13,9 @@ import android.content.Context;
import android.content.SyncResult; import android.content.SyncResult;
import android.os.Bundle; import android.os.Bundle;
import org.apache.commons.codec.Charsets;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import com.etesync.syncadapter.AccountSettings; import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.App; import com.etesync.syncadapter.App;
import com.etesync.syncadapter.Constants; import com.etesync.syncadapter.Constants;
import com.etesync.syncadapter.InvalidAccountException;
import com.etesync.syncadapter.R; import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Exceptions; import com.etesync.syncadapter.journalmanager.Exceptions;
import com.etesync.syncadapter.journalmanager.JournalEntryManager; import com.etesync.syncadapter.journalmanager.JournalEntryManager;
@ -31,6 +24,13 @@ import com.etesync.syncadapter.model.SyncEntry;
import com.etesync.syncadapter.resource.LocalCalendar; import com.etesync.syncadapter.resource.LocalCalendar;
import com.etesync.syncadapter.resource.LocalEvent; import com.etesync.syncadapter.resource.LocalEvent;
import com.etesync.syncadapter.resource.LocalResource; import com.etesync.syncadapter.resource.LocalResource;
import org.apache.commons.codec.Charsets;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import at.bitfire.ical4android.CalendarStorageException; import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.Event; import at.bitfire.ical4android.Event;
import at.bitfire.ical4android.InvalidCalendarException; import at.bitfire.ical4android.InvalidCalendarException;
@ -45,8 +45,8 @@ public class CalendarSyncManager extends SyncManager {
final private HttpUrl remote; final private HttpUrl remote;
public CalendarSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult result, LocalCalendar calendar, HttpUrl remote) throws InvalidAccountException, Exceptions.IntegrityException, Exceptions.GenericCryptoException { 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); super(context, account, settings, extras, authority, result, calendar.getName(), CollectionInfo.Type.CALENDAR, account.name);
localCollection = calendar; localCollection = calendar;
this.remote = remote; this.remote = remote;
} }

View File

@ -28,10 +28,12 @@ import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.JournalModel; import com.etesync.syncadapter.model.JournalModel;
import com.etesync.syncadapter.model.ServiceDB; import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.ServiceEntity; import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.resource.LocalAddressBook;
import com.etesync.syncadapter.ui.DebugInfoActivity; import com.etesync.syncadapter.ui.DebugInfoActivity;
import java.util.logging.Level; import java.util.logging.Level;
import at.bitfire.vcard4android.ContactsStorageException;
import io.requery.Persistable; import io.requery.Persistable;
import io.requery.sql.EntityDataStore; import io.requery.sql.EntityDataStore;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
@ -59,30 +61,26 @@ public class ContactsSyncAdapterService extends SyncAdapterService {
notificationManager.cancel(); notificationManager.cancel();
try { try {
AccountSettings settings = new AccountSettings(getContext(), account); 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)) if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings))
return; return;
new RefreshCollections(account, CollectionInfo.Type.ADDRESS_BOOK).run(); App.log.info("Synchronizing address book: " + addressBook.getURL());
App.log.info("Taking settings from: " + addressBook.getMainAccount());
EntityDataStore<Persistable> data = ((App) getContext().getApplicationContext()).getData(); HttpUrl principal = HttpUrl.get(settings.getUri());
ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, settings, extras, authority, provider, syncResult, addressBook, principal);
ServiceEntity service = JournalModel.Service.fetch(data, account.name, CollectionInfo.Type.ADDRESS_BOOK); syncManager.performSync();
if (service != null) {
HttpUrl principal = HttpUrl.get(settings.getUri());
CollectionInfo info = JournalEntity.getCollections(data, service).get(0);
try {
ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, settings, extras, authority, provider, syncResult, principal, info);
syncManager.performSync();
} catch (InvalidAccountException e) {
App.log.log(Level.SEVERE, "Couldn't get account settings", e);
}
} else
App.log.info("No CardDAV service found in DB");
} catch (Exceptions.ServiceUnavailableException e) {
syncResult.stats.numIoExceptions++;
syncResult.delayUntil = (e.retryAfter > 0) ? e.retryAfter : Constants.DEFAULT_RETRY_DELAY;
} catch (Exception | OutOfMemoryError e) { } catch (Exception | OutOfMemoryError e) {
int syncPhase = R.string.sync_phase_journals; int syncPhase = R.string.sync_phase_journals;
String title = getContext().getString(R.string.sync_error_contacts, account.name); String title = getContext().getString(R.string.sync_error_contacts, account.name);
@ -98,7 +96,7 @@ public class ContactsSyncAdapterService extends SyncAdapterService {
notificationManager.notify(title, getContext().getString(syncPhase)); notificationManager.notify(title, getContext().getString(syncPhase));
} }
App.log.info("Address book sync complete"); App.log.info("Contacts sync complete");
} }
} }

View File

@ -18,19 +18,10 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import org.apache.commons.codec.Charsets;
import org.apache.commons.io.IOUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import com.etesync.syncadapter.AccountSettings; import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.App; import com.etesync.syncadapter.App;
import com.etesync.syncadapter.Constants; import com.etesync.syncadapter.Constants;
import com.etesync.syncadapter.HttpClient; import com.etesync.syncadapter.HttpClient;
import com.etesync.syncadapter.InvalidAccountException;
import com.etesync.syncadapter.R; import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Exceptions; import com.etesync.syncadapter.journalmanager.Exceptions;
import com.etesync.syncadapter.journalmanager.JournalEntryManager; import com.etesync.syncadapter.journalmanager.JournalEntryManager;
@ -40,6 +31,15 @@ import com.etesync.syncadapter.resource.LocalAddressBook;
import com.etesync.syncadapter.resource.LocalContact; import com.etesync.syncadapter.resource.LocalContact;
import com.etesync.syncadapter.resource.LocalGroup; import com.etesync.syncadapter.resource.LocalGroup;
import com.etesync.syncadapter.resource.LocalResource; import com.etesync.syncadapter.resource.LocalResource;
import org.apache.commons.codec.Charsets;
import org.apache.commons.io.IOUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import at.bitfire.ical4android.CalendarStorageException; import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.Contact; import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException; import at.bitfire.vcard4android.ContactsStorageException;
@ -60,10 +60,12 @@ public class ContactsSyncManager extends SyncManager {
final private ContentProviderClient provider; final private ContentProviderClient provider;
final private HttpUrl remote; final private HttpUrl remote;
public ContactsSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, ContentProviderClient provider, SyncResult result, HttpUrl principal, CollectionInfo info) throws InvalidAccountException, Exceptions.IntegrityException, Exceptions.GenericCryptoException { 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, info.uid, CollectionInfo.Type.ADDRESS_BOOK); super(context, account, settings, extras, authority, result, localAddressBook.getURL(), CollectionInfo.Type.ADDRESS_BOOK, localAddressBook.getMainAccount().name);
this.provider = provider; this.provider = provider;
this.remote = principal; this.remote = principal;
localCollection = localAddressBook;
} }
@Override @Override
@ -80,10 +82,7 @@ public class ContactsSyncManager extends SyncManager {
protected boolean prepare() throws ContactsStorageException, CalendarStorageException { protected boolean prepare() throws ContactsStorageException, CalendarStorageException {
if (!super.prepare()) if (!super.prepare())
return false; return false;
// prepare local address book
localCollection = new LocalAddressBook(account, provider);
LocalAddressBook localAddressBook = localAddressBook(); LocalAddressBook localAddressBook = localAddressBook();
localAddressBook.setURL(info.uid);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed // workaround for Android 7 which sets DIRTY flag when only meta-data is changed
@ -101,7 +100,7 @@ public class ContactsSyncManager extends SyncManager {
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1); values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1);
localAddressBook.updateSettings(values); localAddressBook.updateSettings(values);
journal = new JournalEntryManager(httpClient, remote, info.uid); journal = new JournalEntryManager(httpClient, remote, localAddressBook.getURL());
return true; return true;
} }

View File

@ -0,0 +1,97 @@
/*
* 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;
}
}
}

View File

@ -76,7 +76,7 @@ public abstract class SyncAdapterService extends Service {
@Override @Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
App.log.log(Level.INFO, "Sync for " + authority + " has been initiated.", extras.keySet().toArray()); App.log.log(Level.INFO, authority + " sync of " + account + " has been initiated.", extras.keySet().toArray());
// required for dav4android (ServiceLoader) // required for dav4android (ServiceLoader)
Thread.currentThread().setContextClassLoader(getContext().getClassLoader()); Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
@ -143,9 +143,9 @@ public abstract class SyncAdapterService extends Service {
void run() throws Exceptions.HttpException, Exceptions.IntegrityException, InvalidAccountException, Exceptions.GenericCryptoException { void run() throws Exceptions.HttpException, Exceptions.IntegrityException, InvalidAccountException, Exceptions.GenericCryptoException {
App.log.info("Refreshing " + serviceType + " collections of service #" + serviceType.toString()); App.log.info("Refreshing " + serviceType + " collections of service #" + serviceType.toString());
OkHttpClient httpClient = HttpClient.create(context, account);
AccountSettings settings = new AccountSettings(context, account); AccountSettings settings = new AccountSettings(context, account);
OkHttpClient httpClient = HttpClient.create(context, settings);
JournalManager journalsManager = new JournalManager(httpClient, HttpUrl.get(settings.getUri())); JournalManager journalsManager = new JournalManager(httpClient, HttpUrl.get(settings.getUri()));
List<Pair<JournalManager.Journal, CollectionInfo>> journals = new LinkedList<>(); List<Pair<JournalManager.Journal, CollectionInfo>> journals = new LinkedList<>();

View File

@ -18,7 +18,6 @@ import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.App; import com.etesync.syncadapter.App;
import com.etesync.syncadapter.Constants; import com.etesync.syncadapter.Constants;
import com.etesync.syncadapter.HttpClient; import com.etesync.syncadapter.HttpClient;
import com.etesync.syncadapter.InvalidAccountException;
import com.etesync.syncadapter.NotificationHelper; import com.etesync.syncadapter.NotificationHelper;
import com.etesync.syncadapter.R; import com.etesync.syncadapter.R;
import com.etesync.syncadapter.journalmanager.Crypto; import com.etesync.syncadapter.journalmanager.Crypto;
@ -97,7 +96,7 @@ abstract public class SyncManager {
private List<LocalResource> localDeleted; private List<LocalResource> localDeleted;
private LocalResource[] localDirty; private LocalResource[] localDirty;
public SyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult syncResult, String journalUid, CollectionInfo.Type serviceType) throws InvalidAccountException, Exceptions.IntegrityException, Exceptions.GenericCryptoException { 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.context = context;
this.account = account; this.account = account;
this.settings = settings; this.settings = settings;
@ -107,10 +106,10 @@ abstract public class SyncManager {
this.serviceType = serviceType; this.serviceType = serviceType;
// create HttpClient with given logger // create HttpClient with given logger
httpClient = HttpClient.create(context, account); httpClient = HttpClient.create(context, settings);
data = ((App) context.getApplicationContext()).getData(); data = ((App) context.getApplicationContext()).getData();
ServiceEntity serviceEntity = JournalModel.Service.fetch(data, account.name, serviceType); ServiceEntity serviceEntity = JournalModel.Service.fetch(data, accountName, serviceType);
info = JournalEntity.fetch(data, serviceEntity, journalUid).getInfo(); info = JournalEntity.fetch(data, serviceEntity, journalUid).getInfo();
// dismiss previous error notifications // dismiss previous error notifications

View File

@ -72,7 +72,7 @@ public class AboutActivity extends AppCompatActivity {
private final static ComponentInfo components[] = { private final static ComponentInfo components[] = {
new ComponentInfo( new ComponentInfo(
"EteSync", BuildConfig.VERSION_NAME, Constants.webUri.toString(), App.getAppName(), BuildConfig.VERSION_NAME, Constants.webUri.toString(),
DateFormatUtils.format(BuildConfig.buildTime, "yyyy") + " Tom Hacohen", DateFormatUtils.format(BuildConfig.buildTime, "yyyy") + " Tom Hacohen",
R.string.about_license_info_no_warranty, "gpl-3.0-standalone.html" R.string.about_license_info_no_warranty, "gpl-3.0-standalone.html"
), new ComponentInfo( ), new ComponentInfo(

View File

@ -58,6 +58,7 @@ import com.etesync.syncadapter.journalmanager.Crypto;
import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.CollectionInfo;
import com.etesync.syncadapter.model.JournalEntity; import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.ServiceEntity; import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.resource.LocalAddressBook;
import com.etesync.syncadapter.resource.LocalCalendar; import com.etesync.syncadapter.resource.LocalCalendar;
import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment; import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment;
import com.etesync.syncadapter.utils.HintManager; import com.etesync.syncadapter.utils.HintManager;
@ -69,6 +70,7 @@ import java.util.logging.Level;
import at.bitfire.cert4android.CustomCertManager; import at.bitfire.cert4android.CustomCertManager;
import at.bitfire.ical4android.TaskProvider; import at.bitfire.ical4android.TaskProvider;
import at.bitfire.vcard4android.ContactsStorageException;
import io.requery.Persistable; import io.requery.Persistable;
import io.requery.sql.EntityDataStore; import io.requery.sql.EntityDataStore;
import tourguide.tourguide.ToolTip; import tourguide.tourguide.ToolTip;
@ -98,6 +100,9 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
// CardDAV toolbar // CardDAV toolbar
tbCardDAV = (Toolbar)findViewById(R.id.carddav_menu); tbCardDAV = (Toolbar)findViewById(R.id.carddav_menu);
tbCardDAV.setOverflowIcon(icMenu);
tbCardDAV.inflateMenu(R.menu.carddav_actions);
tbCardDAV.setOnMenuItemClickListener(this);
tbCardDAV.setTitle(R.string.settings_carddav); tbCardDAV.setTitle(R.string.settings_carddav);
// CalDAV toolbar // CalDAV toolbar
@ -193,12 +198,18 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
@Override @Override
public boolean onMenuItemClick(MenuItem item) { public boolean onMenuItemClick(MenuItem item) {
CollectionInfo info;
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.create_calendar: case R.id.create_calendar:
CollectionInfo info = new CollectionInfo(); info = new CollectionInfo();
info.type = CollectionInfo.Type.CALENDAR; info.type = CollectionInfo.Type.CALENDAR;
startActivity(CreateCollectionActivity.newIntent(AccountActivity.this, account, info)); startActivity(CreateCollectionActivity.newIntent(AccountActivity.this, account, info));
break; break;
case R.id.create_addressbook:
info = new CollectionInfo();
info.type = CollectionInfo.Type.ADDRESS_BOOK;
startActivity(CreateCollectionActivity.newIntent(AccountActivity.this, account, info));
break;
} }
return false; return false;
} }
@ -357,8 +368,18 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
if (service.equals(CollectionInfo.Type.ADDRESS_BOOK)) { if (service.equals(CollectionInfo.Type.ADDRESS_BOOK)) {
info.carddav = new AccountInfo.ServiceInfo(); info.carddav = new AccountInfo.ServiceInfo();
info.carddav.id = id; info.carddav.id = id;
info.carddav.refreshing = (davService != null && davService.isRefreshing(id)) || ContentResolver.isSyncActive(account, ContactsContract.AUTHORITY); info.carddav.refreshing = (davService != null && davService.isRefreshing(id)) || ContentResolver.isSyncActive(account, App.getAddressBooksAuthority());
info.carddav.journals = JournalEntity.getJournals(data, serviceEntity); info.carddav.journals = JournalEntity.getJournals(data, serviceEntity);
AccountManager accountManager = AccountManager.get(getContext());
for (Account addrBookAccount : accountManager.getAccountsByType(App.getAddressBookAccountType())) {
LocalAddressBook addressBook = new LocalAddressBook(getContext(), addrBookAccount, null);
try {
if (account.equals(addressBook.getMainAccount()))
info.carddav.refreshing |= ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY);
} catch(ContactsStorageException e) {
}
}
} else if (service.equals(CollectionInfo.Type.CALENDAR)) { } else if (service.equals(CollectionInfo.Type.CALENDAR)) {
info.caldav = new AccountInfo.ServiceInfo(); info.caldav = new AccountInfo.ServiceInfo();
info.caldav.id = id; info.caldav.id = id;
@ -457,7 +478,7 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu
protected static void requestSync(Account account) { protected static void requestSync(Account account) {
String authorities[] = { String authorities[] = {
ContactsContract.AUTHORITY, App.getAddressBooksAuthority(),
CalendarContract.AUTHORITY, CalendarContract.AUTHORITY,
TaskProvider.ProviderName.OpenTasks.authority TaskProvider.ProviderName.OpenTasks.authority
}; };

View File

@ -29,6 +29,7 @@ import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import com.etesync.syncadapter.AccountsChangedReceiver; import com.etesync.syncadapter.AccountsChangedReceiver;
import com.etesync.syncadapter.App;
import com.etesync.syncadapter.Constants; import com.etesync.syncadapter.Constants;
import com.etesync.syncadapter.R; import com.etesync.syncadapter.R;
@ -106,7 +107,7 @@ public class AccountListFragment extends ListFragment implements LoaderManager.L
@Override @Override
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
public Account[] loadInBackground() { public Account[] loadInBackground() {
return accountManager.getAccountsByType(Constants.ACCOUNT_TYPE); return accountManager.getAccountsByType(App.getAccountType());
} }
} }

View File

@ -111,7 +111,7 @@ public class AccountSettingsActivity extends AppCompatActivity {
// category: synchronization // category: synchronization
final ListPreference prefSyncContacts = (ListPreference)findPreference("sync_interval_contacts"); final ListPreference prefSyncContacts = (ListPreference)findPreference("sync_interval_contacts");
final Long syncIntervalContacts = settings.getSyncInterval(ContactsContract.AUTHORITY); final Long syncIntervalContacts = settings.getSyncInterval(App.getAddressBooksAuthority());
if (syncIntervalContacts != null) { if (syncIntervalContacts != null) {
prefSyncContacts.setValue(syncIntervalContacts.toString()); prefSyncContacts.setValue(syncIntervalContacts.toString());
if (syncIntervalContacts == AccountSettings.SYNC_INTERVAL_MANUALLY) if (syncIntervalContacts == AccountSettings.SYNC_INTERVAL_MANUALLY)
@ -121,7 +121,7 @@ public class AccountSettingsActivity extends AppCompatActivity {
prefSyncContacts.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { prefSyncContacts.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override @Override
public boolean onPreferenceChange(Preference preference, Object newValue) { public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.setSyncInterval(ContactsContract.AUTHORITY, Long.parseLong((String)newValue)); settings.setSyncInterval(App.getAddressBooksAuthority(), Long.parseLong((String)newValue));
getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this); getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
return false; return false;
} }

View File

@ -55,7 +55,7 @@ public class AddMemberFragment extends DialogFragment {
memberEmail = getArguments().getString(KEY_MEMBER); memberEmail = getArguments().getString(KEY_MEMBER);
try { try {
settings = new AccountSettings(getContext(), account); settings = new AccountSettings(getContext(), account);
httpClient = HttpClient.create(getContext(), account); httpClient = HttpClient.create(getContext(), settings);
} catch (InvalidAccountException e) { } catch (InvalidAccountException e) {
e.printStackTrace(); e.printStackTrace();
} }

View File

@ -161,6 +161,9 @@ public class AppSettingsActivity extends AppCompatActivity {
// re-initialize certificate manager // re-initialize certificate manager
App app = (App)getContext().getApplicationContext(); App app = (App)getContext().getApplicationContext();
app.reinitCertManager(); app.reinitCertManager();
// reinitialize certificate manager of :sync process
getContext().sendBroadcast(new Intent(App.ReinitSettingsReceiver.ACTION_REINIT_SETTINGS));
} }
private void resetCertificates() { private void resetCertificates() {
@ -176,7 +179,7 @@ public class AppSettingsActivity extends AppCompatActivity {
app.reinitLogger(); app.reinitLogger();
// reinitialize logger of :sync process // reinitialize logger of :sync process
getContext().sendBroadcast(new Intent("com.etesync.syncadapter.REINIT_LOGGER")); getContext().sendBroadcast(new Intent(App.ReinitSettingsReceiver.ACTION_REINIT_SETTINGS));
} }
} }

View File

@ -130,8 +130,8 @@ public class CollectionMembersListFragment extends ListFragment implements Adapt
@Override @Override
protected MembersResult doInBackground(Void... voids) { protected MembersResult doInBackground(Void... voids) {
try { try {
OkHttpClient httpClient = HttpClient.create(getContext(), account);
AccountSettings settings = new AccountSettings(getContext(), account); AccountSettings settings = new AccountSettings(getContext(), account);
OkHttpClient httpClient = HttpClient.create(getContext(), settings);
JournalManager journalsManager = new JournalManager(httpClient, HttpUrl.get(settings.getUri())); JournalManager journalsManager = new JournalManager(httpClient, HttpUrl.get(settings.getUri()));
JournalManager.Journal journal = JournalManager.Journal.fakeWithUid(journalEntity.getUid()); JournalManager.Journal journal = JournalManager.Journal.fakeWithUid(journalEntity.getUid());

View File

@ -54,8 +54,10 @@ public class CreateCollectionActivity extends AppCompatActivity {
setContentView(R.layout.activity_create_collection); setContentView(R.layout.activity_create_collection);
final EditText displayName = (EditText) findViewById(R.id.display_name);
if (info.type == CollectionInfo.Type.CALENDAR) { if (info.type == CollectionInfo.Type.CALENDAR) {
setTitle(R.string.create_calendar); setTitle(R.string.create_calendar);
displayName.setHint(R.string.create_calendar_display_name_hint);
final View colorSquare = findViewById(R.id.color); final View colorSquare = findViewById(R.id.color);
colorSquare.setOnClickListener(new View.OnClickListener() { colorSquare.setOnClickListener(new View.OnClickListener() {
@ -75,6 +77,7 @@ public class CreateCollectionActivity extends AppCompatActivity {
}); });
} else { } else {
setTitle(R.string.create_addressbook); setTitle(R.string.create_addressbook);
displayName.setHint(R.string.create_addressbook_display_name_hint);
final View colorGroup = findViewById(R.id.color_group); final View colorGroup = findViewById(R.id.color_group);
colorGroup.setVisibility(View.GONE); colorGroup.setVisibility(View.GONE);

View File

@ -130,7 +130,7 @@ public class CreateCollectionFragment extends DialogFragment implements LoaderMa
// 1. find service ID // 1. find service ID
if (info.type == CollectionInfo.Type.ADDRESS_BOOK) { if (info.type == CollectionInfo.Type.ADDRESS_BOOK) {
authority = ContactsContract.AUTHORITY; authority = App.getAddressBooksAuthority();
} else if (info.type == CollectionInfo.Type.CALENDAR) { } else if (info.type == CollectionInfo.Type.CALENDAR) {
authority = CalendarContract.AUTHORITY; authority = CalendarContract.AUTHORITY;
} else { } else {
@ -142,7 +142,7 @@ public class CreateCollectionFragment extends DialogFragment implements LoaderMa
AccountSettings settings = new AccountSettings(getContext(), account); AccountSettings settings = new AccountSettings(getContext(), account);
HttpUrl principal = HttpUrl.get(settings.getUri()); HttpUrl principal = HttpUrl.get(settings.getUri());
JournalManager journalManager = new JournalManager(HttpClient.create(getContext(), account), principal); JournalManager journalManager = new JournalManager(HttpClient.create(getContext(), settings), principal);
if (info.uid == null) { if (info.uid == null) {
info.uid = JournalManager.Journal.genUid(); info.uid = JournalManager.Journal.genUid();
Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid); Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid);

View File

@ -8,6 +8,7 @@
package com.etesync.syncadapter.ui; package com.etesync.syncadapter.ui;
import android.Manifest;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountManager; import android.accounts.AccountManager;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
@ -20,8 +21,10 @@ import android.content.Loader;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.PowerManager;
import android.provider.CalendarContract; import android.provider.CalendarContract;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider; import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils; import android.text.TextUtils;
@ -50,7 +53,9 @@ import com.etesync.syncadapter.model.EntryEntity;
import com.etesync.syncadapter.model.JournalEntity; import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.ServiceDB; import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.ServiceEntity; import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.resource.LocalAddressBook;
import at.bitfire.vcard4android.ContactsStorageException;
import io.requery.Persistable; import io.requery.Persistable;
import io.requery.sql.EntityDataStore; import io.requery.sql.EntityDataStore;
import lombok.Cleanup; import lombok.Cleanup;
@ -203,8 +208,10 @@ public class DebugInfoActivity extends AppCompatActivity implements LoaderManage
if (logs != null) if (logs != null)
report.append("\nLOGS:\n").append(logs).append("\n"); report.append("\nLOGS:\n").append(logs).append("\n");
final Context context = getContext();
try { try {
PackageManager pm = getContext().getPackageManager(); PackageManager pm = context.getPackageManager();
String installedFrom = pm.getInstallerPackageName(BuildConfig.APPLICATION_ID); String installedFrom = pm.getInstallerPackageName(BuildConfig.APPLICATION_ID);
if (TextUtils.isEmpty(installedFrom)) if (TextUtils.isEmpty(installedFrom))
installedFrom = "APK (directly)"; installedFrom = "APK (directly)";
@ -215,15 +222,31 @@ public class DebugInfoActivity extends AppCompatActivity implements LoaderManage
App.log.log(Level.SEVERE, "Couldn't get software information", ex); App.log.log(Level.SEVERE, "Couldn't get software information", ex);
} }
report.append( report.append("CONFIGURATION\n");
"CONFIGURATION\n" + // power saving
"System-wide synchronization: ").append(ContentResolver.getMasterSyncAutomatically() ? "automatically" : "manually").append("\n"); PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
AccountManager accountManager = AccountManager.get(getContext()); if (powerManager != null && Build.VERSION.SDK_INT >= 23)
for (Account acct : accountManager.getAccountsByType(Constants.ACCOUNT_TYPE)) report.append("Power saving disabled: ")
.append(powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) ? "yes" : "no")
.append("\n");
// permissions
for (String permission : new String[] { Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR,
PermissionsActivity.PERMISSION_READ_TASKS, PermissionsActivity.PERMISSION_WRITE_TASKS })
report.append(permission).append(" permission: ")
.append(ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED ? "granted" : "denied")
.append("\n");
// system-wide sync settings
report.append("System-wide synchronization: ")
.append(ContentResolver.getMasterSyncAutomatically() ? "automatically" : "manually")
.append("\n");
// main accounts
AccountManager accountManager = AccountManager.get(context);
for (Account acct : accountManager.getAccountsByType(context.getString(R.string.account_type)))
try { try {
AccountSettings settings = new AccountSettings(getContext(), acct); AccountSettings settings = new AccountSettings(context, acct);
report.append("Account: ").append(acct.name).append("\n" + report.append("Account: ").append(acct.name).append("\n" +
" Address book sync. interval: ").append(syncStatus(settings, ContactsContract.AUTHORITY)).append("\n" + " Address book sync. interval: ").append(syncStatus(settings, context.getString(R.string.address_books_authority))).append("\n" +
" Calendar sync. interval: ").append(syncStatus(settings, CalendarContract.AUTHORITY)).append("\n" + " Calendar sync. interval: ").append(syncStatus(settings, CalendarContract.AUTHORITY)).append("\n" +
" OpenTasks sync. interval: ").append(syncStatus(settings, "org.dmfs.tasks")).append("\n" + " OpenTasks sync. interval: ").append(syncStatus(settings, "org.dmfs.tasks")).append("\n" +
" WiFi only: ").append(settings.getSyncWifiOnly()); " WiFi only: ").append(settings.getSyncWifiOnly());
@ -235,10 +258,21 @@ public class DebugInfoActivity extends AppCompatActivity implements LoaderManage
} catch(InvalidAccountException e) { } catch(InvalidAccountException e) {
report.append(acct).append(" is invalid (unsupported settings version) or does not exist\n"); report.append(acct).append(" is invalid (unsupported settings version) or does not exist\n");
} }
// address book accounts
for (Account acct : accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)))
try {
LocalAddressBook addressBook = new LocalAddressBook(context, acct, null);
report.append("Address book account: ").append(acct.name).append("\n" +
" Main account: ").append(addressBook.getMainAccount()).append("\n" +
" URL: ").append(addressBook.getURL()).append("\n" +
" Sync automatically: ").append(ContentResolver.getSyncAutomatically(acct, ContactsContract.AUTHORITY)).append("\n");
} catch(ContactsStorageException e) {
report.append(acct).append(" is invalid: ").append(e.getMessage()).append("\n");
}
report.append("\n"); report.append("\n");
report.append("SQLITE DUMP\n"); report.append("SQLITE DUMP\n");
@Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext()); @Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(context);
dbHelper.dump(report); dbHelper.dump(report);
report.append("\n"); report.append("\n");

View File

@ -119,7 +119,7 @@ public class DeleteCollectionFragment extends DialogFragment implements LoaderMa
AccountSettings settings = new AccountSettings(getContext(), account); AccountSettings settings = new AccountSettings(getContext(), account);
HttpUrl principal = HttpUrl.get(settings.getUri()); HttpUrl principal = HttpUrl.get(settings.getUri());
JournalManager journalManager = new JournalManager(HttpClient.create(getContext(), account), principal); JournalManager journalManager = new JournalManager(HttpClient.create(getContext(), settings), principal);
Crypto.CryptoManager crypto = new Crypto.CryptoManager(collectionInfo.version, settings.password(), collectionInfo.uid); Crypto.CryptoManager crypto = new Crypto.CryptoManager(collectionInfo.version, settings.password(), collectionInfo.uid);
journalManager.delete(new JournalManager.Journal(crypto, collectionInfo.toJson(), collectionInfo.uid)); journalManager.delete(new JournalManager.Journal(crypto, collectionInfo.toJson(), collectionInfo.uid));

View File

@ -59,11 +59,7 @@ public class EditCollectionActivity extends CreateCollectionActivity {
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
if (info.type == CollectionInfo.Type.ADDRESS_BOOK) { getMenuInflater().inflate(R.menu.activity_edit_collection, menu);
getMenuInflater().inflate(R.menu.activity_create_collection, menu);
} else {
getMenuInflater().inflate(R.menu.activity_edit_collection, menu);
}
return true; return true;
} }

View File

@ -52,7 +52,7 @@ public class RemoveMemberFragment extends DialogFragment {
memberEmail = getArguments().getString(KEY_MEMBER); memberEmail = getArguments().getString(KEY_MEMBER);
try { try {
settings = new AccountSettings(getContext(), account); settings = new AccountSettings(getContext(), account);
httpClient = HttpClient.create(getContext(), account); httpClient = HttpClient.create(getContext(), settings);
} catch (InvalidAccountException e) { } catch (InvalidAccountException e) {
e.printStackTrace(); e.printStackTrace();
} }

View File

@ -52,7 +52,7 @@ public class StartupDialogFragment extends DialogFragment {
// battery optimization whitelisting // battery optimization whitelisting
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !HintManager.getHintSeen(context, HINT_BATTERY_OPTIMIZATIONS)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !HintManager.getHintSeen(context, HINT_BATTERY_OPTIMIZATIONS)) {
PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE); PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
if (!powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)) if (powerManager != null && !powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID))
dialogs.add(StartupDialogFragment.instantiate(Mode.BATTERY_OPTIMIZATIONS)); dialogs.add(StartupDialogFragment.instantiate(Mode.BATTERY_OPTIMIZATIONS));
} }

View File

@ -193,19 +193,7 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh
} }
public void onManageMembers(MenuItem item) { public void onManageMembers(MenuItem item) {
if (info.type.equals(CollectionInfo.Type.ADDRESS_BOOK)) { if (info.version < 2) {
AlertDialog dialog = new AlertDialog.Builder(this)
.setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.not_allowed_title)
.setMessage(R.string.members_address_book_not_allowed)
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
}).create();
dialog.show();
} else if (info.version < 2) {
AlertDialog dialog = new AlertDialog.Builder(this) AlertDialog dialog = new AlertDialog.Builder(this)
.setIcon(R.drawable.ic_info_dark) .setIcon(R.drawable.ic_info_dark)
.setTitle(R.string.not_allowed_title) .setTitle(R.string.not_allowed_title)
@ -256,7 +244,7 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh
} }
} else { } else {
try { try {
LocalAddressBook resource = new LocalAddressBook(account, getContentResolver().acquireContentProviderClient(ContactsContract.Contacts.CONTENT_URI)); LocalAddressBook resource = LocalAddressBook.findByUid(ViewCollectionActivity.this, getContentResolver().acquireContentProviderClient(ContactsContract.Contacts.CONTENT_URI), account, info.uid);
count = resource.count(); count = resource.count();
} catch (ContactsStorageException e) { } catch (ContactsStorageException e) {
e.printStackTrace(); e.printStackTrace();

View File

@ -307,7 +307,7 @@ public class ImportFragment extends DialogFragment {
finishParsingFile(contacts.length); finishParsingFile(contacts.length);
ContentProviderClient provider = getContext().getContentResolver().acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI); ContentProviderClient provider = getContext().getContentResolver().acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI);
LocalAddressBook localAddressBook = new LocalAddressBook(account, provider); LocalAddressBook localAddressBook = LocalAddressBook.findByUid(getContext(), provider, account, info.uid);
for (Contact contact : contacts) { for (Contact contact : contacts) {
try { try {
@ -332,7 +332,7 @@ public class ImportFragment extends DialogFragment {
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
result.e = e; result.e = e;
return result; return result;
} catch (InvalidCalendarException | IOException e) { } catch (InvalidCalendarException | IOException | ContactsStorageException e) {
result.e = e; result.e = e;
return result; return result;
} }

View File

@ -104,7 +104,7 @@ public class LocalContactImportFragment extends Fragment {
String accountType = cursor.getString(accountTypeIndex); String accountType = cursor.getString(accountTypeIndex);
if (account == null || (!account.name.equals(accountName) || !account.type.equals(accountType))) { if (account == null || (!account.name.equals(accountName) || !account.type.equals(accountType))) {
account = new Account(accountName, accountType); account = new Account(accountName, accountType);
localAddressBooks.add(new LocalAddressBook(account, provider)); localAddressBooks.add(new LocalAddressBook(getContext(), account, provider));
} }
} }
@ -152,8 +152,9 @@ public class LocalContactImportFragment extends Fragment {
private ResultFragment.ImportResult importContacts(LocalAddressBook localAddressBook) { private ResultFragment.ImportResult importContacts(LocalAddressBook localAddressBook) {
ResultFragment.ImportResult result = new ResultFragment.ImportResult(); ResultFragment.ImportResult result = new ResultFragment.ImportResult();
try { try {
LocalAddressBook addressBook = new LocalAddressBook(account, LocalAddressBook addressBook = LocalAddressBook.findByUid(getContext(),
getContext().getContentResolver().acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)); getContext().getContentResolver().acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI),
account, info.uid);
LocalContact[] localContacts = localAddressBook.getAll(); LocalContact[] localContacts = localAddressBook.getAll();
int total = localContacts.length; int total = localContacts.length;
progressDialog.setMax(total); progressDialog.setMax(total);

View File

@ -15,6 +15,8 @@ import android.app.Dialog;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle; import android.os.Bundle;
import android.provider.CalendarContract; import android.provider.CalendarContract;
@ -24,6 +26,7 @@ import android.support.v4.app.DialogFragment;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader; import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.support.v7.app.AlertDialog;
import com.etesync.syncadapter.AccountSettings; import com.etesync.syncadapter.AccountSettings;
import com.etesync.syncadapter.App; import com.etesync.syncadapter.App;
@ -39,7 +42,9 @@ import com.etesync.syncadapter.model.JournalEntity;
import com.etesync.syncadapter.model.ServiceDB; import com.etesync.syncadapter.model.ServiceDB;
import com.etesync.syncadapter.model.ServiceEntity; import com.etesync.syncadapter.model.ServiceEntity;
import com.etesync.syncadapter.resource.LocalTaskList; import com.etesync.syncadapter.resource.LocalTaskList;
import com.etesync.syncadapter.ui.DebugInfoActivity;
import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder.Configuration; import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder.Configuration;
import com.etesync.syncadapter.utils.AndroidCompat;
import java.util.logging.Level; import java.util.logging.Level;
@ -87,11 +92,23 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan
@Override @Override
public void onLoadFinished(Loader<Configuration> loader, Configuration config) { public void onLoadFinished(Loader<Configuration> loader, Configuration config) {
if (createAccount(config.userName, config)) { try {
getActivity().setResult(Activity.RESULT_OK); if (createAccount(config.userName, config)) {
getActivity().finish(); getActivity().setResult(Activity.RESULT_OK);
} else { getActivity().finish();
}
} catch (InvalidAccountException e) {
App.log.severe("Account creation failed!"); App.log.severe("Account creation failed!");
new AlertDialog.Builder(getActivity())
.setTitle(R.string.account_creation_failed)
.setIcon(R.drawable.ic_error_dark)
.setMessage(e.getLocalizedMessage())
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// dismiss
}
}).show();
} }
dismissAllowingStateLoss(); dismissAllowingStateLoss();
@ -145,17 +162,18 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan
} }
protected boolean createAccount(String accountName, BaseConfigurationFinder.Configuration config) { protected boolean createAccount(String accountName, BaseConfigurationFinder.Configuration config) throws InvalidAccountException {
Account account = new Account(accountName, Constants.ACCOUNT_TYPE); Account account = new Account(accountName, App.getAccountType());
// create Android account // create Android account
Bundle userData = AccountSettings.initialUserData(config.url, config.userName); App.log.log(Level.INFO, "Creating Android account with initial config", new Object[] { account, config.userName, config.url });
App.log.log(Level.INFO, "Creating Android account with initial config", new Object[] { account, userData });
AccountManager accountManager = AccountManager.get(getContext()); AccountManager accountManager = AccountManager.get(getContext());
if (!accountManager.addAccountExplicitly(account, config.password, userData)) if (!accountManager.addAccountExplicitly(account, config.password, null))
return false; return false;
AccountSettings.setUserData(accountManager, account, config.url, config.userName);
// add entries for account to service DB // add entries for account to service DB
App.log.log(Level.INFO, "Writing account configuration to database", config); App.log.log(Level.INFO, "Writing account configuration to database", config);
try { try {
@ -171,9 +189,9 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan
insertService(accountName, CollectionInfo.Type.ADDRESS_BOOK, config.cardDAV); insertService(accountName, CollectionInfo.Type.ADDRESS_BOOK, config.cardDAV);
// contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml // contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml
settings.setSyncInterval(ContactsContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL); settings.setSyncInterval(App.getAddressBooksAuthority(), Constants.DEFAULT_SYNC_INTERVAL);
} else { } else {
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0); ContentResolver.setIsSyncable(account, App.getAddressBooksAuthority(), 0);
} }
if (config.calDAV != null) { if (config.calDAV != null) {
@ -195,6 +213,8 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan
} catch(InvalidAccountException e) { } catch(InvalidAccountException e) {
App.log.log(Level.SEVERE, "Couldn't access account settings", e); App.log.log(Level.SEVERE, "Couldn't access account settings", e);
AndroidCompat.removeAccount(accountManager, account);
throw e;
} }
return true; return true;

View File

@ -90,7 +90,7 @@ public class SetupUserInfoFragment extends DialogFragment {
protected SetupUserInfo.SetupUserInfoResult doInBackground(Account... accounts) { protected SetupUserInfo.SetupUserInfoResult doInBackground(Account... accounts) {
try { try {
Crypto.CryptoManager cryptoManager; Crypto.CryptoManager cryptoManager;
OkHttpClient httpClient = HttpClient.create(getContext(), account); OkHttpClient httpClient = HttpClient.create(getContext(), settings);
UserInfoManager userInfoManager = new UserInfoManager(httpClient, HttpUrl.get(settings.getUri())); UserInfoManager userInfoManager = new UserInfoManager(httpClient, HttpUrl.get(settings.getUri()));
UserInfoManager.UserInfo userInfo = userInfoManager.get(account.name); UserInfoManager.UserInfo userInfo = userInfoManager.get(account.name);

View File

@ -0,0 +1,16 @@
package com.etesync.syncadapter.utils;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.os.Build;
public class AndroidCompat {
public static void removeAccount (AccountManager accountManager, Account account) {
if (Build.VERSION.SDK_INT >=
Build.VERSION_CODES.LOLLIPOP_MR1) {
accountManager.removeAccountExplicitly(account);
} else {
accountManager.removeAccount(account, null, null);
}
}
}

View File

@ -10,6 +10,6 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/create_calendar" <item android:id="@+id/create_calendar"
android:title="@string/account_create_new_calendar"/> android:title="@string/create_calendar"/>
</menu> </menu>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/create_addressbook"
android:title="@string/create_addressbook"/>
</menu>

View File

@ -57,7 +57,6 @@
<string name="account_delete">Smazat účet</string> <string name="account_delete">Smazat účet</string>
<string name="account_delete_confirmation_title">Opravdu smazat účet?</string> <string name="account_delete_confirmation_title">Opravdu smazat účet?</string>
<string name="account_delete_confirmation_text">Všechny místní kopie adresáře, kalendářů a úkolů budou smazány.</string> <string name="account_delete_confirmation_text">Všechny místní kopie adresáře, kalendářů a úkolů budou smazány.</string>
<string name="account_create_new_calendar">Vytvořit nový kalendář</string>
<!--PermissionsActivity--> <!--PermissionsActivity-->
<string name="permissions_calendar">Oprávnění pro kalendáře</string> <string name="permissions_calendar">Oprávnění pro kalendáře</string>
<string name="permissions_calendar_request">Vyžádat oprávnění kalendáře</string> <string name="permissions_calendar_request">Vyžádat oprávnění kalendáře</string>

View File

@ -44,7 +44,6 @@
<string name="account_delete">Slet konto</string> <string name="account_delete">Slet konto</string>
<string name="account_delete_confirmation_title">Ønsker du at slette konto?</string> <string name="account_delete_confirmation_title">Ønsker du at slette konto?</string>
<string name="account_delete_confirmation_text">Alle lokale kopier af addessebøger, kalendere og opgavelister vil blive slettet.</string> <string name="account_delete_confirmation_text">Alle lokale kopier af addessebøger, kalendere og opgavelister vil blive slettet.</string>
<string name="account_create_new_calendar">Opret ny kalender</string>
<!--PermissionsActivity--> <!--PermissionsActivity-->
<string name="permissions_calendar">Kalenderadgange</string> <string name="permissions_calendar">Kalenderadgange</string>
<string name="permissions_calendar_request">Anmod om kalenderadgang</string> <string name="permissions_calendar_request">Anmod om kalenderadgang</string>

View File

@ -78,7 +78,6 @@
<string name="account_delete">Konto löschen</string> <string name="account_delete">Konto löschen</string>
<string name="account_delete_confirmation_title">Konto wirklich löschen?</string> <string name="account_delete_confirmation_title">Konto wirklich löschen?</string>
<string name="account_delete_confirmation_text">Alle lokalen gespeicherten Kopien von Addressbüchern, Kalendern und Aufgabenlisten werden gelöscht.</string> <string name="account_delete_confirmation_text">Alle lokalen gespeicherten Kopien von Addressbüchern, Kalendern und Aufgabenlisten werden gelöscht.</string>
<string name="account_create_new_calendar">Neuen Kalender erstellen</string>
<string name="account_delete_collection_last_title">Letzte Sammlung kann nicht gelöscht werden</string> <string name="account_delete_collection_last_title">Letzte Sammlung kann nicht gelöscht werden</string>
<string name="account_delete_collection_last_text">Die letzte Sammlung kann nicht gelöscht werden, bitte erstellen Sie eine neue Sammlung wenn Sie diese löschen wollen.</string> <string name="account_delete_collection_last_text">Die letzte Sammlung kann nicht gelöscht werden, bitte erstellen Sie eine neue Sammlung wenn Sie diese löschen wollen.</string>

View File

@ -57,7 +57,6 @@
<string name="account_delete">Eliminar cuenta</string> <string name="account_delete">Eliminar cuenta</string>
<string name="account_delete_confirmation_title">¿Seguro que deseas eliminar la cuenta?</string> <string name="account_delete_confirmation_title">¿Seguro que deseas eliminar la cuenta?</string>
<string name="account_delete_confirmation_text">Todas las copias locales de tus contactos, calendarios y tareas serán eliminadas.</string> <string name="account_delete_confirmation_text">Todas las copias locales de tus contactos, calendarios y tareas serán eliminadas.</string>
<string name="account_create_new_calendar">Crear nuevo calendario</string>
<!--PermissionsActivity--> <!--PermissionsActivity-->
<string name="permissions_calendar">Permisos de calendario</string> <string name="permissions_calendar">Permisos de calendario</string>
<string name="permissions_calendar_request">Solicitar permisos sobre calendario</string> <string name="permissions_calendar_request">Solicitar permisos sobre calendario</string>

View File

@ -57,7 +57,6 @@
<string name="account_delete">Supprimer le compte</string> <string name="account_delete">Supprimer le compte</string>
<string name="account_delete_confirmation_title">Voulez-vous vraiment supprimer le compte?</string> <string name="account_delete_confirmation_title">Voulez-vous vraiment supprimer le compte?</string>
<string name="account_delete_confirmation_text">Toutes les copies locales des carnets d\'adresses, des calendriers et des listes de tâches seront supprimées.</string> <string name="account_delete_confirmation_text">Toutes les copies locales des carnets d\'adresses, des calendriers et des listes de tâches seront supprimées.</string>
<string name="account_create_new_calendar">Créer un nouveau calendrier</string>
<!--PermissionsActivity--> <!--PermissionsActivity-->
<string name="permissions_calendar">Autorisations calendrier</string> <string name="permissions_calendar">Autorisations calendrier</string>
<string name="permissions_calendar_request">Demande d\'autorisations d\'accéder au calendrier</string> <string name="permissions_calendar_request">Demande d\'autorisations d\'accéder au calendrier</string>

View File

@ -57,7 +57,6 @@
<string name="account_delete">Fiók törlése</string> <string name="account_delete">Fiók törlése</string>
<string name="account_delete_confirmation_title">Valóban törölni akarja a fiókot?</string> <string name="account_delete_confirmation_title">Valóban törölni akarja a fiókot?</string>
<string name="account_delete_confirmation_text">Az összes címjegyzék, naptár és feladatlista helyi példányai törölve lesznek.</string> <string name="account_delete_confirmation_text">Az összes címjegyzék, naptár és feladatlista helyi példányai törölve lesznek.</string>
<string name="account_create_new_calendar">Új naptár létrehozása</string>
<!--PermissionsActivity--> <!--PermissionsActivity-->
<string name="permissions_calendar">Naptárengedély</string> <string name="permissions_calendar">Naptárengedély</string>
<string name="permissions_calendar_request">Naptárhozzáférés igénylése</string> <string name="permissions_calendar_request">Naptárhozzáférés igénylése</string>

View File

@ -55,7 +55,6 @@
<string name="account_delete">Elimina account</string> <string name="account_delete">Elimina account</string>
<string name="account_delete_confirmation_title">Cancellare l\'account?</string> <string name="account_delete_confirmation_title">Cancellare l\'account?</string>
<string name="account_delete_confirmation_text">Tutte le copie locali delle rubriche, dei calendari e degli elenchi attività verranno eliminate.</string> <string name="account_delete_confirmation_text">Tutte le copie locali delle rubriche, dei calendari e degli elenchi attività verranno eliminate.</string>
<string name="account_create_new_calendar">Crea nuovo calendario</string>
<!--PermissionsActivity--> <!--PermissionsActivity-->
<string name="permissions_calendar">Permessi calendario</string> <string name="permissions_calendar">Permessi calendario</string>
<string name="permissions_calendar_request">Richiesta autorizzazione al calendario</string> <string name="permissions_calendar_request">Richiesta autorizzazione al calendario</string>

View File

@ -57,7 +57,6 @@
<string name="account_delete">アカウントを削除</string> <string name="account_delete">アカウントを削除</string>
<string name="account_delete_confirmation_title">アカウントを削除してもよろしいですか?</string> <string name="account_delete_confirmation_title">アカウントを削除してもよろしいですか?</string>
<string name="account_delete_confirmation_text">アドレス帳、カレンダー、タスクリストのローカルコピーがすべて削除されます。</string> <string name="account_delete_confirmation_text">アドレス帳、カレンダー、タスクリストのローカルコピーがすべて削除されます。</string>
<string name="account_create_new_calendar">新しいカレンダーを作成</string>
<!--PermissionsActivity--> <!--PermissionsActivity-->
<string name="permissions_calendar">カレンダー アクセス許可</string> <string name="permissions_calendar">カレンダー アクセス許可</string>
<string name="permissions_calendar_request">カレンダー アクセス許可の要求</string> <string name="permissions_calendar_request">カレンダー アクセス許可の要求</string>

View File

@ -51,7 +51,6 @@
<string name="account_delete">Account verwijderen</string> <string name="account_delete">Account verwijderen</string>
<string name="account_delete_confirmation_title">Account echt verwijderen?</string> <string name="account_delete_confirmation_title">Account echt verwijderen?</string>
<string name="account_delete_confirmation_text">Alle lokale kopieën van adresboeken, agenda\'s en taken worden verwijderd.</string> <string name="account_delete_confirmation_text">Alle lokale kopieën van adresboeken, agenda\'s en taken worden verwijderd.</string>
<string name="account_create_new_calendar">Maak een nieuwe agenda</string>
<!--PermissionsActivity--> <!--PermissionsActivity-->
<string name="permissions_calendar">Agenda rechten</string> <string name="permissions_calendar">Agenda rechten</string>
<string name="permissions_calendar_request">Agenda rechten verkrijgen</string> <string name="permissions_calendar_request">Agenda rechten verkrijgen</string>

View File

@ -57,7 +57,6 @@
<string name="account_delete">Usuń konto</string> <string name="account_delete">Usuń konto</string>
<string name="account_delete_confirmation_title">Naprawdę chcesz usunąć konto?</string> <string name="account_delete_confirmation_title">Naprawdę chcesz usunąć konto?</string>
<string name="account_delete_confirmation_text">Wszystkie lokalne kopie książek adresowych, kalendarzy i list zadań zostaną usunięte.</string> <string name="account_delete_confirmation_text">Wszystkie lokalne kopie książek adresowych, kalendarzy i list zadań zostaną usunięte.</string>
<string name="account_create_new_calendar">Stwórz nowy kalendarz</string>
<!--PermissionsActivity--> <!--PermissionsActivity-->
<string name="permissions_calendar">Uprawnienia kalendarza</string> <string name="permissions_calendar">Uprawnienia kalendarza</string>
<string name="permissions_calendar_request">Zezwól na uprawnienia kalendarza</string> <string name="permissions_calendar_request">Zezwól na uprawnienia kalendarza</string>

View File

@ -57,7 +57,6 @@
<string name="account_delete">Excluir conta</string> <string name="account_delete">Excluir conta</string>
<string name="account_delete_confirmation_title">Deseja excluir a conta?</string> <string name="account_delete_confirmation_title">Deseja excluir a conta?</string>
<string name="account_delete_confirmation_text">Todas as cópias locais dos livros de endereços, calendários e listas de tarefas serão excluídas.</string> <string name="account_delete_confirmation_text">Todas as cópias locais dos livros de endereços, calendários e listas de tarefas serão excluídas.</string>
<string name="account_create_new_calendar">Criar novo calendário</string>
<!--PermissionsActivity--> <!--PermissionsActivity-->
<string name="permissions_calendar">Permissões do calendário</string> <string name="permissions_calendar">Permissões do calendário</string>
<string name="permissions_calendar_request">Solicitar permissão do calendário</string> <string name="permissions_calendar_request">Solicitar permissão do calendário</string>

View File

@ -57,7 +57,6 @@
<string name="account_delete">Обриши налог</string> <string name="account_delete">Обриши налог</string>
<string name="account_delete_confirmation_title">Заиста обрисати налог?</string> <string name="account_delete_confirmation_title">Заиста обрисати налог?</string>
<string name="account_delete_confirmation_text">Све локалне копије адресара, календара и листи задатака ће бити обрисане.</string> <string name="account_delete_confirmation_text">Све локалне копије адресара, календара и листи задатака ће бити обрисане.</string>
<string name="account_create_new_calendar">Направи нови календар</string>
<!--PermissionsActivity--> <!--PermissionsActivity-->
<string name="permissions_calendar">Дозволе за календар</string> <string name="permissions_calendar">Дозволе за календар</string>
<string name="permissions_calendar_request">Захтевај дозволе за календар</string> <string name="permissions_calendar_request">Захтевај дозволе за календар</string>

View File

@ -44,7 +44,6 @@
<string name="account_delete">Hesabı sil</string> <string name="account_delete">Hesabı sil</string>
<string name="account_delete_confirmation_title">Hesap gerçekten silinsin mi?</string> <string name="account_delete_confirmation_title">Hesap gerçekten silinsin mi?</string>
<string name="account_delete_confirmation_text">Rehber, takvim ve iş listelerinin tüm yerel kopyaları silinecektir.</string> <string name="account_delete_confirmation_text">Rehber, takvim ve iş listelerinin tüm yerel kopyaları silinecektir.</string>
<string name="account_create_new_calendar">Yeni takvim oluştur</string>
<!--PermissionsActivity--> <!--PermissionsActivity-->
<string name="permissions_calendar">Takvim izinleri</string> <string name="permissions_calendar">Takvim izinleri</string>
<string name="permissions_calendar_request">Takvim izinleri iste</string> <string name="permissions_calendar_request">Takvim izinleri iste</string>

View File

@ -57,7 +57,6 @@
<string name="account_delete">删除账户</string> <string name="account_delete">删除账户</string>
<string name="account_delete_confirmation_title">真的要删除账户吗?</string> <string name="account_delete_confirmation_title">真的要删除账户吗?</string>
<string name="account_delete_confirmation_text">所有通讯录、日历和任务列表的本机存储将被删除。</string> <string name="account_delete_confirmation_text">所有通讯录、日历和任务列表的本机存储将被删除。</string>
<string name="account_create_new_calendar">创建日历</string>
<!--PermissionsActivity--> <!--PermissionsActivity-->
<string name="permissions_calendar">日历权限</string> <string name="permissions_calendar">日历权限</string>
<string name="permissions_calendar_request">请求日历权限</string> <string name="permissions_calendar_request">请求日历权限</string>

View File

@ -11,6 +11,11 @@
<!-- common strings --> <!-- common strings -->
<string name="app_name">EteSync</string> <string name="app_name">EteSync</string>
<string name="account_type" translatable="false">com.etesync.syncadapter</string>
<string name="account_type_address_book" translatable="false">com.etesync.syncadapter.address_book</string>
<string name="account_title_address_book">EteSync Address book</string>
<string name="address_books_authority" translatable="false">com.etesync.syncadapter.addressbooks</string>
<string name="address_books_authority_title">Address books</string>
<string name="help">Help</string> <string name="help">Help</string>
<string name="manage_accounts">Manage accounts</string> <string name="manage_accounts">Manage accounts</string>
<string name="please_wait">Please wait …</string> <string name="please_wait">Please wait …</string>
@ -93,7 +98,6 @@
<string name="account_show_fingerprint">Show Fingerprint</string> <string name="account_show_fingerprint">Show Fingerprint</string>
<string name="account_delete_confirmation_title">Really delete account?</string> <string name="account_delete_confirmation_title">Really delete account?</string>
<string name="account_delete_confirmation_text">All local copies of address books, calendars and task lists will be deleted.</string> <string name="account_delete_confirmation_text">All local copies of address books, calendars and task lists will be deleted.</string>
<string name="account_create_new_calendar">Create new calendar</string>
<string name="account_delete_collection_last_title">Can\'t delete last collection</string> <string name="account_delete_collection_last_title">Can\'t delete last collection</string>
<string name="account_delete_collection_last_text">Deleting the last collection is not allowed, please create a new one if you\'d like to delete this one.</string> <string name="account_delete_collection_last_text">Deleting the last collection is not allowed, please create a new one if you\'d like to delete this one.</string>
<string name="account_showcase_view_collection">You can click on an item to view the collection. From there you can view the journal, import, and much more...</string> <string name="account_showcase_view_collection">You can click on an item to view the collection. From there you can view the journal, import, and much more...</string>
@ -107,7 +111,6 @@
<string name="members_owner_only">Only the owner of this collection (%s) is allowed to view its members.</string> <string name="members_owner_only">Only the owner of this collection (%s) is allowed to view its members.</string>
<string name="not_allowed_title">Not Allowed</string> <string name="not_allowed_title">Not Allowed</string>
<string name="edit_owner_only">Only the owner of this collection (%s) is allowed to edit it.</string> <string name="edit_owner_only">Only the owner of this collection (%s) is allowed to edit it.</string>
<string name="members_address_book_not_allowed">Sharing of address books is currently not supported.</string>
<string name="members_old_journals_not_allowed">Sharing of old-style journals is not allowed. In order to share this journal, create a new one, and copy its contents over using the \"import\" dialog. If you are experiencing any issues, please contact support.</string> <string name="members_old_journals_not_allowed">Sharing of old-style journals is not allowed. In order to share this journal, create a new one, and copy its contents over using the \"import\" dialog. If you are experiencing any issues, please contact support.</string>
<!-- CollectionMembers --> <!-- CollectionMembers -->
@ -164,6 +167,8 @@
<string name="login_encryption_setup_title">Setting up encryption</string> <string name="login_encryption_setup_title">Setting up encryption</string>
<string name="login_encryption_setup">Please wait, setting up encryption…</string> <string name="login_encryption_setup">Please wait, setting up encryption…</string>
<string name="account_creation_failed">Account creation failed</string>
<!-- SetupUserInfoFragment --> <!-- SetupUserInfoFragment -->
<string name="login_encryption_error_title">Encryption Error</string> <string name="login_encryption_error_title">Encryption Error</string>

View File

@ -7,7 +7,7 @@
--> -->
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.etesync.syncadapter" android:accountType="@string/account_type"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:smallIcon="@mipmap/ic_launcher" android:smallIcon="@mipmap/ic_launcher"

View File

@ -0,0 +1,14 @@
<!--
~ 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
-->
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type_address_book"
android:icon="@mipmap/ic_launcher"
android:label="@string/account_title_address_book"
android:smallIcon="@mipmap/ic_launcher"
android:accountPreferences="@xml/sync_prefs" />

View File

@ -0,0 +1,13 @@
<!--
~ 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
-->
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type"
android:contentAuthority="@string/address_books_authority"
android:isAlwaysSyncable="true"
android:supportsUploading="false" />

View File

@ -7,9 +7,9 @@
--> -->
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.etesync.syncadapter" android:accountType="@string/account_type_address_book"
android:contentAuthority="com.android.contacts" android:contentAuthority="com.android.contacts"
android:allowParallelSyncs="true" android:allowParallelSyncs="true"
android:supportsUploading="true" android:supportsUploading="true"
android:isAlwaysSyncable="true" android:userVisible="false"
android:userVisible="true" /> android:isAlwaysSyncable="true" />

@ -1 +1 @@
Subproject commit 2f6a94e85b9c0a375af8bdb08c8af82287c5fdc4 Subproject commit 49fbb6846153fee5e5cbc71b77e94bef7629936d