diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 00cb60995ce031ddeee5265cb49b2811876486e0..8fc973b0fe921a52fb5dce12ab3aaaac1ad7776e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -6,6 +6,9 @@ stages:
before_script:
- echo email.key=$PEPPER >> local.properties
+ - echo GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID >> local.properties
+ - echo GOOGLE_REDIRECT_URI=$GOOGLE_REDIRECT_URI >> local.properties
+ - echo YAHOO_CLIENT_ID=$YAHOO_CLIENT_ID >> local.properties
- export GRADLE_USER_HOME=$(pwd)/.gradle
- chmod +x ./gradlew
diff --git a/app/build.gradle b/app/build.gradle
index 72b5a40fe6ce7b06976db40c3335308b39a8c21f..1e697ceeb310b3522e088b46e92d7fd32127f3c9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -86,7 +86,7 @@ android {
shrinkResources true
- buildConfigField "String", "EMAIL_KEY", "\"${retrieveEmailKey()}\""
+ buildConfigField "String", "EMAIL_KEY", "\"${retrieveKey("email.key")}\""
}
}
@@ -101,8 +101,14 @@ android {
}
defaultConfig {
+ buildConfigField "String", "GOOGLE_CLIENT_ID", "\"${retrieveKey("GOOGLE_CLIENT_ID")}\""
+ buildConfigField "String", "GOOGLE_REDIRECT_URI", "\"${retrieveKey("GOOGLE_REDIRECT_URI")}\""
+
+ buildConfigField "String", "YAHOO_CLIENT_ID", "\"${retrieveKey("YAHOO_CLIENT_ID")}\""
+
manifestPlaceholders = [
- 'appAuthRedirectScheme': 'net.openid.appauthdemo'
+ 'appAuthRedirectScheme': applicationId,
+ "googleAuthRedirectScheme": retrieveKey("GOOGLE_REDIRECT_URI")
]
}
}
@@ -195,13 +201,13 @@ dependencies {
testImplementation "com.squareup.okhttp3:mockwebserver:${versions.okhttp}"
}
-def retrieveEmailKey() {
+def retrieveKey(String keyName) {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
- String value = properties.getProperty("email.key")
+ String value = properties.getProperty(keyName)
if (value == null) {
- throw new GradleException("email.key property not found in local.properties file")
+ throw new GradleException(keyName + " property not found in local.properties file")
}
return value
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d3f5abf34bba2de03a57b53963f79bff04ce36d2..1fb492915e226078737541c4441ef46a9041215f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -567,8 +567,8 @@
-
-
+
+
diff --git a/app/src/main/java/at/bitfire/davdroid/OpenIdUtils.kt b/app/src/main/java/at/bitfire/davdroid/OpenIdUtils.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f2f9e0de9d4b2e215583a61b1b445b78ce1c4b47
--- /dev/null
+++ b/app/src/main/java/at/bitfire/davdroid/OpenIdUtils.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright MURENA SAS 2023
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package at.bitfire.davdroid
+
+import net.openid.appauth.ClientAuthentication
+import net.openid.appauth.ClientSecretBasic
+import net.openid.appauth.NoClientAuthentication
+
+object OpenIdUtils {
+
+ fun getClientAuthentication(secret: String?): ClientAuthentication {
+ if (secret == null) {
+ return NoClientAuthentication.INSTANCE
+ }
+
+ return ClientSecretBasic(secret)
+ }
+}
diff --git a/app/src/main/java/at/bitfire/davdroid/authorization/IdentityProvider.java b/app/src/main/java/at/bitfire/davdroid/authorization/IdentityProvider.java
index 7277d3bdfca1ad5a021cf04b23502a6ee72bed70..b936ecdf96026862932ba892de2b65049e7f6a7d 100644
--- a/app/src/main/java/at/bitfire/davdroid/authorization/IdentityProvider.java
+++ b/app/src/main/java/at/bitfire/davdroid/authorization/IdentityProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright ECORP SAS 2022
+ * Copyright MURENA SAS 2022, 2023
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -16,200 +16,108 @@
package at.bitfire.davdroid.authorization;
-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 net.openid.appauth.AuthorizationServiceConfiguration;
import net.openid.appauth.AuthorizationServiceConfiguration.RetrieveConfigurationCallback;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
+import java.util.Objects;
-import at.bitfire.davdroid.R;
+import at.bitfire.davdroid.BuildConfig;
/**
* 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;
+ "https://accounts.google.com/.well-known/openid-configuration",
+ null,
+ null,
+ BuildConfig.GOOGLE_CLIENT_ID,
+ null,
+ BuildConfig.GOOGLE_REDIRECT_URI + ":/oauth2redirect",
+ "openid profile email https://www.googleapis.com/auth/carddav https://www.googleapis.com/auth/calendar https://mail.google.com/",
+ null
+ );
- @StringRes
- private final int mDiscoveryEndpointRes;
-
- @StringRes
- private final int mAuthEndpointRes;
-
- @StringRes
- private final int mTokenEndpointRes;
+ @Nullable
+ private final Uri mDiscoveryEndpoint;
- @StringRes
- private final int mClientIdRes;
+ @Nullable
+ private final Uri mAuthEndpoint;
- @StringRes
- private final int mClientSecretRes;
+ @Nullable
+ private final Uri mTokenEndpoint;
+ @NonNull
+ private final String mClientId;
- @StringRes
- private final int mRedirectUriRes;
+ @Nullable
+ private final String mClientSecret;
+ @NonNull
+ private final Uri mRedirectUri;
- @StringRes
- private final int mScopeRes;
+ @Nullable
+ private final String mScope;
- 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;
+ @Nullable
+ private final String mUserInfoEndpoint;
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)) {
+ @Nullable String discoveryEndpoint,
+ @Nullable String authEndpoint,
+ @Nullable String tokenEndpoint,
+ @NonNull String clientId,
+ @Nullable String clientSecret,
+ @NonNull String redirectUri,
+ @Nullable String scope,
+ @Nullable String userInfoEndpoint) {
+ if (discoveryEndpoint == null &&
+ (authEndpoint == null || tokenEndpoint == null)) {
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;
+ this.mDiscoveryEndpoint = retrieveUri(discoveryEndpoint);
+ this.mAuthEndpoint = retrieveUri(authEndpoint);
+ this.mTokenEndpoint = retrieveUri(tokenEndpoint);
+ this.mClientId = clientId;
+ this.mClientSecret = clientSecret;
+ this.mRedirectUri = Objects.requireNonNull(retrieveUri(redirectUri));
+ this.mScope = scope;
+ this.mUserInfoEndpoint = userInfoEndpoint;
}
@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) {
+ @Nullable
+ public String getUserInfoEndpoint() {
+ return mUserInfoEndpoint;
+ }
+
+ public void retrieveConfig(RetrieveConfigurationCallback callback) {
+ if (mDiscoveryEndpoint != null) {
AuthorizationServiceConfiguration.fetchFromUrl(mDiscoveryEndpoint, callback);
} else {
AuthorizationServiceConfiguration config =
@@ -218,19 +126,11 @@ public class IdentityProvider {
}
}
- 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");
+ @Nullable
+ private Uri retrieveUri(@Nullable String value) {
+ if (value == null) {
+ return null;
}
- return value;
- }
-
- private static Uri getUriResource(Resources res, @StringRes int resId, String resName) {
- return Uri.parse(res.getString(resId));
+ return Uri.parse(value);
}
}
-
diff --git a/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt b/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt
index 237651c171b2a75ff0fd3bccb84a01a7d29e4ebc..51478c40a54c434d195fdca872e02c3282219a26 100644
--- a/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt
+++ b/app/src/main/java/at/bitfire/davdroid/db/AppDatabase.kt
@@ -18,6 +18,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
import at.bitfire.davdroid.R
import at.bitfire.davdroid.TextTable
import at.bitfire.davdroid.log.Logger
+import at.bitfire.davdroid.syncadapter.AccountUtils
import at.bitfire.davdroid.ui.AccountsActivity
import at.bitfire.davdroid.ui.NotificationUtils
import dagger.Module
@@ -67,7 +68,7 @@ abstract class AppDatabase: RoomDatabase() {
// remove all accounts because they're unfortunately useless without database
val am = AccountManager.get(context)
- for (account in am.getAccountsByType(context.getString(R.string.account_type)))
+ for (account in AccountUtils.getMainAccounts(context))
am.removeAccount(account, null, null)
}
})
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 45af127502199a0b2b481a617f134c592f3e8c61..6c9b34ae80c9f865c3d74ddcae11c8d8a20495e4 100644
--- a/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt
+++ b/app/src/main/java/at/bitfire/davdroid/db/Credentials.kt
@@ -8,11 +8,12 @@ import net.openid.appauth.AuthState
import java.net.URI
data class Credentials(
- val userName: String? = null,
- val password: String? = null,
- val authState: AuthState? = null,
- val certificateAlias: String? = null,
- val serverUri: URI? = null
+ val userName: String? = null,
+ val password: String? = null,
+ val authState: AuthState? = null,
+ val certificateAlias: String? = null,
+ val serverUri: URI? = null,
+ val clientSecret: String? = null
) {
override fun toString(): String {
@@ -20,4 +21,4 @@ data class Credentials(
return "Credentials(userName=$userName, password=$maskedPassword, certificateAlias=$certificateAlias)"
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.kt b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.kt
index dbacfccab1e7c512580a31a172de22b383814dfb..abada92d0750e31c61f3a52c980b0dcd3a3e819d 100644
--- a/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.kt
+++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.kt
@@ -86,11 +86,7 @@ open class LocalAddressBook(
* @return list of [mainAccount]'s address books
*/
fun findAll(context: Context, provider: ContentProviderClient?, mainAccount: Account?): List {
- val accountManager = AccountManager.get(context)
- val accounts = ArrayList()
- accountManager.getAccountsByType(context.getString(R.string.account_type_eelo_address_book)).forEach { accounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type_google_address_book)).forEach { accounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).forEach { accounts.add(it) }
+ val accounts = AccountUtils.getAddressBookAccounts(context)
return accounts.toTypedArray().map { LocalAddressBook(context, it, provider) }
.filter { mainAccount == null || it.mainAccount == mainAccount }
@@ -128,9 +124,7 @@ open class LocalAddressBook(
* @throws IllegalArgumentException if the given account is not a address book account or does not have a main account
*/
fun mainAccount(context: Context, account: Account): Account =
- if (account.type == context.getString(R.string.account_type_address_book) ||
- account.type == context.getString(R.string.account_type_eelo_address_book) ||
- account.type == context.getString(R.string.account_type_google_address_book)) {
+ if (account.type in AccountUtils.getAddressBookAccountTypes(context)) {
val manager = AccountManager.get(context)
val accountName = manager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME)
diff --git a/app/src/main/java/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt b/app/src/main/java/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt
index 86633b45367578e300408245cae9864c93547ec0..5c597b314585095622246a16613e720f1bcd3156 100644
--- a/app/src/main/java/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt
+++ b/app/src/main/java/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt
@@ -106,7 +106,10 @@ class DavResourceFinder(
private fun findInitialConfiguration(service: Service): Configuration.ServiceInfo? {
// user-given base URI (either mailto: URI or http(s):// URL)
- val baseURI = loginModel.baseURI!!
+ var baseURI = loginModel.baseURI!!
+ if (loginModel.cardDavURI != null && service == Service.CARDDAV) {
+ baseURI = loginModel.cardDavURI!!
+ }
// domain for service discovery
var discoveryFQDN: String? = null
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 eb618d92b85b3a845a2eda7ab5fa0df826938a83..2221fea3e9808f5f204f1d5c9d54c94bfbdd9418 100644
--- a/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt
+++ b/app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt
@@ -33,6 +33,7 @@ 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.AccountUtils
import at.bitfire.davdroid.syncadapter.SyncUtils
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.AndroidEvent
@@ -93,6 +94,7 @@ class AccountSettings(
const val KEY_USERNAME = "user_name"
const val KEY_EMAIL_ADDRESS = "email_address"
const val KEY_AUTH_STATE = "auth_state"
+ const val KEY_CLIENT_SECRET = "client_secret"
const val KEY_CERTIFICATE_ALIAS = "certificate_alias"
const val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false)
@@ -155,6 +157,10 @@ class AccountSettings(
if (credentials.authState != null) {
bundle.putString(KEY_AUTH_STATE, credentials.authState.jsonSerializeString())
}
+
+ if (credentials.clientSecret != null) {
+ bundle.putString(KEY_CLIENT_SECRET, credentials.clientSecret)
+ }
}
if (!baseURL.isNullOrEmpty()) {
@@ -168,8 +174,7 @@ class AccountSettings(
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)))
+ for (account in AccountUtils.getMainAccounts(context))
try {
val settings = AccountSettings(context, account)
@@ -215,16 +220,20 @@ class AccountSettings(
val account: Account
init {
- when (argAccount.type) {
- context.getString(R.string.account_type_address_book), context.getString(R.string.account_type_eelo_address_book), context.getString(R.string.account_type_google_address_book) -> {
+ account = when (argAccount.type) {
+ in AccountUtils.getAddressBookAccountTypes(context) -> {
/* 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)
+ stored in the main account, so resolve and use the main account instead. */
+ LocalAddressBook.mainAccount(context, argAccount)
+ }
+
+ in AccountUtils.getMainAccountTypes(context) -> {
+ argAccount
}
- context.getString(R.string.account_type), context.getString(R.string.google_account_type), context.getString(R.string.eelo_account_type) ->
- account = argAccount
- else ->
+
+ else -> {
throw IllegalArgumentException("Account type not supported. AccountType: ${argAccount.type}")
+ }
}
// synchronize because account migration must only be run one time
@@ -258,7 +267,8 @@ class AccountSettings(
accountManager.getUserData(account, KEY_USERNAME),
accountManager.getPassword(account),
AuthState.jsonDeserialize(accountManager.getUserData(account, KEY_AUTH_STATE)),
- accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS)
+ accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS),
+ clientSecret = accountManager.getUserData(account, KEY_CLIENT_SECRET)
)
}
}
@@ -274,6 +284,7 @@ class AccountSettings(
accountManager.setPassword(account, credentials.password)
accountManager.setUserData(account, KEY_AUTH_STATE, credentials.authState.jsonSerializeString())
accountManager.setUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias)
+ accountManager.setUserData(account, KEY_CLIENT_SECRET, credentials.clientSecret)
}
}
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountUtils.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountUtils.kt
index add388be2f985a47d8b2e74f915ee2faf8cfd063..840e24b216812ef266de69522ed38a71762e1832 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountUtils.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountUtils.kt
@@ -8,6 +8,7 @@ import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import android.os.Bundle
+import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
object AccountUtils {
@@ -24,7 +25,12 @@ object AccountUtils {
* @throws IllegalArgumentException when user data contains non-String values
* @throws IllegalStateException if user data can't be set
*/
- fun createAccount(context: Context, account: Account, userData: Bundle, password: String? = null): Boolean {
+ fun createAccount(
+ context: Context,
+ account: Account,
+ userData: Bundle,
+ password: String? = null
+ ): Boolean {
// validate user data
for (key in userData.keySet()) {
userData.get(key)?.let { entry ->
@@ -63,5 +69,50 @@ object AccountUtils {
return true
}
+ fun getMainAccountTypes(context: Context) =
+ listOf(
+ context.getString(R.string.account_type),
+ context.getString(R.string.eelo_account_type),
+ context.getString(R.string.google_account_type)
+ )
+ fun getMainAccounts(context: Context): List {
+ val accountManager = AccountManager.get(context)
+ val accounts = mutableListOf()
+
+ getMainAccountTypes(context)
+ .forEach {
+ accounts.addAll(accountManager.getAccountsByType(it))
+ }
+
+ return accounts
+ }
+
+ fun getAddressBookAccountTypes(context: Context) =
+ listOf(
+ context.getString(R.string.account_type_address_book),
+ context.getString(R.string.account_type_eelo_address_book),
+ context.getString(R.string.account_type_google_address_book)
+ )
+
+ fun getAddressBookAccounts(context: Context): List {
+ val accountManager = AccountManager.get(context)
+ val accounts = mutableListOf()
+
+ getAddressBookAccountTypes(context)
+ .forEach {
+ accounts.addAll(accountManager.getAccountsByType(it))
+ }
+
+ return accounts
+ }
+
+ fun getAddressBookType(context: Context, mainType: String): String? {
+ return when(mainType) {
+ context.getString(R.string.account_type) -> context.getString(R.string.account_type_address_book)
+ context.getString(R.string.eelo_account_type) -> context.getString(R.string.account_type_eelo_address_book)
+ context.getString(R.string.google_account_type) -> context.getString(R.string.account_type_google_address_book)
+ else -> null
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountsUpdatedListener.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountsUpdatedListener.kt
index 6aa7f006ad61b26a5c0ff411a9df079d1e8bef51..778ee9bb30ae295151fb553368c9233b55bae87c 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountsUpdatedListener.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountsUpdatedListener.kt
@@ -9,7 +9,6 @@ import android.accounts.AccountManager
import android.accounts.OnAccountsUpdateListener
import android.content.Context
import androidx.annotation.AnyThread
-import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
@@ -86,23 +85,15 @@ class AccountsUpdatedListener private constructor(
private fun cleanupAccounts(context: Context, accounts: Array) {
Logger.log.log(Level.INFO, "Cleaning up accounts. Current accounts")
- val accountManager = AccountManager.get(context)
val accountNames = HashSet()
- val accountFromManager = ArrayList()
-
- accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)).forEach { accountFromManager.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.google_account_type)).forEach { accountFromManager.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type)).forEach { accountFromManager.add(it) }
+ val accountFromManager = AccountUtils.getMainAccounts(context)
for (account in accountFromManager.toTypedArray()) {
accountNames += account.name
}
// delete orphaned address book accounts
- val addressBookAccounts = ArrayList()
- accountManager.getAccountsByType(context.getString(R.string.account_type_eelo_address_book)).forEach { addressBookAccounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type_google_address_book)).forEach { addressBookAccounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).forEach { addressBookAccounts.add(it) }
+ val addressBookAccounts = AccountUtils.getAddressBookAccounts(context)
addressBookAccounts.map { LocalAddressBook(context, it, null) }
.forEach {
try {
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt
index 503a7b4b1f8490560d3283441c0b716186d192dc..ae4bc54cce04e1e15356f1efbfca10033dcdf901 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.kt
@@ -12,6 +12,7 @@ import android.os.AsyncTask
import android.os.Bundle
import android.provider.CalendarContract
import at.bitfire.davdroid.HttpClient
+import at.bitfire.davdroid.OpenIdUtils
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Credentials
@@ -28,17 +29,23 @@ import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set
-class CalendarsSyncAdapterService: SyncAdapterService() {
+open class CalendarsSyncAdapterService : SyncAdapterService() {
override fun syncAdapter() = CalendarsSyncAdapter(this, appDatabase)
-
- class CalendarsSyncAdapter(
+ class CalendarsSyncAdapter(
context: Context,
appDatabase: AppDatabase
) : SyncAdapter(context, appDatabase) {
- override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy, provider: ContentProviderClient, syncResult: SyncResult) {
+ override fun sync(
+ account: Account,
+ extras: Bundle,
+ authority: String,
+ httpClient: Lazy,
+ provider: ContentProviderClient,
+ syncResult: SyncResult
+ ) {
try {
val accountSettings = AccountSettings(context, account)
@@ -46,8 +53,10 @@ class CalendarsSyncAdapterService: SyncAdapterService() {
- sync conditions (e.g. "sync only in WiFi") are not met AND
- this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
*/
- if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
+ if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) &&
+ !checkSyncConditions(accountSettings)) {
return
+ }
if (accountSettings.getEventColors())
AndroidCalendar.insertColors(provider, account)
@@ -58,24 +67,46 @@ class CalendarsSyncAdapterService: SyncAdapterService() {
val priorityCalendars = priorityCollections(extras)
val calendars = AndroidCalendar
- .find(account, provider, LocalCalendar.Factory, "${CalendarContract.Calendars.SYNC_EVENTS}!=0", null)
- .sortedByDescending { priorityCalendars.contains(it.id) }
+ .find(
+ account,
+ provider,
+ LocalCalendar.Factory,
+ "${CalendarContract.Calendars.SYNC_EVENTS}!=0",
+ null
+ )
+ .sortedByDescending { priorityCalendars.contains(it.id) }
for (calendar in calendars) {
Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}")
- CalendarSyncManager(context, account, accountSettings, extras, httpClient.value, authority, syncResult, calendar).let {
+ CalendarSyncManager(
+ context,
+ account,
+ accountSettings,
+ extras,
+ httpClient.value,
+ authority,
+ syncResult,
+ calendar
+ ).let {
val authState = accountSettings.credentials().authState
if (authState != null) {
if (authState.needsTokenRefresh) {
val tokenRequest = authState.createTokenRefreshRequest()
-
- AuthorizationService(context).performTokenRequest(tokenRequest) { tokenResponse, ex ->
+ val clientSecretString = accountSettings.credentials().clientSecret
+ val clientSecret =
+ OpenIdUtils.getClientAuthentication(clientSecretString)
+
+ AuthorizationService(context).performTokenRequest(
+ tokenRequest,
+ clientSecret
+ ) { tokenResponse, ex ->
authState.update(tokenResponse, ex)
accountSettings.credentials(
Credentials(
account.name,
null,
authState,
- null
+ null,
+ clientSecret = clientSecretString
)
)
it.accountSettings.credentials(
@@ -83,31 +114,36 @@ class CalendarsSyncAdapterService: SyncAdapterService() {
it.account.name,
null,
authState,
- null
+ null,
+ clientSecret = clientSecretString
)
)
object : AsyncTask() {
override fun doInBackground(vararg params: Void): Void? {
- it.performSync()
+ it.performSyncWithRetry()
return null
}
}.execute()
}
} else {
- it.performSync()
+ it.performSyncWithRetry()
}
} else {
- it.performSync()
+ it.performSyncWithRetry()
}
}
}
- } catch(e: Exception) {
+ } catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't sync calendars", e)
}
Logger.log.info("Calendar sync complete")
}
- private fun updateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
+ private fun updateLocalCalendars(
+ provider: ContentProviderClient,
+ account: Account,
+ settings: AccountSettings
+ ) {
val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
val remoteCalendars = mutableMapOf()
@@ -118,7 +154,13 @@ class CalendarsSyncAdapterService: SyncAdapterService() {
// delete/update local calendars
val updateColors = settings.getManageCalendarColors()
- for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null))
+ for (calendar in AndroidCalendar.find(
+ account,
+ provider,
+ LocalCalendar.Factory,
+ null,
+ null
+ ))
calendar.name?.let {
val url = it.toHttpUrl()
val info = remoteCalendars[url]
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt
index 5de00c3d1cd273fabea671266d7d4afe80ab7c8b..bb89b01a83b9110d87cf413c44e16e86b9c742c6 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.kt
@@ -13,6 +13,7 @@ import android.os.AsyncTask
import android.os.Bundle
import android.provider.ContactsContract
import at.bitfire.davdroid.HttpClient
+import at.bitfire.davdroid.OpenIdUtils
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.log.Logger
@@ -71,15 +72,18 @@ class ContactsSyncAdapterService: SyncAdapterService() {
if (authState != null) {
if (authState.needsTokenRefresh) {
val tokenRequest = authState.createTokenRefreshRequest()
+ val clientSecretString = accountSettings.credentials().clientSecret
+ val clientSecret = OpenIdUtils.getClientAuthentication(clientSecretString)
- AuthorizationService(context).performTokenRequest(tokenRequest) { tokenResponse, ex ->
+ AuthorizationService(context).performTokenRequest(tokenRequest, clientSecret) { tokenResponse, ex ->
authState.update(tokenResponse, ex)
accountSettings.credentials(
Credentials(
account.name,
null,
authState,
- null
+ null,
+ clientSecret = clientSecretString
)
)
it.accountSettings.credentials(
@@ -87,7 +91,8 @@ class ContactsSyncAdapterService: SyncAdapterService() {
it.account.name,
null,
authState,
- null
+ null,
+ clientSecret = clientSecretString
)
)
object : AsyncTask() {
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt
index af37b24b5f429bb858db3bf9c69fd8fa0c353278..93ef72e1f7529ef7a0d045b5ebf685e91e8c6342 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt
@@ -16,7 +16,11 @@
package at.bitfire.davdroid.syncadapter
-import android.accounts.*
+import android.accounts.AbstractAccountAuthenticator
+import android.accounts.Account
+import android.accounts.AccountAuthenticatorResponse
+import android.accounts.AccountManager
+import android.accounts.OnAccountsUpdateListener
import android.app.Service
import android.content.Context
import android.content.Intent
@@ -46,30 +50,16 @@ class EeloAccountAuthenticatorService : Service(), OnAccountsUpdateListener {
fun cleanupAccounts(context: Context, db: AppDatabase) {
Logger.log.info("Cleaning up orphaned accounts")
- val accountManager = AccountManager.get(context)
-
val accountNames = HashSet()
- val accounts = ArrayList()
- accountManager.getAccountsByType(context.getString(R.string.eelo_account_type))
- .forEach { accounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.google_account_type))
- .forEach { accounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type))
- .forEach { accounts.add(it) }
+ val accounts = AccountUtils.getMainAccounts(context)
for (account in accounts.toTypedArray()) {
accountNames += account.name
}
// delete orphaned address book accounts
- val addressBookAccounts = ArrayList()
- accountManager.getAccountsByType(context.getString(R.string.account_type_eelo_address_book))
- .forEach { addressBookAccounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type_google_address_book))
- .forEach { addressBookAccounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
- .forEach { addressBookAccounts.add(it) }
+ val addressBookAccounts = AccountUtils.getAddressBookAccounts(context)
addressBookAccounts.map { LocalAddressBook(context, it, null) }
.forEach {
try {
@@ -135,8 +125,8 @@ class EeloAccountAuthenticatorService : Service(), OnAccountsUpdateListener {
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
+ LoginActivity.ACCOUNT_TYPE,
+ context.getString(R.string.eelo_account_type)
)
val bundle = Bundle(1)
bundle.putParcelable(AccountManager.KEY_INTENT, intent)
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAppDataSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAppDataSyncAdapterService.kt
index 0136139b65addfdae6732526315177e4e2513b1f..703cb25bdab57eeba0c03955674170b40f7905a0 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAppDataSyncAdapterService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAppDataSyncAdapterService.kt
@@ -17,7 +17,9 @@
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
-import android.content.*
+import android.content.ContentProviderClient
+import android.content.Context
+import android.content.SyncResult
import android.os.Bundle
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.db.AppDatabase
@@ -27,9 +29,9 @@ class EeloAppDataSyncAdapterService : SyncAdapterService() {
override fun syncAdapter() = EeloAppDataSyncAdapter(this, appDatabase)
class EeloAppDataSyncAdapter(
- context: Context,
- db: AppDatabase
- ): SyncAdapter(context, db) {
+ context: Context,
+ db: AppDatabase
+ ) : SyncAdapter(context, db) {
override fun sync(
account: Account,
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloCalendarsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloCalendarsSyncAdapterService.kt
index 60ba1088c09f0bd4dc73f347abf512a713ae3a3c..a9367e3632fa82cb37adf258404bb85507204daf 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloCalendarsSyncAdapterService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloCalendarsSyncAdapterService.kt
@@ -16,177 +16,4 @@
package at.bitfire.davdroid.syncadapter
-import android.accounts.Account
-import android.content.ContentProviderClient
-import android.content.ContentResolver
-import android.content.Context
-import android.content.SyncResult
-import android.os.AsyncTask
-import android.os.Bundle
-import android.provider.CalendarContract
-import at.bitfire.davdroid.HttpClient
-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.LocalCalendar
-import at.bitfire.davdroid.settings.AccountSettings
-import at.bitfire.ical4android.AndroidCalendar
-import net.openid.appauth.AuthorizationService
-import okhttp3.HttpUrl
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
-import java.util.logging.Level
-
-class EeloCalendarsSyncAdapterService : SyncAdapterService() {
-
- override fun syncAdapter() = CalendarsSyncAdapter(this, appDatabase)
-
- class CalendarsSyncAdapter(
- context: Context,
- appDatabase: AppDatabase
- ) : SyncAdapter(context, appDatabase) {
- override fun sync(
- account: Account,
- extras: Bundle,
- authority: String,
- httpClient: Lazy,
- provider: ContentProviderClient,
- syncResult: SyncResult
- ) {
- try {
- val accountSettings = AccountSettings(context, account)
-
- /* don't run sync if
- - sync conditions (e.g. "sync only in WiFi") are not met AND
- - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
- */
- if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(
- accountSettings
- )
- )
- return
-
- if (accountSettings.getEventColors())
- AndroidCalendar.insertColors(provider, account)
- else
- AndroidCalendar.removeColors(provider, account)
-
- updateLocalCalendars(provider, account, accountSettings)
-
- val priorityCalendars = priorityCollections(extras)
- val calendars = AndroidCalendar
- .find(
- account,
- provider,
- LocalCalendar.Factory,
- "${CalendarContract.Calendars.SYNC_EVENTS}!=0",
- null
- )
- .sortedByDescending { priorityCalendars.contains(it.id) }
- for (calendar in calendars) {
- Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}")
- CalendarSyncManager(
- context,
- account,
- accountSettings,
- extras,
- httpClient.value,
- authority,
- syncResult,
- calendar
- ).let {
- val authState = accountSettings.credentials().authState
- if (authState != null) {
- if (authState.needsTokenRefresh) {
- val tokenRequest = authState.createTokenRefreshRequest()
-
- AuthorizationService(context).performTokenRequest(tokenRequest) { tokenResponse, ex ->
- authState.update(tokenResponse, ex)
- accountSettings.credentials(
- Credentials(
- account.name,
- null,
- authState,
- null
- )
- )
- it.accountSettings.credentials(
- Credentials(
- it.account.name,
- null,
- authState,
- null
- )
- )
- object : AsyncTask() {
- override fun doInBackground(vararg params: Void): Void? {
- it.performSyncWithRetry()
- return null
- }
- }.execute()
- }
- } else {
- it.performSyncWithRetry()
- }
- } else {
- it.performSyncWithRetry()
- }
- }
- }
- } catch (e: Exception) {
- Logger.log.log(Level.SEVERE, "Couldn't sync calendars", e)
- }
- Logger.log.info("Calendar sync complete")
- }
-
- private fun updateLocalCalendars(
- provider: ContentProviderClient,
- account: Account,
- settings: AccountSettings
- ) {
- val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
-
- val remoteCalendars = mutableMapOf()
-
- if (service != null) {
- for (collection in db.collectionDao().getSyncCalendars(service.id)) {
- remoteCalendars[collection.url] = collection
- }
- }
-
- // delete/update local calendars
- val updateColors = settings.getManageCalendarColors()
- for (calendar in AndroidCalendar.find(
- account,
- provider,
- LocalCalendar.Factory,
- null,
- null
- ))
- calendar.name?.let {
- val url = it.toHttpUrlOrNull()!!
- val info = remoteCalendars[url]
-
- if (info == null) {
- Logger.log.log(Level.INFO, "Deleting obsolete local calendar", url)
- calendar.delete()
- } else {
- // remote CollectionInfo found for this local collection, update data
- Logger.log.log(Level.FINE, "Updating local calendar $url", info)
- calendar.update(info, updateColors)
- // we already have a local calendar for this remote collection, don't take into consideration anymore
- remoteCalendars -= url
- }
- }
-
- // create new local calendars
- for ((_, info) in remoteCalendars) {
- Logger.log.log(Level.INFO, "Adding local calendar", info)
- LocalCalendar.create(account, provider, info)
- }
- }
-
- }
-}
-
+class EeloCalendarsSyncAdapterService : CalendarsSyncAdapterService()
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloEmailSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloEmailSyncAdapterService.kt
index c9d955939bc11cee76da557d0d07f0a40dbf00ed..3575ab895f589633341f3e68f888d1da76c81cc0 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloEmailSyncAdapterService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloEmailSyncAdapterService.kt
@@ -16,32 +16,4 @@
package at.bitfire.davdroid.syncadapter
-import android.accounts.Account
-import android.content.*
-import android.os.Bundle
-import at.bitfire.davdroid.HttpClient
-import at.bitfire.davdroid.MailAccountSyncHelper
-import at.bitfire.davdroid.db.AppDatabase
-
-class EeloEmailSyncAdapterService : SyncAdapterService() {
-
- override fun syncAdapter() = EeloEmailSyncAdapter(this, appDatabase)
-
-
- class EeloEmailSyncAdapter(
- context: Context,
- db: AppDatabase
- ): SyncAdapter(context, db) {
- override fun sync(
- account: Account,
- extras: Bundle,
- authority: String,
- httpClient: Lazy,
- provider: ContentProviderClient,
- syncResult: SyncResult
- ) {
- MailAccountSyncHelper.syncMailAccounts(context.applicationContext)
- }
- }
-}
-
+class EeloEmailSyncAdapterService : EmailSyncAdapterService()
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloMediaSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloMediaSyncAdapterService.kt
index e7af69c3e668e5d58d0887e06d4896ecbba4bdcd..88f0c77244bb1fc1c5d5665a7018493ab21cc487 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloMediaSyncAdapterService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloMediaSyncAdapterService.kt
@@ -17,7 +17,9 @@
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
-import android.content.*
+import android.content.ContentProviderClient
+import android.content.Context
+import android.content.SyncResult
import android.os.Bundle
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.db.AppDatabase
@@ -28,9 +30,9 @@ class EeloMediaSyncAdapterService : SyncAdapterService() {
class EeloMediaSyncAdapter(
- context: Context,
- db: AppDatabase
- ): SyncAdapter(context, db) {
+ context: Context,
+ db: AppDatabase
+ ) : SyncAdapter(context, db) {
override fun sync(
account: Account,
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloMeteredEdriveSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloMeteredEdriveSyncAdapterService.kt
index 2702a7a4c42b6e2579a078c638456d0b2e073608..cb49d1acc680b18daf6e230251412d8780145cd0 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloMeteredEdriveSyncAdapterService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloMeteredEdriveSyncAdapterService.kt
@@ -29,9 +29,9 @@ class EeloMeteredEdriveSyncAdapterService : SyncAdapterService() {
override fun syncAdapter() = EeloMeteredEdriveSyncAdapter(this, appDatabase)
class EeloMeteredEdriveSyncAdapter(
- context: Context,
- db: AppDatabase
- ): SyncAdapter(context, db) {
+ context: Context,
+ db: AppDatabase
+ ) : SyncAdapter(context, db) {
override fun sync(
account: Account,
@@ -44,4 +44,4 @@ class EeloMeteredEdriveSyncAdapterService : SyncAdapterService() {
// Unused
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloNotesSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloNotesSyncAdapterService.kt
index 86a1daa897f51c01d0184e166f1fab6600d8d405..91fed22c83797c3d7df027a55be9dfb6b1ec7985 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloNotesSyncAdapterService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloNotesSyncAdapterService.kt
@@ -17,7 +17,9 @@
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
-import android.content.*
+import android.content.ContentProviderClient
+import android.content.Context
+import android.content.SyncResult
import android.os.Bundle
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.db.AppDatabase
@@ -27,9 +29,9 @@ class EeloNotesSyncAdapterService : SyncAdapterService() {
override fun syncAdapter() = EeloNotesSyncAdapter(this, appDatabase)
class EeloNotesSyncAdapter(
- context: Context,
- db: AppDatabase
- ): SyncAdapter(context, db) {
+ context: Context,
+ db: AppDatabase
+ ) : SyncAdapter(context, db) {
override fun sync(
account: Account,
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloTasksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloTasksSyncAdapterService.kt
index 7afed9507795a057588bbc6f8dc352bafaf58d34..acd197cd02cb608462cec8eb512f15057da5dd21 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloTasksSyncAdapterService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EeloTasksSyncAdapterService.kt
@@ -45,159 +45,4 @@ import java.util.logging.Level
/**
* Synchronization manager for CalDAV collections; handles tasks ({@code VTODO}).
*/
-class EeloTasksSyncAdapterService : SyncAdapterService() {
-
- override fun syncAdapter() = TasksSyncAdapter(this, appDatabase)
-
-
- class TasksSyncAdapter(
- context: Context,
- appDatabase: AppDatabase
- ) : SyncAdapter(context, appDatabase) {
-
- override fun sync(
- account: Account,
- extras: Bundle,
- authority: String,
- httpClient: Lazy,
- provider: ContentProviderClient,
- syncResult: SyncResult
- ) {
- try {
- val providerName = TaskProvider.ProviderName.fromAuthority(authority)
- val taskProvider = TaskProvider.fromProviderClient(context, providerName, provider)
-
- // make sure account can be seen by OpenTasks
- if (Build.VERSION.SDK_INT >= 26)
- AccountManager.get(context).setAccountVisibility(
- account,
- taskProvider.name.packageName,
- AccountManager.VISIBILITY_VISIBLE
- )
-
- val accountSettings = AccountSettings(context, account)
- /* don't run sync if
- - sync conditions (e.g. "sync only in WiFi") are not met AND
- - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
- */
- if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(
- accountSettings
- )
- )
- return
-
- updateLocalTaskLists(taskProvider, account, accountSettings)
-
- val priorityTaskLists = priorityCollections(extras)
- val taskLists = AndroidTaskList
- .find(
- account,
- taskProvider,
- LocalTaskList.Factory,
- "${TaskContract.TaskLists.SYNC_ENABLED}!=0",
- null
- )
- .sortedByDescending { priorityTaskLists.contains(it.id) }
- for (taskList in taskLists) {
- Logger.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]")
- TasksSyncManager(
- context,
- account,
- accountSettings,
- httpClient.value,
- extras,
- authority,
- syncResult,
- taskList
- ).let {
- val authState = accountSettings.credentials().authState
- if (authState != null) {
- if (authState.needsTokenRefresh) {
- val tokenRequest = authState.createTokenRefreshRequest()
-
- AuthorizationService(context).performTokenRequest(tokenRequest,
- AuthorizationService.TokenResponseCallback { tokenResponse, ex ->
- authState.update(tokenResponse, ex)
- accountSettings.credentials(
- Credentials(
- account.name,
- null,
- authState,
- null
- )
- )
- it.accountSettings.credentials(
- Credentials(
- it.account.name,
- null,
- authState,
- null
- )
- )
- object : AsyncTask() {
- override fun doInBackground(vararg params: Void): Void? {
- it.performSyncWithRetry()
- return null
- }
- }.execute()
- })
- } else {
- it.performSyncWithRetry()
- }
- } else {
- it.performSyncWithRetry()
- }
- }
- }
- } catch (e: TaskProvider.ProviderTooOldException) {
- SyncUtils.notifyProviderTooOld(context, e)
- syncResult.databaseError = true
- } catch (e: Exception) {
- Logger.log.log(Level.SEVERE, "Couldn't sync task lists", e)
- syncResult.databaseError = true
- }
-
- Logger.log.info("Task sync complete")
- }
-
- private fun updateLocalTaskLists(
- provider: TaskProvider,
- account: Account,
- settings: AccountSettings
- ) {
- val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
-
- val remoteTaskLists = mutableMapOf()
- if (service != null)
- for (collection in db.collectionDao().getSyncTaskLists(service.id)) {
- remoteTaskLists[collection.url] = collection
- }
-
- // delete/update local task lists
- val updateColors = settings.getManageCalendarColors()
-
- for (list in AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null))
- list.syncId?.let {
- val url = it.toHttpUrlOrNull()!!
- val info = remoteTaskLists[url]
- if (info == null) {
- Logger.log.fine("Deleting obsolete local task list $url")
- list.delete()
- } else {
- // remote CollectionInfo found for this local collection, update data
- Logger.log.log(Level.FINE, "Updating local task list $url", info)
- list.update(info, updateColors)
- // we already have a local task list for this remote collection, don't take into consideration anymore
- remoteTaskLists -= url
- }
- }
-
- // create new local task lists
- for ((_, info) in remoteTaskLists) {
- Logger.log.log(Level.INFO, "Adding local task list", info)
- LocalTaskList.create(account, provider, info)
- }
- }
-
- }
-}
+class EeloTasksSyncAdapterService : TasksSyncAdapterService()
\ No newline at end of file
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/EmailSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/EmailSyncAdapterService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..68ea74fb585e48c3555117e0ca2061e272232fd3
--- /dev/null
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/EmailSyncAdapterService.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright MURENA SAS 2023
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package at.bitfire.davdroid.syncadapter
+
+import android.accounts.Account
+import android.content.ContentProviderClient
+import android.content.Context
+import android.content.SyncResult
+import android.os.Bundle
+import at.bitfire.davdroid.HttpClient
+import at.bitfire.davdroid.MailAccountSyncHelper
+import at.bitfire.davdroid.db.AppDatabase
+
+abstract class EmailSyncAdapterService : SyncAdapterService() {
+
+ override fun syncAdapter() = EmailSyncAdapter(this, appDatabase)
+
+ class EmailSyncAdapter(
+ context: Context,
+ db: AppDatabase
+ ) : SyncAdapter(context, db) {
+ override fun sync(
+ account: Account,
+ extras: Bundle,
+ authority: String,
+ httpClient: Lazy,
+ provider: ContentProviderClient,
+ syncResult: SyncResult
+ ) {
+ MailAccountSyncHelper.syncMailAccounts(context.applicationContext)
+ }
+ }
+}
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt
index c5752de8f44519de0ef0e79f2c666248c09957de..37fdb1429683ac28569d3ac9fc804527322d77af 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt
@@ -16,12 +16,16 @@
package at.bitfire.davdroid.syncadapter
-import android.accounts.*
+import android.accounts.AbstractAccountAuthenticator
+import android.accounts.Account
+import android.accounts.AccountAuthenticatorResponse
+import android.accounts.AccountManager
+import android.accounts.OnAccountsUpdateListener
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Bundle
-import android.util.Log
+import at.bitfire.davdroid.OpenIdUtils
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.log.Logger
@@ -49,29 +53,15 @@ class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener {
fun cleanupAccounts(context: Context, db: AppDatabase) {
Logger.log.info("Cleaning up orphaned accounts")
- val accountManager = AccountManager.get(context)
-
val accountNames = HashSet()
- val accounts = ArrayList()
- accountManager.getAccountsByType(context.getString(R.string.eelo_account_type))
- .forEach { accounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.google_account_type))
- .forEach { accounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type))
- .forEach { accounts.add(it) }
+ val accounts = AccountUtils.getMainAccounts(context)
for (account in accounts.toTypedArray()) {
accountNames += account.name
}
// delete orphaned address book accounts
- val addressBookAccounts = ArrayList()
- accountManager.getAccountsByType(context.getString(R.string.account_type_eelo_address_book))
- .forEach { addressBookAccounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type_google_address_book))
- .forEach { addressBookAccounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
- .forEach { addressBookAccounts.add(it) }
+ val addressBookAccounts = AccountUtils.getAddressBookAccounts(context)
addressBookAccounts.map { LocalAddressBook(context, it, null) }
.forEach {
try {
@@ -137,8 +127,8 @@ class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener {
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
+ LoginActivity.ACCOUNT_TYPE,
+ context.getString(R.string.google_account_type)
)
options?.let {
@@ -173,20 +163,19 @@ class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener {
account: Account?,
authTokenType: String?,
options: Bundle?
- ): Bundle {
+ ): Bundle? {
val accountManager = AccountManager.get(context)
- val authState = AuthState.jsonDeserialize(
- accountManager.getUserData(
- account,
- AccountSettings.KEY_AUTH_STATE
- )
- )
+ val authStateString = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE) ?: return null
+
+ val authState = AuthState.jsonDeserialize(authStateString)
if (authState != null) {
if (authState.needsTokenRefresh) {
val tokenRequest = authState.createTokenRefreshRequest()
+ val clientSecretString = accountManager.getUserData(account, AccountSettings.KEY_CLIENT_SECRET)
+ val clientSecret = OpenIdUtils.getClientAuthentication(clientSecretString)
- AuthorizationService(context).performTokenRequest(tokenRequest) { tokenResponse, ex ->
+ AuthorizationService(context).performTokenRequest(tokenRequest, clientSecret) { tokenResponse, ex ->
authState.update(tokenResponse, ex)
accountManager.setUserData(
account,
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleCalendarsSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleCalendarsSyncAdapterService.kt
index 95c8d5fcd74a15e5ef6d793176336a998ae2a8f2..4c5745a64658b029e0a3677f64ea0ddc48a1db90 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleCalendarsSyncAdapterService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleCalendarsSyncAdapterService.kt
@@ -16,175 +16,4 @@
package at.bitfire.davdroid.syncadapter
-import android.accounts.Account
-import android.content.ContentProviderClient
-import android.content.ContentResolver
-import android.content.Context
-import android.content.SyncResult
-import android.os.AsyncTask
-import android.os.Bundle
-import android.provider.CalendarContract
-import at.bitfire.davdroid.HttpClient
-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.LocalCalendar
-import at.bitfire.davdroid.settings.AccountSettings
-import at.bitfire.ical4android.AndroidCalendar
-import net.openid.appauth.AuthorizationService
-import okhttp3.HttpUrl
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
-import java.util.logging.Level
-
-class GoogleCalendarsSyncAdapterService : SyncAdapterService() {
-
- override fun syncAdapter() = CalendarsSyncAdapter(this, appDatabase)
-
-
- class CalendarsSyncAdapter(
- context: Context,
- db: AppDatabase
- ) : SyncAdapter(context, db) {
-
- override fun sync(
- account: Account,
- extras: Bundle,
- authority: String,
- httpClient: Lazy,
- provider: ContentProviderClient,
- syncResult: SyncResult
- ) {
- try {
- val accountSettings = AccountSettings(context, account)
-
- /* don't run sync if
- - sync conditions (e.g. "sync only in WiFi") are not met AND
- - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
- */
- if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(
- accountSettings
- )
- )
- return
-
- if (accountSettings.getEventColors())
- AndroidCalendar.insertColors(provider, account)
- else
- AndroidCalendar.removeColors(provider, account)
-
- updateLocalCalendars(provider, account, accountSettings)
-
- val priorityCalendars = priorityCollections(extras)
- val calendars = AndroidCalendar
- .find(
- account,
- provider,
- LocalCalendar.Factory,
- "${CalendarContract.Calendars.SYNC_EVENTS}!=0",
- null
- )
- .sortedByDescending { priorityCalendars.contains(it.id) }
- for (calendar in calendars) {
- Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}")
- CalendarSyncManager(
- context,
- account,
- accountSettings,
- extras,
- httpClient.value,
- authority,
- syncResult,
- calendar
- ).let {
- val authState = accountSettings.credentials().authState
- if (authState != null) {
- if (authState.needsTokenRefresh) {
- val tokenRequest = authState.createTokenRefreshRequest()
-
- AuthorizationService(context).performTokenRequest(tokenRequest,
- AuthorizationService.TokenResponseCallback { tokenResponse, ex ->
- authState.update(tokenResponse, ex)
- accountSettings.credentials(
- Credentials(
- account.name,
- null,
- authState,
- null
- )
- )
- it.accountSettings.credentials(
- Credentials(
- it.account.name,
- null,
- authState,
- null
- )
- )
- object : AsyncTask() {
- override fun doInBackground(vararg params: Void): Void? {
- it.performSyncWithRetry()
- return null
- }
- }.execute()
- })
- } else {
- it.performSyncWithRetry()
- }
- } else {
- it.performSyncWithRetry()
- }
- }
- }
- } catch (e: Exception) {
- Logger.log.log(Level.SEVERE, "Couldn't sync calendars", e)
- }
- Logger.log.info("Calendar sync complete")
- }
-
- private fun updateLocalCalendars(
- provider: ContentProviderClient,
- account: Account,
- settings: AccountSettings
- ) {
- val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
-
- val remoteCalendars = mutableMapOf()
- if (service != null)
- for (collection in db.collectionDao().getSyncCalendars(service.id)) {
- remoteCalendars[collection.url] = collection
- }
-
- // delete/update local calendars
- val updateColors = settings.getManageCalendarColors()
- for (calendar in AndroidCalendar.find(
- account,
- provider,
- LocalCalendar.Factory,
- null,
- null
- ))
- calendar.name?.let {
- val url = it.toHttpUrlOrNull()!!
- val info = remoteCalendars[url]
- if (info == null) {
- Logger.log.log(Level.INFO, "Deleting obsolete local calendar", url)
- calendar.delete()
- } else {
- // remote CollectionInfo found for this local collection, update data
- Logger.log.log(Level.FINE, "Updating local calendar $url", info)
- calendar.update(info, updateColors)
- // we already have a local calendar for this remote collection, don't take into consideration anymore
- remoteCalendars -= url
- }
- }
-
- // create new local calendars
- for ((_, info) in remoteCalendars) {
- Logger.log.log(Level.INFO, "Adding local calendar", info)
- LocalCalendar.create(account, provider, info)
- }
- }
- }
-}
+class GoogleCalendarsSyncAdapterService : CalendarsSyncAdapterService()
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleEmailSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleEmailSyncAdapterService.kt
index 03d838817257b0de0cd6050ea03fcf1f5dec591e..fae1bcdcdb637768e423fa051da4b62221c66d7e 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleEmailSyncAdapterService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleEmailSyncAdapterService.kt
@@ -16,31 +16,4 @@
package at.bitfire.davdroid.syncadapter
-import android.accounts.Account
-import android.content.*
-import android.os.Bundle
-import at.bitfire.davdroid.HttpClient
-import at.bitfire.davdroid.MailAccountSyncHelper
-import at.bitfire.davdroid.db.AppDatabase
-
-class GoogleEmailSyncAdapterService : SyncAdapterService() {
-
- override fun syncAdapter() = GoogleEmailSyncAdapter(this, appDatabase)
-
- class GoogleEmailSyncAdapter(
- context: Context,
- db: AppDatabase
- ): SyncAdapter(context, db) {
-
- override fun sync(
- account: Account,
- extras: Bundle,
- authority: String,
- httpClient: Lazy,
- provider: ContentProviderClient,
- syncResult: SyncResult
- ) {
- MailAccountSyncHelper.syncMailAccounts(context.applicationContext)
- }
- }
-}
\ No newline at end of file
+class GoogleEmailSyncAdapterService : EmailSyncAdapterService()
\ No newline at end of file
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleTasksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleTasksSyncAdapterService.kt
index 66e180fb8a0768e0388e7fb668e8687f92ac39d7..082d3fd1d82531f948ccda137bb2f215eaf8b7bd 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleTasksSyncAdapterService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleTasksSyncAdapterService.kt
@@ -44,158 +44,4 @@ import java.util.logging.Level
/**
* Synchronization manager for CalDAV collections; handles tasks ({@code VTODO}).
*/
-class GoogleTasksSyncAdapterService : SyncAdapterService() {
-
- override fun syncAdapter() = TasksSyncAdapter(this, appDatabase)
-
-
- class TasksSyncAdapter(
- context: Context,
- db: AppDatabase
- ) : SyncAdapter(context, db) {
-
- override fun sync(
- account: Account,
- extras: Bundle,
- authority: String,
- httpClient: Lazy,
- provider: ContentProviderClient,
- syncResult: SyncResult
- ) {
- try {
- val providerName = TaskProvider.ProviderName.fromAuthority(authority)
- val taskProvider = TaskProvider.fromProviderClient(context, providerName, provider)
-
- // make sure account can be seen by OpenTasks
- if (Build.VERSION.SDK_INT >= 26)
- AccountManager.get(context).setAccountVisibility(
- account,
- taskProvider.name.packageName,
- AccountManager.VISIBILITY_VISIBLE
- )
-
- val accountSettings = AccountSettings(context, account)
- /* don't run sync if
- - sync conditions (e.g. "sync only in WiFi") are not met AND
- - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
- */
- if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(
- accountSettings
- )
- )
- return
-
- updateLocalTaskLists(taskProvider, account, accountSettings)
-
- val priorityTaskLists = priorityCollections(extras)
- val taskLists = AndroidTaskList
- .find(
- account,
- taskProvider,
- LocalTaskList.Factory,
- "${TaskContract.TaskLists.SYNC_ENABLED}!=0",
- null
- )
- .sortedByDescending { priorityTaskLists.contains(it.id) }
- for (taskList in taskLists) {
- Logger.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]")
- TasksSyncManager(
- context,
- account,
- accountSettings,
- httpClient.value,
- extras,
- authority,
- syncResult,
- taskList
- ).let {
- val authState = accountSettings.credentials().authState
- if (authState != null) {
- if (authState.needsTokenRefresh) {
- val tokenRequest = authState.createTokenRefreshRequest()
-
- AuthorizationService(context).performTokenRequest(tokenRequest,
- AuthorizationService.TokenResponseCallback { tokenResponse, ex ->
- authState.update(tokenResponse, ex)
- accountSettings.credentials(
- Credentials(
- account.name,
- null,
- authState,
- null
- )
- )
- it.accountSettings.credentials(
- Credentials(
- it.account.name,
- null,
- authState,
- null
- )
- )
- object : AsyncTask() {
- override fun doInBackground(vararg params: Void): Void? {
- it.performSyncWithRetry()
- return null
- }
- }.execute()
- })
- } else {
- it.performSyncWithRetry()
- }
- } else {
- it.performSyncWithRetry()
- }
- }
- }
- } catch (e: TaskProvider.ProviderTooOldException) {
- SyncUtils.notifyProviderTooOld(context, e)
- syncResult.databaseError = true
- } catch (e: Exception) {
- Logger.log.log(Level.SEVERE, "Couldn't sync task lists", e)
- syncResult.databaseError = true
- }
-
- Logger.log.info("Task sync complete")
- }
-
- private fun updateLocalTaskLists(
- provider: TaskProvider,
- account: Account,
- settings: AccountSettings
- ) {
- val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
-
- val remoteTaskLists = mutableMapOf()
- if (service != null)
- for (collection in db.collectionDao().getSyncTaskLists(service.id)) {
- remoteTaskLists[collection.url] = collection
- }
-
- // delete/update local task lists
- val updateColors = settings.getManageCalendarColors()
-
- for (list in AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null))
- list.syncId?.let {
- val url = it.toHttpUrlOrNull()!!
- val info = remoteTaskLists[url]
- if (info == null) {
- Logger.log.fine("Deleting obsolete local task list $url")
- list.delete()
- } else {
- // remote CollectionInfo found for this local collection, update data
- Logger.log.log(Level.FINE, "Updating local task list $url", info)
- list.update(info, updateColors)
- // we already have a local task list for this remote collection, don't take into consideration anymore
- remoteTaskLists -= url
- }
- }
-
- // create new local task lists
- for ((_, info) in remoteTaskLists) {
- Logger.log.log(Level.INFO, "Adding local task list", info)
- LocalTaskList.create(account, provider, info)
- }
- }
- }
-}
+class GoogleTasksSyncAdapterService : TasksSyncAdapterService()
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.kt
index 0ce18b78e321942c38f4ef20d9aecc3cc236efa7..4acce13cc47eee17d2da263d36d862b4f2ee6120 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.kt
@@ -437,7 +437,7 @@ abstract class SyncManager, out CollectionType: L
newFileName = local.prepareForUpload()
val uploadUrl = collectionURL.newBuilder().addPathSegment(newFileName).build()
- remoteExceptionContext(DavResource(httpClient.okHttpClient, uploadUrl)) { remote ->
+ remoteExceptionContext(DavResource(httpClient.okHttpClient, uploadUrl, accountSettings.credentials().authState?.accessToken)) { remote ->
Logger.log.info("Uploading new record ${local.id} -> $newFileName")
remote.put(generateUpload(local), ifNoneMatch = true, callback = readTagsFromResponse)
}
@@ -446,7 +446,7 @@ abstract class SyncManager, out CollectionType: L
local.prepareForUpload()
val uploadUrl = collectionURL.newBuilder().addPathSegment(existingFileName).build()
- remoteExceptionContext(DavResource(httpClient.okHttpClient, uploadUrl)) { remote ->
+ remoteExceptionContext(DavResource(httpClient.okHttpClient, uploadUrl, accountSettings.credentials().authState?.accessToken)) { remote ->
val lastScheduleTag = local.scheduleTag
val lastETag = if (lastScheduleTag == null) local.eTag else null
Logger.log.info("Uploading modified record ${local.id} -> $existingFileName (ETag=$lastETag, Schedule-Tag=$lastScheduleTag)")
@@ -797,7 +797,7 @@ abstract class SyncManager, out CollectionType: L
val contentIntent: Intent
var viewItemAction: NotificationCompat.Action? = null
- if ((account.type == context.getString(R.string.account_type) || account.type == context.getString(R.string.eelo_account_type) || account.type == context.getString(R.string.google_account_type))
+ if (account.type in AccountUtils.getMainAccountTypes(context)
&& (e is UnauthorizedException || e is NotFoundException)) {
contentIntent = Intent(context, SettingsActivity::class.java)
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt
index b2d663b69a0c74e2b5622cc4a5eb1700c0db3c20..d797ef509cf8f6ba172d99ae32f4568d1ddeb61f 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncUtils.kt
@@ -160,24 +160,8 @@ object SyncUtils {
}
}
- private fun allAccounts(context: Context): List {
- val accountManager = AccountManager.get(context)
- val accounts = mutableListOf()
-
- accounts.addAll(getAccountsByType(context, accountManager, R.string.account_type))
- accounts.addAll(getAccountsByType(context, accountManager, R.string.eelo_account_type))
- accounts.addAll(getAccountsByType(context, accountManager, R.string.google_account_type))
-
- return accounts
- }
-
- private fun getAccountsByType(context: Context, accountManager: AccountManager, @StringRes type: Int): Array {
- val accountType = context.getString(type)
- return accountManager.getAccountsByType(accountType)
- }
-
fun syncAllAccounts(context: Context): Boolean {
- val accounts = allAccounts(context)
+ val accounts = AccountUtils.getMainAccounts(context)
if (accounts.isEmpty()) {
return false
}
diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt
index 3c5f262e6cbc64904f4b63f7e035be897a41f1ec..7aa0c8712f557e3410c09ffec818eb008c2097bb 100644
--- a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt
+++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.kt
@@ -21,6 +21,7 @@ import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.TaskProvider
import android.os.AsyncTask
+import at.bitfire.davdroid.OpenIdUtils
import at.bitfire.davdroid.db.Credentials
import net.openid.appauth.AuthorizationService
import okhttp3.HttpUrl
@@ -78,15 +79,18 @@ open class TasksSyncAdapterService: SyncAdapterService() {
if (authState != null) {
if (authState.needsTokenRefresh) {
val tokenRequest = authState.createTokenRefreshRequest()
+ val clientSecretString = accountSettings.credentials().clientSecret
+ val clientSecret = OpenIdUtils.getClientAuthentication(clientSecretString)
- AuthorizationService(context).performTokenRequest(tokenRequest) { tokenResponse, ex ->
+ AuthorizationService(context).performTokenRequest(tokenRequest, clientSecret) { tokenResponse, ex ->
authState.update(tokenResponse, ex)
accountSettings.credentials(
Credentials(
account.name,
null,
authState,
- null
+ null,
+ clientSecret = clientSecretString
)
)
it.accountSettings.credentials(
@@ -94,21 +98,22 @@ open class TasksSyncAdapterService: SyncAdapterService() {
it.account.name,
null,
authState,
- null
+ null,
+ clientSecret = clientSecretString
)
)
object : AsyncTask() {
override fun doInBackground(vararg params: Void): Void? {
- it.performSync()
+ it.performSyncWithRetry()
return null
}
}.execute()
}
} else {
- it.performSync()
+ it.performSyncWithRetry()
}
} else {
- it.performSync()
+ it.performSyncWithRetry()
}
}
}
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt
index ad65c31d9a745c22fcd99feab1d80333e67a5311..58d26a2f355d17a55551dfa12f776e7f40861647 100644
--- a/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt
+++ b/app/src/main/java/at/bitfire/davdroid/ui/AccountListFragment.kt
@@ -10,13 +10,19 @@ import android.accounts.AccountManager
import android.accounts.OnAccountsUpdateListener
import android.app.Activity
import android.app.Application
-import android.content.*
+import android.content.ContentResolver
+import android.content.Intent
+import android.content.SyncStatusObserver
import android.content.pm.PackageManager
-import android.net.*
+import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
-import android.view.*
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.View
+import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
@@ -31,6 +37,7 @@ import at.bitfire.davdroid.DavUtils.SyncStatus
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.AccountListBinding
import at.bitfire.davdroid.databinding.AccountListItemBinding
+import at.bitfire.davdroid.syncadapter.AccountUtils
import at.bitfire.davdroid.ui.account.AccountActivity
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
@@ -249,11 +256,7 @@ class AccountListFragment: Fragment() {
val context = getApplication()
val collator = Collator.getInstance()
- val accountsFromManager = ArrayList()
- val accountManager = AccountManager.get(context)
- accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)).forEach { accountsFromManager.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.google_account_type)).forEach { accountsFromManager.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type)).forEach { accountsFromManager.add(it) }
+ val accountsFromManager = AccountUtils.getMainAccounts(context)
val sortedAccounts = accountsFromManager
.sortedWith { a, b ->
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.kt
index a8c63ad873a21176377ad95715f625e8c90b2a77..bdeb3d480eb77708c2def396e7c7cdcb011dfbd7 100644
--- a/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.kt
+++ b/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.kt
@@ -43,6 +43,7 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.SettingsManager
+import at.bitfire.davdroid.syncadapter.AccountUtils
import at.bitfire.ical4android.TaskProvider.ProviderName
import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat
import at.techbee.jtx.JtxContract
@@ -452,8 +453,10 @@ class DebugInfoActivity: AppCompatActivity() {
writer.append("\nACCOUNTS\n\n")
val accountManager = AccountManager.get(context)
- val mainAccounts = getMainAccounts(accountManager)
- val addressBookAccounts = getAddressBookAccounts(accountManager)
+ val mainAccounts = AccountUtils.getMainAccounts(context)
+ val addressBookAccounts = AccountUtils.getAddressBookAccounts(context)
+
+ val orphanAddressBook = mutableListOf()
for (account in mainAccounts) {
dumpMainAccount(account, writer)
@@ -467,13 +470,15 @@ class DebugInfoActivity: AppCompatActivity() {
)
if (mainAccount == account) {
dumpAddressBookAccount(addressBookAccount, accountManager, writer)
- iter.remove()
+ continue
}
+
+ orphanAddressBook.add(addressBookAccount)
}
}
- if (addressBookAccounts.isNotEmpty()) {
+ if (orphanAddressBook.isNotEmpty()) {
writer.append("Address book accounts without main account:\n")
- for (account in addressBookAccounts)
+ for (account in orphanAddressBook)
dumpAddressBookAccount(account, accountManager, writer)
}
@@ -491,28 +496,6 @@ class DebugInfoActivity: AppCompatActivity() {
debugInfo.postValue(debugInfoFile)
}
- private fun getMainAccounts(accountManager: AccountManager): ArrayList {
- val mainAccounts = ArrayList()
- accountManager.getAccountsByType(context.getString(R.string.eelo_account_type))
- .forEach { mainAccounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.google_account_type))
- .forEach { mainAccounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type))
- .forEach { mainAccounts.add(it) }
- return mainAccounts
- }
-
- private fun getAddressBookAccounts(accountManager: AccountManager): ArrayList {
- val addressBookAccounts = ArrayList()
- accountManager.getAccountsByType(context.getString(R.string.account_type_eelo_address_book))
- .forEach { addressBookAccounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type_google_address_book))
- .forEach { addressBookAccounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
- .forEach { addressBookAccounts.add(it) }
- return addressBookAccounts
- }
-
fun generateZip(onSuccess: (File) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
try {
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/account/RenameAccountFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/account/RenameAccountFragment.kt
index 9a68ecd8065c1837d3dea4253974cbee17b33fe4..ead1050ab2409bf11f9158c97e6a309205a374f9 100644
--- a/app/src/main/java/at/bitfire/davdroid/ui/account/RenameAccountFragment.kt
+++ b/app/src/main/java/at/bitfire/davdroid/ui/account/RenameAccountFragment.kt
@@ -36,6 +36,7 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalTaskList
import at.bitfire.davdroid.settings.AccountSettings
+import at.bitfire.davdroid.syncadapter.AccountUtils
import at.bitfire.davdroid.syncadapter.AccountsUpdatedListener
import at.bitfire.ical4android.TaskProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -143,7 +144,7 @@ class RenameAccountFragment: DialogFragment() {
accountManager.renameAccount(oldAccount, newName, {
if (it.result?.name == newName /* success */)
viewModelScope.launch(Dispatchers.Default + NonCancellable) {
- onAccountRenamed(accountManager, oldAccount, newName, syncIntervals)
+ onAccountRenamed(oldAccount, newName, syncIntervals)
// release AccountsUpdatedListener mutex at the end of this async coroutine
accountsUpdatedListener.mutex.release()
@@ -159,27 +160,16 @@ class RenameAccountFragment: DialogFragment() {
}
}
- private fun getAddressBookAccounts(accountManager: AccountManager): ArrayList {
- val addressBookAccounts = ArrayList()
- accountManager.getAccountsByType(context.getString(R.string.account_type_eelo_address_book))
- .forEach { addressBookAccounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type_google_address_book))
- .forEach { addressBookAccounts.add(it) }
- accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
- .forEach { addressBookAccounts.add(it) }
- return addressBookAccounts
- }
-
@SuppressLint("Recycle")
@WorkerThread
- fun onAccountRenamed(accountManager: AccountManager, oldAccount: Account, newName: String, syncIntervals: List>) {
+ fun onAccountRenamed(oldAccount: Account, newName: String, syncIntervals: List>) {
// account has now been renamed
Logger.log.info("Updating account name references")
// cancel maybe running synchronization
ContentResolver.cancelSync(oldAccount, null)
- for (addrBookAccount in getAddressBookAccounts(accountManager))
+ for (addrBookAccount in AccountUtils.getAddressBookAccounts(context))
ContentResolver.cancelSync(addrBookAccount, null)
// update account name references in database
@@ -196,7 +186,7 @@ class RenameAccountFragment: DialogFragment() {
try {
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.let { provider ->
try {
- for (addrBookAccount in getAddressBookAccounts(accountManager)) {
+ for (addrBookAccount in AccountUtils.getAddressBookAccounts(context)) {
val addressBook = LocalAddressBook(context, addrBookAccount, provider)
if (oldAccount == addressBook.mainAccount)
addressBook.mainAccount = Account(newName, oldAccount.type)
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/account/SettingsActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/account/SettingsActivity.kt
index e440a085040d6dea26d9592daf7e101835fb9713..56f097a9fac4a1349ea167c1817c781eea29eae0 100644
--- a/app/src/main/java/at/bitfire/davdroid/ui/account/SettingsActivity.kt
+++ b/app/src/main/java/at/bitfire/davdroid/ui/account/SettingsActivity.kt
@@ -121,7 +121,7 @@ class SettingsActivity: AppCompatActivity() {
}
private fun launchSetup(): Boolean {
- AccountManager.get(context).addAccount(getString(R.string.google_account_type),
+ AccountManager.get(context).addAccount(account.type,
null, null, null, activity, null,
null)
return true
@@ -235,7 +235,7 @@ class SettingsActivity: AppCompatActivity() {
prefUserName.summary = credentials.userName
prefUserName.text = credentials.userName
prefUserName.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newUserName ->
- model.updateCredentials(Credentials(newUserName as String, credentials.password, credentials.authState, credentials.certificateAlias))
+ model.updateCredentials(Credentials(newUserName as String, credentials.password, credentials.authState, credentials.certificateAlias, clientSecret = credentials.clientSecret))
false
}
@@ -254,7 +254,8 @@ class SettingsActivity: AppCompatActivity() {
credentials.userName,
newPassword as String,
credentials.authState,
- credentials.certificateAlias
+ credentials.certificateAlias,
+ clientSecret = credentials.clientSecret
)
)
false
@@ -268,7 +269,7 @@ class SettingsActivity: AppCompatActivity() {
prefCertAlias.summary = credentials.certificateAlias ?: getString(R.string.settings_certificate_alias_empty)
prefCertAlias.setOnPreferenceClickListener {
KeyChain.choosePrivateKeyAlias(requireActivity(), { newAlias ->
- model.updateCredentials(Credentials(credentials.userName, credentials.password, credentials.authState, newAlias))
+ model.updateCredentials(Credentials(credentials.userName, credentials.password, credentials.authState, newAlias, clientSecret = credentials.clientSecret))
}, null, null, null, -1, credentials.certificateAlias)
true
}
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt
index 235eb48eb65736de396ec6bd7d13adf66ee43a63..dff662c4303289eb1d1074d44b587d10c9cd5494 100644
--- a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt
+++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt
@@ -99,8 +99,7 @@ class AccountDetailsFragment : Fragment() {
model.nameError.value = getString(R.string.login_account_name_required)
else {
// check whether account name already exists
- val am = AccountManager.get(requireActivity())
- if (am.getAccountsByType(getString(R.string.account_type)).any { it.name == name }) {
+ if (AccountUtils.getMainAccounts(requireContext()).any { it.name == name }) {
model.nameError.value = getString(R.string.login_account_name_already_taken)
return@setOnClickListener
}
@@ -143,8 +142,8 @@ class AccountDetailsFragment : Fragment() {
} else
v.contactGroupMethod.isEnabled = true
- if (requireActivity().intent.getStringExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE) == LoginActivity.ACCOUNT_PROVIDER_EELO ||
- requireActivity().intent.getStringExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE) == LoginActivity.ACCOUNT_PROVIDER_GOOGLE) {
+ val providedAccountType = requireActivity().intent.getStringExtra(LoginActivity.ACCOUNT_TYPE)
+ if ((providedAccountType != getString(R.string.account_type)) && (providedAccountType in AccountUtils.getMainAccountTypes(requireContext()))) {
v.mainDetailLayout.visibility = View.GONE
v.mainLoadingLayout.visibility = View.VISIBLE
@@ -177,7 +176,7 @@ class AccountDetailsFragment : Fragment() {
.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE)?.onResult(null)
}
- if (requireActivity().intent.getStringExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE) == LoginActivity.ACCOUNT_PROVIDER_EELO) {
+ if (requireActivity().intent.getStringExtra(LoginActivity.ACCOUNT_TYPE) == getString(R.string.eelo_account_type)) {
val intent = Intent("drive.services.InitializerService")
intent.setPackage(getString(R.string.e_drive_package_name))
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, name)
@@ -242,13 +241,13 @@ class AccountDetailsFragment : Fragment() {
baseURL = config.calDAV.principal.toString()
}
- when (activity.intent.getStringExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE)) {
- LoginActivity.ACCOUNT_PROVIDER_EELO -> {
+ when (activity.intent.getStringExtra(LoginActivity.ACCOUNT_TYPE)) {
+ context.getString(R.string.eelo_account_type) -> {
accountType = context.getString(R.string.eelo_account_type)
addressBookAccountType = context.getString(R.string.account_type_eelo_address_book)
baseURL = credentials?.serverUri.toString()
}
- LoginActivity.ACCOUNT_PROVIDER_GOOGLE -> {
+ context.getString(R.string.google_account_type) -> {
accountType = context.getString(R.string.google_account_type)
addressBookAccountType = context.getString(R.string.account_type_google_address_book)
baseURL = null
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt
index 937e1925aad232949680d8b554aa1a53d1be5dfa..b945da0a8cb81e7ea2405a7e2eb79621afb89c19 100644
--- a/app/src/main/java/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt
+++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt
@@ -41,8 +41,8 @@ class DetectConfigurationFragment: Fragment() {
return
}
- val accountType = requireActivity().intent.getStringExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE)
- val isMurenaAccountType = (accountType == LoginActivity.ACCOUNT_PROVIDER_EELO)
+ val accountType = requireActivity().intent.getStringExtra(LoginActivity.ACCOUNT_TYPE)
+ val isMurenaAccountType = (accountType == getString(R.string.eelo_account_type))
model.detectConfiguration(loginModel, isMurenaAccountType).observe(this, { result ->
// save result for next step
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ef16ce84141d73331b7770613a462cb7bd8de43c
--- /dev/null
+++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthFragment.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright MURENA SAS 2023
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package at.bitfire.davdroid.ui.setup
+
+import android.os.Bundle
+import android.text.Layout
+import android.text.SpannableString
+import android.text.style.AlignmentSpan
+import android.view.View
+import at.bitfire.davdroid.R
+import at.bitfire.davdroid.authorization.IdentityProvider
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.json.JSONObject
+
+class GoogleAuthFragment : OpenIdAuthenticationBaseFragment(IdentityProvider.GOOGLE) {
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ handleConfirmationDialog()
+ }
+
+ override fun onAuthenticationComplete(userData: JSONObject) {
+ val emailKey = "email"
+
+ if (!userData.has(emailKey)) {
+ handleLoginFailedToast()
+ return
+ }
+
+ val email = userData.getString(emailKey)
+ if (email.isBlank()) {
+ handleLoginFailedToast()
+ return
+ }
+
+ val baseUrl = "https://apidata.googleusercontent.com/caldav/v2/$email/user"
+ proceedNext(email, baseUrl)
+ }
+
+ private fun handleConfirmationDialog() {
+ if (isAuthFlowComplete()) {
+ return
+ }
+
+ showConfirmationDialog()
+ }
+
+ private fun showConfirmationDialog() {
+ val title = SpannableString(getString(R.string.google_alert_title))
+ title.setSpan(
+ AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
+ 0,
+ title.length,
+ 0
+ )
+
+ MaterialAlertDialogBuilder(requireContext(), R.style.CustomAlertDialogStyle)
+ .setTitle(title)
+ .setMessage(R.string.google_alert_message)
+ .setCancelable(false)
+ .setPositiveButton(R.string.ok) { _, _ ->
+ startAuthFLow()
+ }.show()
+ }
+}
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
deleted file mode 100644
index 01d1d82a5c1f3c9681ac035199194b81bc250fd6..0000000000000000000000000000000000000000
--- a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorFragment.kt
+++ /dev/null
@@ -1,443 +0,0 @@
-/*
- * Copyright ECORP SAS 2022
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package at.bitfire.davdroid.ui.setup
-
-import android.app.Activity
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.net.ConnectivityManager
-import android.os.AsyncTask
-import android.os.Build
-import android.os.Bundle
-import android.os.Handler
-import android.os.Looper
-import android.text.Layout
-import android.text.SpannableString
-import android.text.style.AlignmentSpan
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Toast
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.fragment.app.viewModels
-import at.bitfire.davdroid.R
-import at.bitfire.davdroid.authorization.IdentityProvider
-import at.bitfire.davdroid.databinding.FragmentGoogleAuthenticatorBinding
-import at.bitfire.davdroid.db.Credentials
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import net.openid.appauth.*
-import org.json.JSONException
-import org.json.JSONObject
-import java.io.BufferedReader
-import java.io.IOException
-import java.io.InputStream
-import java.io.InputStreamReader
-import java.net.HttpURLConnection
-import java.net.MalformedURLException
-import java.net.URI
-import java.net.URL
-
-class GoogleAuthenticatorFragment : Fragment(), AuthorizationService.TokenResponseCallback {
-
- private val model by viewModels()
- private val loginModel by activityViewModels()
-
- 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
-
- private fun isNetworkAvailable(): Boolean {
- val connectivityManager = requireActivity().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
- val activeNetworkInfo = connectivityManager.activeNetworkInfo
- return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting
- }
-
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View {
- // Initialise the authorization service
- authorizationService = AuthorizationService(requireContext())
-
- val v = FragmentGoogleAuthenticatorBinding.inflate(inflater, container, false)
- v.lifecycleOwner = this
- v.model = model
-
- activity?.intent?.let {
- model.initialize(it)
- val builder = MaterialAlertDialogBuilder(requireContext(), R.style.CustomAlertDialogStyle)
-
- if (!with(it) { getBooleanExtra(LoginActivity.ACCOUNT_PROVIDER_GOOGLE_AUTH_COMPLETE, false) }) {
- val title = SpannableString(getString(R.string.google_alert_title))
- // alert dialog title align center
- title.setSpan(
- AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
- 0,
- title.length,
- 0
- )
-
- builder.setTitle(title)
- builder.setMessage(getString(R.string.google_alert_message))
- builder.setPositiveButton(android.R.string.yes) { dialog, which ->
- // 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 if (!isNetworkAvailable()) {
- Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
- requireActivity().finish()
- } else {
- Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show()
- requireActivity().finish()
- }
- }
-
- if (idp.name == getString(R.string.google_name)) {
- // Get configurations for the Google account provider
- idp.retrieveConfig(context, retrieveCallback)
- }
- }
- }
- builder.setCancelable(false)
-
- val dialog = builder.create()
- dialog.show()
- }
- else {
- if (authState == null) {
- val response = AuthorizationResponse.fromIntent(requireActivity().intent)
- val ex = AuthorizationException.fromIntent(requireActivity().intent)
- authState = AuthState(response, ex)
-
- if (response != null) {
- exchangeAuthorizationCode(response)
- } else if (!isNetworkAvailable()) {
- Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
- requireActivity().finish()
- } else {
- Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show()
- requireActivity().finish()
- }
- }
- }
- }
-
- return v.root
- }
-
- private fun makeAuthRequest(
- serviceConfig: AuthorizationServiceConfiguration,
- idp: IdentityProvider) {
-
- if (!isNetworkAvailable()) {
- Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
- requireActivity().finish()
- }
-
- val authRequest = AuthorizationRequest.Builder(
- serviceConfig,
- idp.clientId,
- ResponseTypeValues.CODE,
- idp.redirectUri)
- .setScope(idp.scope)
- .build()
-
- authorizationService?.performAuthorizationRequest(
- authRequest,
- createPostAuthorizationIntent(
- requireContext(),
- authRequest,
- serviceConfig.discoveryDoc,
- idp.clientSecret),
- authorizationService?.createCustomTabsIntentBuilder()!!
- .build())
-
- requireActivity().setResult(Activity.RESULT_OK)
- requireActivity().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)
- intent.putExtra(LoginActivity.OPEN_APP_PACKAGE_AFTER_AUTH, requireActivity().intent.getStringExtra(LoginActivity.OPEN_APP_PACKAGE_AFTER_AUTH))
- intent.putExtra(LoginActivity.OPEN_APP_ACTIVITY_AFTER_AUTH, requireActivity().intent.getStringExtra(LoginActivity.OPEN_APP_ACTIVITY_AFTER_AUTH))
-
- var flag = 0
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- flag = flag or PendingIntent.FLAG_MUTABLE
- }
-
- return PendingIntent.getActivity(context, request.hashCode(), intent, flag)
- }
-
- private fun exchangeAuthorizationCode(authorizationResponse: AuthorizationResponse) {
- if (!isNetworkAvailable()) {
- Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
- requireActivity().finish()
- }
-
- val additionalParams = HashMap()
- if (getClientSecretFromIntent(requireActivity().intent) != null) {
- additionalParams["client_secret"] = getClientSecretFromIntent(requireActivity().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) {
- if (!isNetworkAvailable()) {
- Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
- requireActivity().finish()
- }
-
- authorizationService?.performTokenRequest(
- request, this)
- }
-
- override fun onTokenRequestCompleted(response: TokenResponse?, ex: AuthorizationException?) {
- authState?.update(response, ex)
-
- getAccountInfo()
- }
-
- private fun getAccountInfo() {
- if (!isNetworkAvailable()) {
- Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
- requireActivity().finish()
- }
-
- val discoveryDoc = getDiscoveryDocFromIntent(requireActivity().intent)
-
- if (!authState!!.isAuthorized
- || discoveryDoc == null
- || discoveryDoc.userinfoEndpoint == null) {
- Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show()
- requireActivity().finish()
- }
- else {
- object : AsyncTask() {
- override fun doInBackground(vararg params: Void): Void? {
- if (fetchUserInfo()) {
- Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show()
- requireActivity().finish()
- }
-
- 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(): Boolean {
- var error = false
-
- if (authState!!.authorizationServiceConfiguration == null) {
- return true
- }
-
- authState!!.performActionWithFreshTokens(authorizationService!!, AuthState.AuthStateAction { accessToken, _, ex ->
- if (ex != null) {
- error = true
- return@AuthStateAction
- }
-
- val discoveryDoc = getDiscoveryDocFromIntent(requireActivity().intent)
- ?: throw IllegalStateException("no available discovery doc")
-
- val userInfoEndpoint: URL
- try {
- userInfoEndpoint = URL(discoveryDoc.userinfoEndpoint!!.toString())
- }
- catch (urlEx: MalformedURLException) {
- error = true
- 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) {
- error = true
- }
- catch (jsonEx: JSONException) {
- error = true
- }
- finally {
- if (userInfoResponse != null) {
- try {
- userInfoResponse.close()
- }
- catch (ioEx: IOException) {
- error = true
- }
-
- }
- }
- })
-
- return error
- }
-
- @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 (!isNetworkAvailable()) {
- Toast.makeText(context, "Please check your internet connection", Toast.LENGTH_LONG).show()
- requireActivity().finish()
- }
-
- if (userInfoJson != null) {
- try {
-
- var emailAddress = ""
- if (userInfoJson!!.has("email")) {
- emailAddress = userInfoJson!!.getString("email")
- }
-
- if (validate(emailAddress, authState!!))
- requireFragmentManager().beginTransaction()
- .replace(android.R.id.content, DetectConfigurationFragment(), null)
- .addToBackStack(null)
- .commit()
-
- }
- catch (ex: JSONException) {
- Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show()
- requireActivity().finish()
- }
-
- }
- else {
- Toast.makeText(context, "Login failed, please try again later", Toast.LENGTH_LONG).show()
- requireActivity().finish()
- }
-
- }
-
- private fun validate(emailAddress: String, authState: AuthState): Boolean {
- var valid = false
-
- fun validateUrl() {
- model.baseUrlError.value = null
- try {
- val uri = URI("https://apidata.googleusercontent.com/caldav/v2/$emailAddress/user")
- 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, authState, 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
deleted file mode 100644
index d7721753794b516c2f7930a8f300603a895785cd..0000000000000000000000000000000000000000
--- a/app/src/main/java/at/bitfire/davdroid/ui/setup/GoogleAuthenticatorModel.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package at.bitfire.davdroid.ui.setup
-
-import android.content.Intent
-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/InviteSuccessfulFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/InviteSuccessfulFragment.kt
index 4ee8cc6fb57bdacc7e81ed7e8d7bee5a1026ddc6..9af625498a14cf75e2a44a7086d9570371c61dcf 100644
--- a/app/src/main/java/at/bitfire/davdroid/ui/setup/InviteSuccessfulFragment.kt
+++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/InviteSuccessfulFragment.kt
@@ -41,8 +41,9 @@ class InviteSuccessfulFragment : Fragment() {
try {
activity?.let {
val accountManager = AccountManager.get(it)
+ val accountType = it.getString(R.string.eelo_account_type)
accountManager.addAccount(
- "e.foundation.webdav.eelo",
+ accountType,
null,
null,
null,
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 cfdcbceb779daf8ba893f36475ad8c0a4b91d2bf..9d1a8e3dfe640f3cad673c8719036de5e4218068 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
@@ -5,15 +5,9 @@
package at.bitfire.davdroid.ui.setup
import android.os.Bundle
-import android.text.Layout
-import android.text.SpannableString
-import android.text.style.AlignmentSpan
-import android.util.Log
import android.view.MenuItem
-import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
-import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -44,23 +38,20 @@ class LoginActivity: AppCompatActivity() {
*/
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"
+ const val ACCOUNT_TYPE = "account_type"
+ const val OPENID_AUTH_FLOW_COMPLETE = "openId_authFlow_complete"
const val OPEN_APP_PACKAGE_AFTER_AUTH = "open_app_package_after_auth"
const val OPEN_APP_ACTIVITY_AFTER_AUTH = "open_app_activity_after_auth"
const val IGNORE_ACCOUNT_SETUP = "ignore_account_setup"
-
+
const val RETRY_ON_401 = "retry_on_401"
}
@Inject
lateinit var loginFragmentFactories: Map
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -76,25 +67,13 @@ class LoginActivity: AppCompatActivity() {
}
if (fragment != null) {
- when (intent.getStringExtra(SETUP_ACCOUNT_PROVIDER_TYPE)) {
- ACCOUNT_PROVIDER_EELO -> {
- supportFragmentManager.beginTransaction()
- .replace(android.R.id.content, EeloAuthenticatorFragment())
- .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
+ // 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/java/at/bitfire/davdroid/ui/setup/LoginModel.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginModel.kt
index a5fc3cbc6d2b06bf2b2e5ee647a799ccd3c7520b..c3410489edb2e5e52bbf0fb5cfeaa95eda0c77a1 100644
--- a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginModel.kt
+++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginModel.kt
@@ -12,6 +12,7 @@ import java.net.URI
class LoginModel: ViewModel() {
var baseURI: URI? = null
+ var cardDavURI: URI? = null
var credentials: Credentials? = null
var configuration: DavResourceFinder.Configuration? = null
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/MurenaLoginFragmentFactory.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/MurenaLoginFragmentFactory.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b87f7447900ed92b81cfddd9766f475b09d3a0f4
--- /dev/null
+++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/MurenaLoginFragmentFactory.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright MURENA SAS 2023
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package at.bitfire.davdroid.ui.setup
+
+import android.content.Context
+import android.content.Intent
+import androidx.fragment.app.Fragment
+import at.bitfire.davdroid.R
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import dagger.multibindings.IntKey
+import dagger.multibindings.IntoMap
+import javax.inject.Inject
+
+class MurenaLoginFragmentFactory @Inject constructor(@ApplicationContext val context: Context) : LoginCredentialsFragmentFactory {
+
+ override fun getFragment(intent: Intent): Fragment? {
+ val accountType = intent.getStringExtra(LoginActivity.ACCOUNT_TYPE) ?: return null
+
+ return when (accountType) {
+ context.getString(R.string.eelo_account_type) -> EeloAuthenticatorFragment()
+ context.getString(R.string.google_account_type) -> GoogleAuthFragment()
+ else -> null
+ }
+ }
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class MurenaLoginFlowFragmentModule {
+ @Binds
+ @IntoMap
+ @IntKey(/* priority */ 30)
+ abstract fun factory(impl: MurenaLoginFragmentFactory): LoginCredentialsFragmentFactory
+}
\ No newline at end of file
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationBaseFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationBaseFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e4808648f6b1fd2ffdc0aa0e7ff92b121e2a2a18
--- /dev/null
+++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationBaseFragment.kt
@@ -0,0 +1,216 @@
+/*
+ * Copyright MURENA SAS 2023
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package at.bitfire.davdroid.ui.setup
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import at.bitfire.davdroid.R
+import at.bitfire.davdroid.authorization.IdentityProvider
+import at.bitfire.davdroid.db.Credentials
+import at.bitfire.davdroid.log.Logger
+import at.bitfire.davdroid.ui.NetworkUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import net.openid.appauth.AuthState.AuthStateAction
+import net.openid.appauth.AuthorizationException
+import net.openid.appauth.AuthorizationResponse
+import net.openid.appauth.AuthorizationService.TokenResponseCallback
+import net.openid.appauth.AuthorizationServiceConfiguration
+import org.json.JSONObject
+import java.net.URI
+import java.util.logging.Level
+
+abstract class OpenIdAuthenticationBaseFragment(private val identityProvider: IdentityProvider) :
+ Fragment() {
+
+ private val viewModel by viewModels()
+ private val loginModel by activityViewModels()
+
+ private val authReqActivityResultLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+ val intent = result.data
+ intent?.let {
+ val response = AuthorizationResponse.fromIntent(it)
+ val exception = AuthorizationException.fromIntent(it)
+
+ if (response == null || exception != null) {
+ Logger.log.log(Level.SEVERE, "Failed to retrieve auth response", exception)
+ handleLoginFailedToast()
+ return@let
+ }
+
+ performTokenRequest(response, exception)
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ viewModel.identityProvider = identityProvider
+
+ return inflater.inflate(R.layout.frament_openid_auth, container, false)
+ }
+
+ protected fun isAuthFlowComplete(): Boolean {
+ return activity?.intent?.getBooleanExtra(
+ LoginActivity.OPENID_AUTH_FLOW_COMPLETE,
+ false
+ ) == true
+ }
+
+ protected fun startAuthFLow() {
+ if (isAuthFlowComplete()) {
+ return
+ }
+
+ if (handleNoNetworkToast()) {
+ return
+ }
+
+ viewModel.identityProvider?.retrieveConfig { serviceConfiguration, exception ->
+ if (exception != null || serviceConfiguration == null) {
+ Logger.log.log(Level.SEVERE, "failed to fetch configuration", exception)
+ handleLoginFailedToast()
+ return@retrieveConfig
+ }
+
+ obtainAuthCode(serviceConfiguration)
+ }
+ }
+
+ private fun obtainAuthCode(serviceConfiguration: AuthorizationServiceConfiguration) {
+ if (handleNoNetworkToast()) {
+ return
+ }
+
+ val authIntent = viewModel.getAuthIntent(serviceConfiguration)
+ authReqActivityResultLauncher.launch(authIntent)
+ }
+
+ private fun performTokenRequest(
+ authorizationResponse: AuthorizationResponse,
+ authorizationException: AuthorizationException?
+ ) {
+ if (handleNoNetworkToast()) {
+ return
+ }
+
+ viewModel.performTokenRequest(
+ authorizationResponse,
+ authorizationException,
+ getTokenResponseCallback()
+ )
+ }
+
+ private fun getTokenResponseCallback(): TokenResponseCallback {
+ return TokenResponseCallback { response, exception ->
+ if (response == null || exception != null) {
+ Logger.log.log(Level.SEVERE, "failed to retrieve token", exception)
+ handleLoginFailedToast()
+ return@TokenResponseCallback
+ }
+
+ viewModel.retrieveAccountInfo(response, exception, getAuthStateActionCallback())
+ }
+ }
+
+ private fun getAuthStateActionCallback(): AuthStateAction {
+ return AuthStateAction { accessToken, _, exception ->
+ if (exception != null || accessToken == null) {
+ Logger.log.log(Level.SEVERE, "failed to retrieve user info", exception)
+ handleLoginFailedToast()
+ return@AuthStateAction
+ }
+
+ val infoEndpoint = viewModel.getUserInfoEndpoint()
+ if (infoEndpoint == null) {
+ handleLoginFailedToast()
+ return@AuthStateAction
+ }
+
+ lifecycleScope.launch(Dispatchers.IO) {
+ val infoJson = viewModel.retrieveUserInfoFromServer(infoEndpoint, accessToken)
+ if (infoJson == null) {
+ lifecycleScope.launch(Dispatchers.Main) {
+ handleLoginFailedToast()
+ }
+ return@launch
+ }
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ onAuthenticationComplete(infoJson)
+ }
+ }
+ }
+ }
+
+ private fun finishActivity() {
+ this@OpenIdAuthenticationBaseFragment.requireActivity().finish()
+ }
+
+ private fun handleNoNetworkToast(): Boolean {
+ if (NetworkUtils.isConnectedToNetwork(requireContext())) {
+ return false
+ }
+
+ Toast.makeText(requireContext(), R.string.no_internet_toast, Toast.LENGTH_LONG).show()
+ finishActivity()
+ return true
+ }
+
+ protected fun handleLoginFailedToast() {
+ Toast.makeText(requireContext(), R.string.login_failed, Toast.LENGTH_LONG).show()
+ finishActivity()
+ }
+
+ protected fun proceedNext(userName: String, baseUrl: String, cardDavUrl: String? = null) {
+ activity?.intent?.putExtra(LoginActivity.OPENID_AUTH_FLOW_COMPLETE, true)
+
+ if (cardDavUrl != null) {
+ loginModel.cardDavURI = URI(cardDavUrl)
+ }
+
+ val baseUri = URI(baseUrl)
+ loginModel.baseURI = baseUri
+ loginModel.credentials = Credentials(
+ userName,
+ null,
+ viewModel.getAuthState(),
+ null,
+ baseUri,
+ identityProvider.clientSecret
+ )
+
+ parentFragmentManager.beginTransaction()
+ .replace(android.R.id.content, DetectConfigurationFragment(), null)
+ .addToBackStack(null)
+ .commit()
+ }
+
+ protected abstract fun onAuthenticationComplete(userData: JSONObject)
+}
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationViewModel.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f29cec9369fe0f1a59cc462ead2cee87f30c067a
--- /dev/null
+++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationViewModel.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright MURENA SAS 2023
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package at.bitfire.davdroid.ui.setup
+
+import android.app.Application
+import android.content.Intent
+import androidx.annotation.WorkerThread
+import androidx.lifecycle.AndroidViewModel
+import at.bitfire.davdroid.authorization.IdentityProvider
+import at.bitfire.davdroid.log.Logger
+import net.openid.appauth.*
+import net.openid.appauth.AuthState.AuthStateAction
+import net.openid.appauth.AuthorizationService.TokenResponseCallback
+import okio.buffer
+import okio.source
+import org.json.JSONObject
+import java.net.HttpURLConnection
+import java.net.URL
+import java.nio.charset.StandardCharsets
+import java.util.logging.Level
+
+class OpenIdAuthenticationViewModel(application: Application) : AndroidViewModel(application) {
+
+ private val authorizationService: AuthorizationService = AuthorizationService(getApplication())
+ private var authState: AuthState? = null
+ var identityProvider: IdentityProvider? = null
+
+ override fun onCleared() {
+ authorizationService.dispose()
+ super.onCleared()
+ }
+
+ fun getAuthState(): AuthState {
+ return authState!!
+ }
+
+ fun getAuthIntent(serviceConfiguration: AuthorizationServiceConfiguration): Intent {
+ authState = AuthState(serviceConfiguration)
+
+ val authRequestBuilder = AuthorizationRequest.Builder(
+ serviceConfiguration,
+ identityProvider!!.clientId,
+ ResponseTypeValues.CODE,
+ identityProvider!!.redirectUri
+ )
+
+ authRequestBuilder.setScopes(identityProvider!!.scope)
+
+ return authorizationService.getAuthorizationRequestIntent(authRequestBuilder.build())
+ }
+
+ fun performTokenRequest(
+ authorizationResponse: AuthorizationResponse,
+ authorizationException: AuthorizationException?,
+ callback: TokenResponseCallback
+ ) {
+ authState?.update(authorizationResponse, authorizationException)
+ val request = authorizationResponse.createTokenExchangeRequest()
+
+ if (identityProvider?.clientSecret != null) {
+ val clientAuth = ClientSecretBasic(identityProvider!!.clientSecret!!)
+ authorizationService.performTokenRequest(request, clientAuth, callback)
+ return
+ }
+
+ authorizationService.performTokenRequest(request, callback)
+ }
+
+ @WorkerThread
+ fun retrieveUserInfoFromServer(infoEndpoint: String, accessToken: String): JSONObject? {
+ try {
+ val userInfoEndpoint = URL(infoEndpoint)
+ val conn = userInfoEndpoint.openConnection() as HttpURLConnection
+ conn.setRequestProperty("Authorization", "Bearer $accessToken")
+ conn.instanceFollowRedirects = false
+ val response = conn.inputStream.source().buffer()
+ .readString(StandardCharsets.UTF_8)
+ conn.inputStream.close()
+ return JSONObject(response)
+ } catch (ex: Exception) {
+ Logger.log.log(Level.SEVERE, "failed to retrieve userInfo", ex)
+ }
+
+ return null
+ }
+
+ fun getUserInfoEndpoint(): String? {
+ var infoEndpoint = identityProvider?.userInfoEndpoint
+
+ if (infoEndpoint == null) {
+ val discovery = authState?.authorizationServiceConfiguration?.discoveryDoc ?: return null
+ infoEndpoint = discovery.userinfoEndpoint.toString()
+ }
+
+ return infoEndpoint
+ }
+
+ fun retrieveAccountInfo(
+ response: TokenResponse,
+ exception: AuthorizationException?,
+ callback: AuthStateAction
+ ) {
+ authState?.update(response, exception)
+
+ if (identityProvider?.clientSecret != null) {
+ val clientAuth = ClientSecretBasic(identityProvider!!.clientSecret!!)
+ authState?.performActionWithFreshTokens(authorizationService, clientAuth, callback)
+ return
+ }
+
+ authState?.performActionWithFreshTokens(authorizationService, callback)
+ }
+}
diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/SendInviteFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/SendInviteFragment.kt
index 6dc1dfb8964c1e03111b2766668b76d8db563c26..89b7b340bf351d346a20465f87959087f1b5ed8e 100644
--- a/app/src/main/java/at/bitfire/davdroid/ui/setup/SendInviteFragment.kt
+++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/SendInviteFragment.kt
@@ -154,8 +154,9 @@ class SendInviteFragment : Fragment() {
.setNegativeButton(R.string.login_button) { _,_ ->
try {
val accountManager = AccountManager.get(it)
+ val accountType = it.getString(R.string.eelo_account_type)
accountManager.addAccount(
- "e.foundation.webdav.eelo",
+ accountType,
null,
null,
null,
diff --git a/app/src/main/res/layout/fragment_google_authenticator.xml b/app/src/main/res/layout/fragment_google_authenticator.xml
deleted file mode 100644
index 1b3ed112cb000c829234a9e9fd5c25ec65f3b5f7..0000000000000000000000000000000000000000
--- a/app/src/main/res/layout/fragment_google_authenticator.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/frament_openid_auth.xml b/app/src/main/res/layout/frament_openid_auth.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b4bd4feb501c2efcba635df3719e64de7868ec4c
--- /dev/null
+++ b/app/src/main/res/layout/frament_openid_auth.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/account_providers_auth_config.xml b/app/src/main/res/values/account_providers_auth_config.xml
deleted file mode 100644
index b3b10fdbaa873cc965b9d52e768ab19a89bc1349..0000000000000000000000000000000000000000
--- a/app/src/main/res/values/account_providers_auth_config.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
- 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 https://mail.google.com/
-
-
- 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 19d2a57c2b613fb81dfd4a20e5a096e0be35d77b..3217c8adfd2fd72dcc847e20ba04be37dc0b31e2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -568,4 +568,7 @@
Service detection failed
Failed to refresh collection list
+
+ Login failed, please try again later
+ OK