mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2025-12-23 23:17:50 -05:00
Support Fastmail OAuth (#1509)
* Add Fastmail OAuth login implementation
* [CI] Run tests on API level 36, too
* Add Fastmail OAuth login support
* Remove logging and move companion object to bottom
* Remove FastmailLogin and GoogleLogin to OAuthLogin and OAuthGoogle
- Remove FastmailLogin class
- Refactor GoogleLogin class to OAuthGoogle object
- Update AndroidManifest.xml to use ${applicationId} for OAuth redirect URI
- Add OAuthFastmail object for Fastmail OAuth integration
- Update GoogleLoginModel and FastmailLoginModel to use OAuthGoogle and OAuthFastmail respectively
- Add OAuthIntegration object for shared OAuth functionality
* Update Fastmail authentication error message and add redirect URI documentation
* Add error handling for refresh token exception
This commit is contained in:
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.openid.appauth.AuthState
|
||||
import net.openid.appauth.AuthorizationException
|
||||
import net.openid.appauth.AuthorizationRequest
|
||||
import net.openid.appauth.AuthorizationResponse
|
||||
import net.openid.appauth.AuthorizationService
|
||||
import net.openid.appauth.AuthorizationServiceConfiguration
|
||||
import net.openid.appauth.ResponseTypeValues
|
||||
import net.openid.appauth.TokenResponse
|
||||
import java.net.URI
|
||||
import java.util.logging.Logger
|
||||
|
||||
class GoogleLogin(
|
||||
val authService: AuthorizationService
|
||||
) {
|
||||
|
||||
private val logger: Logger = Logger.getGlobal()
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
// davx5integration@gmail.com (for davx5-ose)
|
||||
private const val CLIENT_ID = "1069050168830-eg09u4tk1cmboobevhm4k3bj1m4fav9i.apps.googleusercontent.com"
|
||||
|
||||
private val SCOPES = arrayOf(
|
||||
"https://www.googleapis.com/auth/calendar", // CalDAV
|
||||
"https://www.googleapis.com/auth/carddav" // CardDAV
|
||||
)
|
||||
|
||||
/**
|
||||
* Gets the Google CalDAV/CardDAV base URI. See https://developers.google.com/calendar/caldav/v2/guide;
|
||||
* _calid_ of the primary calendar is the account name.
|
||||
*
|
||||
* This URL allows CardDAV (over well-known URLs) and CalDAV detection including calendar-homesets and secondary
|
||||
* calendars.
|
||||
*/
|
||||
fun googleBaseUri(googleAccount: String): URI =
|
||||
URI("https", "apidata.googleusercontent.com", "/caldav/v2/$googleAccount/user", null)
|
||||
|
||||
private val serviceConfig = AuthorizationServiceConfiguration(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth".toUri(),
|
||||
"https://oauth2.googleapis.com/token".toUri()
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
fun signIn(email: String, customClientId: String?, locale: String?): AuthorizationRequest {
|
||||
val builder = AuthorizationRequest.Builder(
|
||||
serviceConfig,
|
||||
customClientId ?: CLIENT_ID,
|
||||
ResponseTypeValues.CODE,
|
||||
(BuildConfig.APPLICATION_ID + ":/oauth2/redirect").toUri()
|
||||
)
|
||||
return builder
|
||||
.setScopes(*SCOPES)
|
||||
.setLoginHint(email)
|
||||
.setUiLocales(locale)
|
||||
.build()
|
||||
}
|
||||
|
||||
suspend fun authenticate(authResponse: AuthorizationResponse): Credentials {
|
||||
val authState = AuthState(authResponse, null) // authorization code must not be stored; exchange it to refresh token
|
||||
val credentials = CompletableDeferred<Credentials>()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
authService.performTokenRequest(authResponse.createTokenExchangeRequest()) { tokenResponse: TokenResponse?, refreshTokenException: AuthorizationException? ->
|
||||
logger.info("Refresh token response: ${tokenResponse?.jsonSerializeString()}")
|
||||
|
||||
if (tokenResponse != null) {
|
||||
// success, save authState (= refresh token)
|
||||
authState.update(tokenResponse, refreshTokenException)
|
||||
credentials.complete(Credentials(authState = authState))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return credentials.await()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import net.openid.appauth.AuthorizationRequest
|
||||
import net.openid.appauth.AuthorizationServiceConfiguration
|
||||
import net.openid.appauth.ResponseTypeValues
|
||||
import java.net.URI
|
||||
|
||||
object OAuthFastmail {
|
||||
|
||||
// DAVx5 Client ID (issued by Fastmail)
|
||||
private const val CLIENT_ID = "34ce41ae"
|
||||
|
||||
private val SCOPES = arrayOf(
|
||||
"https://www.fastmail.com/dev/protocol-caldav", // CalDAV
|
||||
"https://www.fastmail.com/dev/protocol-carddav" // CardDAV
|
||||
)
|
||||
|
||||
/**
|
||||
* The base URL for Fastmail. Note that this URL is used for both CalDAV and CardDAV;
|
||||
* the SRV records of the domain are checked to determine the respective service base URL.
|
||||
*/
|
||||
val baseUri: URI = URI.create("https://fastmail.com/")
|
||||
|
||||
private val serviceConfig = AuthorizationServiceConfiguration(
|
||||
"https://api.fastmail.com/oauth/authorize".toUri(),
|
||||
"https://api.fastmail.com/oauth/refresh".toUri()
|
||||
)
|
||||
|
||||
|
||||
fun signIn(email: String, locale: String?): AuthorizationRequest {
|
||||
val builder = AuthorizationRequest.Builder(
|
||||
serviceConfig,
|
||||
CLIENT_ID,
|
||||
ResponseTypeValues.CODE,
|
||||
OAuthIntegration.redirectUri
|
||||
)
|
||||
return builder
|
||||
.setScopes(*SCOPES)
|
||||
.setLoginHint(email)
|
||||
.setUiLocales(locale)
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import net.openid.appauth.AuthorizationRequest
|
||||
import net.openid.appauth.AuthorizationServiceConfiguration
|
||||
import net.openid.appauth.ResponseTypeValues
|
||||
import java.net.URI
|
||||
|
||||
object OAuthGoogle {
|
||||
|
||||
// davx5integration@gmail.com (for davx5-ose)
|
||||
private const val CLIENT_ID = "1069050168830-eg09u4tk1cmboobevhm4k3bj1m4fav9i.apps.googleusercontent.com"
|
||||
|
||||
private val SCOPES = arrayOf(
|
||||
"https://www.googleapis.com/auth/calendar", // CalDAV
|
||||
"https://www.googleapis.com/auth/carddav" // CardDAV
|
||||
)
|
||||
|
||||
/**
|
||||
* Gets the Google CalDAV/CardDAV base URI. See https://developers.google.com/calendar/caldav/v2/guide;
|
||||
* _calid_ of the primary calendar is the account name.
|
||||
*
|
||||
* This URL allows CardDAV (over well-known URLs) and CalDAV detection including calendar-homesets and secondary
|
||||
* calendars.
|
||||
*/
|
||||
fun baseUri(googleAccount: String): URI =
|
||||
URI("https", "apidata.googleusercontent.com", "/caldav/v2/$googleAccount/user", null)
|
||||
|
||||
private val serviceConfig = AuthorizationServiceConfiguration(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth".toUri(),
|
||||
"https://oauth2.googleapis.com/token".toUri()
|
||||
)
|
||||
|
||||
|
||||
fun signIn(email: String, customClientId: String?, locale: String?): AuthorizationRequest {
|
||||
val builder = AuthorizationRequest.Builder(
|
||||
serviceConfig,
|
||||
customClientId ?: CLIENT_ID,
|
||||
ResponseTypeValues.CODE,
|
||||
OAuthIntegration.redirectUri
|
||||
)
|
||||
return builder
|
||||
.setScopes(*SCOPES)
|
||||
.setLoginHint(email)
|
||||
.setUiLocales(locale)
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.network.OAuthIntegration.redirectUri
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.openid.appauth.AuthState
|
||||
import net.openid.appauth.AuthorizationException
|
||||
import net.openid.appauth.AuthorizationResponse
|
||||
import net.openid.appauth.AuthorizationService
|
||||
import net.openid.appauth.TokenResponse
|
||||
|
||||
/**
|
||||
* Integration with OpenID AppAuth (Android)
|
||||
*/
|
||||
object OAuthIntegration {
|
||||
|
||||
/** redirect URI, must be registered in Manifest */
|
||||
val redirectUri =
|
||||
(BuildConfig.APPLICATION_ID + ":/oauth2/redirect").toUri()
|
||||
|
||||
/**
|
||||
* Called by the authorization service when the login is finished and [redirectUri] is launched.
|
||||
*/
|
||||
suspend fun authenticate(authService: AuthorizationService, authResponse: AuthorizationResponse): Credentials {
|
||||
val authState = AuthState(authResponse, null) // authorization code must not be stored; exchange it to refresh token
|
||||
val credentials = CompletableDeferred<Credentials>()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
authService.performTokenRequest(authResponse.createTokenExchangeRequest()) { tokenResponse: TokenResponse?, refreshTokenException: AuthorizationException? ->
|
||||
if (tokenResponse != null) {
|
||||
// success, save authState (= refresh token)
|
||||
authState.update(tokenResponse, refreshTokenException)
|
||||
credentials.complete(Credentials(authState = authState))
|
||||
} else if (refreshTokenException != null)
|
||||
credentials.completeExceptionally(refreshTokenException)
|
||||
}
|
||||
}
|
||||
|
||||
return credentials.await()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.ui.setup
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Email
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.Constants.withStatParams
|
||||
import at.bitfire.davdroid.R
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
|
||||
object FastmailLogin : LoginType {
|
||||
|
||||
override val title: Int
|
||||
get() = R.string.login_fastmail
|
||||
|
||||
override val helpUrl: Uri
|
||||
get() = Constants.HOMEPAGE_URL.buildUpon()
|
||||
.appendPath(Constants.HOMEPAGE_PATH_TESTED_SERVICES)
|
||||
.appendPath("fastmail")
|
||||
.withStatParams("LoginTypeFastmail")
|
||||
.build()
|
||||
|
||||
|
||||
@Composable
|
||||
override fun LoginScreen(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
initialLoginInfo: LoginInfo,
|
||||
onLogin: (LoginInfo) -> Unit
|
||||
) {
|
||||
val model: FastmailLoginModel = hiltViewModel(
|
||||
creationCallback = { factory: FastmailLoginModel.Factory ->
|
||||
factory.create(loginInfo = initialLoginInfo)
|
||||
}
|
||||
)
|
||||
|
||||
val uiState = model.uiState
|
||||
LaunchedEffect(uiState.result) {
|
||||
if (uiState.result != null) {
|
||||
onLogin(uiState.result)
|
||||
model.resetResult()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.error) {
|
||||
if (uiState.error != null)
|
||||
snackbarHostState.showSnackbar(uiState.error)
|
||||
}
|
||||
|
||||
// contract to open the browser for authentication
|
||||
val authRequestContract = rememberLauncherForActivityResult(contract = model.AuthorizationContract()) { authResponse ->
|
||||
if (authResponse != null)
|
||||
model.authenticate(authResponse)
|
||||
else
|
||||
model.authCodeFailed()
|
||||
}
|
||||
|
||||
FastmailLoginScreen(
|
||||
email = uiState.email,
|
||||
onSetEmail = model::setEmail,
|
||||
canContinue = uiState.canContinue,
|
||||
onLogin = {
|
||||
if (uiState.canContinue) {
|
||||
val authRequest = model.signIn()
|
||||
|
||||
try {
|
||||
authRequestContract.launch(authRequest)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Logger.getGlobal().log(Level.WARNING, "Couldn't start OAuth intent", e)
|
||||
model.signInFailed()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FastmailLoginScreen(
|
||||
email: String,
|
||||
onSetEmail: (String) -> Unit = {},
|
||||
canContinue: Boolean,
|
||||
onLogin: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.padding(8.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.login_fastmail),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
OutlinedTextField(
|
||||
email,
|
||||
singleLine = true,
|
||||
onValueChange = onSetEmail,
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Email, null)
|
||||
},
|
||||
label = { Text(stringResource(R.string.login_fastmail_account)) },
|
||||
placeholder = { Text("example@fastmail.com") },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Email,
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
.focusRequester(focusRequester)
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
if (email.isEmpty())
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
Button(
|
||||
enabled = canContinue,
|
||||
onClick = { onLogin() },
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.wrapContentSize()
|
||||
) {
|
||||
Text(stringResource(R.string.login_fastmail_sign_in))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
fun FastmailLoginScreen_Preview_Empty() {
|
||||
FastmailLoginScreen(
|
||||
email = "",
|
||||
canContinue = false
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
fun FastmailLoginScreen_Preview_WithDefaultEmail() {
|
||||
FastmailLoginScreen(
|
||||
email = "example@gmail.com",
|
||||
canContinue = true
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.ui.setup
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.network.OAuthFastmail
|
||||
import at.bitfire.davdroid.network.OAuthIntegration
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.launch
|
||||
import net.openid.appauth.AuthorizationRequest
|
||||
import net.openid.appauth.AuthorizationResponse
|
||||
import net.openid.appauth.AuthorizationService
|
||||
import java.util.Locale
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
|
||||
@HiltViewModel(assistedFactory = FastmailLoginModel.Factory::class)
|
||||
class FastmailLoginModel @AssistedInject constructor(
|
||||
@Assisted val initialLoginInfo: LoginInfo,
|
||||
private val authService: AuthorizationService,
|
||||
@ApplicationContext val context: Context,
|
||||
private val logger: Logger
|
||||
) : ViewModel() {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(loginInfo: LoginInfo): FastmailLoginModel
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
authService.dispose()
|
||||
}
|
||||
|
||||
|
||||
data class UiState(
|
||||
val email: String = "",
|
||||
val error: String? = null,
|
||||
|
||||
/** login info (set after successful login) */
|
||||
val result: LoginInfo? = null
|
||||
) {
|
||||
val canContinue = email.isNotEmpty()
|
||||
val emailWithDomain = if (email.contains("@")) email else "$email@fastmail.com"
|
||||
}
|
||||
|
||||
var uiState by mutableStateOf(UiState())
|
||||
private set
|
||||
|
||||
init {
|
||||
uiState = uiState.copy(
|
||||
email = initialLoginInfo.credentials?.username ?: "",
|
||||
error = null,
|
||||
result = null
|
||||
)
|
||||
}
|
||||
|
||||
fun setEmail(email: String) {
|
||||
uiState = uiState.copy(email = email)
|
||||
}
|
||||
|
||||
fun signIn() =
|
||||
OAuthFastmail.signIn(
|
||||
email = uiState.emailWithDomain,
|
||||
locale = Locale.getDefault().toLanguageTag()
|
||||
)
|
||||
|
||||
fun signInFailed() {
|
||||
uiState = uiState.copy(error = context.getString(R.string.install_browser))
|
||||
}
|
||||
|
||||
fun authenticate(authResponse: AuthorizationResponse) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val credentials = OAuthIntegration.authenticate(authService, authResponse)
|
||||
|
||||
// success, provide login info to continue
|
||||
uiState = uiState.copy(
|
||||
result = LoginInfo(
|
||||
baseUri = OAuthFastmail.baseUri,
|
||||
credentials = credentials,
|
||||
suggestedAccountName = uiState.emailWithDomain
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.log(Level.WARNING, "Fastmail authentication failed", e)
|
||||
uiState = uiState.copy(error = e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun authCodeFailed() {
|
||||
uiState = uiState.copy(error = context.getString(R.string.login_oauth_couldnt_obtain_auth_code))
|
||||
}
|
||||
|
||||
fun resetResult() {
|
||||
uiState = uiState.copy(result = null)
|
||||
}
|
||||
|
||||
|
||||
inner class AuthorizationContract() : ActivityResultContract<AuthorizationRequest, AuthorizationResponse?>() {
|
||||
override fun createIntent(context: Context, input: AuthorizationRequest) =
|
||||
authService.getAuthorizationRequestIntent(input)
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): AuthorizationResponse? =
|
||||
intent?.let { AuthorizationResponse.fromIntent(it) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,6 +50,7 @@ import at.bitfire.davdroid.Constants.withStatParams
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString
|
||||
import at.bitfire.davdroid.ui.setup.GoogleLogin.GOOGLE_POLICY_URL
|
||||
import at.bitfire.davdroid.ui.setup.GoogleLogin.helpUrl
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
|
||||
@@ -70,13 +71,6 @@ object GoogleLogin : LoginType {
|
||||
const val GOOGLE_POLICY_URL =
|
||||
"https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes"
|
||||
|
||||
// Support site
|
||||
val URI_TESTED_WITH_GOOGLE: Uri =
|
||||
Constants.HOMEPAGE_URL.buildUpon()
|
||||
.appendPath(Constants.HOMEPAGE_PATH_TESTED_SERVICES)
|
||||
.appendPath("google")
|
||||
.build()
|
||||
|
||||
|
||||
@Composable
|
||||
override fun LoginScreen(
|
||||
@@ -171,7 +165,7 @@ fun GoogleLoginScreen(
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
uriHandler.openUri(GoogleLogin.URI_TESTED_WITH_GOOGLE.toString())
|
||||
uriHandler.openUri(helpUrl.toString())
|
||||
},
|
||||
colors = ButtonDefaults.outlinedButtonColors(),
|
||||
modifier = Modifier.wrapContentSize()
|
||||
|
||||
@@ -14,7 +14,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.network.GoogleLogin
|
||||
import at.bitfire.davdroid.network.OAuthGoogle
|
||||
import at.bitfire.davdroid.network.OAuthIntegration
|
||||
import at.bitfire.davdroid.util.trimToNull
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
@@ -42,8 +43,6 @@ class GoogleLoginModel @AssistedInject constructor(
|
||||
fun create(loginInfo: LoginInfo): GoogleLoginModel
|
||||
}
|
||||
|
||||
val googleLogin = GoogleLogin(authService)
|
||||
|
||||
override fun onCleared() {
|
||||
authService.dispose()
|
||||
}
|
||||
@@ -81,7 +80,7 @@ class GoogleLoginModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
fun signIn() =
|
||||
googleLogin.signIn(
|
||||
OAuthGoogle.signIn(
|
||||
email = uiState.emailWithDomain,
|
||||
customClientId = uiState.customClientId.trimToNull(),
|
||||
locale = Locale.getDefault().toLanguageTag()
|
||||
@@ -94,12 +93,12 @@ class GoogleLoginModel @AssistedInject constructor(
|
||||
fun authenticate(authResponse: AuthorizationResponse) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val credentials = googleLogin.authenticate(authResponse)
|
||||
val credentials = OAuthIntegration.authenticate(authService, authResponse)
|
||||
|
||||
// success, provide login info to continue
|
||||
uiState = uiState.copy(
|
||||
result = LoginInfo(
|
||||
baseUri = GoogleLogin.googleBaseUri(uiState.emailWithDomain),
|
||||
baseUri = OAuthGoogle.baseUri(uiState.emailWithDomain),
|
||||
credentials = credentials,
|
||||
suggestedAccountName = uiState.emailWithDomain
|
||||
)
|
||||
|
||||
@@ -302,6 +302,9 @@
|
||||
<string name="login_client_certificate_selected">Client certificate: %s</string>
|
||||
<string name="login_no_certificate_found">No certificate found</string>
|
||||
<string name="login_install_certificate">Install certificate</string>
|
||||
<string name="login_fastmail">Fastmail</string>
|
||||
<string name="login_fastmail_account">Fastmail account</string>
|
||||
<string name="login_fastmail_sign_in">Sign in with Fastmail</string>
|
||||
<string name="login_type_google">Google Contacts / Calendar</string>
|
||||
<string name="login_google_see_tested_with">Please see our \"Tested with Google\" page for up-to-date information.</string>
|
||||
<string name="login_google_unexpected_warnings">You may experience unexpected warnings and/or have to create your own client ID.</string>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data
|
||||
tools:ignore="AppLinkUrlError"
|
||||
android:scheme="at.bitfire.davdroid"
|
||||
android:scheme="${applicationId}"
|
||||
android:path="/oauth2/redirect"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
@@ -30,7 +30,7 @@ fun StandardLoginTypePage(
|
||||
selectedLoginType: LoginType,
|
||||
onSelectLoginType: (LoginType) -> Unit,
|
||||
|
||||
@Suppress("UNUSED_PARAMETER") // for build variants
|
||||
@Suppress("unused") // for build variants
|
||||
setInitialLoginInfo: (LoginInfo) -> Unit,
|
||||
|
||||
onContinue: () -> Unit = {}
|
||||
|
||||
@@ -23,6 +23,7 @@ class StandardLoginTypesProvider @Inject constructor(
|
||||
)
|
||||
|
||||
val specificLoginTypes = listOf(
|
||||
FastmailLogin,
|
||||
GoogleLogin,
|
||||
NextcloudLogin
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user