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