mirror of
https://github.com/etesync/android
synced 2024-12-23 07:08:16 +00:00
Kotlin: more kotlin migration.
This commit is contained in:
parent
4c4c94ca1c
commit
4d516c5fe1
@ -105,7 +105,7 @@ public class DavResourceFinderTest {
|
|||||||
"</resourcetype>";
|
"</resourcetype>";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
App.log.info("Sending props: " + props);
|
App.Companion.getLog().info("Sending props: " + props);
|
||||||
return new MockResponse()
|
return new MockResponse()
|
||||||
.setResponseCode(207)
|
.setResponseCode(207)
|
||||||
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
|
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
|
||||||
|
@ -1,357 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
|
||||||
* which accompanies this distribution, and is available at
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*/
|
|
||||||
package com.etesync.syncadapter;
|
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.ContentProviderClient;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.PeriodicSync;
|
|
||||||
import android.os.Build;
|
|
||||||
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.Nullable;
|
|
||||||
|
|
||||||
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 java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
|
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
|
||||||
import at.bitfire.vcard4android.GroupMethod;
|
|
||||||
|
|
||||||
public class AccountSettings {
|
|
||||||
private final static int CURRENT_VERSION = 2;
|
|
||||||
private final static String
|
|
||||||
KEY_SETTINGS_VERSION = "version",
|
|
||||||
KEY_URI = "uri",
|
|
||||||
KEY_USERNAME = "user_name",
|
|
||||||
KEY_TOKEN = "auth_token",
|
|
||||||
KEY_ASYMMETRIC_PRIVATE_KEY = "asymmetric_private_key",
|
|
||||||
KEY_ASYMMETRIC_PUBLIC_KEY = "asymmetric_public_key",
|
|
||||||
KEY_WIFI_ONLY = "wifi_only", // sync on WiFi only (default: false)
|
|
||||||
KEY_WIFI_ONLY_SSID = "wifi_only_ssid"; // restrict sync to specific WiFi SSID
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Time range limitation to the past [in days]
|
|
||||||
* value = null default value (DEFAULT_TIME_RANGE_PAST_DAYS)
|
|
||||||
* < 0 (-1) no limit
|
|
||||||
* >= 0 entries more than n days in the past won't be synchronized
|
|
||||||
*/
|
|
||||||
private final static String KEY_TIME_RANGE_PAST_DAYS = "time_range_past_days";
|
|
||||||
private final static int DEFAULT_TIME_RANGE_PAST_DAYS = 90;
|
|
||||||
|
|
||||||
/* Whether DAVdroid sets the local calendar color to the value from service DB at every sync
|
|
||||||
value = null (not existing) true (default)
|
|
||||||
"0" false */
|
|
||||||
private final static String KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contact group method:
|
|
||||||
* value = null (not existing) groups as separate VCards (default)
|
|
||||||
* "CATEGORIES" groups are per-contact CATEGORIES
|
|
||||||
*/
|
|
||||||
private final static String KEY_CONTACT_GROUP_METHOD = "contact_group_method";
|
|
||||||
|
|
||||||
public final static long SYNC_INTERVAL_MANUALLY = -1;
|
|
||||||
|
|
||||||
final Context context;
|
|
||||||
final AccountManager accountManager;
|
|
||||||
final Account account;
|
|
||||||
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public AccountSettings(@NonNull Context context, @NonNull Account account) throws InvalidAccountException {
|
|
||||||
this.context = context;
|
|
||||||
this.account = account;
|
|
||||||
|
|
||||||
accountManager = AccountManager.get(context);
|
|
||||||
|
|
||||||
synchronized (AccountSettings.class) {
|
|
||||||
String versionStr = accountManager.getUserData(account, KEY_SETTINGS_VERSION);
|
|
||||||
if (versionStr == null)
|
|
||||||
throw new InvalidAccountException(account);
|
|
||||||
|
|
||||||
int version = 0;
|
|
||||||
try {
|
|
||||||
version = Integer.parseInt(versionStr);
|
|
||||||
} catch (NumberFormatException ignored) {
|
|
||||||
}
|
|
||||||
App.log.fine("Account " + account.name + " has version " + version + ", current version: " + CURRENT_VERSION);
|
|
||||||
|
|
||||||
if (version < CURRENT_VERSION)
|
|
||||||
update(version);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX: Workaround a bug in Android where passing a bundle to addAccountExplicitly doesn't work.
|
|
||||||
public static void setUserData(AccountManager accountManager, Account account, URI uri, String userName) {
|
|
||||||
accountManager.setUserData(account, KEY_SETTINGS_VERSION, String.valueOf(CURRENT_VERSION));
|
|
||||||
accountManager.setUserData(account, KEY_USERNAME, userName);
|
|
||||||
accountManager.setUserData(account, KEY_URI, uri.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// authentication settings
|
|
||||||
|
|
||||||
public URI getUri() {
|
|
||||||
try {
|
|
||||||
return new URI(accountManager.getUserData(account, KEY_URI));
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUri(@NonNull URI uri) {
|
|
||||||
accountManager.setUserData(account, KEY_URI, uri.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAuthToken() {
|
|
||||||
return accountManager.getUserData(account, KEY_TOKEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAuthToken(@NonNull String token) {
|
|
||||||
accountManager.setUserData(account, KEY_TOKEN, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Crypto.AsymmetricKeyPair getKeyPair() {
|
|
||||||
if (accountManager.getUserData(account, KEY_ASYMMETRIC_PUBLIC_KEY) != null) {
|
|
||||||
byte[] pubkey = Base64.decode(accountManager.getUserData(account, KEY_ASYMMETRIC_PUBLIC_KEY), Base64.NO_WRAP);
|
|
||||||
byte[] privkey = Base64.decode(accountManager.getUserData(account, KEY_ASYMMETRIC_PRIVATE_KEY), Base64.NO_WRAP);
|
|
||||||
|
|
||||||
return new Crypto.AsymmetricKeyPair(privkey, pubkey);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setKeyPair(@NonNull Crypto.AsymmetricKeyPair keyPair) {
|
|
||||||
accountManager.setUserData(account, KEY_ASYMMETRIC_PUBLIC_KEY, Base64.encodeToString(keyPair.getPublicKey(), Base64.NO_WRAP));
|
|
||||||
accountManager.setUserData(account, KEY_ASYMMETRIC_PRIVATE_KEY, Base64.encodeToString(keyPair.getPrivateKey(), Base64.NO_WRAP));
|
|
||||||
}
|
|
||||||
|
|
||||||
public String username() {
|
|
||||||
return accountManager.getUserData(account, KEY_USERNAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void username(@NonNull String userName) {
|
|
||||||
accountManager.setUserData(account, KEY_USERNAME, userName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String password() {
|
|
||||||
return accountManager.getPassword(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void password(@NonNull String password) {
|
|
||||||
accountManager.setPassword(account, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// sync. settings
|
|
||||||
|
|
||||||
public Long getSyncInterval(@NonNull String authority) {
|
|
||||||
if (ContentResolver.getIsSyncable(account, authority) <= 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (ContentResolver.getSyncAutomatically(account, authority)) {
|
|
||||||
List<PeriodicSync> syncs = ContentResolver.getPeriodicSyncs(account, authority);
|
|
||||||
if (syncs.isEmpty())
|
|
||||||
return SYNC_INTERVAL_MANUALLY;
|
|
||||||
else
|
|
||||||
return syncs.get(0).period;
|
|
||||||
} else
|
|
||||||
return SYNC_INTERVAL_MANUALLY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSyncInterval(@NonNull String authority, long seconds) {
|
|
||||||
if (seconds == SYNC_INTERVAL_MANUALLY) {
|
|
||||||
ContentResolver.setSyncAutomatically(account, authority, false);
|
|
||||||
} else {
|
|
||||||
ContentResolver.setSyncAutomatically(account, authority, true);
|
|
||||||
ContentResolver.addPeriodicSync(account, authority, new Bundle(), seconds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getSyncWifiOnly() {
|
|
||||||
return accountManager.getUserData(account, KEY_WIFI_ONLY) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSyncWiFiOnly(boolean wiFiOnly) {
|
|
||||||
accountManager.setUserData(account, KEY_WIFI_ONLY, wiFiOnly ? "1" : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public String getSyncWifiOnlySSID() {
|
|
||||||
return accountManager.getUserData(account, KEY_WIFI_ONLY_SSID);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSyncWifiOnlySSID(String ssid) {
|
|
||||||
accountManager.setUserData(account, KEY_WIFI_ONLY_SSID, ssid);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// CalDAV settings
|
|
||||||
|
|
||||||
public boolean getManageCalendarColors() {
|
|
||||||
return accountManager.getUserData(account, KEY_MANAGE_CALENDAR_COLORS) == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setManageCalendarColors(boolean manage) {
|
|
||||||
accountManager.setUserData(account, KEY_MANAGE_CALENDAR_COLORS, manage ? null : "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// CardDAV settings
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public GroupMethod getGroupMethod() {
|
|
||||||
final String name = accountManager.getUserData(account, KEY_CONTACT_GROUP_METHOD);
|
|
||||||
return name != null ?
|
|
||||||
GroupMethod.valueOf(name) :
|
|
||||||
GroupMethod.GROUP_VCARDS;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGroupMethod(@NonNull GroupMethod method) {
|
|
||||||
final String name = method == GroupMethod.GROUP_VCARDS ? null : method.name();
|
|
||||||
accountManager.setUserData(account, KEY_CONTACT_GROUP_METHOD, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// update from previous account settings
|
|
||||||
|
|
||||||
private void update(int fromVersion) {
|
|
||||||
int toVersion = CURRENT_VERSION;
|
|
||||||
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) {
|
|
||||||
long affected = -1;
|
|
||||||
long newCount = -1;
|
|
||||||
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 {
|
|
||||||
// get previous address book settings (including URL)
|
|
||||||
byte[] raw = ContactsContract.SyncState.get(provider, account);
|
|
||||||
if (raw == null)
|
|
||||||
App.log.info("No contacts sync state, ignoring account");
|
|
||||||
else {
|
|
||||||
Parcel parcel = Parcel.obtain();
|
|
||||||
parcel.unmarshall(raw, 0, raw.length);
|
|
||||||
parcel.setDataPosition(0);
|
|
||||||
Bundle params = parcel.readBundle();
|
|
||||||
parcel.recycle();
|
|
||||||
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 newAddressBook = 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);
|
|
||||||
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");
|
|
||||||
newCount = newAddressBook.count();
|
|
||||||
}
|
|
||||||
|
|
||||||
ContactsContract.SyncState.set(provider, account, null);
|
|
||||||
}
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
throw new ContactsStorageException("Couldn't migrate contacts to new address book", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
provider.release();
|
|
||||||
|
|
||||||
// request sync of new address book account
|
|
||||||
ContentResolver.setIsSyncable(account, App.getAddressBooksAuthority(), 1);
|
|
||||||
setSyncInterval(App.getAddressBooksAuthority(), Constants.DEFAULT_SYNC_INTERVAL);
|
|
||||||
|
|
||||||
// Error handling
|
|
||||||
if ((affected != -1) && (affected != newCount)) {
|
|
||||||
NotificationHelper notificationHelper = new NotificationHelper(context, "account-migration", Constants.NOTIFICATION_ACCOUNT_UPDATE);
|
|
||||||
notificationHelper.setThrowable(new AccountMigrationException("Failed to upgrade account"));
|
|
||||||
notificationHelper.notify("Account upgrade failed", "upgrading account");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AppUpdatedReceiver extends BroadcastReceiver {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressLint("UnsafeProtectedBroadcastReceiver,MissingPermission")
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
App.log.info("EteSync was updated, checking for AccountSettings version");
|
|
||||||
|
|
||||||
// peek into AccountSettings to initiate a possible migration
|
|
||||||
AccountManager accountManager = AccountManager.get(context);
|
|
||||||
for (Account account : accountManager.getAccountsByType(App.getAccountType()))
|
|
||||||
try {
|
|
||||||
App.log.info("Checking account " + account.name);
|
|
||||||
new AccountSettings(context, account);
|
|
||||||
} catch (InvalidAccountException e) {
|
|
||||||
App.log.log(Level.SEVERE, "Couldn't check for updated account settings", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AccountMigrationException extends Exception {
|
|
||||||
public AccountMigrationException(String msg) {
|
|
||||||
super(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
335
app/src/main/java/com/etesync/syncadapter/AccountSettings.kt
Normal file
335
app/src/main/java/com/etesync/syncadapter/AccountSettings.kt
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.accounts.Account
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ContentProviderClient
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.PeriodicSync
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.RemoteException
|
||||||
|
import android.provider.ContactsContract
|
||||||
|
|
||||||
|
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 java.net.URI
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
import at.bitfire.vcard4android.ContactsStorageException
|
||||||
|
import at.bitfire.vcard4android.GroupMethod
|
||||||
|
|
||||||
|
class AccountSettings @TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
@Throws(InvalidAccountException::class)
|
||||||
|
constructor(internal val context: Context, internal val account: Account) {
|
||||||
|
internal val accountManager: AccountManager
|
||||||
|
|
||||||
|
|
||||||
|
// authentication settings
|
||||||
|
|
||||||
|
var uri: URI?
|
||||||
|
get() {
|
||||||
|
try {
|
||||||
|
return URI(accountManager.getUserData(account, KEY_URI))
|
||||||
|
} catch (e: URISyntaxException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
set(uri) = accountManager.setUserData(account, KEY_URI, uri.toString())
|
||||||
|
|
||||||
|
var authToken: String
|
||||||
|
get() = accountManager.getUserData(account, KEY_TOKEN)
|
||||||
|
set(token) = accountManager.setUserData(account, KEY_TOKEN, token)
|
||||||
|
|
||||||
|
|
||||||
|
var keyPair: Crypto.AsymmetricKeyPair?
|
||||||
|
get() {
|
||||||
|
if (accountManager.getUserData(account, KEY_ASYMMETRIC_PUBLIC_KEY) != null) {
|
||||||
|
val pubkey = Base64.decode(accountManager.getUserData(account, KEY_ASYMMETRIC_PUBLIC_KEY), Base64.NO_WRAP)
|
||||||
|
val privkey = Base64.decode(accountManager.getUserData(account, KEY_ASYMMETRIC_PRIVATE_KEY), Base64.NO_WRAP)
|
||||||
|
|
||||||
|
return Crypto.AsymmetricKeyPair(privkey, pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
set(keyPair) {
|
||||||
|
accountManager.setUserData(account, KEY_ASYMMETRIC_PUBLIC_KEY, Base64.encodeToString(keyPair?.publicKey, Base64.NO_WRAP))
|
||||||
|
accountManager.setUserData(account, KEY_ASYMMETRIC_PRIVATE_KEY, Base64.encodeToString(keyPair?.privateKey, Base64.NO_WRAP))
|
||||||
|
}
|
||||||
|
|
||||||
|
val syncWifiOnly: Boolean
|
||||||
|
get() = accountManager.getUserData(account, KEY_WIFI_ONLY) != null
|
||||||
|
|
||||||
|
var syncWifiOnlySSID: String?
|
||||||
|
get() = accountManager.getUserData(account, KEY_WIFI_ONLY_SSID)
|
||||||
|
set(ssid) = accountManager.setUserData(account, KEY_WIFI_ONLY_SSID, ssid)
|
||||||
|
|
||||||
|
|
||||||
|
// CalDAV settings
|
||||||
|
|
||||||
|
var manageCalendarColors: Boolean
|
||||||
|
get() = accountManager.getUserData(account, KEY_MANAGE_CALENDAR_COLORS) == null
|
||||||
|
set(manage) = accountManager.setUserData(account, KEY_MANAGE_CALENDAR_COLORS, if (manage) null else "0")
|
||||||
|
|
||||||
|
|
||||||
|
// CardDAV settings
|
||||||
|
|
||||||
|
var groupMethod: GroupMethod
|
||||||
|
get() {
|
||||||
|
val name = accountManager.getUserData(account, KEY_CONTACT_GROUP_METHOD)
|
||||||
|
return if (name != null)
|
||||||
|
GroupMethod.valueOf(name)
|
||||||
|
else
|
||||||
|
GroupMethod.GROUP_VCARDS
|
||||||
|
}
|
||||||
|
set(method) {
|
||||||
|
val name = if (method == GroupMethod.GROUP_VCARDS) null else method.name
|
||||||
|
accountManager.setUserData(account, KEY_CONTACT_GROUP_METHOD, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
|
||||||
|
accountManager = AccountManager.get(context)
|
||||||
|
|
||||||
|
synchronized(AccountSettings::class.java) {
|
||||||
|
val versionStr = accountManager.getUserData(account, KEY_SETTINGS_VERSION)
|
||||||
|
?: throw InvalidAccountException(account)
|
||||||
|
|
||||||
|
var version = 0
|
||||||
|
try {
|
||||||
|
version = Integer.parseInt(versionStr)
|
||||||
|
} catch (ignored: NumberFormatException) {
|
||||||
|
}
|
||||||
|
|
||||||
|
App.log.fine("Account " + account.name + " has version " + version + ", current version: " + CURRENT_VERSION)
|
||||||
|
|
||||||
|
if (version < CURRENT_VERSION)
|
||||||
|
update(version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun username(): String {
|
||||||
|
return accountManager.getUserData(account, KEY_USERNAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun username(userName: String) {
|
||||||
|
accountManager.setUserData(account, KEY_USERNAME, userName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun password(): String {
|
||||||
|
return accountManager.getPassword(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun password(password: String) {
|
||||||
|
accountManager.setPassword(account, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// sync. settings
|
||||||
|
|
||||||
|
fun getSyncInterval(authority: String): Long? {
|
||||||
|
if (ContentResolver.getIsSyncable(account, authority) <= 0)
|
||||||
|
return null
|
||||||
|
|
||||||
|
if (ContentResolver.getSyncAutomatically(account, authority)) {
|
||||||
|
val syncs = ContentResolver.getPeriodicSyncs(account, authority)
|
||||||
|
return if (syncs.isEmpty())
|
||||||
|
SYNC_INTERVAL_MANUALLY
|
||||||
|
else
|
||||||
|
syncs[0].period
|
||||||
|
} else
|
||||||
|
return SYNC_INTERVAL_MANUALLY
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSyncInterval(authority: String, seconds: Long) {
|
||||||
|
if (seconds == SYNC_INTERVAL_MANUALLY) {
|
||||||
|
ContentResolver.setSyncAutomatically(account, authority, false)
|
||||||
|
} else {
|
||||||
|
ContentResolver.setSyncAutomatically(account, authority, true)
|
||||||
|
ContentResolver.addPeriodicSync(account, authority, Bundle(), seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSyncWiFiOnly(wiFiOnly: Boolean) {
|
||||||
|
accountManager.setUserData(account, KEY_WIFI_ONLY, if (wiFiOnly) "1" else null)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// update from previous account settings
|
||||||
|
|
||||||
|
private fun update(fromVersion: Int) {
|
||||||
|
val toVersion = CURRENT_VERSION
|
||||||
|
App.log.info("Updating account " + account.name + " from version " + fromVersion + " to " + toVersion)
|
||||||
|
try {
|
||||||
|
updateInner(fromVersion)
|
||||||
|
accountManager.setUserData(account, KEY_SETTINGS_VERSION, toVersion.toString())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
App.log.log(Level.SEVERE, "Couldn't update account settings", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(ContactsStorageException::class)
|
||||||
|
private fun updateInner(fromVersion: Int) {
|
||||||
|
if (fromVersion < 2) {
|
||||||
|
var affected: Long = -1
|
||||||
|
var newCount: Long = -1
|
||||||
|
val provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
|
||||||
|
?: // no access to contacts provider
|
||||||
|
return
|
||||||
|
|
||||||
|
// don't run syncs during the migration
|
||||||
|
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0)
|
||||||
|
ContentResolver.setIsSyncable(account, App.addressBooksAuthority, 0)
|
||||||
|
ContentResolver.cancelSync(account, null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// get previous address book settings (including URL)
|
||||||
|
val raw = ContactsContract.SyncState.get(provider, account)
|
||||||
|
if (raw == null)
|
||||||
|
App.log.info("No contacts sync state, ignoring account")
|
||||||
|
else {
|
||||||
|
val parcel = Parcel.obtain()
|
||||||
|
parcel.unmarshall(raw, 0, raw.size)
|
||||||
|
parcel.setDataPosition(0)
|
||||||
|
val params = parcel.readBundle()
|
||||||
|
parcel.recycle()
|
||||||
|
val url = params.getString("url")
|
||||||
|
if (url == null)
|
||||||
|
App.log.info("No address book URL, ignoring account")
|
||||||
|
else {
|
||||||
|
// create new address book
|
||||||
|
val info = 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)
|
||||||
|
val addressBookAccount = Account(LocalAddressBook.accountName(account, info), App.addressBookAccountType)
|
||||||
|
if (!accountManager.addAccountExplicitly(addressBookAccount, null, null))
|
||||||
|
throw ContactsStorageException("Couldn't create address book account")
|
||||||
|
|
||||||
|
LocalAddressBook.setUserData(accountManager, addressBookAccount, account, info.uid)
|
||||||
|
val newAddressBook = LocalAddressBook(context, addressBookAccount, provider)
|
||||||
|
|
||||||
|
// move contacts to new address book
|
||||||
|
App.log.info("Moving contacts from $account to $addressBookAccount")
|
||||||
|
val newAccount = ContentValues(2)
|
||||||
|
newAccount.put(ContactsContract.RawContacts.ACCOUNT_NAME, addressBookAccount.name)
|
||||||
|
newAccount.put(ContactsContract.RawContacts.ACCOUNT_TYPE, addressBookAccount.type)
|
||||||
|
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 + "=?",
|
||||||
|
arrayOf(account.name, account.type)).toLong()
|
||||||
|
App.log.info(affected.toString() + " contacts moved to new address book")
|
||||||
|
newCount = newAddressBook.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
ContactsContract.SyncState.set(provider, account, null)
|
||||||
|
}
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
throw ContactsStorageException("Couldn't migrate contacts to new address book", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.release()
|
||||||
|
|
||||||
|
// request sync of new address book account
|
||||||
|
ContentResolver.setIsSyncable(account, App.addressBooksAuthority, 1)
|
||||||
|
setSyncInterval(App.addressBooksAuthority!!, Constants.DEFAULT_SYNC_INTERVAL.toLong())
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
if (affected != -1L && affected != newCount) {
|
||||||
|
val notificationHelper = NotificationHelper(context, "account-migration", Constants.NOTIFICATION_ACCOUNT_UPDATE)
|
||||||
|
notificationHelper.setThrowable(AccountMigrationException("Failed to upgrade account"))
|
||||||
|
notificationHelper.notify("Account upgrade failed", "upgrading account")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppUpdatedReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
@SuppressLint("UnsafeProtectedBroadcastReceiver,MissingPermission")
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
App.log.info("EteSync was updated, checking for AccountSettings version")
|
||||||
|
|
||||||
|
// peek into AccountSettings to initiate a possible migration
|
||||||
|
val accountManager = AccountManager.get(context)
|
||||||
|
for (account in accountManager.getAccountsByType(App.accountType))
|
||||||
|
try {
|
||||||
|
App.log.info("Checking account " + account.name)
|
||||||
|
AccountSettings(context, account)
|
||||||
|
} catch (e: InvalidAccountException) {
|
||||||
|
App.log.log(Level.SEVERE, "Couldn't check for updated account settings", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccountMigrationException(msg: String) : Exception(msg)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val CURRENT_VERSION = 2
|
||||||
|
private val KEY_SETTINGS_VERSION = "version"
|
||||||
|
private val KEY_URI = "uri"
|
||||||
|
private val KEY_USERNAME = "user_name"
|
||||||
|
private val KEY_TOKEN = "auth_token"
|
||||||
|
private val KEY_ASYMMETRIC_PRIVATE_KEY = "asymmetric_private_key"
|
||||||
|
private val KEY_ASYMMETRIC_PUBLIC_KEY = "asymmetric_public_key"
|
||||||
|
private val KEY_WIFI_ONLY = "wifi_only"
|
||||||
|
// sync on WiFi only (default: false)
|
||||||
|
private val KEY_WIFI_ONLY_SSID = "wifi_only_ssid" // restrict sync to specific WiFi SSID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time range limitation to the past [in days]
|
||||||
|
* value = null default value (DEFAULT_TIME_RANGE_PAST_DAYS)
|
||||||
|
* < 0 (-1) no limit
|
||||||
|
* >= 0 entries more than n days in the past won't be synchronized
|
||||||
|
*/
|
||||||
|
private val KEY_TIME_RANGE_PAST_DAYS = "time_range_past_days"
|
||||||
|
private val DEFAULT_TIME_RANGE_PAST_DAYS = 90
|
||||||
|
|
||||||
|
/* Whether DAVdroid sets the local calendar color to the value from service DB at every sync
|
||||||
|
value = null (not existing) true (default)
|
||||||
|
"0" false */
|
||||||
|
private val KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact group method:
|
||||||
|
* value = null (not existing) groups as separate VCards (default)
|
||||||
|
* "CATEGORIES" groups are per-contact CATEGORIES
|
||||||
|
*/
|
||||||
|
private val KEY_CONTACT_GROUP_METHOD = "contact_group_method"
|
||||||
|
|
||||||
|
val SYNC_INTERVAL_MANUALLY: Long = -1
|
||||||
|
|
||||||
|
// XXX: Workaround a bug in Android where passing a bundle to addAccountExplicitly doesn't work.
|
||||||
|
fun setUserData(accountManager: AccountManager, account: Account, uri: URI, userName: String) {
|
||||||
|
accountManager.setUserData(account, KEY_SETTINGS_VERSION, CURRENT_VERSION.toString())
|
||||||
|
accountManager.setUserData(account, KEY_USERNAME, userName)
|
||||||
|
accountManager.setUserData(account, KEY_URI, uri.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,132 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
|
||||||
* which accompanies this distribution, and is available at
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.etesync.syncadapter;
|
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.database.DatabaseUtils;
|
|
||||||
import android.os.Binder;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.etesync.syncadapter.model.ServiceEntity;
|
|
||||||
import com.etesync.syncadapter.resource.LocalAddressBook;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
|
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
|
||||||
import io.requery.Persistable;
|
|
||||||
import io.requery.sql.EntityDataStore;
|
|
||||||
|
|
||||||
public class AccountUpdateService extends Service {
|
|
||||||
|
|
||||||
public static final String
|
|
||||||
ACTION_ACCOUNTS_UPDATED = "accountsUpdated";
|
|
||||||
|
|
||||||
private final IBinder binder = new InfoBinder();
|
|
||||||
|
|
||||||
private final Set<Long> runningRefresh = new HashSet<>();
|
|
||||||
private final List<WeakReference<RefreshingStatusListener>> refreshingStatusListeners = new LinkedList<>();
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
||||||
if (intent != null) {
|
|
||||||
String action = intent.getAction();
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case ACTION_ACCOUNTS_UPDATED:
|
|
||||||
cleanupAccounts();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* BOUND SERVICE PART
|
|
||||||
for communicating with the activities
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return binder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface RefreshingStatusListener {
|
|
||||||
void onDavRefreshStatusChanged(long id, boolean refreshing);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class InfoBinder extends Binder {
|
|
||||||
public boolean isRefreshing(long id) {
|
|
||||||
return runningRefresh.contains(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addRefreshingStatusListener(@NonNull RefreshingStatusListener listener, boolean callImmediate) {
|
|
||||||
refreshingStatusListeners.add(new WeakReference<>(listener));
|
|
||||||
if (callImmediate)
|
|
||||||
for (long id : runningRefresh)
|
|
||||||
listener.onDavRefreshStatusChanged(id, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeRefreshingStatusListener(@NonNull RefreshingStatusListener listener) {
|
|
||||||
for (Iterator<WeakReference<RefreshingStatusListener>> iterator = refreshingStatusListeners.iterator(); iterator.hasNext(); ) {
|
|
||||||
RefreshingStatusListener item = iterator.next().get();
|
|
||||||
if (listener.equals(item))
|
|
||||||
iterator.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ACTION RUNNABLES
|
|
||||||
which actually do the work
|
|
||||||
*/
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
void cleanupAccounts() {
|
|
||||||
App.log.info("Cleaning up orphaned accounts");
|
|
||||||
|
|
||||||
List<String> accountNames = new LinkedList<>();
|
|
||||||
AccountManager am = AccountManager.get(this);
|
|
||||||
for (Account account : am.getAccountsByType(App.getAccountType())) {
|
|
||||||
accountNames.add(account.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
EntityDataStore<Persistable> data = ((App) getApplication()).getData();
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
} else {
|
|
||||||
data.delete(ServiceEntity.class).where(ServiceEntity.ACCOUNT.notIn(accountNames)).get().value();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.etesync.syncadapter
|
||||||
|
|
||||||
|
import android.accounts.Account
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.database.DatabaseUtils
|
||||||
|
import android.os.Binder
|
||||||
|
import android.os.IBinder
|
||||||
|
|
||||||
|
import com.etesync.syncadapter.model.ServiceEntity
|
||||||
|
import com.etesync.syncadapter.resource.LocalAddressBook
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.HashSet
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
import at.bitfire.vcard4android.ContactsStorageException
|
||||||
|
import io.requery.Persistable
|
||||||
|
import io.requery.sql.EntityDataStore
|
||||||
|
|
||||||
|
class AccountUpdateService : Service() {
|
||||||
|
|
||||||
|
private val binder = InfoBinder()
|
||||||
|
|
||||||
|
private val runningRefresh = HashSet<Long>()
|
||||||
|
private val refreshingStatusListeners = LinkedList<WeakReference<RefreshingStatusListener>>()
|
||||||
|
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (intent != null) {
|
||||||
|
val action = intent.action
|
||||||
|
|
||||||
|
when (action) {
|
||||||
|
ACTION_ACCOUNTS_UPDATED -> cleanupAccounts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Service.START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* BOUND SERVICE PART
|
||||||
|
for communicating with the activities
|
||||||
|
*/
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
return binder
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RefreshingStatusListener {
|
||||||
|
fun onDavRefreshStatusChanged(id: Long, refreshing: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class InfoBinder : Binder() {
|
||||||
|
fun isRefreshing(id: Long): Boolean {
|
||||||
|
return runningRefresh.contains(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addRefreshingStatusListener(listener: RefreshingStatusListener, callImmediate: Boolean) {
|
||||||
|
refreshingStatusListeners.add(WeakReference(listener))
|
||||||
|
if (callImmediate)
|
||||||
|
for (id in runningRefresh)
|
||||||
|
listener.onDavRefreshStatusChanged(id, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeRefreshingStatusListener(listener: RefreshingStatusListener) {
|
||||||
|
val iterator = refreshingStatusListeners.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val item = iterator.next().get()
|
||||||
|
if (listener == item)
|
||||||
|
iterator.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ACTION RUNNABLES
|
||||||
|
which actually do the work
|
||||||
|
*/
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
internal fun cleanupAccounts() {
|
||||||
|
App.log.info("Cleaning up orphaned accounts")
|
||||||
|
|
||||||
|
val accountNames = LinkedList<String>()
|
||||||
|
val am = AccountManager.get(this)
|
||||||
|
for (account in am.getAccountsByType(App.accountType)) {
|
||||||
|
accountNames.add(account.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
val data = (application as App).data
|
||||||
|
|
||||||
|
// delete orphaned address book accounts
|
||||||
|
for (addrBookAccount in am.getAccountsByType(App.addressBookAccountType)) {
|
||||||
|
val addressBook = LocalAddressBook(this, addrBookAccount, null)
|
||||||
|
try {
|
||||||
|
if (!accountNames.contains(addressBook.mainAccount.name))
|
||||||
|
addressBook.delete()
|
||||||
|
} catch (e: ContactsStorageException) {
|
||||||
|
App.log.log(Level.SEVERE, "Couldn't get address book main account", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (accountNames.isEmpty()) {
|
||||||
|
data.delete(ServiceEntity::class.java).get().value()
|
||||||
|
} else {
|
||||||
|
data.delete(ServiceEntity::class.java).where(ServiceEntity.ACCOUNT.notIn(accountNames)).get().value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val ACTION_ACCOUNTS_UPDATED = "accountsUpdated"
|
||||||
|
}
|
||||||
|
}
|
@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
|
||||||
* which accompanies this distribution, and is available at
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.etesync.syncadapter;
|
|
||||||
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.accounts.OnAccountsUpdateListener;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class AccountsChangedReceiver extends BroadcastReceiver {
|
|
||||||
|
|
||||||
protected static final List<OnAccountsUpdateListener> listeners = new LinkedList<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(intent.getAction())) {
|
|
||||||
Intent serviceIntent = new Intent(context, AccountUpdateService.class);
|
|
||||||
serviceIntent.setAction(AccountUpdateService.ACTION_ACCOUNTS_UPDATED);
|
|
||||||
context.startService(serviceIntent);
|
|
||||||
|
|
||||||
for (OnAccountsUpdateListener listener : listeners)
|
|
||||||
listener.onAccountsUpdated(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void registerListener(OnAccountsUpdateListener listener, boolean callImmediately) {
|
|
||||||
listeners.add(listener);
|
|
||||||
if (callImmediately)
|
|
||||||
listener.onAccountsUpdated(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unregisterListener(OnAccountsUpdateListener listener) {
|
|
||||||
listeners.remove(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.etesync.syncadapter
|
||||||
|
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.accounts.OnAccountsUpdateListener
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
import java.util.LinkedList
|
||||||
|
|
||||||
|
class AccountsChangedReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION == intent.action) {
|
||||||
|
val serviceIntent = Intent(context, AccountUpdateService::class.java)
|
||||||
|
serviceIntent.action = AccountUpdateService.ACTION_ACCOUNTS_UPDATED
|
||||||
|
context.startService(serviceIntent)
|
||||||
|
|
||||||
|
for (listener in listeners)
|
||||||
|
listener.onAccountsUpdated(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
protected val listeners: MutableList<OnAccountsUpdateListener> = LinkedList()
|
||||||
|
|
||||||
|
fun registerListener(listener: OnAccountsUpdateListener, callImmediately: Boolean) {
|
||||||
|
listeners.add(listener)
|
||||||
|
if (callImmediately)
|
||||||
|
listener.onAccountsUpdated(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregisterListener(listener: OnAccountsUpdateListener) {
|
||||||
|
listeners.remove(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,462 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
|
||||||
* which accompanies this distribution, and is available at
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.etesync.syncadapter;
|
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.DatabaseUtils;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Process;
|
|
||||||
import android.os.StrictMode;
|
|
||||||
import android.provider.CalendarContract;
|
|
||||||
import android.provider.ContactsContract;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v4.app.NotificationCompat;
|
|
||||||
import android.support.v4.app.NotificationManagerCompat;
|
|
||||||
import android.support.v4.content.ContextCompat;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.etesync.syncadapter.log.LogcatHandler;
|
|
||||||
import com.etesync.syncadapter.log.PlainTextFormatter;
|
|
||||||
import com.etesync.syncadapter.model.CollectionInfo;
|
|
||||||
import com.etesync.syncadapter.model.JournalEntity;
|
|
||||||
import com.etesync.syncadapter.model.Models;
|
|
||||||
import com.etesync.syncadapter.model.ServiceDB;
|
|
||||||
import com.etesync.syncadapter.model.ServiceEntity;
|
|
||||||
import com.etesync.syncadapter.model.Settings;
|
|
||||||
import com.etesync.syncadapter.resource.LocalAddressBook;
|
|
||||||
import com.etesync.syncadapter.resource.LocalCalendar;
|
|
||||||
import com.etesync.syncadapter.ui.AccountsActivity;
|
|
||||||
import com.etesync.syncadapter.utils.HintManager;
|
|
||||||
import com.etesync.syncadapter.utils.LanguageUtils;
|
|
||||||
|
|
||||||
import org.acra.ACRA;
|
|
||||||
import org.acra.annotation.AcraCore;
|
|
||||||
import org.acra.annotation.AcraMailSender;
|
|
||||||
import org.acra.annotation.AcraToast;
|
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.logging.FileHandler;
|
|
||||||
import java.util.logging.Handler;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javax.net.ssl.HostnameVerifier;
|
|
||||||
|
|
||||||
import at.bitfire.cert4android.CustomCertManager;
|
|
||||||
import at.bitfire.ical4android.CalendarStorageException;
|
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
|
||||||
import io.requery.Persistable;
|
|
||||||
import io.requery.android.sqlite.DatabaseSource;
|
|
||||||
import io.requery.meta.EntityModel;
|
|
||||||
import io.requery.sql.Configuration;
|
|
||||||
import io.requery.sql.EntityDataStore;
|
|
||||||
import okhttp3.internal.tls.OkHostnameVerifier;
|
|
||||||
|
|
||||||
@AcraCore(buildConfigClass = BuildConfig.class,
|
|
||||||
logcatArguments = {"-t", "500", "-v", "time"})
|
|
||||||
@AcraMailSender(mailTo = "reports@etesync.com",
|
|
||||||
subject = R.string.crash_email_subject,
|
|
||||||
reportFileName = "ACRA-report.stacktrace.json")
|
|
||||||
@AcraToast(resText = R.string.crash_message,
|
|
||||||
length = Toast.LENGTH_LONG)
|
|
||||||
public class App extends Application {
|
|
||||||
public static final String
|
|
||||||
DISTRUST_SYSTEM_CERTIFICATES = "distrustSystemCerts",
|
|
||||||
LOG_TO_EXTERNAL_STORAGE = "logToExternalStorage",
|
|
||||||
OVERRIDE_PROXY = "overrideProxy",
|
|
||||||
OVERRIDE_PROXY_HOST = "overrideProxyHost",
|
|
||||||
OVERRIDE_PROXY_PORT = "overrideProxyPort",
|
|
||||||
FORCE_LANGUAGE = "forceLanguage",
|
|
||||||
CHANGE_NOTIFICATION = "changeNotification";
|
|
||||||
|
|
||||||
public static final String OVERRIDE_PROXY_HOST_DEFAULT = "localhost";
|
|
||||||
public static final int OVERRIDE_PROXY_PORT_DEFAULT = 8118;
|
|
||||||
|
|
||||||
public static final String DEFAULT_LANGUAGE = "default";
|
|
||||||
public static Locale sDefaultLocacle = Locale.getDefault();
|
|
||||||
|
|
||||||
private static String appName;
|
|
||||||
|
|
||||||
private CustomCertManager certManager;
|
|
||||||
|
|
||||||
private static SSLSocketFactoryCompat sslSocketFactoryCompat;
|
|
||||||
|
|
||||||
private static HostnameVerifier hostnameVerifier;
|
|
||||||
|
|
||||||
public final static Logger log = Logger.getLogger("syncadapter");
|
|
||||||
static {
|
|
||||||
at.bitfire.cert4android.Constants.log = Logger.getLogger("syncadapter.cert4android");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String accountType;
|
|
||||||
private static String addressBookAccountType;
|
|
||||||
private static String addressBooksAuthority;
|
|
||||||
|
|
||||||
public static String getAppName() {
|
|
||||||
return App.appName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CustomCertManager getCertManager() {
|
|
||||||
return this.certManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SSLSocketFactoryCompat getSslSocketFactoryCompat() {
|
|
||||||
return App.sslSocketFactoryCompat;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static HostnameVerifier getHostnameVerifier() {
|
|
||||||
return App.hostnameVerifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getAccountType() {
|
|
||||||
return App.accountType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getAddressBookAccountType() {
|
|
||||||
return App.addressBookAccountType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getAddressBooksAuthority() {
|
|
||||||
return App.addressBooksAuthority;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressLint("HardwareIds")
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
reinitCertManager();
|
|
||||||
reinitLogger();
|
|
||||||
StrictMode.enableDefaults();
|
|
||||||
initPrefVersion();
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
loadLanguage();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(Context base) {
|
|
||||||
super.attachBaseContext(base);
|
|
||||||
|
|
||||||
// The following line triggers the initialization of ACRA
|
|
||||||
ACRA.init(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadLanguage() {
|
|
||||||
ServiceDB.OpenHelper serviceDB = new ServiceDB.OpenHelper(this);
|
|
||||||
String lang = new Settings(serviceDB.getReadableDatabase()).getString(App.FORCE_LANGUAGE, null);
|
|
||||||
if (lang != null && !lang.equals(DEFAULT_LANGUAGE)) {
|
|
||||||
LanguageUtils.INSTANCE.setLanguage(this, lang);
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceDB.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void reinitCertManager() {
|
|
||||||
if (BuildConfig.customCerts) {
|
|
||||||
if (certManager != null)
|
|
||||||
certManager.close();
|
|
||||||
|
|
||||||
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(this);
|
|
||||||
Settings settings = new Settings(dbHelper.getReadableDatabase());
|
|
||||||
|
|
||||||
certManager = new CustomCertManager(this, !settings.getBoolean(DISTRUST_SYSTEM_CERTIFICATES, false));
|
|
||||||
sslSocketFactoryCompat = new SSLSocketFactoryCompat(certManager);
|
|
||||||
hostnameVerifier = certManager.hostnameVerifier(OkHostnameVerifier.INSTANCE);
|
|
||||||
|
|
||||||
dbHelper.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void reinitLogger() {
|
|
||||||
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(this);
|
|
||||||
Settings settings = new Settings(dbHelper.getReadableDatabase());
|
|
||||||
|
|
||||||
boolean logToFile = settings.getBoolean(LOG_TO_EXTERNAL_STORAGE, false),
|
|
||||||
logVerbose = logToFile || Log.isLoggable(log.getName(), Log.DEBUG);
|
|
||||||
|
|
||||||
App.log.info("Verbose logging: " + logVerbose);
|
|
||||||
|
|
||||||
// set logging level according to preferences
|
|
||||||
final Logger rootLogger = Logger.getLogger("");
|
|
||||||
rootLogger.setLevel(logVerbose ? Level.ALL : Level.INFO);
|
|
||||||
|
|
||||||
// remove all handlers and add our own logcat handler
|
|
||||||
rootLogger.setUseParentHandlers(false);
|
|
||||||
for (Handler handler : rootLogger.getHandlers())
|
|
||||||
rootLogger.removeHandler(handler);
|
|
||||||
rootLogger.addHandler(LogcatHandler.INSTANCE);
|
|
||||||
|
|
||||||
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
|
|
||||||
// log to external file according to preferences
|
|
||||||
if (logToFile) {
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
|
|
||||||
builder .setSmallIcon(R.drawable.ic_sd_storage_light)
|
|
||||||
.setLargeIcon(getLauncherBitmap(this))
|
|
||||||
.setContentTitle(getString(R.string.logging_davdroid_file_logging))
|
|
||||||
.setLocalOnly(true);
|
|
||||||
|
|
||||||
File dir = getExternalFilesDir(null);
|
|
||||||
if (dir != null)
|
|
||||||
try {
|
|
||||||
String fileName = new File(dir, "etesync-" + Process.myPid() + "-" +
|
|
||||||
DateFormatUtils.format(System.currentTimeMillis(), "yyyyMMdd-HHmmss") + ".txt").toString();
|
|
||||||
log.info("Logging to " + fileName);
|
|
||||||
|
|
||||||
FileHandler fileHandler = new FileHandler(fileName);
|
|
||||||
fileHandler.setFormatter(PlainTextFormatter.DEFAULT);
|
|
||||||
log.addHandler(fileHandler);
|
|
||||||
builder .setContentText(dir.getPath())
|
|
||||||
.setSubText(getString(R.string.logging_to_external_storage_warning))
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
||||||
.setStyle(new NotificationCompat.BigTextStyle()
|
|
||||||
.bigText(getString(R.string.logging_to_external_storage, dir.getPath())))
|
|
||||||
.setOngoing(true);
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.log(Level.SEVERE, "Couldn't create external log file", e);
|
|
||||||
|
|
||||||
builder .setContentText(getString(R.string.logging_couldnt_create_file, e.getLocalizedMessage()))
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_ERROR);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
builder.setContentText(getString(R.string.logging_no_external_storage));
|
|
||||||
|
|
||||||
nm.notify(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING, builder.build());
|
|
||||||
} else
|
|
||||||
nm.cancel(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING);
|
|
||||||
|
|
||||||
dbHelper.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public static Bitmap getLauncherBitmap(@NonNull Context context) {
|
|
||||||
Bitmap bitmapLogo = null;
|
|
||||||
Drawable drawableLogo = ContextCompat.getDrawable(context, R.mipmap.ic_launcher);
|
|
||||||
|
|
||||||
if (drawableLogo instanceof BitmapDrawable)
|
|
||||||
bitmapLogo = ((BitmapDrawable)drawableLogo).getBitmap();
|
|
||||||
return bitmapLogo;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static class ReinitSettingsReceiver extends BroadcastReceiver {
|
|
||||||
|
|
||||||
public static final String ACTION_REINIT_SETTINGS = BuildConfig.APPLICATION_ID + ".REINIT_SETTINGS";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
log.info("Received broadcast: re-initializing settings (logger/cert manager)");
|
|
||||||
|
|
||||||
App app = (App)context.getApplicationContext();
|
|
||||||
app.reinitLogger();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private EntityDataStore<Persistable> dataStore;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {@link EntityDataStore} single instance for the application.
|
|
||||||
* <p/>
|
|
||||||
* Note if you're using Dagger you can make this part of your application level module returning
|
|
||||||
* {@code @Provides @Singleton}.
|
|
||||||
*/
|
|
||||||
public EntityDataStore<Persistable> getData() {
|
|
||||||
if (dataStore == null) {
|
|
||||||
// override onUpgrade to handle migrating to a new version
|
|
||||||
DatabaseSource source = new MyDatabaseSource(this, Models.DEFAULT, 4);
|
|
||||||
Configuration configuration = source.getConfiguration();
|
|
||||||
dataStore = new EntityDataStore<>(configuration);
|
|
||||||
}
|
|
||||||
return dataStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class MyDatabaseSource extends DatabaseSource {
|
|
||||||
MyDatabaseSource(Context context, EntityModel entityModel, int version) {
|
|
||||||
super(context, entityModel, version);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
|
||||||
super.onUpgrade(db, oldVersion, newVersion);
|
|
||||||
|
|
||||||
if (oldVersion < 3) {
|
|
||||||
db.execSQL("PRAGMA foreign_keys=OFF;");
|
|
||||||
|
|
||||||
db.execSQL("CREATE TABLE new_Journal (id integer primary key autoincrement not null, deleted boolean not null, encryptedKey varbinary(255), info varchar(255), owner varchar(255), service integer, serviceModel integer, uid varchar(64) not null, readOnly boolean default false, foreign key (serviceModel) references Service (id) on delete cascade);");
|
|
||||||
db.execSQL("CREATE TABLE new_Entry (id integer primary key autoincrement not null, content varchar(255), journal integer, uid varchar(64) not null, foreign key (journal) references new_Journal (id) on delete cascade);");
|
|
||||||
|
|
||||||
db.execSQL("INSERT INTO new_Journal SELECT id, deleted, encryptedKey, info, owner, service, serviceModel, uid, 0 from Journal;");
|
|
||||||
db.execSQL("INSERT INTO new_Entry SELECT id, content, journal, uid from Entry;");
|
|
||||||
|
|
||||||
db.execSQL("DROP TABLE Journal;");
|
|
||||||
db.execSQL("DROP TABLE Entry;");
|
|
||||||
db.execSQL("ALTER TABLE new_Journal RENAME TO Journal;");
|
|
||||||
db.execSQL("ALTER TABLE new_Entry RENAME TO Entry;");
|
|
||||||
// Add back indexes
|
|
||||||
db.execSQL("CREATE UNIQUE INDEX journal_unique_together on Journal (serviceModel, uid);");
|
|
||||||
db.execSQL("CREATE UNIQUE INDEX entry_unique_together on Entry (journal, uid);");
|
|
||||||
db.execSQL("PRAGMA foreign_keys=ON;");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update from previous account settings
|
|
||||||
|
|
||||||
private final static String PREF_VERSION = "version";
|
|
||||||
|
|
||||||
/** Init the preferences version of the app.
|
|
||||||
* This is used to initialise the first version if not alrady set. */
|
|
||||||
private void initPrefVersion() {
|
|
||||||
SharedPreferences prefs = getSharedPreferences("app", Context.MODE_PRIVATE);
|
|
||||||
if (prefs.getInt(PREF_VERSION, 0) == 0) {
|
|
||||||
prefs.edit().putInt(PREF_VERSION, BuildConfig.VERSION_CODE).apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void update(int fromVersion) {
|
|
||||||
App.log.info("Updating from version " + fromVersion + " to " + BuildConfig.VERSION_CODE);
|
|
||||||
|
|
||||||
if (fromVersion < 6) {
|
|
||||||
EntityDataStore<Persistable> data = this.getData();
|
|
||||||
|
|
||||||
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(this);
|
|
||||||
|
|
||||||
List<CollectionInfo> collections = readCollections(dbHelper);
|
|
||||||
for (CollectionInfo info : collections) {
|
|
||||||
JournalEntity journalEntity = new JournalEntity(data, info);
|
|
||||||
data.insert(journalEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
|
||||||
db.delete(ServiceDB.Collections._TABLE, null, null);
|
|
||||||
db.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fromVersion < 7) {
|
|
||||||
/* Fix all of the etags to be non-null */
|
|
||||||
AccountManager am = AccountManager.get(this);
|
|
||||||
for (Account account : am.getAccountsByType(App.getAccountType())) {
|
|
||||||
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.Factory.INSTANCE, null, null);
|
|
||||||
for (LocalCalendar calendar : calendars) {
|
|
||||||
calendar.fixEtags();
|
|
||||||
}
|
|
||||||
} catch (CalendarStorageException|InvalidAccountException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Account account : am.getAccountsByType(App.getAddressBookAccountType())) {
|
|
||||||
LocalAddressBook addressBook = new LocalAddressBook(this, account, this.getContentResolver().acquireContentProviderClient(ContactsContract.Contacts.CONTENT_URI));
|
|
||||||
try {
|
|
||||||
addressBook.fixEtags();
|
|
||||||
} catch (ContactsStorageException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fromVersion < 10) {
|
|
||||||
HintManager.INSTANCE.setHintSeen(this, AccountsActivity.Companion.getHINT_ACCOUNT_ADD(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fromVersion < 11) {
|
|
||||||
ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(this);
|
|
||||||
|
|
||||||
migrateServices(dbHelper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AppUpdatedReceiver extends BroadcastReceiver {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressLint("UnsafeProtectedBroadcastReceiver,MissingPermission")
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
App.log.info("EteSync was updated, checking for app version");
|
|
||||||
|
|
||||||
App app = (App) context.getApplicationContext();
|
|
||||||
SharedPreferences prefs = app.getSharedPreferences("app", Context.MODE_PRIVATE);
|
|
||||||
int fromVersion = prefs.getInt(PREF_VERSION, 1);
|
|
||||||
app.update(fromVersion);
|
|
||||||
prefs.edit().putInt(PREF_VERSION, BuildConfig.VERSION_CODE).apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private List<CollectionInfo> readCollections(ServiceDB.OpenHelper dbHelper) {
|
|
||||||
SQLiteDatabase db = dbHelper.getWritableDatabase();
|
|
||||||
List<CollectionInfo> collections = new LinkedList<>();
|
|
||||||
Cursor cursor = db.query(ServiceDB.Collections._TABLE, null, null, null, null, null, null);
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
DatabaseUtils.cursorRowToContentValues(cursor, values);
|
|
||||||
collections.add(CollectionInfo.fromDB(values));
|
|
||||||
}
|
|
||||||
|
|
||||||
db.close();
|
|
||||||
cursor.close();
|
|
||||||
return collections;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void migrateServices(ServiceDB.OpenHelper dbHelper) {
|
|
||||||
SQLiteDatabase db = dbHelper.getReadableDatabase();
|
|
||||||
EntityDataStore<Persistable> data = this.getData();
|
|
||||||
Cursor cursor = db.query(ServiceDB.Services._TABLE, null, null, null, null, null, null);
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
DatabaseUtils.cursorRowToContentValues(cursor, values);
|
|
||||||
ServiceEntity service = new ServiceEntity();
|
|
||||||
service.setAccount(values.getAsString(ServiceDB.Services.ACCOUNT_NAME));
|
|
||||||
service.setType(CollectionInfo.Type.valueOf(values.getAsString(ServiceDB.Services.SERVICE)));
|
|
||||||
data.insert(service);
|
|
||||||
|
|
||||||
for (JournalEntity journalEntity : data.select(JournalEntity.class).where(JournalEntity.SERVICE.eq(values.getAsLong(ServiceDB.Services.ID))).get()) {
|
|
||||||
journalEntity.setServiceModel(service);
|
|
||||||
data.update(journalEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
db.delete(ServiceDB.Services._TABLE, null, null);
|
|
||||||
db.close();
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
433
app/src/main/java/com/etesync/syncadapter/App.kt
Normal file
433
app/src/main/java/com/etesync/syncadapter/App.kt
Normal file
@ -0,0 +1,433 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.etesync.syncadapter
|
||||||
|
|
||||||
|
import android.accounts.Account
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.database.DatabaseUtils
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Process
|
||||||
|
import android.os.StrictMode
|
||||||
|
import android.provider.CalendarContract
|
||||||
|
import android.provider.ContactsContract
|
||||||
|
import android.support.v4.app.NotificationCompat
|
||||||
|
import android.support.v4.app.NotificationManagerCompat
|
||||||
|
import android.support.v4.content.ContextCompat
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
|
||||||
|
import com.etesync.syncadapter.log.LogcatHandler
|
||||||
|
import com.etesync.syncadapter.log.PlainTextFormatter
|
||||||
|
import com.etesync.syncadapter.model.CollectionInfo
|
||||||
|
import com.etesync.syncadapter.model.JournalEntity
|
||||||
|
import com.etesync.syncadapter.model.Models
|
||||||
|
import com.etesync.syncadapter.model.ServiceDB
|
||||||
|
import com.etesync.syncadapter.model.ServiceEntity
|
||||||
|
import com.etesync.syncadapter.model.Settings
|
||||||
|
import com.etesync.syncadapter.resource.LocalAddressBook
|
||||||
|
import com.etesync.syncadapter.resource.LocalCalendar
|
||||||
|
import com.etesync.syncadapter.ui.AccountsActivity
|
||||||
|
import com.etesync.syncadapter.utils.HintManager
|
||||||
|
import com.etesync.syncadapter.utils.LanguageUtils
|
||||||
|
|
||||||
|
import org.acra.ACRA
|
||||||
|
import org.acra.annotation.AcraCore
|
||||||
|
import org.acra.annotation.AcraMailSender
|
||||||
|
import org.acra.annotation.AcraToast
|
||||||
|
import org.apache.commons.lang3.time.DateFormatUtils
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.logging.FileHandler
|
||||||
|
import java.util.logging.Handler
|
||||||
|
import java.util.logging.Level
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
import javax.net.ssl.HostnameVerifier
|
||||||
|
|
||||||
|
import at.bitfire.cert4android.CustomCertManager
|
||||||
|
import at.bitfire.ical4android.CalendarStorageException
|
||||||
|
import at.bitfire.vcard4android.ContactsStorageException
|
||||||
|
import io.requery.Persistable
|
||||||
|
import io.requery.android.sqlite.DatabaseSource
|
||||||
|
import io.requery.meta.EntityModel
|
||||||
|
import io.requery.sql.Configuration
|
||||||
|
import io.requery.sql.EntityDataStore
|
||||||
|
import okhttp3.internal.tls.OkHostnameVerifier
|
||||||
|
|
||||||
|
@AcraCore(buildConfigClass = BuildConfig::class, logcatArguments = arrayOf("-t", "500", "-v", "time"))
|
||||||
|
@AcraMailSender(mailTo = "reports@etesync.com", subject = R.string.crash_email_subject, reportFileName = "ACRA-report.stacktrace.json")
|
||||||
|
@AcraToast(resText = R.string.crash_message, length = Toast.LENGTH_LONG)
|
||||||
|
class App : Application() {
|
||||||
|
|
||||||
|
var certManager: CustomCertManager? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return [EntityDataStore] single instance for the application.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Note if you're using Dagger you can make this part of your application level module returning
|
||||||
|
* `@Provides @Singleton`.
|
||||||
|
*/
|
||||||
|
// override onUpgrade to handle migrating to a new version
|
||||||
|
val data: EntityDataStore<Persistable>
|
||||||
|
get() = initDataStore()
|
||||||
|
|
||||||
|
fun initDataStore(): EntityDataStore<Persistable> {
|
||||||
|
val source = MyDatabaseSource(this, Models.DEFAULT, 4)
|
||||||
|
val configuration = source.configuration
|
||||||
|
return EntityDataStore(configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("HardwareIds")
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
reinitCertManager()
|
||||||
|
reinitLogger()
|
||||||
|
StrictMode.enableDefaults()
|
||||||
|
initPrefVersion()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
loadLanguage()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
|
||||||
|
// The following line triggers the initialization of ACRA
|
||||||
|
ACRA.init(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadLanguage() {
|
||||||
|
val serviceDB = ServiceDB.OpenHelper(this)
|
||||||
|
val lang = Settings(serviceDB.readableDatabase).getString(App.FORCE_LANGUAGE, null)
|
||||||
|
if (lang != null && lang != DEFAULT_LANGUAGE) {
|
||||||
|
LanguageUtils.setLanguage(this, lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceDB.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reinitCertManager() {
|
||||||
|
if (BuildConfig.customCerts) {
|
||||||
|
if (certManager != null)
|
||||||
|
certManager!!.close()
|
||||||
|
|
||||||
|
val dbHelper = ServiceDB.OpenHelper(this)
|
||||||
|
val settings = Settings(dbHelper.readableDatabase)
|
||||||
|
|
||||||
|
certManager = CustomCertManager(this, !settings.getBoolean(DISTRUST_SYSTEM_CERTIFICATES, false))
|
||||||
|
sslSocketFactoryCompat = SSLSocketFactoryCompat(certManager!!)
|
||||||
|
hostnameVerifier = certManager!!.hostnameVerifier(OkHostnameVerifier.INSTANCE)
|
||||||
|
|
||||||
|
dbHelper.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reinitLogger() {
|
||||||
|
val dbHelper = ServiceDB.OpenHelper(this)
|
||||||
|
val settings = Settings(dbHelper.readableDatabase)
|
||||||
|
|
||||||
|
val logToFile = settings.getBoolean(LOG_TO_EXTERNAL_STORAGE, false)
|
||||||
|
val logVerbose = logToFile || Log.isLoggable(log.name, Log.DEBUG)
|
||||||
|
|
||||||
|
App.log.info("Verbose logging: $logVerbose")
|
||||||
|
|
||||||
|
// set logging level according to preferences
|
||||||
|
val rootLogger = Logger.getLogger("")
|
||||||
|
rootLogger.level = if (logVerbose) Level.ALL else Level.INFO
|
||||||
|
|
||||||
|
// remove all handlers and add our own logcat handler
|
||||||
|
rootLogger.useParentHandlers = false
|
||||||
|
for (handler in rootLogger.handlers)
|
||||||
|
rootLogger.removeHandler(handler)
|
||||||
|
rootLogger.addHandler(LogcatHandler.INSTANCE)
|
||||||
|
|
||||||
|
val nm = NotificationManagerCompat.from(this)
|
||||||
|
// log to external file according to preferences
|
||||||
|
if (logToFile) {
|
||||||
|
val builder = NotificationCompat.Builder(this)
|
||||||
|
builder.setSmallIcon(R.drawable.ic_sd_storage_light)
|
||||||
|
.setLargeIcon(getLauncherBitmap(this))
|
||||||
|
.setContentTitle(getString(R.string.logging_davdroid_file_logging))
|
||||||
|
.setLocalOnly(true)
|
||||||
|
|
||||||
|
val dir = getExternalFilesDir(null)
|
||||||
|
if (dir != null)
|
||||||
|
try {
|
||||||
|
val fileName = File(dir, "etesync-" + Process.myPid() + "-" +
|
||||||
|
DateFormatUtils.format(System.currentTimeMillis(), "yyyyMMdd-HHmmss") + ".txt").toString()
|
||||||
|
log.info("Logging to $fileName")
|
||||||
|
|
||||||
|
val fileHandler = FileHandler(fileName)
|
||||||
|
fileHandler.formatter = PlainTextFormatter.DEFAULT
|
||||||
|
log.addHandler(fileHandler)
|
||||||
|
builder.setContentText(dir.path)
|
||||||
|
.setSubText(getString(R.string.logging_to_external_storage_warning))
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setStyle(NotificationCompat.BigTextStyle()
|
||||||
|
.bigText(getString(R.string.logging_to_external_storage, dir.path)))
|
||||||
|
.setOngoing(true)
|
||||||
|
|
||||||
|
} catch (e: IOException) {
|
||||||
|
log.log(Level.SEVERE, "Couldn't create external log file", e)
|
||||||
|
|
||||||
|
builder.setContentText(getString(R.string.logging_couldnt_create_file, e.localizedMessage))
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
builder.setContentText(getString(R.string.logging_no_external_storage))
|
||||||
|
|
||||||
|
nm.notify(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING, builder.build())
|
||||||
|
} else
|
||||||
|
nm.cancel(Constants.NOTIFICATION_EXTERNAL_FILE_LOGGING)
|
||||||
|
|
||||||
|
dbHelper.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ReinitSettingsReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
log.info("Received broadcast: re-initializing settings (logger/cert manager)")
|
||||||
|
|
||||||
|
val app = context.applicationContext as App
|
||||||
|
app.reinitLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val ACTION_REINIT_SETTINGS = BuildConfig.APPLICATION_ID + ".REINIT_SETTINGS"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MyDatabaseSource internal constructor(context: Context, entityModel: EntityModel, version: Int) : DatabaseSource(context, entityModel, version) {
|
||||||
|
|
||||||
|
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
super.onUpgrade(db, oldVersion, newVersion)
|
||||||
|
|
||||||
|
if (oldVersion < 3) {
|
||||||
|
db.execSQL("PRAGMA foreign_keys=OFF;")
|
||||||
|
|
||||||
|
db.execSQL("CREATE TABLE new_Journal (id integer primary key autoincrement not null, deleted boolean not null, encryptedKey varbinary(255), info varchar(255), owner varchar(255), service integer, serviceModel integer, uid varchar(64) not null, readOnly boolean default false, foreign key (serviceModel) references Service (id) on delete cascade);")
|
||||||
|
db.execSQL("CREATE TABLE new_Entry (id integer primary key autoincrement not null, content varchar(255), journal integer, uid varchar(64) not null, foreign key (journal) references new_Journal (id) on delete cascade);")
|
||||||
|
|
||||||
|
db.execSQL("INSERT INTO new_Journal SELECT id, deleted, encryptedKey, info, owner, service, serviceModel, uid, 0 from Journal;")
|
||||||
|
db.execSQL("INSERT INTO new_Entry SELECT id, content, journal, uid from Entry;")
|
||||||
|
|
||||||
|
db.execSQL("DROP TABLE Journal;")
|
||||||
|
db.execSQL("DROP TABLE Entry;")
|
||||||
|
db.execSQL("ALTER TABLE new_Journal RENAME TO Journal;")
|
||||||
|
db.execSQL("ALTER TABLE new_Entry RENAME TO Entry;")
|
||||||
|
// Add back indexes
|
||||||
|
db.execSQL("CREATE UNIQUE INDEX journal_unique_together on Journal (serviceModel, uid);")
|
||||||
|
db.execSQL("CREATE UNIQUE INDEX entry_unique_together on Entry (journal, uid);")
|
||||||
|
db.execSQL("PRAGMA foreign_keys=ON;")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Init the preferences version of the app.
|
||||||
|
* This is used to initialise the first version if not alrady set. */
|
||||||
|
private fun initPrefVersion() {
|
||||||
|
val prefs = getSharedPreferences("app", Context.MODE_PRIVATE)
|
||||||
|
if (prefs.getInt(PREF_VERSION, 0) == 0) {
|
||||||
|
prefs.edit().putInt(PREF_VERSION, BuildConfig.VERSION_CODE).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun update(fromVersion: Int) {
|
||||||
|
App.log.info("Updating from version " + fromVersion + " to " + BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
|
if (fromVersion < 6) {
|
||||||
|
val data = this.data
|
||||||
|
|
||||||
|
val dbHelper = ServiceDB.OpenHelper(this)
|
||||||
|
|
||||||
|
val collections = readCollections(dbHelper)
|
||||||
|
for (info in collections) {
|
||||||
|
val journalEntity = JournalEntity(data, info)
|
||||||
|
data.insert(journalEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
val db = dbHelper.writableDatabase
|
||||||
|
db.delete(ServiceDB.Collections._TABLE, null, null)
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromVersion < 7) {
|
||||||
|
/* Fix all of the etags to be non-null */
|
||||||
|
val am = AccountManager.get(this)
|
||||||
|
for (account in am.getAccountsByType(App.accountType)) {
|
||||||
|
try {
|
||||||
|
// Generate account settings to make sure account is migrated.
|
||||||
|
AccountSettings(this, account)
|
||||||
|
|
||||||
|
val calendars = LocalCalendar.find(account, this.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!,
|
||||||
|
LocalCalendar.Factory.INSTANCE, null, null) as Array<LocalCalendar>
|
||||||
|
for (calendar in calendars) {
|
||||||
|
calendar.fixEtags()
|
||||||
|
}
|
||||||
|
} catch (e: CalendarStorageException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} catch (e: InvalidAccountException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for (account in am.getAccountsByType(App.addressBookAccountType)) {
|
||||||
|
val addressBook = LocalAddressBook(this, account, this.contentResolver.acquireContentProviderClient(ContactsContract.Contacts.CONTENT_URI))
|
||||||
|
try {
|
||||||
|
addressBook.fixEtags()
|
||||||
|
} catch (e: ContactsStorageException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromVersion < 10) {
|
||||||
|
HintManager.setHintSeen(this, AccountsActivity.HINT_ACCOUNT_ADD, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromVersion < 11) {
|
||||||
|
val dbHelper = ServiceDB.OpenHelper(this)
|
||||||
|
|
||||||
|
migrateServices(dbHelper)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppUpdatedReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
@SuppressLint("UnsafeProtectedBroadcastReceiver,MissingPermission")
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
App.log.info("EteSync was updated, checking for app version")
|
||||||
|
|
||||||
|
val app = context.applicationContext as App
|
||||||
|
val prefs = app.getSharedPreferences("app", Context.MODE_PRIVATE)
|
||||||
|
val fromVersion = prefs.getInt(PREF_VERSION, 1)
|
||||||
|
app.update(fromVersion)
|
||||||
|
prefs.edit().putInt(PREF_VERSION, BuildConfig.VERSION_CODE).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readCollections(dbHelper: ServiceDB.OpenHelper): List<CollectionInfo> {
|
||||||
|
val db = dbHelper.writableDatabase
|
||||||
|
val collections = LinkedList<CollectionInfo>()
|
||||||
|
val cursor = db.query(ServiceDB.Collections._TABLE, null, null, null, null, null, null)
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val values = ContentValues()
|
||||||
|
DatabaseUtils.cursorRowToContentValues(cursor, values)
|
||||||
|
collections.add(CollectionInfo.fromDB(values))
|
||||||
|
}
|
||||||
|
|
||||||
|
db.close()
|
||||||
|
cursor.close()
|
||||||
|
return collections
|
||||||
|
}
|
||||||
|
|
||||||
|
fun migrateServices(dbHelper: ServiceDB.OpenHelper) {
|
||||||
|
val db = dbHelper.readableDatabase
|
||||||
|
val data = this.data
|
||||||
|
val cursor = db.query(ServiceDB.Services._TABLE, null, null, null, null, null, null)
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val values = ContentValues()
|
||||||
|
DatabaseUtils.cursorRowToContentValues(cursor, values)
|
||||||
|
val service = ServiceEntity()
|
||||||
|
service.account = values.getAsString(ServiceDB.Services.ACCOUNT_NAME)
|
||||||
|
service.type = CollectionInfo.Type.valueOf(values.getAsString(ServiceDB.Services.SERVICE))
|
||||||
|
data.insert(service)
|
||||||
|
|
||||||
|
for (journalEntity in data.select(JournalEntity::class.java).where(JournalEntity.SERVICE.eq(values.getAsLong(ServiceDB.Services.ID))).get()) {
|
||||||
|
journalEntity.serviceModel = service
|
||||||
|
data.update(journalEntity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.delete(ServiceDB.Services._TABLE, null, null)
|
||||||
|
db.close()
|
||||||
|
cursor.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DISTRUST_SYSTEM_CERTIFICATES = "distrustSystemCerts"
|
||||||
|
val LOG_TO_EXTERNAL_STORAGE = "logToExternalStorage"
|
||||||
|
val OVERRIDE_PROXY = "overrideProxy"
|
||||||
|
val OVERRIDE_PROXY_HOST = "overrideProxyHost"
|
||||||
|
val OVERRIDE_PROXY_PORT = "overrideProxyPort"
|
||||||
|
val FORCE_LANGUAGE = "forceLanguage"
|
||||||
|
val CHANGE_NOTIFICATION = "changeNotification"
|
||||||
|
|
||||||
|
val OVERRIDE_PROXY_HOST_DEFAULT = "localhost"
|
||||||
|
val OVERRIDE_PROXY_PORT_DEFAULT = 8118
|
||||||
|
|
||||||
|
val DEFAULT_LANGUAGE = "default"
|
||||||
|
var sDefaultLocacle = Locale.getDefault()
|
||||||
|
|
||||||
|
lateinit var appName: String
|
||||||
|
private set
|
||||||
|
|
||||||
|
var sslSocketFactoryCompat: SSLSocketFactoryCompat? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
var hostnameVerifier: HostnameVerifier? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
val log = Logger.getLogger("syncadapter")
|
||||||
|
|
||||||
|
init {
|
||||||
|
at.bitfire.cert4android.Constants.log = Logger.getLogger("syncadapter.cert4android")
|
||||||
|
}
|
||||||
|
|
||||||
|
lateinit var accountType: String
|
||||||
|
private set
|
||||||
|
lateinit var addressBookAccountType: String
|
||||||
|
private set
|
||||||
|
lateinit var addressBooksAuthority: String
|
||||||
|
private set
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
fun getLauncherBitmap(context: Context): Bitmap? {
|
||||||
|
var bitmapLogo: Bitmap? = null
|
||||||
|
val drawableLogo = ContextCompat.getDrawable(context, R.mipmap.ic_launcher)
|
||||||
|
|
||||||
|
if (drawableLogo is BitmapDrawable)
|
||||||
|
bitmapLogo = drawableLogo.bitmap
|
||||||
|
return bitmapLogo
|
||||||
|
}
|
||||||
|
|
||||||
|
// update from previous account settings
|
||||||
|
|
||||||
|
private val PREF_VERSION = "version"
|
||||||
|
}
|
||||||
|
}
|
@ -1,186 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
|
||||||
* which accompanies this distribution, and is available at
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.etesync.syncadapter;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.etesync.syncadapter.model.ServiceDB;
|
|
||||||
import com.etesync.syncadapter.model.Settings;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.net.Proxy;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import okhttp3.Interceptor;
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor;
|
|
||||||
|
|
||||||
public class HttpClient {
|
|
||||||
private static final OkHttpClient client = new OkHttpClient();
|
|
||||||
private static final UserAgentInterceptor userAgentInterceptor = new UserAgentInterceptor();
|
|
||||||
|
|
||||||
private static final String userAgent;
|
|
||||||
|
|
||||||
static {
|
|
||||||
String date = new SimpleDateFormat("yyyy/MM/dd", Locale.US).format(new Date(BuildConfig.buildTime));
|
|
||||||
userAgent = App.getAppName() + "/" + BuildConfig.VERSION_NAME + " (" + date + "; okhttp3) Android/" + Build.VERSION.RELEASE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpClient() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OkHttpClient create(@Nullable Context context, @NonNull final Logger logger, @Nullable String host, @NonNull String token) {
|
|
||||||
OkHttpClient.Builder builder = defaultBuilder(context, logger);
|
|
||||||
|
|
||||||
// use account settings for authentication
|
|
||||||
builder = addAuthentication(builder, host, token);
|
|
||||||
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OkHttpClient create(@Nullable Context context, @NonNull AccountSettings settings, @NonNull final Logger logger) {
|
|
||||||
return create(context, logger, settings.getUri().getHost(), settings.getAuthToken());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OkHttpClient create(@NonNull Context context, @NonNull Logger logger) {
|
|
||||||
return defaultBuilder(context, logger).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OkHttpClient create(@NonNull Context context, @NonNull AccountSettings settings) {
|
|
||||||
return create(context, settings, App.log);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OkHttpClient create(@Nullable Context context) {
|
|
||||||
return create(context, App.log);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OkHttpClient create(@Nullable Context context, @NonNull URI uri, String authToken) {
|
|
||||||
return create(context, App.log, uri.getHost(), authToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static OkHttpClient.Builder defaultBuilder(@Nullable Context context, @NonNull final Logger logger) {
|
|
||||||
OkHttpClient.Builder builder = client.newBuilder();
|
|
||||||
|
|
||||||
// use MemorizingTrustManager to manage self-signed certificates
|
|
||||||
if (context != null) {
|
|
||||||
App app = (App) context.getApplicationContext();
|
|
||||||
if (App.getSslSocketFactoryCompat() != null && app.getCertManager() != null)
|
|
||||||
builder.sslSocketFactory(App.getSslSocketFactoryCompat(), app.getCertManager());
|
|
||||||
if (App.getHostnameVerifier() != null)
|
|
||||||
builder.hostnameVerifier(App.getHostnameVerifier());
|
|
||||||
}
|
|
||||||
|
|
||||||
// set timeouts
|
|
||||||
builder.connectTimeout(30, TimeUnit.SECONDS);
|
|
||||||
builder.writeTimeout(30, TimeUnit.SECONDS);
|
|
||||||
builder.readTimeout(120, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
// custom proxy support
|
|
||||||
if (context != null) {
|
|
||||||
SQLiteOpenHelper dbHelper = new ServiceDB.OpenHelper(context);
|
|
||||||
try {
|
|
||||||
Settings settings = new Settings(dbHelper.getReadableDatabase());
|
|
||||||
if (settings.getBoolean(App.OVERRIDE_PROXY, false)) {
|
|
||||||
InetSocketAddress address = new InetSocketAddress(
|
|
||||||
settings.getString(App.OVERRIDE_PROXY_HOST, App.OVERRIDE_PROXY_HOST_DEFAULT),
|
|
||||||
settings.getInt(App.OVERRIDE_PROXY_PORT, App.OVERRIDE_PROXY_PORT_DEFAULT)
|
|
||||||
);
|
|
||||||
|
|
||||||
Proxy proxy = new Proxy(Proxy.Type.HTTP, address);
|
|
||||||
builder.proxy(proxy);
|
|
||||||
App.log.log(Level.INFO, "Using proxy", proxy);
|
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException | NullPointerException e) {
|
|
||||||
App.log.log(Level.SEVERE, "Can't set proxy, ignoring", e);
|
|
||||||
} finally {
|
|
||||||
dbHelper.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add User-Agent to every request
|
|
||||||
builder.addNetworkInterceptor(userAgentInterceptor);
|
|
||||||
|
|
||||||
// add network logging, if requested
|
|
||||||
if (logger.isLoggable(Level.FINEST)) {
|
|
||||||
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
|
|
||||||
@Override
|
|
||||||
public void log(String message) {
|
|
||||||
logger.finest(message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
|
|
||||||
builder.addInterceptor(loggingInterceptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static OkHttpClient.Builder addAuthentication(@NonNull OkHttpClient.Builder builder, @Nullable String host, @NonNull String token) {
|
|
||||||
TokenAuthenticator authHandler = new TokenAuthenticator(host, token);
|
|
||||||
|
|
||||||
return builder.addNetworkInterceptor(authHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class TokenAuthenticator implements Interceptor {
|
|
||||||
protected static final String
|
|
||||||
HEADER_AUTHORIZATION = "Authorization";
|
|
||||||
final String host, token;
|
|
||||||
|
|
||||||
|
|
||||||
private TokenAuthenticator(String host, String token) {
|
|
||||||
this.host = host;
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Response intercept(Chain chain) throws IOException {
|
|
||||||
Request request = chain.request();
|
|
||||||
|
|
||||||
/* Only add to the host we want. */
|
|
||||||
if ((host == null)
|
|
||||||
|| (request.url().host().equals(host))) {
|
|
||||||
if ((token != null)
|
|
||||||
&& (request.header(HEADER_AUTHORIZATION) == null)) {
|
|
||||||
request = request.newBuilder()
|
|
||||||
.header(HEADER_AUTHORIZATION, "Token " + token)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return chain.proceed(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class UserAgentInterceptor implements Interceptor {
|
|
||||||
@Override
|
|
||||||
public Response intercept(Chain chain) throws IOException {
|
|
||||||
Locale locale = Locale.getDefault();
|
|
||||||
Request request = chain.request().newBuilder()
|
|
||||||
.header("User-Agent", userAgent)
|
|
||||||
.header("Accept-Language", locale.getLanguage() + "-" + locale.getCountry() + ", " + locale.getLanguage() + ";q=0.7, *;q=0.5")
|
|
||||||
.build();
|
|
||||||
return chain.proceed(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
165
app/src/main/java/com/etesync/syncadapter/HttpClient.kt
Normal file
165
app/src/main/java/com/etesync/syncadapter/HttpClient.kt
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
import com.etesync.syncadapter.model.ServiceDB
|
||||||
|
import com.etesync.syncadapter.model.Settings
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
|
import java.net.URI
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.logging.Level
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
|
||||||
|
object HttpClient {
|
||||||
|
private val client = OkHttpClient()
|
||||||
|
private val userAgentInterceptor = UserAgentInterceptor()
|
||||||
|
|
||||||
|
private val userAgent: String
|
||||||
|
|
||||||
|
init {
|
||||||
|
val date = SimpleDateFormat("yyyy/MM/dd", Locale.US).format(Date(BuildConfig.buildTime))
|
||||||
|
userAgent = App.appName + "/" + BuildConfig.VERSION_NAME + " (" + date + "; okhttp3) Android/" + Build.VERSION.RELEASE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create(context: Context?, logger: Logger, host: String?, token: String): OkHttpClient {
|
||||||
|
var builder = defaultBuilder(context, logger)
|
||||||
|
|
||||||
|
// use account settings for authentication
|
||||||
|
builder = addAuthentication(builder, host, token)
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun create(context: Context?, settings: AccountSettings, logger: Logger = App.log): OkHttpClient {
|
||||||
|
return create(context, logger, settings.uri!!.host, settings.authToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun create(context: Context, logger: Logger = App.log): OkHttpClient {
|
||||||
|
return defaultBuilder(context, logger).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create(context: Context?, uri: URI, authToken: String): OkHttpClient {
|
||||||
|
return create(context, App.log, uri.host, authToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun defaultBuilder(context: Context?, logger: Logger): OkHttpClient.Builder {
|
||||||
|
val builder = client.newBuilder()
|
||||||
|
|
||||||
|
// use MemorizingTrustManager to manage self-signed certificates
|
||||||
|
if (context != null) {
|
||||||
|
val app = context.applicationContext as App
|
||||||
|
if (App.sslSocketFactoryCompat != null && app.certManager != null)
|
||||||
|
builder.sslSocketFactory(App.sslSocketFactoryCompat!!, app.certManager!!)
|
||||||
|
if (App.hostnameVerifier != null)
|
||||||
|
builder.hostnameVerifier(App.hostnameVerifier!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set timeouts
|
||||||
|
builder.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
|
builder.writeTimeout(30, TimeUnit.SECONDS)
|
||||||
|
builder.readTimeout(120, TimeUnit.SECONDS)
|
||||||
|
|
||||||
|
// custom proxy support
|
||||||
|
if (context != null) {
|
||||||
|
val dbHelper = ServiceDB.OpenHelper(context)
|
||||||
|
try {
|
||||||
|
val settings = Settings(dbHelper.readableDatabase)
|
||||||
|
if (settings.getBoolean(App.OVERRIDE_PROXY, false)) {
|
||||||
|
val address = InetSocketAddress(
|
||||||
|
settings.getString(App.OVERRIDE_PROXY_HOST, App.OVERRIDE_PROXY_HOST_DEFAULT),
|
||||||
|
settings.getInt(App.OVERRIDE_PROXY_PORT, App.OVERRIDE_PROXY_PORT_DEFAULT)
|
||||||
|
)
|
||||||
|
|
||||||
|
val proxy = Proxy(Proxy.Type.HTTP, address)
|
||||||
|
builder.proxy(proxy)
|
||||||
|
App.log.log(Level.INFO, "Using proxy", proxy)
|
||||||
|
}
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
App.log.log(Level.SEVERE, "Can't set proxy, ignoring", e)
|
||||||
|
} catch (e: NullPointerException) {
|
||||||
|
App.log.log(Level.SEVERE, "Can't set proxy, ignoring", e)
|
||||||
|
} finally {
|
||||||
|
dbHelper.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add User-Agent to every request
|
||||||
|
builder.addNetworkInterceptor(userAgentInterceptor)
|
||||||
|
|
||||||
|
// add network logging, if requested
|
||||||
|
if (logger.isLoggable(Level.FINEST)) {
|
||||||
|
val loggingInterceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger { message -> logger.finest(message) })
|
||||||
|
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
|
||||||
|
builder.addInterceptor(loggingInterceptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addAuthentication(builder: OkHttpClient.Builder, host: String?, token: String): OkHttpClient.Builder {
|
||||||
|
val authHandler = TokenAuthenticator(host, token)
|
||||||
|
|
||||||
|
return builder.addNetworkInterceptor(authHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TokenAuthenticator internal constructor(internal val host: String?, internal val token: String?) : Interceptor {
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
var request = chain.request()
|
||||||
|
|
||||||
|
/* Only add to the host we want. */
|
||||||
|
if (host == null || request.url().host() == host) {
|
||||||
|
if (token != null && request.header(HEADER_AUTHORIZATION) == null) {
|
||||||
|
request = request.newBuilder()
|
||||||
|
.header(HEADER_AUTHORIZATION, "Token $token")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
protected val HEADER_AUTHORIZATION = "Authorization"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class UserAgentInterceptor : Interceptor {
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val locale = Locale.getDefault()
|
||||||
|
val request = chain.request().newBuilder()
|
||||||
|
.header("User-Agent", userAgent)
|
||||||
|
.header("Accept-Language", locale.language + "-" + locale.country + ", " + locale.language + ";q=0.7, *;q=0.5")
|
||||||
|
.build()
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -6,14 +6,8 @@
|
|||||||
* http://www.gnu.org/licenses/gpl.html
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.etesync.syncadapter;
|
package com.etesync.syncadapter
|
||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account
|
||||||
|
|
||||||
public class InvalidAccountException extends Exception {
|
class InvalidAccountException(account: Account) : Exception("Invalid account: $account")
|
||||||
|
|
||||||
public InvalidAccountException(Account account) {
|
|
||||||
super("Invalid account: " + account);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,165 +0,0 @@
|
|||||||
package com.etesync.syncadapter;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.database.sqlite.SQLiteException;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.v4.app.NotificationCompat;
|
|
||||||
import android.support.v4.app.NotificationManagerCompat;
|
|
||||||
|
|
||||||
import com.etesync.syncadapter.journalmanager.Exceptions;
|
|
||||||
import com.etesync.syncadapter.ui.AccountSettingsActivity;
|
|
||||||
import com.etesync.syncadapter.ui.DebugInfoActivity;
|
|
||||||
import com.etesync.syncadapter.ui.WebViewActivity;
|
|
||||||
|
|
||||||
import java.util.logging.Level;
|
|
||||||
|
|
||||||
import at.bitfire.ical4android.CalendarStorageException;
|
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
|
||||||
|
|
||||||
public class NotificationHelper {
|
|
||||||
private static final String CHANNEL_ID = "EteSync_default";
|
|
||||||
|
|
||||||
final NotificationManagerCompat notificationManager;
|
|
||||||
final Context context;
|
|
||||||
final String notificationTag;
|
|
||||||
final int notificationId;
|
|
||||||
Intent detailsIntent;
|
|
||||||
int messageString;
|
|
||||||
|
|
||||||
public Intent getDetailsIntent() {
|
|
||||||
return detailsIntent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Throwable throwable = null;
|
|
||||||
|
|
||||||
public NotificationHelper(Context context, String notificationTag, int notificationId) {
|
|
||||||
this.notificationManager = NotificationManagerCompat.from(context);
|
|
||||||
this.context = context;
|
|
||||||
this.notificationTag = notificationTag;
|
|
||||||
this.notificationId = notificationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setThrowable(Throwable e) {
|
|
||||||
throwable = e;
|
|
||||||
if (e instanceof Exceptions.UnauthorizedException) {
|
|
||||||
App.log.log(Level.SEVERE, "Not authorized anymore", e);
|
|
||||||
messageString = R.string.sync_error_unauthorized;
|
|
||||||
} else if (e instanceof Exceptions.UserInactiveException) {
|
|
||||||
App.log.log(Level.SEVERE, "User inactive");
|
|
||||||
messageString = R.string.sync_error_user_inactive;
|
|
||||||
} else if (e instanceof Exceptions.ServiceUnavailableException) {
|
|
||||||
App.log.log(Level.SEVERE, "Service unavailable");
|
|
||||||
messageString = R.string.sync_error_unavailable;
|
|
||||||
} else if (e instanceof Exceptions.HttpException) {
|
|
||||||
App.log.log(Level.SEVERE, "HTTP Exception during sync", e);
|
|
||||||
messageString = R.string.sync_error_http_dav;
|
|
||||||
} else if (e instanceof CalendarStorageException || e instanceof ContactsStorageException || e instanceof SQLiteException) {
|
|
||||||
App.log.log(Level.SEVERE, "Couldn't access local storage", e);
|
|
||||||
messageString = R.string.sync_error_local_storage;
|
|
||||||
} else if (e instanceof Exceptions.IntegrityException) {
|
|
||||||
App.log.log(Level.SEVERE, "Integrity error", e);
|
|
||||||
messageString = R.string.sync_error_integrity;
|
|
||||||
} else {
|
|
||||||
App.log.log(Level.SEVERE, "Unknown sync error", e);
|
|
||||||
messageString = R.string.sync_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
detailsIntent = new Intent(context, NotificationHandlerActivity.class);
|
|
||||||
detailsIntent.putExtra(DebugInfoActivity.Companion.getKEY_THROWABLE(), e);
|
|
||||||
detailsIntent.setData(Uri.parse("uri://" + getClass().getName() + "/" + notificationTag));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notify(String title, String state) {
|
|
||||||
String message = context.getString(messageString, state);
|
|
||||||
notify(title, message, null, detailsIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notify(String title, String content, String bigText, Intent intent) {
|
|
||||||
notify(title, content, bigText, intent, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notify(String title, String content, String bigText, Intent intent, int icon) {
|
|
||||||
createNotificationChannel();
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
|
|
||||||
String category =
|
|
||||||
throwable == null ?
|
|
||||||
NotificationCompat.CATEGORY_STATUS : NotificationCompat.CATEGORY_ERROR;
|
|
||||||
if (icon == -1) {
|
|
||||||
//Check if error was configured
|
|
||||||
if (throwable == null) {
|
|
||||||
icon = R.drawable.ic_sync_dark;
|
|
||||||
} else {
|
|
||||||
icon = R.drawable.ic_error_light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setLargeIcon(App.getLauncherBitmap(context))
|
|
||||||
.setContentTitle(title)
|
|
||||||
.setContentText(content)
|
|
||||||
.setChannelId(CHANNEL_ID)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setCategory(category)
|
|
||||||
.setSmallIcon(icon)
|
|
||||||
.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
|
||||||
;
|
|
||||||
|
|
||||||
if (bigText != null) builder.setStyle(new NotificationCompat.BigTextStyle()
|
|
||||||
.bigText(bigText));
|
|
||||||
|
|
||||||
notificationManager.notify(notificationTag, notificationId, builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void cancel() {
|
|
||||||
notificationManager.cancel(notificationTag, notificationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class NotificationHandlerActivity extends Activity {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedBundle) {
|
|
||||||
super.onCreate(savedBundle);
|
|
||||||
Bundle extras = getIntent().getExtras();
|
|
||||||
Exception e = (Exception) extras.get(DebugInfoActivity.Companion.getKEY_THROWABLE());
|
|
||||||
|
|
||||||
Intent detailsIntent;
|
|
||||||
if (e instanceof Exceptions.UnauthorizedException) {
|
|
||||||
detailsIntent = new Intent(this, AccountSettingsActivity.class);
|
|
||||||
} else if (e instanceof Exceptions.UserInactiveException) {
|
|
||||||
WebViewActivity.Companion.openUrl(this, Constants.dashboard);
|
|
||||||
return;
|
|
||||||
} else if (e instanceof AccountSettings.AccountMigrationException) {
|
|
||||||
WebViewActivity.Companion.openUrl(this, Constants.faqUri.buildUpon().encodedFragment("account-migration-error").build());
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
detailsIntent = new Intent(this, DebugInfoActivity.class);
|
|
||||||
}
|
|
||||||
detailsIntent.putExtras(getIntent().getExtras());
|
|
||||||
startActivity(detailsIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createNotificationChannel() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
CharSequence name = context.getString(R.string.notification_channel_name);
|
|
||||||
NotificationChannel channel =
|
|
||||||
new NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
|
|
||||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
|
||||||
notificationManager.createNotificationChannel(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
155
app/src/main/java/com/etesync/syncadapter/NotificationHelper.kt
Normal file
155
app/src/main/java/com/etesync/syncadapter/NotificationHelper.kt
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package com.etesync.syncadapter
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.database.sqlite.SQLiteException
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.app.NotificationCompat
|
||||||
|
import android.support.v4.app.NotificationManagerCompat
|
||||||
|
|
||||||
|
import com.etesync.syncadapter.journalmanager.Exceptions
|
||||||
|
import com.etesync.syncadapter.ui.AccountSettingsActivity
|
||||||
|
import com.etesync.syncadapter.ui.DebugInfoActivity
|
||||||
|
import com.etesync.syncadapter.ui.WebViewActivity
|
||||||
|
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
|
import at.bitfire.ical4android.CalendarStorageException
|
||||||
|
import at.bitfire.vcard4android.ContactsStorageException
|
||||||
|
|
||||||
|
class NotificationHelper(internal val context: Context, internal val notificationTag: String, internal val notificationId: Int) {
|
||||||
|
|
||||||
|
internal val notificationManager: NotificationManagerCompat
|
||||||
|
lateinit var detailsIntent: Intent
|
||||||
|
internal set
|
||||||
|
internal var messageString: Int = 0
|
||||||
|
|
||||||
|
private var throwable: Throwable? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.notificationManager = NotificationManagerCompat.from(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setThrowable(e: Throwable) {
|
||||||
|
throwable = e
|
||||||
|
if (e is Exceptions.UnauthorizedException) {
|
||||||
|
App.log.log(Level.SEVERE, "Not authorized anymore", e)
|
||||||
|
messageString = R.string.sync_error_unauthorized
|
||||||
|
} else if (e is Exceptions.UserInactiveException) {
|
||||||
|
App.log.log(Level.SEVERE, "User inactive")
|
||||||
|
messageString = R.string.sync_error_user_inactive
|
||||||
|
} else if (e is Exceptions.ServiceUnavailableException) {
|
||||||
|
App.log.log(Level.SEVERE, "Service unavailable")
|
||||||
|
messageString = R.string.sync_error_unavailable
|
||||||
|
} else if (e is Exceptions.HttpException) {
|
||||||
|
App.log.log(Level.SEVERE, "HTTP Exception during sync", e)
|
||||||
|
messageString = R.string.sync_error_http_dav
|
||||||
|
} else if (e is CalendarStorageException || e is ContactsStorageException || e is SQLiteException) {
|
||||||
|
App.log.log(Level.SEVERE, "Couldn't access local storage", e)
|
||||||
|
messageString = R.string.sync_error_local_storage
|
||||||
|
} else if (e is Exceptions.IntegrityException) {
|
||||||
|
App.log.log(Level.SEVERE, "Integrity error", e)
|
||||||
|
messageString = R.string.sync_error_integrity
|
||||||
|
} else {
|
||||||
|
App.log.log(Level.SEVERE, "Unknown sync error", e)
|
||||||
|
messageString = R.string.sync_error
|
||||||
|
}
|
||||||
|
|
||||||
|
detailsIntent = Intent(context, NotificationHandlerActivity::class.java)
|
||||||
|
detailsIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e)
|
||||||
|
detailsIntent.data = Uri.parse("uri://" + javaClass.name + "/" + notificationTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notify(title: String, state: String) {
|
||||||
|
val message = context.getString(messageString, state)
|
||||||
|
notify(title, message, null, detailsIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun notify(title: String, content: String, bigText: String?, intent: Intent, icon: Int = -1) {
|
||||||
|
var icon = icon
|
||||||
|
createNotificationChannel()
|
||||||
|
val builder = NotificationCompat.Builder(context)
|
||||||
|
val category = if (throwable == null)
|
||||||
|
NotificationCompat.CATEGORY_STATUS
|
||||||
|
else
|
||||||
|
NotificationCompat.CATEGORY_ERROR
|
||||||
|
if (icon == -1) {
|
||||||
|
//Check if error was configured
|
||||||
|
if (throwable == null) {
|
||||||
|
icon = R.drawable.ic_sync_dark
|
||||||
|
} else {
|
||||||
|
icon = R.drawable.ic_error_light
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setLargeIcon(App.getLauncherBitmap(context))
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(content)
|
||||||
|
.setChannelId(CHANNEL_ID)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setCategory(category)
|
||||||
|
.setSmallIcon(icon)
|
||||||
|
.setContentIntent(PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||||
|
|
||||||
|
if (bigText != null)
|
||||||
|
builder.setStyle(NotificationCompat.BigTextStyle()
|
||||||
|
.bigText(bigText))
|
||||||
|
|
||||||
|
notificationManager.notify(notificationTag, notificationId, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
notificationManager.cancel(notificationTag, notificationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationHandlerActivity : Activity() {
|
||||||
|
|
||||||
|
public override fun onCreate(savedBundle: Bundle?) {
|
||||||
|
super.onCreate(savedBundle)
|
||||||
|
val extras = intent.extras
|
||||||
|
val e = extras!!.get(DebugInfoActivity.KEY_THROWABLE) as Exception
|
||||||
|
|
||||||
|
val detailsIntent: Intent
|
||||||
|
if (e is Exceptions.UnauthorizedException) {
|
||||||
|
detailsIntent = Intent(this, AccountSettingsActivity::class.java)
|
||||||
|
} else if (e is Exceptions.UserInactiveException) {
|
||||||
|
WebViewActivity.openUrl(this, Constants.dashboard)
|
||||||
|
return
|
||||||
|
} else if (e is AccountSettings.AccountMigrationException) {
|
||||||
|
WebViewActivity.openUrl(this, Constants.faqUri.buildUpon().encodedFragment("account-migration-error").build())
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
detailsIntent = Intent(this, DebugInfoActivity::class.java)
|
||||||
|
}
|
||||||
|
detailsIntent.putExtras(intent.extras!!)
|
||||||
|
startActivity(detailsIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val name = context.getString(R.string.notification_channel_name)
|
||||||
|
val channel = NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW)
|
||||||
|
val notificationManager = context.getSystemService(NotificationManager::class.java)
|
||||||
|
notificationManager!!.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val CHANNEL_ID = "EteSync_default"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © Ricki Hirner (bitfire web engineering).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
|
||||||
* which accompanies this distribution, and is available at
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.etesync.syncadapter;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.etesync.syncadapter.resource.LocalTaskList;
|
|
||||||
|
|
||||||
public class PackageChangedReceiver extends BroadcastReceiver {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction()) ||
|
|
||||||
Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(intent.getAction()))
|
|
||||||
updateTaskSync(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void updateTaskSync(@NonNull Context context) {
|
|
||||||
boolean tasksInstalled = LocalTaskList.tasksProviderAvailable(context);
|
|
||||||
App.log.info("Package (un)installed; OpenTasks provider now available = " + tasksInstalled);
|
|
||||||
|
|
||||||
// check all accounts and (de)activate OpenTasks if a CalDAV service is defined
|
|
||||||
// FIXME: Do something if we ever bring back tasks.
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
import com.etesync.syncadapter.resource.LocalTaskList
|
||||||
|
|
||||||
|
class PackageChangedReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (Intent.ACTION_PACKAGE_ADDED == intent.action || Intent.ACTION_PACKAGE_FULLY_REMOVED == intent.action)
|
||||||
|
updateTaskSync(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
internal fun updateTaskSync(context: Context) {
|
||||||
|
val tasksInstalled = LocalTaskList.tasksProviderAvailable(context)
|
||||||
|
App.log.info("Package (un)installed; OpenTasks provider now available = $tasksInstalled")
|
||||||
|
|
||||||
|
// check all accounts and (de)activate OpenTasks if a CalDAV service is defined
|
||||||
|
// FIXME: Do something if we ever bring back tasks.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,168 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
|
||||||
* All rights reserved. This program and the accompanying materials
|
|
||||||
* are made available under the terms of the GNU Public License v3.0
|
|
||||||
* which accompanies this distribution, and is available at
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.etesync.syncadapter;
|
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
|
||||||
import javax.net.ssl.SSLSocket;
|
|
||||||
import javax.net.ssl.SSLSocketFactory;
|
|
||||||
import javax.net.ssl.X509TrustManager;
|
|
||||||
|
|
||||||
public class SSLSocketFactoryCompat extends SSLSocketFactory {
|
|
||||||
|
|
||||||
private SSLSocketFactory delegate;
|
|
||||||
|
|
||||||
// Android 5.0+ (API level21) provides reasonable default settings
|
|
||||||
// but it still allows SSLv3
|
|
||||||
// https://developer.android.com/reference/javax/net/ssl/SSLSocket.html
|
|
||||||
static String protocols[] = null, cipherSuites[] = null;
|
|
||||||
static {
|
|
||||||
try {
|
|
||||||
SSLSocket socket = (SSLSocket)SSLSocketFactory.getDefault().createSocket();
|
|
||||||
if (socket != null) {
|
|
||||||
/* set reasonable protocol versions */
|
|
||||||
// - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0)
|
|
||||||
// - remove all SSL versions (especially SSLv3) because they're insecure now
|
|
||||||
List<String> protocols = new LinkedList<>();
|
|
||||||
for (String protocol : socket.getSupportedProtocols())
|
|
||||||
if (!protocol.toUpperCase(Locale.US).contains("SSL"))
|
|
||||||
protocols.add(protocol);
|
|
||||||
App.log.info("Setting allowed TLS protocols: " + TextUtils.join(", ", protocols));
|
|
||||||
SSLSocketFactoryCompat.protocols = protocols.toArray(new String[protocols.size()]);
|
|
||||||
|
|
||||||
/* set up reasonable cipher suites */
|
|
||||||
// choose known secure cipher suites
|
|
||||||
List<String> allowedCiphers = Arrays.asList(
|
|
||||||
// first priority
|
|
||||||
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
|
|
||||||
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
|
|
||||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
|
|
||||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
|
|
||||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
|
||||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
|
||||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
|
||||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
|
||||||
// second priority
|
|
||||||
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
|
|
||||||
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
|
|
||||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
|
|
||||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
|
|
||||||
// compat
|
|
||||||
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
|
|
||||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
|
|
||||||
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
|
|
||||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"
|
|
||||||
);
|
|
||||||
List<String> availableCiphers = Arrays.asList(socket.getSupportedCipherSuites());
|
|
||||||
App.log.info("Available cipher suites: " + TextUtils.join(", ", availableCiphers));
|
|
||||||
|
|
||||||
/* For maximum security, preferredCiphers should *replace* enabled ciphers (thus
|
|
||||||
* disabling ciphers which are enabled by default, but have become unsecure), but for
|
|
||||||
* the security level of DAVdroid and maximum compatibility, disabling of insecure
|
|
||||||
* ciphers should be a server-side task */
|
|
||||||
|
|
||||||
// for the final set of enabled ciphers, take the ciphers enabled by default, ...
|
|
||||||
HashSet<String> enabledCiphers = new HashSet<>(Arrays.asList(socket.getEnabledCipherSuites()));
|
|
||||||
App.log.info("Cipher suites enabled by default: " + TextUtils.join(", ", enabledCiphers));
|
|
||||||
// ... add explicitly allowed ciphers ...
|
|
||||||
enabledCiphers.addAll(allowedCiphers);
|
|
||||||
// ... and keep only those which are actually available
|
|
||||||
enabledCiphers.retainAll(availableCiphers);
|
|
||||||
|
|
||||||
App.log.info("Enabling (only) those TLS ciphers: " + TextUtils.join(", ", enabledCiphers));
|
|
||||||
SSLSocketFactoryCompat.cipherSuites = enabledCiphers.toArray(new String[enabledCiphers.size()]);
|
|
||||||
socket.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
App.log.severe("Couldn't determine default TLS settings");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public SSLSocketFactoryCompat(@NonNull X509TrustManager trustManager) {
|
|
||||||
try {
|
|
||||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
|
||||||
sslContext.init(null, new X509TrustManager[] { trustManager }, null);
|
|
||||||
delegate = sslContext.getSocketFactory();
|
|
||||||
} catch (GeneralSecurityException e) {
|
|
||||||
throw new AssertionError(); // The system has no TLS. Just give up.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void upgradeTLS(SSLSocket ssl) {
|
|
||||||
if (protocols != null)
|
|
||||||
ssl.setEnabledProtocols(protocols);
|
|
||||||
|
|
||||||
if (cipherSuites != null)
|
|
||||||
ssl.setEnabledCipherSuites(cipherSuites);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getDefaultCipherSuites() {
|
|
||||||
return cipherSuites;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getSupportedCipherSuites() {
|
|
||||||
return cipherSuites;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
|
|
||||||
Socket ssl = delegate.createSocket(s, host, port, autoClose);
|
|
||||||
if (ssl instanceof SSLSocket)
|
|
||||||
upgradeTLS((SSLSocket)ssl);
|
|
||||||
return ssl;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(String host, int port) throws IOException {
|
|
||||||
Socket ssl = delegate.createSocket(host, port);
|
|
||||||
if (ssl instanceof SSLSocket)
|
|
||||||
upgradeTLS((SSLSocket)ssl);
|
|
||||||
return ssl;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
|
|
||||||
Socket ssl = delegate.createSocket(host, port, localHost, localPort);
|
|
||||||
if (ssl instanceof SSLSocket)
|
|
||||||
upgradeTLS((SSLSocket)ssl);
|
|
||||||
return ssl;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
|
||||||
Socket ssl = delegate.createSocket(host, port);
|
|
||||||
if (ssl instanceof SSLSocket)
|
|
||||||
upgradeTLS((SSLSocket)ssl);
|
|
||||||
return ssl;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
|
|
||||||
Socket ssl = delegate.createSocket(address, port, localAddress, localPort);
|
|
||||||
if (ssl instanceof SSLSocket)
|
|
||||||
upgradeTLS((SSLSocket)ssl);
|
|
||||||
return ssl;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.Socket
|
||||||
|
import java.security.GeneralSecurityException
|
||||||
|
import java.util.Arrays
|
||||||
|
import java.util.HashSet
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.SSLSocket
|
||||||
|
import javax.net.ssl.SSLSocketFactory
|
||||||
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
|
class SSLSocketFactoryCompat(trustManager: X509TrustManager) : SSLSocketFactory() {
|
||||||
|
|
||||||
|
private var delegate: SSLSocketFactory? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
try {
|
||||||
|
val sslContext = SSLContext.getInstance("TLS")
|
||||||
|
sslContext.init(null, arrayOf(trustManager), null)
|
||||||
|
delegate = sslContext.socketFactory
|
||||||
|
} catch (e: GeneralSecurityException) {
|
||||||
|
throw AssertionError() // The system has no TLS. Just give up.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun upgradeTLS(ssl: SSLSocket) {
|
||||||
|
if (protocols != null)
|
||||||
|
ssl.enabledProtocols = protocols
|
||||||
|
|
||||||
|
if (cipherSuites != null)
|
||||||
|
ssl.enabledCipherSuites = cipherSuites
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getDefaultCipherSuites(): Array<String>? {
|
||||||
|
return cipherSuites
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSupportedCipherSuites(): Array<String>? {
|
||||||
|
return cipherSuites
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
|
||||||
|
val ssl = delegate!!.createSocket(s, host, port, autoClose)
|
||||||
|
if (ssl is SSLSocket)
|
||||||
|
upgradeTLS(ssl)
|
||||||
|
return ssl
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun createSocket(host: String, port: Int): Socket {
|
||||||
|
val ssl = delegate!!.createSocket(host, port)
|
||||||
|
if (ssl is SSLSocket)
|
||||||
|
upgradeTLS(ssl)
|
||||||
|
return ssl
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket {
|
||||||
|
val ssl = delegate!!.createSocket(host, port, localHost, localPort)
|
||||||
|
if (ssl is SSLSocket)
|
||||||
|
upgradeTLS(ssl)
|
||||||
|
return ssl
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun createSocket(host: InetAddress, port: Int): Socket {
|
||||||
|
val ssl = delegate!!.createSocket(host, port)
|
||||||
|
if (ssl is SSLSocket)
|
||||||
|
upgradeTLS(ssl)
|
||||||
|
return ssl
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket {
|
||||||
|
val ssl = delegate!!.createSocket(address, port, localAddress, localPort)
|
||||||
|
if (ssl is SSLSocket)
|
||||||
|
upgradeTLS(ssl)
|
||||||
|
return ssl
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
// Android 5.0+ (API level21) provides reasonable default settings
|
||||||
|
// but it still allows SSLv3
|
||||||
|
// https://developer.android.com/reference/javax/net/ssl/SSLSocket.html
|
||||||
|
internal var protocols: Array<String>? = null
|
||||||
|
internal var cipherSuites: Array<String>? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
try {
|
||||||
|
val socket = SSLSocketFactory.getDefault().createSocket() as SSLSocket
|
||||||
|
if (socket != null) {
|
||||||
|
/* set reasonable protocol versions */
|
||||||
|
// - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0)
|
||||||
|
// - remove all SSL versions (especially SSLv3) because they're insecure now
|
||||||
|
val protocols = LinkedList<String>()
|
||||||
|
for (protocol in socket.supportedProtocols)
|
||||||
|
if (!protocol.toUpperCase(Locale.US).contains("SSL"))
|
||||||
|
protocols.add(protocol)
|
||||||
|
App.log.info("Setting allowed TLS protocols: " + TextUtils.join(", ", protocols))
|
||||||
|
SSLSocketFactoryCompat.protocols = protocols.toTypedArray()
|
||||||
|
|
||||||
|
/* set up reasonable cipher suites */
|
||||||
|
// choose known secure cipher suites
|
||||||
|
val allowedCiphers = Arrays.asList(
|
||||||
|
// first priority
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
|
||||||
|
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
|
||||||
|
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||||
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||||
|
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||||
|
// second priority
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
|
||||||
|
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
|
||||||
|
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
|
||||||
|
// compat
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
|
||||||
|
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
|
||||||
|
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"
|
||||||
|
)
|
||||||
|
val availableCiphers = Arrays.asList(*socket.supportedCipherSuites)
|
||||||
|
App.log.info("Available cipher suites: " + TextUtils.join(", ", availableCiphers))
|
||||||
|
|
||||||
|
/* For maximum security, preferredCiphers should *replace* enabled ciphers (thus
|
||||||
|
* disabling ciphers which are enabled by default, but have become unsecure), but for
|
||||||
|
* the security level of DAVdroid and maximum compatibility, disabling of insecure
|
||||||
|
* ciphers should be a server-side task */
|
||||||
|
|
||||||
|
// for the final set of enabled ciphers, take the ciphers enabled by default, ...
|
||||||
|
val enabledCiphers = HashSet(Arrays.asList(*socket.enabledCipherSuites))
|
||||||
|
App.log.info("Cipher suites enabled by default: " + TextUtils.join(", ", enabledCiphers))
|
||||||
|
// ... add explicitly allowed ciphers ...
|
||||||
|
enabledCiphers.addAll(allowedCiphers)
|
||||||
|
// ... and keep only those which are actually available
|
||||||
|
enabledCiphers.retainAll(availableCiphers)
|
||||||
|
|
||||||
|
App.log.info("Enabling (only) those TLS ciphers: " + TextUtils.join(", ", enabledCiphers))
|
||||||
|
SSLSocketFactoryCompat.cipherSuites = enabledCiphers.toTypedArray()
|
||||||
|
socket.close()
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
App.log.severe("Couldn't determine default TLS settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -75,7 +75,7 @@ public class ServiceDB {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(SQLiteDatabase db) {
|
public void onCreate(SQLiteDatabase db) {
|
||||||
App.log.info("Creating database " + db.getPath());
|
App.Companion.getLog().info("Creating database " + db.getPath());
|
||||||
|
|
||||||
db.execSQL("CREATE TABLE " + Settings._TABLE + "(" +
|
db.execSQL("CREATE TABLE " + Settings._TABLE + "(" +
|
||||||
Settings.NAME + " TEXT NOT NULL," +
|
Settings.NAME + " TEXT NOT NULL," +
|
||||||
|
@ -73,7 +73,7 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
|
|||||||
AccountManager accountManager = AccountManager.get(context);
|
AccountManager accountManager = AccountManager.get(context);
|
||||||
|
|
||||||
List<LocalAddressBook> result = new LinkedList<>();
|
List<LocalAddressBook> result = new LinkedList<>();
|
||||||
for (Account account : accountManager.getAccountsByType(App.getAddressBookAccountType())) {
|
for (Account account : accountManager.getAccountsByType(App.Companion.getAddressBookAccountType())) {
|
||||||
LocalAddressBook addressBook = new LocalAddressBook(context, account, provider);
|
LocalAddressBook addressBook = new LocalAddressBook(context, account, provider);
|
||||||
if (mainAccount == null || addressBook.getMainAccount().equals(mainAccount))
|
if (mainAccount == null || addressBook.getMainAccount().equals(mainAccount))
|
||||||
result.add(addressBook);
|
result.add(addressBook);
|
||||||
@ -85,7 +85,7 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
|
|||||||
public static LocalAddressBook findByUid(@NonNull Context context, @NonNull ContentProviderClient provider, @Nullable Account mainAccount, String uid) throws ContactsStorageException {
|
public static LocalAddressBook findByUid(@NonNull Context context, @NonNull ContentProviderClient provider, @Nullable Account mainAccount, String uid) throws ContactsStorageException {
|
||||||
AccountManager accountManager = AccountManager.get(context);
|
AccountManager accountManager = AccountManager.get(context);
|
||||||
|
|
||||||
for (Account account : accountManager.getAccountsByType(App.getAddressBookAccountType())) {
|
for (Account account : accountManager.getAccountsByType(App.Companion.getAddressBookAccountType())) {
|
||||||
LocalAddressBook addressBook = new LocalAddressBook(context, account, provider);
|
LocalAddressBook addressBook = new LocalAddressBook(context, account, provider);
|
||||||
if (addressBook.getURL().equals(uid) && (mainAccount == null || addressBook.getMainAccount().equals(mainAccount)))
|
if (addressBook.getURL().equals(uid) && (mainAccount == null || addressBook.getMainAccount().equals(mainAccount)))
|
||||||
return addressBook;
|
return addressBook;
|
||||||
@ -98,7 +98,7 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
|
|||||||
CollectionInfo info = journalEntity.getInfo();
|
CollectionInfo info = journalEntity.getInfo();
|
||||||
AccountManager accountManager = AccountManager.get(context);
|
AccountManager accountManager = AccountManager.get(context);
|
||||||
|
|
||||||
Account account = new Account(accountName(mainAccount, info), App.getAddressBookAccountType());
|
Account account = new Account(accountName(mainAccount, info), App.Companion.getAddressBookAccountType());
|
||||||
if (!accountManager.addAccountExplicitly(account, null, null))
|
if (!accountManager.addAccountExplicitly(account, null, null))
|
||||||
throw new ContactsStorageException("Couldn't create address book account");
|
throw new ContactsStorageException("Couldn't create address book account");
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
|
|||||||
new String[] { account.name, account.type });
|
new String[] { account.name, account.type });
|
||||||
}
|
}
|
||||||
} catch(RemoteException e) {
|
} catch(RemoteException e) {
|
||||||
App.log.log(Level.WARNING, "Couldn't re-assign contacts to new account name", e);
|
App.Companion.getLog().log(Level.WARNING, "Couldn't re-assign contacts to new account name", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
@ -180,7 +180,7 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
|
|||||||
*/
|
*/
|
||||||
public int verifyDirty() throws ContactsStorageException {
|
public int verifyDirty() throws ContactsStorageException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
||||||
App.log.severe("verifyDirty() should not be called on Android <7");
|
App.Companion.getLog().severe("verifyDirty() should not be called on Android <7");
|
||||||
|
|
||||||
int reallyDirty = 0;
|
int reallyDirty = 0;
|
||||||
for (LocalContact contact : getDirtyContacts()) {
|
for (LocalContact contact : getDirtyContacts()) {
|
||||||
@ -189,10 +189,10 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
|
|||||||
currentHash = contact.dataHashCode();
|
currentHash = contact.dataHashCode();
|
||||||
if (lastHash == currentHash) {
|
if (lastHash == currentHash) {
|
||||||
// hash is code still the same, contact is not "really dirty" (only metadata been have changed)
|
// hash is code still the same, contact is not "really dirty" (only metadata been have changed)
|
||||||
App.log.log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact);
|
App.Companion.getLog().log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact);
|
||||||
contact.resetDirty();
|
contact.resetDirty();
|
||||||
} else {
|
} else {
|
||||||
App.log.log(Level.FINE, "Contact data has changed from hash " + lastHash + " to " + currentHash, contact);
|
App.Companion.getLog().log(Level.FINE, "Contact data has changed from hash " + lastHash + " to " + currentHash, contact);
|
||||||
reallyDirty++;
|
reallyDirty++;
|
||||||
}
|
}
|
||||||
} catch(FileNotFoundException e) {
|
} catch(FileNotFoundException e) {
|
||||||
@ -351,7 +351,7 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
|
|||||||
/** should be done using {@link Groups.SUMMARY_COUNT}, but it's not implemented in Android yet */
|
/** should be done using {@link Groups.SUMMARY_COUNT}, but it's not implemented in Android yet */
|
||||||
for (LocalGroup group : (LocalGroup[])queryGroups(null, null))
|
for (LocalGroup group : (LocalGroup[])queryGroups(null, null))
|
||||||
if (group.getMembers().length == 0) {
|
if (group.getMembers().length == 0) {
|
||||||
App.log.log(Level.FINE, "Deleting group", group);
|
App.Companion.getLog().log(Level.FINE, "Deleting group", group);
|
||||||
group.delete();
|
group.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -424,7 +424,7 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
|
|||||||
try {
|
try {
|
||||||
int fixed = provider.update(syncAdapterURI(RawContacts.CONTENT_URI),
|
int fixed = provider.update(syncAdapterURI(RawContacts.CONTENT_URI),
|
||||||
values, where, null);
|
values, where, null);
|
||||||
App.log.info("Fixed entries: " + String.valueOf(fixed));
|
App.Companion.getLog().info("Fixed entries: " + String.valueOf(fixed));
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new ContactsStorageException("Couldn't query contacts", e);
|
throw new ContactsStorageException("Couldn't query contacts", e);
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
|
|||||||
if (ret.length == 1) {
|
if (ret.length == 1) {
|
||||||
return (LocalCalendar) ret[0];
|
return (LocalCalendar) ret[0];
|
||||||
} else {
|
} else {
|
||||||
App.log.severe("No calendar found for name " + name);
|
App.Companion.getLog().severe("No calendar found for name " + name);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,14 +163,14 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
|
|||||||
@SuppressWarnings("Recycle")
|
@SuppressWarnings("Recycle")
|
||||||
public void processDirtyExceptions() throws CalendarStorageException {
|
public void processDirtyExceptions() throws CalendarStorageException {
|
||||||
// process deleted exceptions
|
// process deleted exceptions
|
||||||
App.log.info("Processing deleted exceptions");
|
App.Companion.getLog().info("Processing deleted exceptions");
|
||||||
try {
|
try {
|
||||||
Cursor cursor = provider.query(
|
Cursor cursor = provider.query(
|
||||||
syncAdapterURI(Events.CONTENT_URI),
|
syncAdapterURI(Events.CONTENT_URI),
|
||||||
new String[] { Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE },
|
new String[] { Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE },
|
||||||
Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null);
|
Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null);
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
App.log.fine("Found deleted exception, removing; then re-schuling original event");
|
App.Companion.getLog().fine("Found deleted exception, removing; then re-schuling original event");
|
||||||
long id = cursor.getLong(0), // can't be null (by definition)
|
long id = cursor.getLong(0), // can't be null (by definition)
|
||||||
originalID = cursor.getLong(1); // can't be null (by query)
|
originalID = cursor.getLong(1); // can't be null (by query)
|
||||||
|
|
||||||
@ -201,14 +201,14 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// process dirty exceptions
|
// process dirty exceptions
|
||||||
App.log.info("Processing dirty exceptions");
|
App.Companion.getLog().info("Processing dirty exceptions");
|
||||||
try {
|
try {
|
||||||
Cursor cursor = provider.query(
|
Cursor cursor = provider.query(
|
||||||
syncAdapterURI(Events.CONTENT_URI),
|
syncAdapterURI(Events.CONTENT_URI),
|
||||||
new String[] { Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE },
|
new String[] { Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE },
|
||||||
Events.DIRTY + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null);
|
Events.DIRTY + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null);
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
App.log.fine("Found dirty exception, increasing SEQUENCE to re-schedule");
|
App.Companion.getLog().fine("Found dirty exception, increasing SEQUENCE to re-schedule");
|
||||||
long id = cursor.getLong(0), // can't be null (by definition)
|
long id = cursor.getLong(0), // can't be null (by definition)
|
||||||
originalID = cursor.getLong(1); // can't be null (by query)
|
originalID = cursor.getLong(1); // can't be null (by query)
|
||||||
int sequence = cursor.isNull(2) ? 0 : cursor.getInt(2);
|
int sequence = cursor.isNull(2) ? 0 : cursor.getInt(2);
|
||||||
@ -279,7 +279,7 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
|
|||||||
try {
|
try {
|
||||||
int fixed = provider.update(syncAdapterURI(Events.CONTENT_URI),
|
int fixed = provider.update(syncAdapterURI(Events.CONTENT_URI),
|
||||||
values, where, whereArgs);
|
values, where, whereArgs);
|
||||||
App.log.info("Fixed entries: " + String.valueOf(fixed));
|
App.Companion.getLog().info("Fixed entries: " + String.valueOf(fixed));
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new CalendarStorageException("Couldn't fix etags", e);
|
throw new CalendarStorageException("Couldn't fix etags", e);
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@ public class LocalContact extends AndroidContact implements LocalResource {
|
|||||||
// 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
|
||||||
int hashCode = dataHashCode();
|
int hashCode = dataHashCode();
|
||||||
values.put(COLUMN_HASHCODE, hashCode);
|
values.put(COLUMN_HASHCODE, hashCode);
|
||||||
App.log.finer("Clearing dirty flag with eTag = " + eTag + ", contact hash = " + hashCode);
|
App.Companion.getLog().finer("Clearing dirty flag with eTag = " + eTag + ", contact hash = " + hashCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
addressBook.provider.update(rawContactSyncURI(), values, null, null);
|
addressBook.provider.update(rawContactSyncURI(), values, null, null);
|
||||||
@ -128,7 +128,7 @@ public class LocalContact extends AndroidContact implements LocalResource {
|
|||||||
final Contact contact;
|
final Contact contact;
|
||||||
contact = getContact();
|
contact = getContact();
|
||||||
|
|
||||||
App.log.log(Level.FINE, "Preparing upload of VCard " + getUuid(), contact);
|
App.Companion.getLog().log(Level.FINE, "Preparing upload of VCard " + getUuid(), contact);
|
||||||
|
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
contact.write(VCardVersion.V4_0, GROUP_VCARDS, os);
|
contact.write(VCardVersion.V4_0, GROUP_VCARDS, os);
|
||||||
@ -194,7 +194,7 @@ public class LocalContact extends AndroidContact implements LocalResource {
|
|||||||
*/
|
*/
|
||||||
protected int dataHashCode() throws FileNotFoundException, ContactsStorageException {
|
protected int dataHashCode() throws FileNotFoundException, ContactsStorageException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
||||||
App.log.severe("dataHashCode() should not be called on Android <7");
|
App.Companion.getLog().severe("dataHashCode() should not be called on Android <7");
|
||||||
|
|
||||||
// reset contact so that getContact() reads from database
|
// reset contact so that getContact() reads from database
|
||||||
contact = null;
|
contact = null;
|
||||||
@ -202,18 +202,18 @@ public class LocalContact extends AndroidContact implements LocalResource {
|
|||||||
// groupMemberships is filled by getContact()
|
// groupMemberships is filled by getContact()
|
||||||
int dataHash = getContact().hashCode(),
|
int dataHash = getContact().hashCode(),
|
||||||
groupHash = groupMemberships.hashCode();
|
groupHash = groupMemberships.hashCode();
|
||||||
App.log.finest("Calculated data hash = " + dataHash + ", group memberships hash = " + groupHash);
|
App.Companion.getLog().finest("Calculated data hash = " + dataHash + ", group memberships hash = " + groupHash);
|
||||||
return dataHash ^ groupHash;
|
return dataHash ^ groupHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateHashCode(@Nullable BatchOperation batch) throws ContactsStorageException {
|
public void updateHashCode(@Nullable BatchOperation batch) throws ContactsStorageException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
||||||
App.log.severe("updateHashCode() should not be called on Android <7");
|
App.Companion.getLog().severe("updateHashCode() should not be called on Android <7");
|
||||||
|
|
||||||
ContentValues values = new ContentValues(1);
|
ContentValues values = new ContentValues(1);
|
||||||
try {
|
try {
|
||||||
int hashCode = dataHashCode();
|
int hashCode = dataHashCode();
|
||||||
App.log.fine("Storing contact hash = " + hashCode);
|
App.Companion.getLog().fine("Storing contact hash = " + hashCode);
|
||||||
values.put(COLUMN_HASHCODE, hashCode);
|
values.put(COLUMN_HASHCODE, hashCode);
|
||||||
|
|
||||||
if (batch == null)
|
if (batch == null)
|
||||||
@ -231,7 +231,7 @@ public class LocalContact extends AndroidContact implements LocalResource {
|
|||||||
|
|
||||||
public int getLastHashCode() throws ContactsStorageException {
|
public int getLastHashCode() throws ContactsStorageException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
||||||
App.log.severe("getLastHashCode() should not be called on Android <7");
|
App.Companion.getLog().severe("getLastHashCode() should not be called on Android <7");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Cursor c = addressBook.provider.query(rawContactSyncURI(), new String[] { COLUMN_HASHCODE }, null, null, null);
|
Cursor c = addressBook.provider.query(rawContactSyncURI(), new String[] { COLUMN_HASHCODE }, null, null, null);
|
||||||
|
@ -81,7 +81,7 @@ public class LocalEvent extends AndroidEvent implements LocalResource {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getContent() throws IOException, ContactsStorageException, CalendarStorageException {
|
public String getContent() throws IOException, ContactsStorageException, CalendarStorageException {
|
||||||
App.log.log(Level.FINE, "Preparing upload of event " + getFileName(), getEvent());
|
App.Companion.getLog().log(Level.FINE, "Preparing upload of event " + getFileName(), getEvent());
|
||||||
|
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
getEvent().write(os);
|
getEvent().write(os);
|
||||||
|
@ -68,7 +68,7 @@ public class LocalGroup extends AndroidGroup implements LocalResource {
|
|||||||
final Contact contact;
|
final Contact contact;
|
||||||
contact = getContact();
|
contact = getContact();
|
||||||
|
|
||||||
App.log.log(Level.FINE, "Preparing upload of VCard " + getUuid(), contact);
|
App.Companion.getLog().log(Level.FINE, "Preparing upload of VCard " + getUuid(), contact);
|
||||||
|
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
contact.write(VCardVersion.V4_0, GROUP_VCARDS, os);
|
contact.write(VCardVersion.V4_0, GROUP_VCARDS, os);
|
||||||
@ -176,7 +176,7 @@ public class LocalGroup extends AndroidGroup implements LocalResource {
|
|||||||
BatchOperation batch = new BatchOperation(addressBook.provider);
|
BatchOperation batch = new BatchOperation(addressBook.provider);
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
long id = cursor.getLong(0);
|
long id = cursor.getLong(0);
|
||||||
App.log.fine("Assigning members to group " + id);
|
App.Companion.getLog().fine("Assigning members to group " + id);
|
||||||
|
|
||||||
// required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
// required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
||||||
Set<Long> changeContactIDs = new HashSet<>();
|
Set<Long> changeContactIDs = new HashSet<>();
|
||||||
@ -198,13 +198,13 @@ public class LocalGroup extends AndroidGroup implements LocalResource {
|
|||||||
|
|
||||||
// insert memberships
|
// insert memberships
|
||||||
for (String uid : members) {
|
for (String uid : members) {
|
||||||
App.log.fine("Assigning member: " + uid);
|
App.Companion.getLog().fine("Assigning member: " + uid);
|
||||||
try {
|
try {
|
||||||
LocalContact member = addressBook.findContactByUID(uid);
|
LocalContact member = addressBook.findContactByUID(uid);
|
||||||
member.addToGroup(batch, id);
|
member.addToGroup(batch, id);
|
||||||
changeContactIDs.add(member.getId());
|
changeContactIDs.add(member.getId());
|
||||||
} catch(FileNotFoundException e) {
|
} catch(FileNotFoundException e) {
|
||||||
App.log.log(Level.WARNING, "Group member not found: " + uid, e);
|
App.Companion.getLog().log(Level.WARNING, "Group member not found: " + uid, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
|
|||||||
contactsProvider.release()
|
contactsProvider.release()
|
||||||
|
|
||||||
val accountManager = AccountManager.get(context)
|
val accountManager = AccountManager.get(context)
|
||||||
for (addressBookAccount in accountManager.getAccountsByType(App.getAddressBookAccountType())) {
|
for (addressBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) {
|
||||||
App.log.log(Level.INFO, "Running sync for address book", addressBookAccount)
|
App.log.log(Level.INFO, "Running sync for address book", addressBookAccount)
|
||||||
val syncExtras = Bundle(extras)
|
val syncExtras = Bundle(extras)
|
||||||
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true)
|
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true)
|
||||||
|
@ -159,7 +159,7 @@ class AboutActivity : BaseActivity() {
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val components = arrayOf(ComponentInfo(
|
private val components = arrayOf(ComponentInfo(
|
||||||
App.getAppName(), BuildConfig.VERSION_NAME, Constants.webUri.toString(),
|
App.appName, 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"
|
||||||
), ComponentInfo(
|
), ComponentInfo(
|
||||||
|
@ -281,11 +281,11 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
|||||||
if (service == CollectionInfo.Type.ADDRESS_BOOK) {
|
if (service == CollectionInfo.Type.ADDRESS_BOOK) {
|
||||||
info.carddav = AccountInfo.ServiceInfo()
|
info.carddav = AccountInfo.ServiceInfo()
|
||||||
info.carddav!!.id = id
|
info.carddav!!.id = id
|
||||||
info.carddav!!.refreshing = davService != null && davService!!.isRefreshing(id) || ContentResolver.isSyncActive(account, App.getAddressBooksAuthority())
|
info.carddav!!.refreshing = davService != null && davService!!.isRefreshing(id) || ContentResolver.isSyncActive(account, App.addressBooksAuthority)
|
||||||
info.carddav!!.journals = JournalEntity.getJournals(data, serviceEntity)
|
info.carddav!!.journals = JournalEntity.getJournals(data, serviceEntity)
|
||||||
|
|
||||||
val accountManager = AccountManager.get(context)
|
val accountManager = AccountManager.get(context)
|
||||||
for (addrBookAccount in accountManager.getAccountsByType(App.getAddressBookAccountType())) {
|
for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) {
|
||||||
val addressBook = LocalAddressBook(context, addrBookAccount, null)
|
val addressBook = LocalAddressBook(context, addrBookAccount, null)
|
||||||
try {
|
try {
|
||||||
if (account == addressBook.mainAccount)
|
if (account == addressBook.mainAccount)
|
||||||
@ -396,7 +396,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
|||||||
private val HINT_VIEW_COLLECTION = "ViewCollection"
|
private val HINT_VIEW_COLLECTION = "ViewCollection"
|
||||||
|
|
||||||
protected fun requestSync(account: Account?) {
|
protected fun requestSync(account: Account?) {
|
||||||
val authorities = arrayOf(App.getAddressBooksAuthority(), CalendarContract.AUTHORITY, TaskProvider.ProviderName.OpenTasks.authority)
|
val authorities = arrayOf(App.addressBooksAuthority, CalendarContract.AUTHORITY, TaskProvider.ProviderName.OpenTasks.authority)
|
||||||
|
|
||||||
for (authority in authorities) {
|
for (authority in authorities) {
|
||||||
val extras = Bundle()
|
val extras = Bundle()
|
||||||
|
@ -93,7 +93,7 @@ class AccountListFragment : ListFragment(), LoaderManager.LoaderCallbacks<Array<
|
|||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
override fun loadInBackground(): Array<Account>? {
|
override fun loadInBackground(): Array<Account>? {
|
||||||
return accountManager.getAccountsByType(App.getAccountType())
|
return accountManager.getAccountsByType(App.accountType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ class AccountSettingsActivity : BaseActivity() {
|
|||||||
|
|
||||||
// category: synchronization
|
// category: synchronization
|
||||||
val prefSyncContacts = findPreference("sync_interval_contacts") as ListPreference
|
val prefSyncContacts = findPreference("sync_interval_contacts") as ListPreference
|
||||||
val syncIntervalContacts = settings.getSyncInterval(App.getAddressBooksAuthority())
|
val syncIntervalContacts = settings.getSyncInterval(App.addressBooksAuthority)
|
||||||
if (syncIntervalContacts != null) {
|
if (syncIntervalContacts != null) {
|
||||||
prefSyncContacts.value = syncIntervalContacts.toString()
|
prefSyncContacts.value = syncIntervalContacts.toString()
|
||||||
if (syncIntervalContacts == AccountSettings.SYNC_INTERVAL_MANUALLY)
|
if (syncIntervalContacts == AccountSettings.SYNC_INTERVAL_MANUALLY)
|
||||||
@ -105,7 +105,7 @@ class AccountSettingsActivity : BaseActivity() {
|
|||||||
else
|
else
|
||||||
prefSyncContacts.summary = getString(R.string.settings_sync_summary_periodically, prefSyncContacts.entry)
|
prefSyncContacts.summary = getString(R.string.settings_sync_summary_periodically, prefSyncContacts.entry)
|
||||||
prefSyncContacts.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue ->
|
prefSyncContacts.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { preference, newValue ->
|
||||||
settings.setSyncInterval(App.getAddressBooksAuthority(), java.lang.Long.parseLong(newValue as String))
|
settings.setSyncInterval(App.addressBooksAuthority, java.lang.Long.parseLong(newValue as String))
|
||||||
loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment)
|
loaderManager.restartLoader(0, arguments, this@AccountSettingsFragment)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -155,7 +155,7 @@ class AppSettingsActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun resetCertificates() {
|
private fun resetCertificates() {
|
||||||
(context!!.applicationContext as App).certManager.resetCertificates()
|
(context!!.applicationContext as App).certManager?.resetCertificates()
|
||||||
Snackbar.make(view!!, getString(R.string.app_settings_reset_certificates_success), Snackbar.LENGTH_LONG).show()
|
Snackbar.make(view!!, getString(R.string.app_settings_reset_certificates_success), Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,15 +20,17 @@ open class BaseActivity : AppCompatActivity() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
val app = applicationContext as App
|
val app = applicationContext as App
|
||||||
if (app.certManager != null)
|
val certManager = app.certManager
|
||||||
app.certManager.appInForeground = true
|
if (certManager != null)
|
||||||
|
certManager.appInForeground = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
val app = applicationContext as App
|
val app = applicationContext as App
|
||||||
if (app.certManager != null)
|
val certManager = app.certManager
|
||||||
app.certManager.appInForeground = false
|
if (certManager != null)
|
||||||
|
certManager.appInForeground = false
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -89,7 +89,7 @@ class CreateCollectionFragment : DialogFragment(), LoaderManager.LoaderCallbacks
|
|||||||
|
|
||||||
// 1. find service ID
|
// 1. find service ID
|
||||||
if (info.type == CollectionInfo.Type.ADDRESS_BOOK) {
|
if (info.type == CollectionInfo.Type.ADDRESS_BOOK) {
|
||||||
authority = App.getAddressBooksAuthority()
|
authority = App.addressBooksAuthority
|
||||||
} else if (info.type == CollectionInfo.Type.CALENDAR) {
|
} else if (info.type == CollectionInfo.Type.CALENDAR) {
|
||||||
authority = CalendarContract.AUTHORITY
|
authority = CalendarContract.AUTHORITY
|
||||||
} else {
|
} else {
|
||||||
|
@ -168,7 +168,7 @@ class DebugInfoActivity : BaseActivity(), LoaderManager.LoaderCallbacks<String>
|
|||||||
for (acct in accountManager.getAccountsByType(context.getString(R.string.account_type)))
|
for (acct in accountManager.getAccountsByType(context.getString(R.string.account_type)))
|
||||||
try {
|
try {
|
||||||
val settings = AccountSettings(context, acct)
|
val settings = AccountSettings(context, acct)
|
||||||
report.append("Account: ").append(acct.name).append("\n" + " Address book sync. interval: ").append(syncStatus(settings, App.getAddressBooksAuthority())).append("\n" + " Calendar sync. interval: ").append(syncStatus(settings, CalendarContract.AUTHORITY)).append("\n" + " OpenTasks sync. interval: ").append(syncStatus(settings, "org.dmfs.tasks")).append("\n" + " WiFi only: ").append(settings.syncWifiOnly)
|
report.append("Account: ").append(acct.name).append("\n" + " Address book sync. interval: ").append(syncStatus(settings, App.addressBooksAuthority)).append("\n" + " Calendar sync. interval: ").append(syncStatus(settings, CalendarContract.AUTHORITY)).append("\n" + " OpenTasks sync. interval: ").append(syncStatus(settings, "org.dmfs.tasks")).append("\n" + " WiFi only: ").append(settings.syncWifiOnly)
|
||||||
if (settings.syncWifiOnlySSID != null)
|
if (settings.syncWifiOnlySSID != null)
|
||||||
report.append(", SSID: ").append(settings.syncWifiOnlySSID)
|
report.append(", SSID: ").append(settings.syncWifiOnlySSID)
|
||||||
report.append("\n [CardDAV] Contact group method: ").append(settings.groupMethod)
|
report.append("\n [CardDAV] Contact group method: ").append(settings.groupMethod)
|
||||||
@ -179,7 +179,7 @@ class DebugInfoActivity : BaseActivity(), LoaderManager.LoaderCallbacks<String>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// address book accounts
|
// address book accounts
|
||||||
for (acct in accountManager.getAccountsByType(App.getAddressBookAccountType()))
|
for (acct in accountManager.getAccountsByType(App.addressBookAccountType))
|
||||||
try {
|
try {
|
||||||
val addressBook = LocalAddressBook(context, acct, null)
|
val addressBook = LocalAddressBook(context, acct, null)
|
||||||
report.append("Address book account: ").append(acct.name).append("\n" + " Main account: ").append(addressBook.mainAccount).append("\n" + " URL: ").append(addressBook.url).append("\n" + " Sync automatically: ").append(ContentResolver.getSyncAutomatically(acct, ContactsContract.AUTHORITY)).append("\n")
|
report.append("Address book account: ").append(acct.name).append("\n" + " Main account: ").append(addressBook.mainAccount).append("\n" + " URL: ").append(addressBook.url).append("\n" + " Sync automatically: ").append(ContentResolver.getSyncAutomatically(acct, ContactsContract.AUTHORITY)).append("\n")
|
||||||
|
@ -20,8 +20,8 @@ internal class AccountResolver(private val context: Context) {
|
|||||||
// Hardcoded swaps for known accounts:
|
// Hardcoded swaps for known accounts:
|
||||||
if (accountName == "com.google") {
|
if (accountName == "com.google") {
|
||||||
accountName = "com.google.android.googlequicksearchbox"
|
accountName = "com.google.android.googlequicksearchbox"
|
||||||
} else if (accountName == App.getAddressBookAccountType()) {
|
} else if (accountName == App.addressBookAccountType) {
|
||||||
accountName = App.getAccountType()
|
accountName = App.accountType
|
||||||
} else if (accountName == "at.bitfire.davdroid.address_book") {
|
} else if (accountName == "at.bitfire.davdroid.address_book") {
|
||||||
accountName = "at.bitfire.davdroid"
|
accountName = "at.bitfire.davdroid"
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ class LoginCredentialsChangeFragment : DialogFragment(), LoaderManager.LoaderCal
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.setAuthToken(data.authtoken!!)
|
settings.authToken = data.authtoken!!
|
||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
App.log.severe("Configuration detection failed")
|
App.log.severe("Configuration detection failed")
|
||||||
|
@ -94,7 +94,7 @@ class SetupEncryptionFragment : DialogFragment() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
val cryptoManager: Crypto.CryptoManager
|
val cryptoManager: Crypto.CryptoManager
|
||||||
val httpClient = HttpClient.create(getContext(), config.url, config.authtoken)
|
val httpClient = HttpClient.create(getContext(), config.url, config.authtoken!!)
|
||||||
|
|
||||||
val userInfoManager = UserInfoManager(httpClient, HttpUrl.get(config.url)!!)
|
val userInfoManager = UserInfoManager(httpClient, HttpUrl.get(config.url)!!)
|
||||||
val userInfo = userInfoManager[config.userName]
|
val userInfo = userInfoManager[config.userName]
|
||||||
@ -116,7 +116,7 @@ class SetupEncryptionFragment : DialogFragment() {
|
|||||||
|
|
||||||
@Throws(InvalidAccountException::class)
|
@Throws(InvalidAccountException::class)
|
||||||
protected fun createAccount(accountName: String, config: BaseConfigurationFinder.Configuration): Boolean {
|
protected fun createAccount(accountName: String, config: BaseConfigurationFinder.Configuration): Boolean {
|
||||||
val account = Account(accountName, App.getAccountType())
|
val account = Account(accountName, App.accountType)
|
||||||
|
|
||||||
// create Android account
|
// create Android account
|
||||||
App.log.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, config.userName, config.url))
|
App.log.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, config.userName, config.url))
|
||||||
@ -132,7 +132,7 @@ class SetupEncryptionFragment : DialogFragment() {
|
|||||||
try {
|
try {
|
||||||
val settings = AccountSettings(context!!, account)
|
val settings = AccountSettings(context!!, account)
|
||||||
|
|
||||||
settings.authToken = config.authtoken
|
settings.authToken = config.authtoken!!
|
||||||
if (config.keyPair != null) {
|
if (config.keyPair != null) {
|
||||||
settings.keyPair = config.keyPair!!
|
settings.keyPair = config.keyPair!!
|
||||||
}
|
}
|
||||||
@ -142,9 +142,9 @@ class SetupEncryptionFragment : DialogFragment() {
|
|||||||
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(App.getAddressBooksAuthority(), Constants.DEFAULT_SYNC_INTERVAL.toLong())
|
settings.setSyncInterval(App.addressBooksAuthority, Constants.DEFAULT_SYNC_INTERVAL.toLong())
|
||||||
} else {
|
} else {
|
||||||
ContentResolver.setIsSyncable(account, App.getAddressBooksAuthority(), 0)
|
ContentResolver.setIsSyncable(account, App.addressBooksAuthority, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.calDAV != null) {
|
if (config.calDAV != null) {
|
||||||
|
Loading…
Reference in New Issue
Block a user