Benoit Marty
1 year ago
committed by
Benoit Marty
22 changed files with 466 additions and 12 deletions
@ -0,0 +1,47 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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? |
||||
} |
@ -0,0 +1,26 @@
@@ -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> |
@ -0,0 +1,87 @@
@@ -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 @@
@@ -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()) |
||||
} |
||||
} |
@ -0,0 +1,65 @@
@@ -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 @@
@@ -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) |
||||
} |
||||
} |
@ -0,0 +1,24 @@
@@ -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…
Reference in new issue