diff --git a/app/build.gradle b/app/build.gradle
index 66f3414527e3a8e058478d06b6a84bb29bf252df..b07c211f2a92415b7919754f1202e9b705a0c081 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -26,6 +26,10 @@ android {
buildConfigField "String", "userAgent", "\"DAVx5\""
+ manifestPlaceholders = [
+ 'appAuthRedirectScheme': 'com.googleusercontent.apps.628867657910-7ade6gut5rhabdgjq6k4rln9i1u9ppca'
+ ]
+
testInstrumentationRunner "at.bitfire.davdroid.CustomTestRunner"
kapt {
@@ -151,7 +155,8 @@ dependencies {
implementation "org.apache.commons:commons-lang3:${versions.commonsLang}"
//noinspection GradleDependency
implementation "org.apache.commons:commons-text:${versions.commonsText}"
-
+ //google auth
+ implementation 'net.openid:appauth:0.7.0'
// for tests
androidTestImplementation "com.google.dagger:hilt-android-testing:${versions.hilt}"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:${versions.hilt}"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c867faddbe9731b8c96e4c00c9cbefd82e5411e8..282c54b2c73c103f118fca9cf5320a9c67926985 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -280,6 +280,71 @@
android:resource="@xml/debug_paths" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/at/bitfire/davdroid/authorization/IdentityProvider.java b/app/src/main/java/at/bitfire/davdroid/authorization/IdentityProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..10652a3fc5bf8b1b3cea0c4573f8e96056f0ec7c
--- /dev/null
+++ b/app/src/main/java/at/bitfire/davdroid/authorization/IdentityProvider.java
@@ -0,0 +1,256 @@
+package at.bitfire.davdroid.authorization;
+
+/*
+ * Copyright 2015 The AppAuth Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.Uri;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+import foundation.e.accountmanager.R;
+
+import net.openid.appauth.AuthorizationServiceConfiguration;
+import net.openid.appauth.AuthorizationServiceConfiguration.RetrieveConfigurationCallback;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * An abstraction of identity providers, containing all necessary info for the demo app.
+ */
+public class IdentityProvider
+{
+
+ /**
+ * Value used to indicate that a configured property is not specified or required.
+ */
+ public static final int NOT_SPECIFIED = -1;
+
+ public static final IdentityProvider GOOGLE = new IdentityProvider(
+ "Google",
+ R.string.google_discovery_uri,
+ NOT_SPECIFIED, // auth endpoint is discovered
+ NOT_SPECIFIED, // token endpoint is discovered
+ R.string.google_client_id,
+ NOT_SPECIFIED, // client secret is not required for Google
+ R.string.google_auth_redirect_uri,
+ R.string.google_scope_string,
+ R.string.google_name);
+
+ public static final List PROVIDERS = Arrays.asList(
+ GOOGLE);
+
+ public static List getEnabledProviders(Context context)
+ {
+ ArrayList providers = new ArrayList<>();
+ for (IdentityProvider provider : PROVIDERS)
+ {
+ provider.readConfiguration(context);
+ providers.add(provider);
+ }
+ return providers;
+ }
+
+ @NonNull
+ public final String name;
+
+ @StringRes
+ public final int buttonContentDescriptionRes;
+
+ @StringRes
+ private final int mDiscoveryEndpointRes;
+
+ @StringRes
+ private final int mAuthEndpointRes;
+
+ @StringRes
+ private final int mTokenEndpointRes;
+
+ @StringRes
+ private final int mClientIdRes;
+
+ @StringRes
+ private final int mClientSecretRes;
+
+ @StringRes
+ private final int mRedirectUriRes;
+
+ @StringRes
+ private final int mScopeRes;
+
+ private boolean mConfigurationRead = false;
+ private Uri mDiscoveryEndpoint;
+ private Uri mAuthEndpoint;
+ private Uri mTokenEndpoint;
+ private String mClientId;
+ private String mClientSecret;
+ private Uri mRedirectUri;
+ private String mScope;
+
+ IdentityProvider(
+ @NonNull String name,
+ @StringRes int discoveryEndpointRes,
+ @StringRes int authEndpointRes,
+ @StringRes int tokenEndpointRes,
+ @StringRes int clientIdRes,
+ @StringRes int clientSecretRes,
+ @StringRes int redirectUriRes,
+ @StringRes int scopeRes,
+ @StringRes int buttonContentDescriptionRes)
+ {
+ if (!isSpecified(discoveryEndpointRes)
+ && !isSpecified(authEndpointRes)
+ && !isSpecified(tokenEndpointRes))
+ {
+ throw new IllegalArgumentException(
+ "the discovery endpoint or the auth and token endpoints must be specified");
+ }
+
+ this.name = name;
+ this.mDiscoveryEndpointRes = discoveryEndpointRes;
+ this.mAuthEndpointRes = authEndpointRes;
+ this.mTokenEndpointRes = tokenEndpointRes;
+ this.mClientIdRes = checkSpecified(clientIdRes, "clientIdRes");
+ this.mClientSecretRes = clientSecretRes;
+ this.mRedirectUriRes = checkSpecified(redirectUriRes, "redirectUriRes");
+ this.mScopeRes = checkSpecified(scopeRes, "scopeRes");
+ this.buttonContentDescriptionRes =
+ checkSpecified(buttonContentDescriptionRes, "buttonContentDescriptionRes");
+ }
+
+ /**
+ * This must be called before any of the getters will function.
+ */
+ public void readConfiguration(Context context)
+ {
+ if (mConfigurationRead)
+ {
+ return;
+ }
+
+ Resources res = context.getResources();
+
+ mDiscoveryEndpoint = isSpecified(mDiscoveryEndpointRes)
+ ? getUriResource(res, mDiscoveryEndpointRes, "discoveryEndpointRes")
+ : null;
+ mAuthEndpoint = isSpecified(mAuthEndpointRes)
+ ? getUriResource(res, mAuthEndpointRes, "authEndpointRes")
+ : null;
+ mTokenEndpoint = isSpecified(mTokenEndpointRes)
+ ? getUriResource(res, mTokenEndpointRes, "tokenEndpointRes")
+ : null;
+ mClientId = res.getString(mClientIdRes);
+ mClientSecret = isSpecified(mClientSecretRes) ? res.getString(mClientSecretRes) : null;
+ mRedirectUri = getUriResource(res, mRedirectUriRes, "mRedirectUriRes");
+ mScope = res.getString(mScopeRes);
+
+ mConfigurationRead = true;
+ }
+
+ private void checkConfigurationRead()
+ {
+ if (!mConfigurationRead)
+ {
+ throw new IllegalStateException("Configuration not read");
+ }
+ }
+
+ @Nullable
+ public Uri getDiscoveryEndpoint()
+ {
+ checkConfigurationRead();
+ return mDiscoveryEndpoint;
+ }
+
+ @Nullable
+ public Uri getAuthEndpoint()
+ {
+ checkConfigurationRead();
+ return mAuthEndpoint;
+ }
+
+ @Nullable
+ public Uri getTokenEndpoint()
+ {
+ checkConfigurationRead();
+ return mTokenEndpoint;
+ }
+
+ @NonNull
+ public String getClientId()
+ {
+ checkConfigurationRead();
+ return mClientId;
+ }
+
+ @Nullable
+ public String getClientSecret()
+ {
+ checkConfigurationRead();
+ return mClientSecret;
+ }
+
+ @NonNull
+ public Uri getRedirectUri()
+ {
+ checkConfigurationRead();
+ return mRedirectUri;
+ }
+
+ @NonNull
+ public String getScope()
+ {
+ checkConfigurationRead();
+ return mScope;
+ }
+
+ public void retrieveConfig(Context context,
+ RetrieveConfigurationCallback callback)
+ {
+ readConfiguration(context);
+ if (getDiscoveryEndpoint() != null)
+ {
+ AuthorizationServiceConfiguration.fetchFromUrl(mDiscoveryEndpoint, callback);
+ }
+ else
+ {
+ AuthorizationServiceConfiguration config =
+ new AuthorizationServiceConfiguration(mAuthEndpoint, mTokenEndpoint, null);
+ callback.onFetchConfigurationCompleted(config, null);
+ }
+ }
+
+ private static boolean isSpecified(int value)
+ {
+ return value != NOT_SPECIFIED;
+ }
+
+ private static int checkSpecified(int value, String valueName)
+ {
+ if (value == NOT_SPECIFIED)
+ {
+ throw new IllegalArgumentException(valueName + " must be specified");
+ }
+ return value;
+ }
+
+ private static Uri getUriResource(Resources res, @StringRes int resId, String resName)
+ {
+ return Uri.parse(res.getString(resId));
+ }
+}
diff --git a/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt b/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt
index f2f4758b1185a3000dd4896e43b2c498ec182ad8..c3becfeaf945f31fa3499c2afdf4a55d3bef3647 100644
--- a/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt
+++ b/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt
@@ -1,18 +1,44 @@
-/***************************************************************************************************
- * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
- **************************************************************************************************/
+/*
+ * 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 at.bitfire.davdroid.db
+package foundation.e.accountmanager.model
-data class Credentials(
- val userName: String? = null,
- val password: String? = null,
- val certificateAlias: String? = null
+class Credentials(
+ val userName: String? = null,
+ val password: String? = null,
+ val accessToken: String? = null,
+ val refreshToken: String? = null,
+ val certificateAlias: String? = null
) {
- override fun toString(): String {
- val maskedPassword = "*****".takeIf { password != null }
- return "Credentials(userName=$userName, password=$maskedPassword, certificateAlias=$certificateAlias)"
+ enum class Type {
+ UsernamePassword,
+ OAuth,
+ ClientCertificate
}
-}
\ No newline at end of file
+ val type: Type
+
+ init {
+ type = when {
+ !certificateAlias.isNullOrEmpty() ->
+ Type.ClientCertificate
+ !userName.isNullOrEmpty() && !accessToken.isNullOrEmpty()
+ && !refreshToken.isNullOrEmpty() ->
+ Type.OAuth
+ !userName.isNullOrEmpty() && !password.isNullOrEmpty() ->
+ Type.UsernamePassword
+ else ->
+ throw IllegalArgumentException("Invalid account type/credentials")
+ }
+ }
+
+ override fun toString() =
+ "Credentials(type=$type, userName=$userName, certificateAlias=$certificateAlias)"
+
+}
diff --git a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt
index eb009afb55e2e8cbb4aded1425e8801d9bc0783a..539779f5b8eca611d676d29ba512c555d5ee6c4e 100644
--- a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt
+++ b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt
@@ -15,83 +15,48 @@ import android.os.Bundle
import android.os.Parcel
import android.os.RemoteException
import android.provider.CalendarContract
-import android.provider.CalendarContract.ExtendedProperties
import android.provider.ContactsContract
-import android.util.Base64
-import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat
-import androidx.preference.PreferenceManager
-import at.bitfire.davdroid.InvalidAccountException
-import at.bitfire.davdroid.R
-import at.bitfire.davdroid.closeCompat
-import at.bitfire.davdroid.db.AppDatabase
-import at.bitfire.davdroid.db.Collection
-import at.bitfire.davdroid.db.Credentials
-import at.bitfire.davdroid.db.Service
-import at.bitfire.davdroid.log.Logger
-import at.bitfire.davdroid.resource.LocalAddressBook
-import at.bitfire.davdroid.resource.LocalTask
-import at.bitfire.davdroid.resource.TaskUtils
-import at.bitfire.davdroid.syncadapter.SyncUtils
-
+import foundation.e.accountmanager.*
+import foundation.e.accountmanager.log.Logger
+import foundation.e.accountmanager.model.AppDatabase
+import foundation.e.accountmanager.model.Collection
+import foundation.e.accountmanager.model.Credentials
+import foundation.e.accountmanager.model.Service
+import foundation.e.accountmanager.resource.LocalAddressBook
+import foundation.e.accountmanager.resource.LocalTask
import foundation.e.ical4android.AndroidCalendar
import foundation.e.ical4android.TaskProvider
import foundation.e.ical4android.TaskProvider.ProviderName.OpenTasks
import foundation.e.vcard4android.ContactsStorageException
import foundation.e.vcard4android.GroupMethod
-
-import at.techbee.jtx.JtxContract.asSyncAdapter
-import dagger.hilt.EntryPoint
-import dagger.hilt.InstallIn
-import dagger.hilt.android.EntryPointAccessors
-import dagger.hilt.components.SingletonComponent
-import net.fortuna.ical4j.model.Property
-import net.fortuna.ical4j.model.property.Url
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import okhttp3.HttpUrl
import org.apache.commons.lang3.StringUtils
import org.dmfs.tasks.contract.TaskContract
-import java.io.ByteArrayInputStream
-import java.io.ObjectInputStream
import java.util.logging.Level
/**
* Manages settings of an account.
*
- * @param context Required to access account settings
- * @param argAccount Account to take settings from. If this account is an address book account,
- * settings will be taken from the corresponding main account instead.
- *
* @throws InvalidAccountException on construction when the account doesn't exist (anymore)
- * @throws IllegalArgumentException when the account type is not _DAVx5_ or _DAVx5 address book_
*/
-@Suppress("FunctionName")
class AccountSettings(
val context: Context,
- argAccount: Account
+ val account: Account
) {
- @EntryPoint
- @InstallIn(SingletonComponent::class)
- interface AccountSettingsEntryPoint {
- fun appDatabase(): AppDatabase
- fun settingsManager(): SettingsManager
- }
-
companion object {
- const val CURRENT_VERSION = 13
+ const val CURRENT_VERSION = 10
const val KEY_SETTINGS_VERSION = "version"
- const val KEY_SYNC_INTERVAL_ADDRESSBOOKS = "sync_interval_addressbooks"
- const val KEY_SYNC_INTERVAL_CALENDARS = "sync_interval_calendars"
-
- /** Stores the tasks sync interval (in seconds) so that it can be set again when the provider is switched */
- const val KEY_SYNC_INTERVAL_TASKS = "sync_interval_tasks"
-
const val KEY_USERNAME = "user_name"
+ const val KEY_ACCESS_TOKEN = "access_token"
+ const val KEY_REFRESH_TOKEN = "refresh_token"
const val KEY_CERTIFICATE_ALIAS = "certificate_alias"
const val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false)
+ const val WIFI_ONLY_DEFAULT = false
const val KEY_WIFI_ONLY_SSIDS = "wifi_only_ssids" // restrict sync to specific WiFi SSIDs
/** Time range limitation to the past [in days]. Values:
@@ -110,108 +75,47 @@ class AccountSettings(
*/
const val KEY_DEFAULT_ALARM = "default_alarm"
- /** Whether DAVx5 sets the local calendar color to the value from service DB at every sync
- value = *null* (not existing): true (default);
- "0" false */
+ /* Whether DAVx5 sets the local calendar color to the value from service DB at every sync
+ value = null (not existing) true (default)
+ "0" false */
const val KEY_MANAGE_CALENDAR_COLORS = "manage_calendar_colors"
- /** Whether DAVx5 populates and uses CalendarContract.Colors
- value = *null* (not existing) false (default);
- "1" true */
+ /* Whether DAVx5 populates and uses CalendarContract.Colors
+ value = null (not existing) false (default)
+ "1" true */
const val KEY_EVENT_COLORS = "event_colors"
/** Contact group method:
- *null (not existing)* groups as separate vCards (default);
- "CATEGORIES" groups are per-contact CATEGORIES
+ value = null (not existing) groups as separate VCards (default)
+ "CATEGORIES" groups are per-contact CATEGORIES
*/
const val KEY_CONTACT_GROUP_METHOD = "contact_group_method"
- /** UI preference: Show only personal collections
- value = *null* (not existing) show all collections (default);
- "1" show only personal collections */
- const val KEY_SHOW_ONLY_PERSONAL = "show_only_personal"
-
const val SYNC_INTERVAL_MANUALLY = -1L
-
- fun initialUserData(credentials: Credentials?): Bundle {
+ fun initialUserData(credentials: Credentials): Bundle {
val bundle = Bundle(2)
bundle.putString(KEY_SETTINGS_VERSION, CURRENT_VERSION.toString())
- if (credentials != null) {
- if (credentials.userName != null)
+ when (credentials.type) {
+ Credentials.Type.UsernamePassword ->
+ bundle.putString(KEY_USERNAME, credentials.userName)
+ Credentials.Type.OAuth ->
bundle.putString(KEY_USERNAME, credentials.userName)
- if (credentials.certificateAlias != null)
+ Credentials.Type.ClientCertificate ->
bundle.putString(KEY_CERTIFICATE_ALIAS, credentials.certificateAlias)
}
return bundle
}
- fun repairSyncIntervals(context: Context) {
- val addressBooksAuthority = context.getString(R.string.address_books_authority)
- val taskAuthority = TaskUtils.currentProvider(context)?.authority
-
- val am = AccountManager.get(context)
- for (account in am.getAccountsByType(context.getString(R.string.account_type)))
- try {
- val settings = AccountSettings(context, account)
-
- // repair address book sync
- settings.getSavedAddressbooksSyncInterval()?.let { shouldBe ->
- val current = settings.getSyncInterval(addressBooksAuthority)
- if (current != shouldBe) {
- Logger.log.warning("${account.name}: $addressBooksAuthority sync interval should be $shouldBe but is $current -> setting to $current")
- settings.setSyncInterval(addressBooksAuthority, shouldBe)
- }
- }
-
- // repair calendar sync
- settings.getSavedCalendarsSyncInterval()?.let { shouldBe ->
- val current = settings.getSyncInterval(CalendarContract.AUTHORITY)
- if (current != shouldBe) {
- Logger.log.warning("${account.name}: ${CalendarContract.AUTHORITY} sync interval should be $shouldBe but is $current -> setting to $current")
- settings.setSyncInterval(CalendarContract.AUTHORITY, shouldBe)
- }
- }
-
- if (taskAuthority != null)
- // repair calendar sync
- settings.getSavedTasksSyncInterval()?.let { shouldBe ->
- val current = settings.getSyncInterval(taskAuthority)
- if (current != shouldBe) {
- Logger.log.warning("${account.name}: $taskAuthority sync interval should be $shouldBe but is $current -> setting to $current")
- settings.setSyncInterval(taskAuthority, shouldBe)
- }
- }
- } catch (ignored: InvalidAccountException) {
- // account doesn't exist (anymore)
- }
- }
-
}
- val db = EntryPointAccessors.fromApplication(context, AccountSettingsEntryPoint::class.java).appDatabase()
- val settings = EntryPointAccessors.fromApplication(context, AccountSettingsEntryPoint::class.java).settingsManager()
-
val accountManager: AccountManager = AccountManager.get(context)
- val account: Account
+ val settings = Settings.getInstance(context)
init {
- when (argAccount.type) {
- context.getString(R.string.account_type_address_book) -> {
- /* argAccount is an address book account, which is not a main account. However settings are
- stored in the main account, so resolve and use the main account instead. */
- account = LocalAddressBook.mainAccount(context, argAccount)
- }
- context.getString(R.string.account_type) ->
- account = argAccount
- else ->
- throw IllegalArgumentException("Account type not supported")
- }
-
- // synchronize because account migration must only be run one time
synchronized(AccountSettings::class.java) {
val versionStr = accountManager.getUserData(account, KEY_SETTINGS_VERSION) ?: throw InvalidAccountException(account)
var version = 0
@@ -230,9 +134,11 @@ class AccountSettings(
// authentication settings
fun credentials() = Credentials(
- accountManager.getUserData(account, KEY_USERNAME),
- accountManager.getPassword(account),
- accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS)
+ accountManager.getUserData(account, KEY_USERNAME),
+ accountManager.getPassword(account),
+ accountManager.getUserData(account, KEY_ACCESS_TOKEN),
+ accountManager.getUserData(account, KEY_REFRESH_TOKEN),
+ accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS)
)
fun credentials(credentials: Credentials) {
@@ -254,92 +160,28 @@ class AccountSettings(
SYNC_INTERVAL_MANUALLY
}
- /**
- * Sets the sync interval and enables/disables automatic sync for the given account and authority.
- * Does *not* call [ContentResolver.setIsSyncable].
- *
- * This method blocks until the settings have arrived in the sync framework, so it should not
- * be called from the UI thread.
- *
- * @param authority sync authority (like [CalendarContract.AUTHORITY])
- * @param seconds if [SYNC_INTERVAL_MANUALLY]: automatic sync will be disabled;
- * otherwise: automatic sync will be enabled and set to the given number of seconds
- *
- * @return whether the sync interval was successfully set
- */
- @WorkerThread
- fun setSyncInterval(authority: String, seconds: Long): Boolean {
- /* Ugly hack: because there is no callback for when the sync status/interval has been
- updated, we need to make this call blocking. */
- val setInterval: () -> Boolean =
- if (seconds == SYNC_INTERVAL_MANUALLY) {
- {
- Logger.log.fine("Disabling automatic sync of $account/$authority")
- ContentResolver.setSyncAutomatically(account, authority, false)
-
- /* return */ !ContentResolver.getSyncAutomatically(account, authority)
- }
- } else {
- {
- Logger.log.fine("Setting automatic sync of $account/$authority to $seconds seconds")
- ContentResolver.setSyncAutomatically(account, authority, true)
- ContentResolver.addPeriodicSync(account, authority, Bundle(), seconds)
-
- /* return */ ContentResolver.getSyncAutomatically(account, authority) &&
- ContentResolver.getPeriodicSyncs(account, authority).firstOrNull()?.period == seconds
- }
- }
-
- // try up to 10 times with 100 ms pause
- var success = false
- for (idxTry in 0 until 10) {
- success = setInterval()
- if (success)
- break
- Thread.sleep(100)
+ 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)
}
-
- if (!success)
- return false
-
- // store sync interval in account settings (used when the provider is switched)
- when {
- authority == context.getString(R.string.address_books_authority) ->
- accountManager.setUserData(account, KEY_SYNC_INTERVAL_ADDRESSBOOKS, seconds.toString())
-
- authority == CalendarContract.AUTHORITY ->
- accountManager.setUserData(account, KEY_SYNC_INTERVAL_CALENDARS, seconds.toString())
-
- TaskProvider.ProviderName.values().any { it.authority == authority } ->
- accountManager.setUserData(account, KEY_SYNC_INTERVAL_TASKS, seconds.toString())
- }
-
- return true
}
- fun getSavedAddressbooksSyncInterval() = accountManager.getUserData(account, KEY_SYNC_INTERVAL_ADDRESSBOOKS)?.toLong()
- fun getSavedCalendarsSyncInterval() = accountManager.getUserData(account, KEY_SYNC_INTERVAL_CALENDARS)?.toLong()
- fun getSavedTasksSyncInterval() = accountManager.getUserData(account, KEY_SYNC_INTERVAL_TASKS)?.toLong()
-
- fun getSyncWifiOnly() =
- if (settings.containsKey(KEY_WIFI_ONLY))
- settings.getBoolean(KEY_WIFI_ONLY)
- else
- accountManager.getUserData(account, KEY_WIFI_ONLY) != null
+ fun getSyncWifiOnly() = if (settings.has(KEY_WIFI_ONLY))
+ settings.getBoolean(KEY_WIFI_ONLY) ?: WIFI_ONLY_DEFAULT
+ else
+ accountManager.getUserData(account, KEY_WIFI_ONLY) != null
fun setSyncWiFiOnly(wiFiOnly: Boolean) =
- accountManager.setUserData(account, KEY_WIFI_ONLY, if (wiFiOnly) "1" else null)
-
- fun getSyncWifiOnlySSIDs(): List? =
- if (getSyncWifiOnly()) {
- val strSsids = if (settings.containsKey(KEY_WIFI_ONLY_SSIDS))
- settings.getString(KEY_WIFI_ONLY_SSIDS)
- else
- accountManager.getUserData(account, KEY_WIFI_ONLY_SSIDS)
- strSsids?.split(',')
- } else
- null
+ accountManager.setUserData(account, KEY_WIFI_ONLY, if (wiFiOnly) "1" else null)
+
+ fun getSyncWifiOnlySSIDs(): List? = (if (settings.has(KEY_WIFI_ONLY_SSIDS))
+ settings.getString(KEY_WIFI_ONLY_SSIDS)
+ else
+ accountManager.getUserData(account, KEY_WIFI_ONLY_SSIDS))?.split(',')
fun setSyncWifiOnlySSIDs(ssids: List?) =
- accountManager.setUserData(account, KEY_WIFI_ONLY_SSIDS, StringUtils.trimToNull(ssids?.joinToString(",")))
+ accountManager.setUserData(account, KEY_WIFI_ONLY_SSIDS, StringUtils.trimToNull(ssids?.joinToString(",")))
// CalDAV settings
@@ -357,7 +199,7 @@ class AccountSettings(
}
fun setTimeRangePastDays(days: Int?) =
- accountManager.setUserData(account, KEY_TIME_RANGE_PAST_DAYS, (days ?: -1).toString())
+ accountManager.setUserData(account, KEY_TIME_RANGE_PAST_DAYS, (days ?: -1).toString())
/**
* Takes the default alarm setting (in this order) from
@@ -369,8 +211,8 @@ class AccountSettings(
* non-full-day event without reminder. *null*: No default reminders shall be created.
*/
fun getDefaultAlarm() =
- accountManager.getUserData(account, KEY_DEFAULT_ALARM)?.toInt() ?:
- settings.getIntOrNull(KEY_DEFAULT_ALARM)?.takeIf { it != -1 }
+ accountManager.getUserData(account, KEY_DEFAULT_ALARM)?.toInt() ?:
+ settings.getInt(KEY_DEFAULT_ALARM).takeIf { it != -1 }
/**
* Sets the default alarm value in the local account settings, if the new value differs
@@ -382,31 +224,31 @@ class AccountSettings(
* start of every non-full-day event without reminder. *null*: No default reminders shall be created.
*/
fun setDefaultAlarm(minBefore: Int?) =
- accountManager.setUserData(account, KEY_DEFAULT_ALARM,
- if (minBefore == settings.getIntOrNull(KEY_DEFAULT_ALARM)?.takeIf { it != -1 })
- null
- else
- minBefore?.toString())
-
- fun getManageCalendarColors() = if (settings.containsKey(KEY_MANAGE_CALENDAR_COLORS))
- settings.getBoolean(KEY_MANAGE_CALENDAR_COLORS)
+ accountManager.setUserData(account, KEY_DEFAULT_ALARM,
+ if (minBefore == settings.getInt(KEY_DEFAULT_ALARM).takeIf { it != -1 })
+ null
+ else
+ minBefore?.toString())
+
+ fun getManageCalendarColors() = if (settings.has(KEY_MANAGE_CALENDAR_COLORS))
+ settings.getBoolean(KEY_MANAGE_CALENDAR_COLORS) ?: false
else
accountManager.getUserData(account, KEY_MANAGE_CALENDAR_COLORS) == null
fun setManageCalendarColors(manage: Boolean) =
- accountManager.setUserData(account, KEY_MANAGE_CALENDAR_COLORS, if (manage) null else "0")
+ accountManager.setUserData(account, KEY_MANAGE_CALENDAR_COLORS, if (manage) null else "0")
- fun getEventColors() = if (settings.containsKey(KEY_EVENT_COLORS))
- settings.getBoolean(KEY_EVENT_COLORS)
- else
- accountManager.getUserData(account, KEY_EVENT_COLORS) != null
+ fun getEventColors() = if (settings.has(KEY_EVENT_COLORS))
+ settings.getBoolean(KEY_EVENT_COLORS) ?: false
+ else
+ accountManager.getUserData(account, KEY_EVENT_COLORS) != null
fun setEventColors(useColors: Boolean) =
- accountManager.setUserData(account, KEY_EVENT_COLORS, if (useColors) "1" else null)
+ accountManager.setUserData(account, KEY_EVENT_COLORS, if (useColors) "1" else null)
// CardDAV settings
fun getGroupMethod(): GroupMethod {
val name = settings.getString(KEY_CONTACT_GROUP_METHOD) ?:
- accountManager.getUserData(account, KEY_CONTACT_GROUP_METHOD)
+ accountManager.getUserData(account, KEY_CONTACT_GROUP_METHOD)
if (name != null)
try {
return GroupMethod.valueOf(name)
@@ -421,28 +263,6 @@ class AccountSettings(
}
- // UI settings
-
- /**
- * Whether only personal collections should be shown.
- *
- * @return [Pair] of values:
- *
- * 1. (first) whether only personal collections should be shown
- * 2. (second) whether the user shall be able to change the setting (= setting not locked)
- */
- fun getShowOnlyPersonal(): Pair =
- when (settings.getIntOrNull(KEY_SHOW_ONLY_PERSONAL)) {
- 0 -> Pair(false, false)
- 1 -> Pair(true, false)
- else /* including -1 */ -> Pair(accountManager.getUserData(account, KEY_SHOW_ONLY_PERSONAL) != null, true)
- }
-
- fun setShowOnlyPersonal(showOnlyPersonal: Boolean) {
- accountManager.setUserData(account, KEY_SHOW_ONLY_PERSONAL, if (showOnlyPersonal) "1" else null)
- }
-
-
// update from previous account settings
private fun update(baseVersion: Int) {
@@ -462,127 +282,6 @@ class AccountSettings(
}
- @Suppress("unused","FunctionName")
- /**
- * Not a per-account migration, but not a database migration, too, so it fits best there.
- * Best future solution would be that SettingsManager manages versions and migrations.
- *
- * Updates proxy settings from override_proxy_* to proxy_type, proxy_host, proxy_port.
- */
- private fun update_12_13() {
- // proxy settings are managed by SharedPreferencesProvider
- val preferences = PreferenceManager.getDefaultSharedPreferences(context)
-
- // old setting names
- val overrideProxy = "override_proxy"
- val overrideProxyHost = "override_proxy_host"
- val overrideProxyPort = "override_proxy_port"
-
- val edit = preferences.edit()
- if (preferences.contains(overrideProxy)) {
- if (preferences.getBoolean(overrideProxy, false))
- // override_proxy set, migrate to proxy_type = HTTP
- edit.putInt(Settings.PROXY_TYPE, Settings.PROXY_TYPE_HTTP)
- edit.remove(overrideProxy)
- }
- if (preferences.contains(overrideProxyHost)) {
- preferences.getString(overrideProxyHost, null)?.let { host ->
- edit.putString(Settings.PROXY_HOST, host)
- }
- edit.remove(overrideProxyHost)
- }
- if (preferences.contains(overrideProxyPort)) {
- val port = preferences.getInt(overrideProxyPort, 0)
- if (port != 0)
- edit.putInt(Settings.PROXY_PORT, port)
- edit.remove(overrideProxyPort)
- }
- edit.apply()
- }
-
-
- @Suppress("unused","FunctionName")
- /**
- * Store event URLs as URL (extended property) instead of unknown property. At the same time,
- * convert legacy unknown properties to the current format.
- */
- private fun update_11_12() {
- if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED)
- context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.use { provider ->
- // Attention: CalendarProvider does NOT limit the results of the ExtendedProperties query
- // to the given account! So all extended properties will be processed number-of-accounts times.
- val extUri = ExtendedProperties.CONTENT_URI.asSyncAdapter(account)
-
- provider.query(extUri, arrayOf(
- ExtendedProperties._ID, // idx 0
- ExtendedProperties.NAME, // idx 1
- ExtendedProperties.VALUE // idx 2
- ), null, null, null)?.use { cursor ->
- while (cursor.moveToNext()) {
- val id = cursor.getLong(0)
- val rawValue = cursor.getString(2)
-
- val uri by lazy {
- ContentUris.withAppendedId(ExtendedProperties.CONTENT_URI, id).asSyncAdapter(account)
- }
-
- when (cursor.getString(1)) {
- UnknownProperty.CONTENT_ITEM_TYPE -> {
- // unknown property; check whether it's a URL
- try {
- val property = UnknownProperty.fromJsonString(rawValue)
- if (property is Url) { // rewrite to MIMETYPE_URL
- val newValues = ContentValues(2)
- newValues.put(ExtendedProperties.NAME, AndroidEvent.MIMETYPE_URL)
- newValues.put(ExtendedProperties.VALUE, property.value)
- provider.update(uri, newValues, null, null)
- }
- } catch (e: Exception) {
- Logger.log.log(Level.WARNING, "Couldn't rewrite URL from unknown property to ${AndroidEvent.MIMETYPE_URL}", e)
- }
- }
- "unknown-property" -> {
- // unknown property (deprecated format); convert to current format
- try {
- val stream = ByteArrayInputStream(Base64.decode(rawValue, Base64.NO_WRAP))
- ObjectInputStream(stream).use {
- (it.readObject() as? Property)?.let { property ->
- // rewrite to current format
- val newValues = ContentValues(2)
- newValues.put(ExtendedProperties.NAME, UnknownProperty.CONTENT_ITEM_TYPE)
- newValues.put(ExtendedProperties.VALUE, UnknownProperty.toJsonString(property))
- provider.update(uri, newValues, null, null)
- }
- }
- } catch(e: Exception) {
- Logger.log.log(Level.WARNING, "Couldn't rewrite deprecated unknown property to current format", e)
- }
- }
- "unknown-property.v2" -> {
- // unknown property (deprecated MIME type); rewrite to current MIME type
- val newValues = ContentValues(1)
- newValues.put(ExtendedProperties.NAME, UnknownProperty.CONTENT_ITEM_TYPE)
- provider.update(uri, newValues, null, null)
- }
- }
- }
- }
- }
- }
-
- @Suppress("unused","FunctionName")
- /**
- * The tasks sync interval should be stored in account settings. It's used to set the sync interval
- * again when the tasks provider is switched.
- */
- private fun update_10_11() {
- TaskUtils.currentProvider(context)?.let { provider ->
- val interval = getSyncInterval(provider.authority)
- if (interval != null)
- accountManager.setUserData(account, KEY_SYNC_INTERVAL_TASKS, interval.toString())
- }
- }
-
@Suppress("unused","FunctionName")
/**
* Task synchronization now handles alarms, categories, relations and unknown properties.
@@ -592,17 +291,16 @@ class AccountSettings(
**/
private fun update_9_10() {
TaskProvider.acquire(context, OpenTasks)?.use { provider ->
- val tasksUri = provider.tasksUri().asSyncAdapter(account)
+ val tasksUri = TaskProvider.syncAdapterUri(provider.tasksUri(), account)
val emptyETag = ContentValues(1)
emptyETag.putNull(LocalTask.COLUMN_ETAG)
provider.client.update(tasksUri, emptyETag, "${TaskContract.Tasks._DIRTY}=0 AND ${TaskContract.Tasks._DELETED}=0", null)
}
- @SuppressLint("Recycle")
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED)
context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { provider ->
- provider.update(CalendarContract.Calendars.CONTENT_URI.asSyncAdapter(account),
- AndroidCalendar.calendarBaseValues, null, null)
+ provider.update(AndroidCalendar.syncAdapterURI(CalendarContract.Calendars.CONTENT_URI, account),
+ AndroidCalendar.calendarBaseValues, null, null)
provider.closeCompat()
}
}
@@ -613,6 +311,7 @@ class AccountSettings(
* Disable it on those accounts for the future.
*/
private fun update_8_9() {
+ val db = AppDatabase.getInstance(context)
val hasCalDAV = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV) != null
if (!hasCalDAV && ContentResolver.getIsSyncable(account, OpenTasks.authority) != 0) {
Logger.log.info("Disabling OpenTasks sync for $account")
@@ -630,10 +329,10 @@ class AccountSettings(
TaskProvider.acquire(context, OpenTasks)?.use { provider ->
// ETag is now in sync_version instead of sync1
// UID is now in _uid instead of sync2
- provider.client.query(provider.tasksUri().asSyncAdapter(account),
- arrayOf(TaskContract.Tasks._ID, TaskContract.Tasks.SYNC1, TaskContract.Tasks.SYNC2),
- "${TaskContract.Tasks.ACCOUNT_TYPE}=? AND ${TaskContract.Tasks.ACCOUNT_NAME}=?",
- arrayOf(account.type, account.name), null)!!.use { cursor ->
+ provider.client.query(TaskProvider.syncAdapterUri(provider.tasksUri(), account),
+ arrayOf(TaskContract.Tasks._ID, TaskContract.Tasks.SYNC1, TaskContract.Tasks.SYNC2),
+ "${TaskContract.Tasks.ACCOUNT_TYPE}=? AND ${TaskContract.Tasks.ACCOUNT_NAME}=?",
+ arrayOf(account.type, account.name), null)!!.use { cursor ->
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
val eTag = cursor.getString(1)
@@ -645,8 +344,8 @@ class AccountSettings(
values.putNull(TaskContract.Tasks.SYNC2)
Logger.log.log(Level.FINER, "Updating task $id", values)
provider.client.update(
- ContentUris.withAppendedId(provider.tasksUri(), id).asSyncAdapter(account),
- values, null, null)
+ TaskProvider.syncAdapterUri(ContentUris.withAppendedId(provider.tasksUri(), id), account),
+ values, null, null)
}
}
}
@@ -689,7 +388,7 @@ class AccountSettings(
parcel.unmarshall(raw, 0, raw.size)
parcel.setDataPosition(0)
val params = parcel.readBundle()!!
- val url = params.getString("url")?.toHttpUrlOrNull()
+ val url = params.getString("url")?.let { HttpUrl.parse(it) }
if (url == null)
Logger.log.info("No address book URL, ignoring account")
else {
@@ -706,12 +405,12 @@ class AccountSettings(
newAccount.put(ContactsContract.RawContacts.ACCOUNT_NAME, addressBookAccount.name)
newAccount.put(ContactsContract.RawContacts.ACCOUNT_TYPE, addressBookAccount.type)
val 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))
+ .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))
Logger.log.info("$affected contacts moved to new address book")
}
@@ -730,14 +429,14 @@ class AccountSettings(
// request sync of new address book account
ContentResolver.setIsSyncable(account, context.getString(R.string.address_books_authority), 1)
- setSyncInterval(context.getString(R.string.address_books_authority), 4*3600)
+ setSyncInterval(context.getString(R.string.address_books_authority), Constants.DEFAULT_SYNC_INTERVAL)
}
/* Android 7.1.1 OpenTasks fix */
@Suppress("unused")
private fun update_4_5() {
// call PackageChangedReceiver which then enables/disables OpenTasks sync when it's (not) available
- SyncUtils.updateTaskSync(context)
+ PackageChangedReceiver.updateTaskSync(context)
}
@Suppress("unused")
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..09fce35c6926c07abaaf3888d15b8bd495d44d32
--- /dev/null
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt
@@ -0,0 +1,116 @@
+
+/*
+ * 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 at.bitfire.davdroid.syncadapter
+
+
+import android.accounts.*
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.database.DatabaseUtils
+import android.os.Bundle
+import androidx.annotation.WorkerThread
+import at.bitfire.davdroid.db.AppDatabase
+import at.bitfire.davdroid.log.Logger
+import at.bitfire.davdroid.resource.LocalAddressBook
+import at.bitfire.davdroid.ui.setup.LoginActivity
+import foundation.e.accountmanager.R
+import java.util.*
+import java.util.logging.Level
+import kotlin.concurrent.thread
+
+/**
+ * Account authenticator for the eelo account type.
+ *
+ * Gets started when an eelo account is removed, too, so it also watches for account removals
+ * and contains the corresponding cleanup code.
+ */
+class EeloAccountAuthenticatorService : Service(), OnAccountsUpdateListener {
+
+ companion object {
+
+ fun cleanupAccounts(context: Context) {
+ Logger.log.info("Cleaning up orphaned accounts")
+
+ val accountManager = AccountManager.get(context)
+ val accountNames = accountManager.getAccountsByType(context.getString(R.string.account_type))
+ .map { it.name }
+
+ // delete orphaned address book accounts
+ accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
+ .map { LocalAddressBook(context, it, null) }
+ .forEach {
+ try {
+ if (!accountNames.contains(it.mainAccount.name))
+ it.delete()
+ } catch(e: Exception) {
+ Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e)
+ }
+ }
+
+ // delete orphaned services in DB
+ val db = AppDatabase.getInstance(context)
+ val serviceDao = db.serviceDao()
+ if (accountNames.isEmpty())
+ serviceDao.deleteAll()
+ else
+ serviceDao.deleteExceptAccounts(accountNames.toTypedArray())
+ }
+
+ }
+
+
+ private lateinit var accountManager: AccountManager
+ private lateinit var accountAuthenticator: AccountAuthenticator
+
+ override fun onCreate() {
+ accountManager = AccountManager.get(this)
+ accountManager.addOnAccountsUpdatedListener(this, null, true)
+
+ accountAuthenticator = AccountAuthenticator(this)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ accountManager.removeOnAccountsUpdatedListener(this)
+ }
+
+ override fun onBind(intent: Intent?) =
+ accountAuthenticator.iBinder.takeIf { intent?.action == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT }
+
+
+ override fun onAccountsUpdated(accounts: Array?) {
+ thread {
+ cleanupAccounts(this)
+ }
+ }
+
+
+ private class AccountAuthenticator(
+ val context: Context
+ ) : AbstractAccountAuthenticator(context) {
+
+ override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array?, options: Bundle?): Bundle {
+ val intent = Intent(context, LoginActivity::class.java)
+ intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
+ intent.putExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, LoginActivity.ACCOUNT_PROVIDER_EELO)
+ val bundle = Bundle(1)
+ bundle.putParcelable(AccountManager.KEY_INTENT, intent)
+ return bundle
+ }
+
+ override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = null
+ override fun getAuthTokenLabel(p0: String?) = null
+ override fun confirmCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Bundle?) = null
+ override fun updateCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
+ override fun getAuthToken(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
+ override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array?) = null
+
+ }
+}
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e9cb400bfc5c13d6c9725563db23eda9a68978c0
--- /dev/null
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt
@@ -0,0 +1,117 @@
+/*
+ * 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
+ */
+
+import android.accounts.*
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.database.DatabaseUtils
+import android.os.Bundle
+import androidx.annotation.WorkerThread
+import at.bitfire.davdroid.db.AppDatabase
+
+import at.bitfire.davdroid.log.Logger
+import at.bitfire.davdroid.resource.LocalAddressBook
+import at.bitfire.davdroid.ui.setup.LoginActivity
+import foundation.e.accountmanager.R
+
+
+
+
+import java.util.*
+import java.util.logging.Level
+import kotlin.concurrent.thread
+
+/**
+ * Account authenticator for the Google account type.
+ *
+ * Gets started when a Google account is removed, too, so it also watches for account removals
+ * and contains the corresponding cleanup code.
+ */
+class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener {
+
+ companion object {
+
+ fun cleanupAccounts(context: Context) {
+ Logger.log.info("Cleaning up orphaned accounts")
+
+ val accountManager = AccountManager.get(context)
+ val accountNames = accountManager.getAccountsByType(context.getString(R.string.account_type))
+ .map { it.name }
+
+ // delete orphaned address book accounts
+ accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
+ .map { LocalAddressBook(context, it, null) }
+ .forEach {
+ try {
+ if (!accountNames.contains(it.mainAccount.name))
+ it.delete()
+ } catch(e: Exception) {
+ Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e)
+ }
+ }
+
+ // delete orphaned services in DB
+ val db = AppDatabase.getInstance(context)
+ val serviceDao = db.serviceDao()
+ if (accountNames.isEmpty())
+ serviceDao.deleteAll()
+ else
+ serviceDao.deleteExceptAccounts(accountNames.toTypedArray())
+ }
+
+ }
+
+ private lateinit var accountManager: AccountManager
+ private lateinit var accountAuthenticator: AccountAuthenticator
+
+ override fun onCreate() {
+ accountManager = AccountManager.get(this)
+ accountManager.addOnAccountsUpdatedListener(this, null, true)
+
+ accountAuthenticator = AccountAuthenticator(this)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ accountManager.removeOnAccountsUpdatedListener(this)
+ }
+
+ override fun onBind(intent: Intent?) =
+ accountAuthenticator.iBinder.takeIf { intent?.action == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT }
+
+
+ override fun onAccountsUpdated(accounts: Array?) {
+ thread {
+ cleanupAccounts(this)
+ }
+ }
+
+
+ private class AccountAuthenticator(
+ val context: Context
+ ) : AbstractAccountAuthenticator(context) {
+
+ override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array?, options: Bundle?): Bundle {
+ val intent = Intent(context, LoginActivity::class.java)
+ intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
+ intent.putExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, LoginActivity.ACCOUNT_PROVIDER_GOOGLE)
+ val bundle = Bundle(1)
+ bundle.putParcelable(AccountManager.KEY_INTENT, intent)
+ return bundle
+ }
+
+ override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = null
+ override fun getAuthTokenLabel(p0: String?) = null
+ override fun confirmCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Bundle?) = null
+ override fun updateCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
+ override fun getAuthToken(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
+ override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array?) = null
+
+ }
+}
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/DavResourceFinder.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/DavResourceFinder.kt
index 6074304790d3fac8c0ce1aadc4129b3760f4deec..9af3d3cc79039576809acd2f38fe6ce7aff0cb3a 100644
--- a/app/src/main/java/at/bitfire/davdroid/ui/setup/DavResourceFinder.kt
+++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/DavResourceFinder.kt
@@ -2,7 +2,6 @@
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui.setup
-
import android.content.Context
import foundation.e.dav4jvm.DavResource
import foundation.e.dav4jvm.Response
@@ -10,13 +9,11 @@ import foundation.e.dav4jvm.UrlUtils
import foundation.e.dav4jvm.exception.DavException
import foundation.e.dav4jvm.exception.HttpException
import foundation.e.dav4jvm.property.*
-
-import at.bitfire.davdroid.DavUtils
-import at.bitfire.davdroid.HttpClient
-import at.bitfire.davdroid.db.Collection
-import at.bitfire.davdroid.log.StringHandler
+import foundation.e.accountmanager.DavUtils
+import foundation.e.accountmanager.HttpClient
+import foundation.e.accountmanager.log.StringHandler
+import foundation.e.accountmanager.model.Collection
import okhttp3.HttpUrl
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.apache.commons.lang3.builder.ReflectionToStringBuilder
import org.xbill.DNS.Lookup
import org.xbill.DNS.Type
@@ -30,8 +27,8 @@ import java.util.logging.Level
import java.util.logging.Logger
class DavResourceFinder(
- val context: Context,
- private val loginModel: LoginModel
+ val context: Context,
+ private val loginModel: LoginModel
): AutoCloseable {
enum class Service(val wellKnownName: String) {
@@ -41,22 +38,17 @@ class DavResourceFinder(
override fun toString() = wellKnownName
}
- val log: Logger = Logger.getLogger("davx5.DavResourceFinder")
+ val log = Logger.getLogger("davdroid.DavResourceFinder")
private val logBuffer = StringHandler()
init {
log.level = Level.FINEST
log.addHandler(logBuffer)
}
- var encountered401 = false
-
- private val httpClient: HttpClient = HttpClient.Builder(context, logger = log).let {
- loginModel.credentials?.let { credentials ->
- it.addAuthentication(null, credentials)
- }
- it.setForeground(true)
- it.build()
- }
+ private val httpClient: HttpClient = HttpClient.Builder(context, logger = log)
+ .addAuthentication(null, loginModel.credentials!!)
+ .setForeground(true)
+ .build()
override fun close() {
httpClient.close()
@@ -76,14 +68,14 @@ class DavResourceFinder(
cardDavConfig = findInitialConfiguration(Service.CARDDAV)
} catch (e: Exception) {
log.log(Level.INFO, "CardDAV service detection failed", e)
- processException(e)
+ rethrowIfInterrupted(e)
}
try {
calDavConfig = findInitialConfiguration(Service.CALDAV)
} catch (e: Exception) {
log.log(Level.INFO, "CalDAV service detection failed", e)
- processException(e)
+ rethrowIfInterrupted(e)
}
} catch(e: Exception) {
// we have been interrupted; reset results so that an error message will be shown
@@ -92,9 +84,8 @@ class DavResourceFinder(
}
return Configuration(
- cardDavConfig, calDavConfig,
- encountered401,
- logBuffer.toString()
+ cardDavConfig, calDavConfig,
+ logBuffer.toString()
)
}
@@ -110,11 +101,11 @@ class DavResourceFinder(
log.info("Finding initial ${service.wellKnownName} service configuration")
if (baseURI.scheme.equals("http", true) || baseURI.scheme.equals("https", true)) {
- baseURI.toHttpUrlOrNull()?.let { baseURL ->
+ HttpUrl.get(baseURI)?.let { baseURL ->
// remember domain for service discovery
// try service discovery only for https:// URLs because only secure service discovery is implemented
- if (baseURL.scheme.equals("https", true))
- discoveryFQDN = baseURL.host
+ if (baseURL.scheme().equals("https", true))
+ discoveryFQDN = baseURL.host()
checkUserGivenURL(baseURL, service, config)
@@ -123,7 +114,7 @@ class DavResourceFinder(
config.principal = getCurrentUserPrincipal(baseURL.resolve("/.well-known/" + service.wellKnownName)!!, service)
} catch(e: Exception) {
log.log(Level.FINE, "Well-known URL detection failed", e)
- processException(e)
+ rethrowIfInterrupted(e)
}
}
} else if (baseURI.scheme.equals("mailto", true)) {
@@ -142,14 +133,28 @@ class DavResourceFinder(
config.principal = discoverPrincipalUrl(it, service)
} catch(e: Exception) {
log.log(Level.FINE, "$service service discovery failed", e)
- processException(e)
+ rethrowIfInterrupted(e)
}
}
- // detect email address
- if (service == Service.CALDAV)
- config.principal?.let {
- config.emails.addAll(queryEmailAddress(it))
+ if (config.principal != null && service == Service.CALDAV)
+ // query email address (CalDAV scheduling: calendar-user-address-set)
+ try {
+ DavResource(httpClient.okHttpClient, config.principal!!, null, log).propfind(0, CalendarUserAddressSet.NAME) { response, _ ->
+ response[CalendarUserAddressSet::class.java]?.let { addressSet ->
+ for (href in addressSet.hrefs)
+ try {
+ val uri = URI(href)
+ if (uri.scheme.equals("mailto", true))
+ config.email = uri.schemeSpecificPart
+ } catch(e: URISyntaxException) {
+ log.log(Level.WARNING, "Couldn't parse user address", e)
+ }
+ }
+ }
+ } catch(e: Exception) {
+ log.log(Level.WARNING, "Couldn't query user email address", e)
+ rethrowIfInterrupted(e)
}
// return config or null if config doesn't contain useful information
@@ -163,23 +168,23 @@ class DavResourceFinder(
private fun checkUserGivenURL(baseURL: HttpUrl, service: Service, config: Configuration.ServiceInfo) {
log.info("Checking user-given URL: $baseURL")
- val davBase = DavResource(httpClient.okHttpClient, baseURL, log)
+ val davBase = DavResource(httpClient.okHttpClient, baseURL, loginModel.credentials!!.accessToken, log)
try {
when (service) {
Service.CARDDAV -> {
davBase.propfind(0,
- ResourceType.NAME, DisplayName.NAME, AddressbookDescription.NAME,
- AddressbookHomeSet.NAME,
- CurrentUserPrincipal.NAME
+ ResourceType.NAME, DisplayName.NAME, AddressbookDescription.NAME,
+ AddressbookHomeSet.NAME,
+ CurrentUserPrincipal.NAME
) { response, _ ->
scanCardDavResponse(response, config)
}
}
Service.CALDAV -> {
davBase.propfind(0,
- ResourceType.NAME, DisplayName.NAME, CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME, CurrentUserPrivilegeSet.NAME, SupportedCalendarComponentSet.NAME,
- CalendarHomeSet.NAME,
- CurrentUserPrincipal.NAME
+ ResourceType.NAME, DisplayName.NAME, CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME, CurrentUserPrivilegeSet.NAME, SupportedCalendarComponentSet.NAME,
+ CalendarHomeSet.NAME,
+ CurrentUserPrincipal.NAME
) { response, _ ->
scanCalDavResponse(response, config)
}
@@ -187,37 +192,10 @@ class DavResourceFinder(
}
} catch(e: Exception) {
log.log(Level.FINE, "PROPFIND/OPTIONS on user-given URL failed", e)
- processException(e)
+ rethrowIfInterrupted(e)
}
}
- /**
- * Queries a user's email address using CalDAV scheduling: calendar-user-address-set.
- * @param principal principal URL of the user
- * @return list of found email addresses (empty if none)
- */
- fun queryEmailAddress(principal: HttpUrl): List {
- val mailboxes = LinkedList()
- try {
- DavResource(httpClient.okHttpClient, principal, log).propfind(0, CalendarUserAddressSet.NAME) { response, _ ->
- response[CalendarUserAddressSet::class.java]?.let { addressSet ->
- for (href in addressSet.hrefs)
- try {
- val uri = URI(href)
- if (uri.scheme.equals("mailto", true))
- mailboxes.add(uri.schemeSpecificPart)
- } catch(e: URISyntaxException) {
- log.log(Level.WARNING, "Couldn't parse user address", e)
- }
- }
- }
- } catch(e: Exception) {
- log.log(Level.WARNING, "Couldn't query user email address", e)
- processException(e)
- }
- return mailboxes
- }
-
/**
* If [dav] references an address book, an address book home set, and/or a princiapl,
* it will added to, config.collections, config.homesets and/or config.principal.
@@ -279,7 +257,7 @@ class DavResourceFinder(
principal = dav.requestedUrl.resolve(it)
}
- // Is it a calendar and/or principal?
+ // Is it a calendar book and/or principal?
dav[ResourceType::class.java]?.let {
if (it.types.contains(ResourceType.CALENDAR)) {
val info = Collection.fromDavResponse(dav)!!
@@ -296,7 +274,7 @@ class DavResourceFinder(
for (href in homeSet.hrefs) {
dav.requestedUrl.resolve(href)?.let {
val location = UrlUtils.withTrailingSlash(it)
- log.info("Found calendar home-set at $location")
+ log.info("Found calendar book home-set at $location")
config.homeSets += location
}
}
@@ -313,7 +291,7 @@ class DavResourceFinder(
fun providesService(url: HttpUrl, service: Service): Boolean {
var provided = false
try {
- DavResource(httpClient.okHttpClient, url, log).options { capabilities, _ ->
+ DavResource(httpClient.okHttpClient, url, loginModel.credentials!!.accessToken, log).options { capabilities, _ ->
if ((service == Service.CARDDAV && capabilities.contains("addressbook")) ||
(service == Service.CALDAV && capabilities.contains("calendar-access")))
provided = true
@@ -335,7 +313,7 @@ class DavResourceFinder(
* @return principal URL, or null if none found
*/
@Throws(IOException::class, HttpException::class, DavException::class)
- fun discoverPrincipalUrl(domain: String, service: Service): HttpUrl? {
+ private fun discoverPrincipalUrl(domain: String, service: Service): HttpUrl? {
val scheme: String
val fqdn: String
var port = 443
@@ -343,11 +321,9 @@ class DavResourceFinder(
val query = "_${service.wellKnownName}s._tcp.$domain"
log.fine("Looking up SRV records for $query")
-
val srvLookup = Lookup(query, Type.SRV)
DavUtils.prepareLookup(context, srvLookup)
- val srv = DavUtils.selectSRVRecord(srvLookup.run().orEmpty())
-
+ val srv = DavUtils.selectSRVRecord(srvLookup.run())
if (srv != null) {
// choose SRV record to use (query may return multiple SRV records)
scheme = "https"
@@ -375,10 +351,10 @@ class DavResourceFinder(
for (path in paths)
try {
val initialContextPath = HttpUrl.Builder()
- .scheme(scheme)
- .host(fqdn).port(port)
- .encodedPath(path)
- .build()
+ .scheme(scheme)
+ .host(fqdn).port(port)
+ .encodedPath(path)
+ .build()
log.info("Trying to determine principal from initial context path=$initialContextPath")
val principal = getCurrentUserPrincipal(initialContextPath, service)
@@ -386,7 +362,7 @@ class DavResourceFinder(
principal?.let { return it }
} catch(e: Exception) {
log.log(Level.WARNING, "No resource found", e)
- processException(e)
+ rethrowIfInterrupted(e)
}
return null
}
@@ -401,7 +377,7 @@ class DavResourceFinder(
@Throws(IOException::class, HttpException::class, DavException::class)
fun getCurrentUserPrincipal(url: HttpUrl, service: Service?): HttpUrl? {
var principal: HttpUrl? = null
- DavResource(httpClient.okHttpClient, url, log).propfind(0, CurrentUserPrincipal.NAME) { response, _ ->
+ DavResource(httpClient.okHttpClient, url, loginModel.credentials!!.accessToken, log).propfind(0, CurrentUserPrincipal.NAME) { response, _ ->
response[CurrentUserPrincipal::class.java]?.href?.let { href ->
response.requestedUrl.resolve(href)?.let {
log.info("Found current-user-principal: $it")
@@ -418,15 +394,11 @@ class DavResourceFinder(
}
/**
- * Processes a thrown exception likes this:
- *
- * - If the Exception is an [UnauthorizedException] (HTTP 401), [encountered401] is set to *true*.
- * - Re-throws the exception if it signals that the current thread was interrupted to stop the current operation.
+ * Re-throws the exception if it signals that the current thread was interrupted
+ * to stop the current operation.
*/
- private fun processException(e: Exception) {
- if (e is UnauthorizedException)
- encountered401 = true
- else if ((e is InterruptedIOException && e !is SocketTimeoutException) || e is InterruptedException)
+ private fun rethrowIfInterrupted(e: Exception) {
+ if ((e is InterruptedIOException && e !is SocketTimeoutException) || e is InterruptedException)
throw e
}
@@ -434,19 +406,18 @@ class DavResourceFinder(
// data classes
class Configuration(
- val cardDAV: ServiceInfo?,
- val calDAV: ServiceInfo?,
+ val cardDAV: ServiceInfo?,
+ val calDAV: ServiceInfo?,
- val encountered401: Boolean,
- val logs: String
+ val logs: String
) {
data class ServiceInfo(
- var principal: HttpUrl? = null,
- val homeSets: MutableSet = HashSet(),
- val collections: MutableMap = HashMap(),
+ var principal: HttpUrl? = null,
+ val homeSets: MutableSet = HashSet(),
+ val collections: MutableMap = HashMap(),
- val emails: MutableList = LinkedList()
+ var email: String? = null
)
override fun toString(): String {
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a082f3fad55add941df30a8ef1f5f43db39d6861
--- /dev/null
+++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt
@@ -0,0 +1,359 @@
+package at.bitfire.davdroid.ui.setup
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.net.MailTo
+import android.os.*
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import foundation.e.dav4jvm.Constants
+
+import foundation.e.accountmanager.authorization.IdentityProvider
+
+import foundation.e.accountmanager.R
+import androidx.lifecycle.ViewModelProviders
+import net.openid.appauth.*
+import org.json.JSONException
+import java.io.BufferedReader
+import java.io.IOException
+import java.io.InputStream
+import java.io.InputStreamReader
+import java.net.*
+import org.json.JSONObject
+import java.util.HashMap
+
+import java.util.logging.Level
+import foundation.e.accountmanager.databinding.FragmentGoogleAuthenticatorBinding
+import foundation.e.accountmanager.model.Credentials
+import kotlinx.android.synthetic.main.fragment_google_authenticator.*
+
+class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenResponseCallback {
+
+ private lateinit var model: GoogleAuthenticatorModel
+ private lateinit var loginModel: LoginModel
+
+ private val extraAuthServiceDiscovery = "authServiceDiscovery"
+ private val extraClientSecret = "clientSecret"
+
+ private var authState: AuthState? = null
+ private var authorizationService: AuthorizationService? = null
+
+ private val bufferSize = 1024
+ private var userInfoJson: JSONObject? = null
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?): View? {
+
+ model = ViewModelProviders.of(this).get(GoogleAuthenticatorModel::class.java)
+ loginModel = ViewModelProviders.of(requireActivity()).get(LoginModel::class.java)
+
+ // Initialise the authorization service
+ authorizationService = AuthorizationService(context!!)
+
+ val v = FragmentGoogleAuthenticatorBinding.inflate(inflater, container, false)
+ v.lifecycleOwner = this
+ v.model = model
+
+ activity?.intent?.let {
+ model.initialize(it)
+
+ if (!with(it) { getBooleanExtra(LoginActivity.ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE, false) }) {
+ // Get all the account providers
+ val providers = IdentityProvider.getEnabledProviders(context)
+
+ // Iterate over the account providers
+ for (idp in providers) {
+ val retrieveCallback = AuthorizationServiceConfiguration.RetrieveConfigurationCallback { serviceConfiguration, ex ->
+ if (ex == null && serviceConfiguration != null) {
+ makeAuthRequest(serviceConfiguration, idp)
+ }
+ else {
+ // TODO Handle error
+ }
+ }
+
+ if (idp.name == getString(R.string.google_name)) {
+ // Get configurations for the Google account provider
+ idp.retrieveConfig(context, retrieveCallback)
+ }
+ }
+ }
+ else {
+ if (authState == null) {
+ val response = AuthorizationResponse.fromIntent(activity!!.intent)
+ val ex = AuthorizationException.fromIntent(activity!!.intent)
+ authState = AuthState(response, ex)
+
+ if (response != null) {
+ exchangeAuthorizationCode(response)
+ }
+ else {
+ // TODO Handle error
+ }
+ }
+ }
+ }
+
+ return v.root
+ }
+
+ private fun makeAuthRequest(
+ serviceConfig: AuthorizationServiceConfiguration,
+ idp: IdentityProvider) {
+
+ val authRequest = AuthorizationRequest.Builder(
+ serviceConfig,
+ idp.clientId,
+ ResponseTypeValues.CODE,
+ idp.redirectUri)
+ .setScope(idp.scope)
+ .build()
+
+ authorizationService?.performAuthorizationRequest(
+ authRequest,
+ createPostAuthorizationIntent(
+ context!!,
+ authRequest,
+ serviceConfig.discoveryDoc,
+ idp.clientSecret),
+ authorizationService?.createCustomTabsIntentBuilder()!!
+ .build())
+
+ activity?.finish()
+ }
+
+ private fun createPostAuthorizationIntent(
+ context: Context,
+ request: AuthorizationRequest,
+ discoveryDoc: AuthorizationServiceDiscovery?,
+ clientSecret: String?): PendingIntent {
+ val intent = Intent(context, LoginActivity::class.java)
+
+ if (discoveryDoc != null) {
+ intent.putExtra(extraAuthServiceDiscovery, discoveryDoc.docJson.toString())
+ }
+
+ if (clientSecret != null) {
+ intent.putExtra(extraClientSecret, clientSecret)
+ }
+
+ intent.putExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, LoginActivity.ACCOUNT_PROVIDER_GOOGLE)
+ intent.putExtra(LoginActivity.ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE, true)
+
+ return PendingIntent.getActivity(context, request.hashCode(), intent, 0)
+ }
+
+ private fun exchangeAuthorizationCode(authorizationResponse: AuthorizationResponse) {
+ val additionalParams = HashMap()
+ if (getClientSecretFromIntent(activity!!.intent) != null) {
+ additionalParams["client_secret"] = getClientSecretFromIntent(activity!!.intent)
+ }
+ performTokenRequest(authorizationResponse.createTokenExchangeRequest(additionalParams))
+ }
+
+ private fun getClientSecretFromIntent(intent: Intent): String? {
+ return if (!intent.hasExtra(extraClientSecret)) {
+ null
+ }
+ else intent.getStringExtra(extraClientSecret)
+ }
+
+
+ private fun performTokenRequest(request: TokenRequest) {
+ authorizationService?.performTokenRequest(
+ request, this)
+ }
+
+ override fun onTokenRequestCompleted(response: TokenResponse?, ex: AuthorizationException?) {
+ authState?.update(response, ex)
+ progress_bar.visibility = View.GONE
+ auth_token_success_text_view.visibility = View.VISIBLE
+
+ getAccountInfo()
+ }
+
+ private fun getAccountInfo() {
+ val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent)
+
+ if (!authState!!.isAuthorized
+ || discoveryDoc == null
+ || discoveryDoc.userinfoEndpoint == null) {
+ //TODO Error occurred
+ }
+ else {
+ object : AsyncTask() {
+ override fun doInBackground(vararg params: Void): Void? {
+ fetchUserInfo()
+ return null
+ }
+ }.execute()
+ }
+ }
+
+ private fun getDiscoveryDocFromIntent(intent: Intent): AuthorizationServiceDiscovery? {
+ if (!intent.hasExtra(extraAuthServiceDiscovery)) {
+ return null
+ }
+ val discoveryJson = intent.getStringExtra(extraAuthServiceDiscovery)
+ try {
+ return AuthorizationServiceDiscovery(JSONObject(discoveryJson))
+ }
+ catch (ex: JSONException) {
+ throw IllegalStateException("Malformed JSON in discovery doc")
+ }
+ catch (ex: AuthorizationServiceDiscovery.MissingArgumentException) {
+ throw IllegalStateException("Malformed JSON in discovery doc")
+ }
+
+ }
+
+ private fun fetchUserInfo() {
+ if (authState!!.authorizationServiceConfiguration == null) {
+ // TODO Handle error due to unavailable service configuration
+ return
+ }
+
+ authState!!.performActionWithFreshTokens(authorizationService!!, AuthState.AuthStateAction { accessToken, _, ex ->
+ if (ex != null) {
+ // TODO An exception occurred, handle error
+ return@AuthStateAction
+ }
+
+ val discoveryDoc = getDiscoveryDocFromIntent(activity!!.intent)
+ ?: throw IllegalStateException("no available discovery doc")
+
+ val userInfoEndpoint: URL
+ try {
+ userInfoEndpoint = URL(discoveryDoc.userinfoEndpoint!!.toString())
+ }
+ catch (urlEx: MalformedURLException) {
+ // TODO Handle error due to malformed URL
+ return@AuthStateAction
+ }
+
+ var userInfoResponse: InputStream? = null
+ try {
+ val conn = userInfoEndpoint.openConnection() as HttpURLConnection
+ conn.setRequestProperty("Authorization", "Bearer " + accessToken!!)
+ conn.instanceFollowRedirects = false
+ userInfoResponse = conn.inputStream
+ val response = readStream(userInfoResponse)
+ updateUserInfo(JSONObject(response))
+ }
+ catch (ioEx: IOException) {
+ // TODO Handle network error
+ }
+ catch (jsonEx: JSONException) {
+ // TODO Handle JSON parse error
+ }
+ finally {
+ if (userInfoResponse != null) {
+ try {
+ userInfoResponse.close()
+ }
+ catch (ioEx: IOException) {
+ // TODO Handle network exception while closing response stream
+ }
+
+ }
+ }
+ })
+ }
+
+ @Throws(IOException::class)
+ private fun readStream(stream: InputStream?): String {
+ val br = BufferedReader(InputStreamReader(stream!!))
+ val buffer = CharArray(bufferSize)
+ val sb = StringBuilder()
+ var readCount = br.read(buffer)
+ while (readCount != -1) {
+ sb.append(buffer, 0, readCount)
+ readCount = br.read(buffer)
+ }
+ return sb.toString()
+ }
+
+ private fun updateUserInfo(jsonObject: JSONObject) {
+ Handler(Looper.getMainLooper()).post {
+ userInfoJson = jsonObject
+ onAccountInfoGotten()
+ }
+ }
+
+ private fun onAccountInfoGotten() {
+ if (userInfoJson != null) {
+ try {
+
+ var emailAddress = ""
+ if (userInfoJson!!.has("email")) {
+ emailAddress = userInfoJson!!.getString("email")
+ }
+
+ if (validate(emailAddress, authState!!.accessToken!!, authState!!.refreshToken!!))
+ requireFragmentManager().beginTransaction()
+ .replace(android.R.id.content, DetectConfigurationFragment(), null)
+ .addToBackStack(null)
+ .commit()
+
+ /*account.setName(name)
+ account.setEmailAddress(emailAddress)
+ account.setAuthToken(authState!!.accessToken)
+ account.setRefreshToken(authState!!.refreshToken)*/
+ }
+ catch (ex: JSONException) {
+ // TODO Handle JSON parse error
+ }
+
+ }
+ else {
+ //TODO Handle error
+ }
+
+ }
+
+ private fun validate(emailAddress: String, accessToken: String, refreshToken: String): Boolean {
+ var valid = false
+
+ fun validateUrl() {
+ model.baseUrlError.value = null
+ try {
+ val uri = URI("https://www.google.com/calendar/dav/$emailAddress/events")
+ if (uri.scheme.equals("http", true) || uri.scheme.equals("https", true)) {
+ valid = true
+ loginModel.baseURI = uri
+ } else
+ model.baseUrlError.value = getString(R.string.login_url_must_be_http_or_https)
+ } catch (e: Exception) {
+ model.baseUrlError.value = e.localizedMessage
+ }
+ }
+
+ when {
+
+ model.loginWithUrlAndTokens.value == true -> {
+ validateUrl()
+
+ model.usernameError.value = null
+
+ if (loginModel.baseURI != null) {
+ valid = true
+ loginModel.credentials = Credentials(emailAddress, null, accessToken, refreshToken, null)
+ }
+ }
+
+ }
+
+ return valid
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ authorizationService?.dispose()
+ }
+
+
+}
+
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorModel.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6adf20ff60db64985cfb2c5069e4c69fc74b2bda
--- /dev/null
+++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorModel.kt
@@ -0,0 +1,49 @@
+package at.bitfire.davdroid.ui.setup
+
+import android.content.Intent
+import android.net.Uri
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+class GoogleAuthenticatorModel: ViewModel() {
+
+ private var initialized = false
+
+ val loginWithUrlAndTokens = MutableLiveData()
+
+ val baseUrl = MutableLiveData()
+ val baseUrlError = MutableLiveData()
+
+ val emailAddress = MutableLiveData()
+ val emailAddressError = MutableLiveData()
+
+ val username = MutableLiveData()
+ val usernameError = MutableLiveData()
+
+ val password = MutableLiveData()
+ val passwordError = MutableLiveData()
+
+ val certificateAlias = MutableLiveData()
+ val certificateAliasError = MutableLiveData()
+
+ init {
+ loginWithUrlAndTokens.value = true
+ }
+
+ fun initialize(intent: Intent) {
+ if (initialized)
+ return
+
+ // we've got initial login data
+ val givenUrl = intent.getStringExtra(LoginActivity.EXTRA_URL)
+ val givenUsername = intent.getStringExtra(LoginActivity.EXTRA_USERNAME)
+ val givenPassword = intent.getStringExtra(LoginActivity.EXTRA_PASSWORD)
+
+ baseUrl.value = givenUrl
+
+ password.value = givenPassword
+
+ initialized = true
+ }
+
+}
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt
index e03c3989969426c7bbf9507d9dd63f4946c53fde..de06c142b5190f639103bfbfd681cae7d3fb31cf 100644
--- a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt
+++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginActivity.kt
@@ -41,7 +41,10 @@ class LoginActivity: AppCompatActivity() {
* When set, the password field will be set to this value.
*/
const val EXTRA_PASSWORD = "password"
-
+ const val SETUP_ACCOUNT_PROVIDER_TYPE = "setup_account_provider_type"
+ const val ACCOUNT_PROVIDER_EELO = "eelo"
+ const val ACCOUNT_PROVIDER_GOOGLE = "google"
+ const val ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE = "google_auth_complete"
}
@Inject
@@ -63,9 +66,27 @@ class LoginActivity: AppCompatActivity() {
}
if (fragment != null) {
- supportFragmentManager.beginTransaction()
- .replace(android.R.id.content, fragment)
- .commit()
+ when (intent.getStringExtra(SETUP_ACCOUNT_PROVIDER_TYPE)) {
+ ACCOUNT_PROVIDER_EELO -> {
+ // Set the eelo Contacts and Calendar service URL
+ intent.putExtra(EXTRA_URL, "https://drive.eelo.io")
+ // first call, add first login fragment
+ supportFragmentManager.beginTransaction()
+ .replace(android.R.id.content, fragment)
+ .commit()
+ }
+ ACCOUNT_PROVIDER_GOOGLE -> {
+ supportFragmentManager.beginTransaction()
+ .replace(android.R.id.content, GoogleAuthenticatorFragment())
+ .commit()
+ }
+ else ->
+ // first call, add first login fragment
+ supportFragmentManager.beginTransaction()
+ .replace(android.R.id.content, fragment)
+ .commit()
+ }
+
} else
Logger.log.severe("Couldn't create LoginFragment")
}
diff --git a/app/src/main/res/layout/fragment_google_authenticator.xml b/app/src/main/res/layout/fragment_google_authenticator.xml
new file mode 100644
index 0000000000000000000000000000000000000000..317a6952ac1c15a9b7700e41e437b4fbcfefa77f
--- /dev/null
+++ b/app/src/main/res/layout/fragment_google_authenticator.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/email_providers_auth_config.xml b/app/src/main/res/values/email_providers_auth_config.xml
new file mode 100644
index 0000000000000000000000000000000000000000..78fff1469345fce50a2f2ca1cd2d804dc93d314d
--- /dev/null
+++ b/app/src/main/res/values/email_providers_auth_config.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ Google
+
+ 100496780587-pbiu5eudcjm6cge2phduc6mt8mgbsmsr.apps.googleusercontent.com
+
+ https://accounts.google.com/.well-known/openid-configuration
+
+ openid profile email https://www.googleapis.com/auth/carddav https://www.googleapis.com/auth/calendar
+
+
+
+ com.googleusercontent.apps.100496780587-pbiu5eudcjm6cge2phduc6mt8mgbsmsr:/oauth2redirect
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e37ce368c7bde6c7e2ff508833915009be9717e6..76bd7dd904bedb816d02e2d05a6c93015c716e5b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4,6 +4,10 @@
DAVx⁵
https://www.davx5.com/
davx5app
+ Google
+ bitfire.at.davdroid.google
+ eelo
+ bitfire.at.davdroid.eelo
Account does not exist (anymore)
bitfire.at.davdroid
diff --git a/app/src/main/res/xml/eelo_account_authenticator.xml b/app/src/main/res/xml/eelo_account_authenticator.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0f39608e6e3715d0281bdb296c153cbcda15a23c
--- /dev/null
+++ b/app/src/main/res/xml/eelo_account_authenticator.xml
@@ -0,0 +1,14 @@
+
+
+
diff --git a/app/src/main/res/xml/google_account_authenticator.xml b/app/src/main/res/xml/google_account_authenticator.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1d053be69b9f42ea879a6db800d47bafcff9ca6c
--- /dev/null
+++ b/app/src/main/res/xml/google_account_authenticator.xml
@@ -0,0 +1,14 @@
+
+
+
diff --git a/gradle.properties b/gradle.properties
index c6bf06227a4bf6557135f618573cf62c3cf6fad4..52bbc61602c7ef486b5feb97faed45fd854b79a7 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -16,3 +16,4 @@ android.useAndroidX=true
org.gradle.daemon=true
org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1536M"
org.gradle.parallel=true
+android.enableJetifier=true
\ No newline at end of file