From 4c9bed9d8a27ae78572243fa6257df4bd55c0bd2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 12:50:35 +0200 Subject: [PATCH] Oidc with CustomTab --- app/src/main/AndroidManifest.xml | 8 ++ .../io/element/android/appnav/RootFlowNode.kt | 19 +++- .../android/appnav/intent/IntentResolver.kt | 47 ++++++++++ .../features/login/api/oidc/OidcAction.kt | 22 +++++ .../features/login/api/oidc/OidcActionFlow.kt | 21 +++++ .../login/api/oidc/OidcIntentResolver.kt | 23 +++++ features/login/impl/build.gradle.kts | 1 + .../login/impl/src/main/AndroidManifest.xml | 26 ++++++ .../features/login/impl/LoginFlowNode.kt | 10 ++- .../login/impl/oidc/CustomTabHandler.kt | 87 +++++++++++++++++++ .../impl/oidc/DefaultOidcIntentResolver.kt | 33 +++++++ .../features/login/impl/oidc/OidcEvents.kt | 2 + .../features/login/impl/oidc/OidcPresenter.kt | 1 + .../features/login/impl/oidc/OidcUrlParser.kt | 9 +- .../features/login/impl/oidc/web/CustomTab.kt | 65 ++++++++++++++ .../impl/oidc/web/DefaultOidcActionFlow.kt | 39 +++++++++ .../login/impl/root/LoginRootPresenter.kt | 36 ++++++++ .../login/impl/oidc/OidcPresenterTest.kt | 1 + .../login/impl/oidc/OidcUrlParserTest.kt | 1 + gradle/libs.versions.toml | 2 + libraries/deeplink/build.gradle.kts | 1 + tools/adb/oidc.sh | 24 +++++ 22 files changed, 466 insertions(+), 12 deletions(-) create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt create mode 100644 features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt create mode 100644 features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt create mode 100644 features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt create mode 100644 features/login/impl/src/main/AndroidManifest.xml create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.kt create mode 100755 tools/adb/oidc.sh diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 22a63b60c9..875f9f0435 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,6 +49,14 @@ android:host="open" android:scheme="elementx" /> + + + + + + + + ( backstack = BackStack( @@ -204,8 +208,11 @@ class RootFlowNode @AssistedInject constructor( } suspend fun handleIntent(intent: Intent) { - deeplinkParser.getFromIntent(intent) - ?.let { navigateTo(it) } + val resolvedIntent = intentResolver.resolve(intent) ?: return + when (resolvedIntent) { + is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData) + is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction) + } } private suspend fun navigateTo(deeplinkData: DeeplinkData) { @@ -223,6 +230,10 @@ class RootFlowNode @AssistedInject constructor( } } + private fun onOidcAction(oidcAction: OidcAction) { + oidcActionFlow.post(oidcAction) + } + private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode { return attachChild { backstack.newRoot(NavTarget.LoggedInFlow(sessionId)) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt new file mode 100644 index 0000000000..b567395c1e --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.appnav.intent + +import android.content.Intent +import io.element.android.features.login.api.oidc.OidcAction +import io.element.android.features.login.api.oidc.OidcIntentResolver +import io.element.android.libraries.deeplink.DeeplinkData +import io.element.android.libraries.deeplink.DeeplinkParser +import timber.log.Timber +import javax.inject.Inject + +sealed interface ResolvedIntent { + data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent + data class Oidc(val oidcAction: OidcAction) : ResolvedIntent +} + +class IntentResolver @Inject constructor( + private val deeplinkParser: DeeplinkParser, + private val oidcIntentResolver: OidcIntentResolver +) { + fun resolve(intent: Intent): ResolvedIntent? { + val deepLinkData = deeplinkParser.getFromIntent(intent) + if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData) + + val oidcAction = oidcIntentResolver.resolve(intent) + if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction) + + // Unknown intent + Timber.w("Unknown intent") + return null + } +} diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt new file mode 100644 index 0000000000..6e90a390c4 --- /dev/null +++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.login.api.oidc + +sealed interface OidcAction { + object GoBack : OidcAction + data class Success(val url: String) : OidcAction +} diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt new file mode 100644 index 0000000000..004e7c8a51 --- /dev/null +++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.login.api.oidc + +interface OidcActionFlow { + fun post(oidcAction: OidcAction) +} diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt new file mode 100644 index 0000000000..a6ecf26fca --- /dev/null +++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.login.api.oidc + +import android.content.Intent + +interface OidcIntentResolver { + fun resolve(intent: Intent): OidcAction? +} diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index 5666fc1179..c43a5c2cd3 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.libraries.elementresources) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) + implementation(libs.androidx.browser) api(projects.features.login.api) ksp(libs.showkase.processor) diff --git a/features/login/impl/src/main/AndroidManifest.xml b/features/login/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..172e8645c1 --- /dev/null +++ b/features/login/impl/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt index 37cfec229b..f1f56888bb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -29,6 +29,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.login.impl.changeserver.ChangeServerNode +import io.element.android.features.login.impl.oidc.CustomTabHandler import io.element.android.features.login.impl.oidc.OidcNode import io.element.android.features.login.impl.root.LoginRootNode import io.element.android.libraries.architecture.BackstackNode @@ -42,6 +43,7 @@ import kotlinx.parcelize.Parcelize class LoginFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val customTabHandler: CustomTabHandler, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -50,7 +52,6 @@ class LoginFlowNode @AssistedInject constructor( buildContext = buildContext, plugins = plugins, ) { - sealed interface NavTarget : Parcelable { @Parcelize object Root : NavTarget @@ -71,7 +72,12 @@ class LoginFlowNode @AssistedInject constructor( } override fun onOidcDetails(oidcDetails: OidcDetails) { - backstack.push(NavTarget.OidcView(oidcDetails)) + if (customTabHandler.supportCustomTab()) { + customTabHandler.open(oidcDetails.url) + } else { + // Fallback to WebView mode + backstack.push(NavTarget.OidcView(oidcDetails)) + } } } createNode(buildContext, plugins = listOf(callback)) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt new file mode 100644 index 0000000000..593469a12d --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.login.impl.oidc + +import android.content.ComponentName +import android.content.Context +import android.net.Uri +import androidx.browser.customtabs.CustomTabsClient +import androidx.browser.customtabs.CustomTabsServiceConnection +import androidx.browser.customtabs.CustomTabsSession +import io.element.android.features.login.impl.oidc.web.openUrlInChromeCustomTab +import io.element.android.libraries.di.ApplicationContext +import javax.inject.Inject + +class CustomTabHandler @Inject constructor( + @ApplicationContext private val context: Context, +) { + private var customTabsSession: CustomTabsSession? = null + private var customTabsClient: CustomTabsClient? = null + private var customTabsServiceConnection: CustomTabsServiceConnection? = null + + /** + * Return true if the device supports Custom tab, i.e. there is an third party app with + * CustomTab support (ex: Chrome, Firefox, etc.). + */ + fun supportCustomTab(): Boolean { + val packageName = CustomTabsClient.getPackageName(context, null) + return packageName != null + } + + fun prepareCustomTab(url: String) { + val packageName = CustomTabsClient.getPackageName(context, null) + + // packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device + if (packageName != null) { + customTabsServiceConnection = object : CustomTabsServiceConnection() { + override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) { + customTabsClient = client + .also { it.warmup(0L) } + prefetchUrl(url) + } + + override fun onServiceDisconnected(name: ComponentName?) { + } + } + .also { + CustomTabsClient.bindCustomTabsService( + context, + // Despite the API, packageName cannot be null + packageName, + it + ) + } + } + } + + private fun prefetchUrl(url: String) { + if (customTabsSession == null) { + customTabsSession = customTabsClient?.newSession(null) + } + + customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null) + } + + fun disposeCustomTab() { + customTabsServiceConnection?.let { context.unbindService(it) } + customTabsServiceConnection = null + } + + fun open(url: String) { + openUrlInChromeCustomTab(context, customTabsSession, false, url) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt new file mode 100644 index 0000000000..8b6844e0f3 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.login.impl.oidc + +import android.content.Intent +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.login.api.oidc.OidcAction +import io.element.android.features.login.api.oidc.OidcIntentResolver +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultOidcIntentResolver @Inject constructor( + private val oidcUrlParser: OidcUrlParser, +) : OidcIntentResolver { + override fun resolve(intent: Intent): OidcAction? { + return oidcUrlParser.parse(intent.dataString.orEmpty()) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt index 0c40f457bf..4f62c6476d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt @@ -16,6 +16,8 @@ package io.element.android.features.login.impl.oidc +import io.element.android.features.login.api.oidc.OidcAction + sealed interface OidcEvents { object Cancel : OidcEvents data class OidcActionEvent(val oidcAction: OidcAction): OidcEvents diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt index 1c7debfd6f..f66caef5b2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.element.android.features.login.api.oidc.OidcAction import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt index 090fd62501..487df70253 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt @@ -16,13 +16,15 @@ package io.element.android.features.login.impl.oidc +import io.element.android.features.login.api.oidc.OidcAction import io.element.android.libraries.matrix.api.auth.OidcConfig +import javax.inject.Inject /** * Simple parser for oidc url interception. * TODO Find documentation about the format. */ -class OidcUrlParser { +class OidcUrlParser @Inject constructor() { // When user press button "Cancel", we get the url: // `io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO` @@ -40,8 +42,3 @@ class OidcUrlParser { error("Not supported: $url") } } - -sealed interface OidcAction { - object GoBack : OidcAction - data class Success(val url: String) : OidcAction -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt new file mode 100644 index 0000000000..d6c9de7e68 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.login.impl.oidc.web + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsIntent +import androidx.browser.customtabs.CustomTabsSession + +/** + * Open url in custom tab or, if not available, in the default browser. + * If several compatible browsers are installed, the user will be proposed to choose one. + * Ref: https://developer.chrome.com/multidevice/android/customtabs. + */ +fun openUrlInChromeCustomTab( + context: Context, + session: CustomTabsSession?, + darkTheme: Boolean, + url: String +) { + try { + CustomTabsIntent.Builder() + .setDefaultColorSchemeParams( + CustomTabColorSchemeParams.Builder() + // TODO .setToolbarColor(ThemeUtils.getColor(context, android.R.attr.colorBackground)) + // TODO .setNavigationBarColor(ThemeUtils.getColor(context, android.R.attr.colorBackground)) + .build() + ) + .setColorScheme( + when (darkTheme) { + false -> CustomTabsIntent.COLOR_SCHEME_LIGHT + true -> CustomTabsIntent.COLOR_SCHEME_DARK + } + ) + // Note: setting close button icon does not work + // .setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp)) + // .setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out) + // .setExitAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out) + .apply { session?.let { setSession(it) } } + .build() + .apply { + intent.flags += Intent.FLAG_ACTIVITY_NEW_TASK + } + .launchUrl(context, Uri.parse(url)) + } catch (activityNotFoundException: ActivityNotFoundException) { + // TODO context.toast(R.string.error_no_external_application_found) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.kt new file mode 100644 index 0000000000..be4ec88ca2 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.login.impl.oidc.web + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.login.api.oidc.OidcAction +import io.element.android.features.login.api.oidc.OidcActionFlow +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) +class DefaultOidcActionFlow @Inject constructor() : OidcActionFlow { + private val mutableStateFlow = MutableStateFlow(null) + + override fun post(oidcAction: OidcAction) { + mutableStateFlow.value = oidcAction + } + + suspend fun collect(lambda: suspend (OidcAction?) -> Unit) { + mutableStateFlow.collect(lambda) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt index 5dec3b6537..5bf9f2ad05 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt @@ -24,6 +24,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.features.login.api.oidc.OidcAction +import io.element.android.features.login.impl.oidc.web.DefaultOidcActionFlow import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter @@ -36,6 +38,7 @@ import javax.inject.Inject class LoginRootPresenter @Inject constructor( private val authenticationService: MatrixAuthenticationService, + private val defaultOidcActionFlow: DefaultOidcActionFlow, ) : Presenter { @Composable @@ -64,6 +67,14 @@ class LoginRootPresenter @Inject constructor( mutableStateOf(LoginFormState.Default) } + LaunchedEffect(Unit) { + launch { + defaultOidcActionFlow.collect { + onOidcAction(it, loggedInState) + } + } + } + fun handleEvents(event: LoginRootEvents) { when (event) { LoginRootEvents.RetryFetchServerInfo -> localCoroutineScope.getHomeServerDetails(homeserver, getHomeServerDetailsAction) @@ -131,4 +142,29 @@ class LoginRootPresenter @Inject constructor( private fun updateFormState(formState: MutableState, updateLambda: LoginFormState.() -> LoginFormState) { formState.value = updateLambda(formState.value) } + + private suspend fun onOidcAction(oidcAction: OidcAction?, loggedInState: MutableState) { + oidcAction ?: return + loggedInState.value = LoggedInState.LoggingIn + when (oidcAction) { + OidcAction.GoBack -> { + authenticationService.cancelOidcLogin() + .onSuccess { + loggedInState.value = LoggedInState.NotLoggedIn + } + .onFailure { failure -> + loggedInState.value = LoggedInState.ErrorLoggingIn(failure) + } + } + is OidcAction.Success -> { + authenticationService.loginWithOidc(oidcAction.url) + .onSuccess { sessionId -> + loggedInState.value = LoggedInState.LoggedIn(sessionId) + } + .onFailure { failure -> + loggedInState.value = LoggedInState.ErrorLoggingIn(failure) + } + } + } + } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt index 0fba87f2e3..f69cecd0f2 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt @@ -22,6 +22,7 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.login.api.oidc.OidcAction import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt index d13f24c885..a0275f8f47 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt @@ -17,6 +17,7 @@ package io.element.android.features.login.impl.oidc import com.google.common.truth.Truth.assertThat +import io.element.android.features.login.api.oidc.OidcAction import io.element.android.libraries.matrix.api.auth.OidcConfig import org.junit.Assert import org.junit.Test diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9783cb1cf7..8e3522055c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ lifecycle = "2.6.1" activity = "1.7.2" startup = "1.1.1" media3 = "1.0.2" +browser = "1.5.0" # Compose compose_bom = "2023.05.01" @@ -70,6 +71,7 @@ androidx_datastore_datastore = { module = "androidx.datastore:datastore", versio androidx_exifinterface = "androidx.exifinterface:exifinterface:1.3.6" androidx_constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } androidx_recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } +androidx_browser = { module = "androidx.browser:browser", version.ref = "browser" } androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } androidx_splash = "androidx.core:core-splashscreen:1.0.1" diff --git a/libraries/deeplink/build.gradle.kts b/libraries/deeplink/build.gradle.kts index d850074d66..08ec84c227 100644 --- a/libraries/deeplink/build.gradle.kts +++ b/libraries/deeplink/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(libs.dagger) implementation(libs.androidx.corektx) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.architecture) testImplementation(libs.test.junit) testImplementation(libs.test.truth) diff --git a/tools/adb/oidc.sh b/tools/adb/oidc.sh new file mode 100755 index 0000000000..52be556cbb --- /dev/null +++ b/tools/adb/oidc.sh @@ -0,0 +1,24 @@ +#! /bin/bash +# +# Copyright (c) 2023 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Format is: + +# Error +# adb shell am start -a android.intent.action.VIEW -d io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO + +# Success +adb shell am start -a android.intent.action.VIEW -d io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB