Oidc with CustomTab
This commit is contained in:
parent
cef1691e53
commit
4c9bed9d8a
22 changed files with 466 additions and 12 deletions
|
|
@ -49,6 +49,14 @@
|
||||||
android:host="open"
|
android:host="open"
|
||||||
android:scheme="elementx" />
|
android:scheme="elementx" />
|
||||||
</intent-filter-->
|
</intent-filter-->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="io.element" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|
|
||||||
|
|
@ -38,14 +38,17 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.appnav.di.MatrixClientsHolder
|
import io.element.android.appnav.di.MatrixClientsHolder
|
||||||
|
import io.element.android.appnav.intent.IntentResolver
|
||||||
|
import io.element.android.appnav.intent.ResolvedIntent
|
||||||
import io.element.android.appnav.root.RootPresenter
|
import io.element.android.appnav.root.RootPresenter
|
||||||
import io.element.android.appnav.root.RootView
|
import io.element.android.appnav.root.RootView
|
||||||
|
import io.element.android.features.login.api.oidc.OidcAction
|
||||||
|
import io.element.android.features.login.api.oidc.OidcActionFlow
|
||||||
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
|
||||||
import io.element.android.libraries.architecture.BackstackNode
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.deeplink.DeeplinkData
|
import io.element.android.libraries.deeplink.DeeplinkData
|
||||||
import io.element.android.libraries.deeplink.DeeplinkParser
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
|
|
@ -65,7 +68,8 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
private val matrixClientsHolder: MatrixClientsHolder,
|
private val matrixClientsHolder: MatrixClientsHolder,
|
||||||
private val presenter: RootPresenter,
|
private val presenter: RootPresenter,
|
||||||
private val bugReportEntryPoint: BugReportEntryPoint,
|
private val bugReportEntryPoint: BugReportEntryPoint,
|
||||||
private val deeplinkParser: DeeplinkParser,
|
private val intentResolver: IntentResolver,
|
||||||
|
private val oidcActionFlow: OidcActionFlow,
|
||||||
) :
|
) :
|
||||||
BackstackNode<RootFlowNode.NavTarget>(
|
BackstackNode<RootFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
|
|
@ -204,8 +208,11 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun handleIntent(intent: Intent) {
|
suspend fun handleIntent(intent: Intent) {
|
||||||
deeplinkParser.getFromIntent(intent)
|
val resolvedIntent = intentResolver.resolve(intent) ?: return
|
||||||
?.let { navigateTo(it) }
|
when (resolvedIntent) {
|
||||||
|
is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
|
||||||
|
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
|
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 {
|
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
|
||||||
return attachChild {
|
return attachChild {
|
||||||
backstack.newRoot(NavTarget.LoggedInFlow(sessionId))
|
backstack.newRoot(NavTarget.LoggedInFlow(sessionId))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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?
|
||||||
|
}
|
||||||
|
|
@ -45,6 +45,7 @@ dependencies {
|
||||||
implementation(projects.libraries.elementresources)
|
implementation(projects.libraries.elementresources)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
implementation(libs.androidx.browser)
|
||||||
api(projects.features.login.api)
|
api(projects.features.login.api)
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
||||||
|
|
|
||||||
26
features/login/impl/src/main/AndroidManifest.xml
Normal file
26
features/login/impl/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright (c) 2022 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.
|
||||||
|
-->
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<!-- To open URL in CustomTab (prefetch, etc.). It makes CustomTabsClient.getPackageName() work
|
||||||
|
see https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs -->
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
@ -29,6 +29,7 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerNode
|
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.oidc.OidcNode
|
||||||
import io.element.android.features.login.impl.root.LoginRootNode
|
import io.element.android.features.login.impl.root.LoginRootNode
|
||||||
import io.element.android.libraries.architecture.BackstackNode
|
import io.element.android.libraries.architecture.BackstackNode
|
||||||
|
|
@ -42,6 +43,7 @@ import kotlinx.parcelize.Parcelize
|
||||||
class LoginFlowNode @AssistedInject constructor(
|
class LoginFlowNode @AssistedInject constructor(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val customTabHandler: CustomTabHandler,
|
||||||
) : BackstackNode<LoginFlowNode.NavTarget>(
|
) : BackstackNode<LoginFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Root,
|
initialElement = NavTarget.Root,
|
||||||
|
|
@ -50,7 +52,6 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Root : NavTarget
|
object Root : NavTarget
|
||||||
|
|
@ -71,7 +72,12 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOidcDetails(oidcDetails: OidcDetails) {
|
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<LoginRootNode>(buildContext, plugins = listOf(callback))
|
createNode<LoginRootNode>(buildContext, plugins = listOf(callback))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.oidc
|
package io.element.android.features.login.impl.oidc
|
||||||
|
|
||||||
|
import io.element.android.features.login.api.oidc.OidcAction
|
||||||
|
|
||||||
sealed interface OidcEvents {
|
sealed interface OidcEvents {
|
||||||
object Cancel : OidcEvents
|
object Cancel : OidcEvents
|
||||||
data class OidcActionEvent(val oidcAction: OidcAction): OidcEvents
|
data class OidcActionEvent(val oidcAction: OidcAction): OidcEvents
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import androidx.compose.runtime.setValue
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
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.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,15 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.oidc
|
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 io.element.android.libraries.matrix.api.auth.OidcConfig
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple parser for oidc url interception.
|
* Simple parser for oidc url interception.
|
||||||
* TODO Find documentation about the format.
|
* TODO Find documentation about the format.
|
||||||
*/
|
*/
|
||||||
class OidcUrlParser {
|
class OidcUrlParser @Inject constructor() {
|
||||||
|
|
||||||
// When user press button "Cancel", we get the url:
|
// When user press button "Cancel", we get the url:
|
||||||
// `io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO`
|
// `io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO`
|
||||||
|
|
@ -40,8 +42,3 @@ class OidcUrlParser {
|
||||||
error("Not supported: $url")
|
error("Not supported: $url")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface OidcAction {
|
|
||||||
object GoBack : OidcAction
|
|
||||||
data class Success(val url: String) : OidcAction
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<OidcAction?>(null)
|
||||||
|
|
||||||
|
override fun post(oidcAction: OidcAction) {
|
||||||
|
mutableStateFlow.value = oidcAction
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun collect(lambda: suspend (OidcAction?) -> Unit) {
|
||||||
|
mutableStateFlow.collect(lambda)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
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.features.login.impl.util.LoginConstants
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -36,6 +38,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class LoginRootPresenter @Inject constructor(
|
class LoginRootPresenter @Inject constructor(
|
||||||
private val authenticationService: MatrixAuthenticationService,
|
private val authenticationService: MatrixAuthenticationService,
|
||||||
|
private val defaultOidcActionFlow: DefaultOidcActionFlow,
|
||||||
) : Presenter<LoginRootState> {
|
) : Presenter<LoginRootState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -64,6 +67,14 @@ class LoginRootPresenter @Inject constructor(
|
||||||
mutableStateOf(LoginFormState.Default)
|
mutableStateOf(LoginFormState.Default)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
launch {
|
||||||
|
defaultOidcActionFlow.collect {
|
||||||
|
onOidcAction(it, loggedInState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvents(event: LoginRootEvents) {
|
fun handleEvents(event: LoginRootEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
LoginRootEvents.RetryFetchServerInfo -> localCoroutineScope.getHomeServerDetails(homeserver, getHomeServerDetailsAction)
|
LoginRootEvents.RetryFetchServerInfo -> localCoroutineScope.getHomeServerDetails(homeserver, getHomeServerDetailsAction)
|
||||||
|
|
@ -131,4 +142,29 @@ class LoginRootPresenter @Inject constructor(
|
||||||
private fun updateFormState(formState: MutableState<LoginFormState>, updateLambda: LoginFormState.() -> LoginFormState) {
|
private fun updateFormState(formState: MutableState<LoginFormState>, updateLambda: LoginFormState.() -> LoginFormState) {
|
||||||
formState.value = updateLambda(formState.value)
|
formState.value = updateLambda(formState.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun onOidcAction(oidcAction: OidcAction?, loggedInState: MutableState<LoggedInState>) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.architecture.Async
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA
|
import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.login.impl.oidc
|
package io.element.android.features.login.impl.oidc
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
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 io.element.android.libraries.matrix.api.auth.OidcConfig
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ lifecycle = "2.6.1"
|
||||||
activity = "1.7.2"
|
activity = "1.7.2"
|
||||||
startup = "1.1.1"
|
startup = "1.1.1"
|
||||||
media3 = "1.0.2"
|
media3 = "1.0.2"
|
||||||
|
browser = "1.5.0"
|
||||||
|
|
||||||
# Compose
|
# Compose
|
||||||
compose_bom = "2023.05.01"
|
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_exifinterface = "androidx.exifinterface:exifinterface:1.3.6"
|
||||||
androidx_constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
|
androidx_constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
|
||||||
androidx_recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
|
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_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||||
androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
|
androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
|
||||||
androidx_splash = "androidx.core:core-splashscreen:1.0.1"
|
androidx_splash = "androidx.core:core-splashscreen:1.0.1"
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ dependencies {
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
implementation(libs.androidx.corektx)
|
implementation(libs.androidx.corektx)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
|
|
|
||||||
24
tools/adb/oidc.sh
Executable file
24
tools/adb/oidc.sh
Executable file
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue