From ed6d1cf5619e6a7337ff5983ca42665ca2a85243 Mon Sep 17 00:00:00 2001 From: Cosmin Rasa Date: Thu, 16 Oct 2025 14:55:00 +0300 Subject: [PATCH] Improve browser selection in WebAuthenticationProvider --- .../WebAuthenticationProvider.kt | 35 ++++++++++++------ .../DefaultWebAuthenticationProviderTest.kt | 37 +++++++++++++++---- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/web-authentication-ui/src/main/java/com/okta/webauthenticationui/WebAuthenticationProvider.kt b/web-authentication-ui/src/main/java/com/okta/webauthenticationui/WebAuthenticationProvider.kt index 610fe4f4..c71dcdb7 100644 --- a/web-authentication-ui/src/main/java/com/okta/webauthenticationui/WebAuthenticationProvider.kt +++ b/web-authentication-ui/src/main/java/com/okta/webauthenticationui/WebAuthenticationProvider.kt @@ -20,12 +20,12 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.content.pm.PackageManager -import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Browser import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsService +import androidx.core.net.toUri import com.okta.authfoundation.events.EventCoordinator import com.okta.webauthenticationui.events.CustomizeBrowserEvent import com.okta.webauthenticationui.events.CustomizeCustomTabsEvent @@ -56,6 +56,8 @@ internal class DefaultWebAuthenticationProvider( const val X_OKTA_USER_AGENT = "X-Okta-User-Agent-Extended" val USER_AGENT_HEADER = "web-authentication-ui/${Build.VERSION.SDK_INT} com.okta.webauthenticationui/2.0.0" + + val HTTP_URI_FOR_BROWSER_VALIDATION = "http://".toUri() } override fun launch( @@ -76,7 +78,7 @@ internal class DefaultWebAuthenticationProvider( tabsIntent.intent.putExtra(Browser.EXTRA_HEADERS, headers) try { - tabsIntent.launchUrl(context, Uri.parse(url.toString())) + tabsIntent.launchUrl(context, url.toString().toUri()) return null } catch (e: ActivityNotFoundException) { return e @@ -88,20 +90,29 @@ internal class DefaultWebAuthenticationProvider( eventCoordinator.sendEvent(event) val pm: PackageManager = context.packageManager - val serviceIntent = Intent() - serviceIntent.action = CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION + val serviceIntent = Intent(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION) val resolveInfoList = pm.queryIntentServices(serviceIntent, event.queryIntentServicesFlags) - val customTabsBrowsersPackages = mutableSetOf() - for (info in resolveInfoList) { - customTabsBrowsersPackages.add(info.serviceInfo.packageName) + + val customTabsBrowsersPackages = resolveInfoList + .mapNotNull { it.serviceInfo?.packageName } + .toSet() + + val preferredBrowser = event.preferredBrowsers.firstOrNull { customTabsBrowsersPackages.contains(it) } + if (preferredBrowser != null && isBrowserPackage(pm, preferredBrowser)) { + return preferredBrowser } - for (browser in event.preferredBrowsers) { - if (customTabsBrowsersPackages.contains(browser)) { - return browser - } + return customTabsBrowsersPackages.firstOrNull { + isBrowserPackage(pm, it) } + } - return customTabsBrowsersPackages.firstOrNull() + private fun isBrowserPackage(pm: PackageManager, packageName: String): Boolean { + val intent = Intent(Intent.ACTION_VIEW, HTTP_URI_FOR_BROWSER_VALIDATION).apply { + addCategory(Intent.CATEGORY_BROWSABLE) + setPackage(packageName) + } + val resolveInfoList = pm.queryIntentActivities(intent, 0) + return resolveInfoList.isNotEmpty() } } diff --git a/web-authentication-ui/src/test/java/com/okta/webauthenticationui/DefaultWebAuthenticationProviderTest.kt b/web-authentication-ui/src/test/java/com/okta/webauthenticationui/DefaultWebAuthenticationProviderTest.kt index 185e4cde..00493b10 100644 --- a/web-authentication-ui/src/test/java/com/okta/webauthenticationui/DefaultWebAuthenticationProviderTest.kt +++ b/web-authentication-ui/src/test/java/com/okta/webauthenticationui/DefaultWebAuthenticationProviderTest.kt @@ -42,7 +42,8 @@ import org.robolectric.shadows.ShadowResolveInfo @RunWith(RobolectricTestRunner::class) class DefaultWebAuthenticationProviderTest { - @Test fun testLaunch() { + @Test + fun testLaunch() { val activity = Robolectric.buildActivity(Activity::class.java) val webAuthenticationProvider = DefaultWebAuthenticationProvider(EventCoordinator(emptyList())) assertThat(webAuthenticationProvider.launch(activity.get(), "https://example.com/not_used".toHttpUrl())).isNull() @@ -54,7 +55,8 @@ class DefaultWebAuthenticationProviderTest { assertThat(cctActivity.`package`).isNull() } - @Test fun testLaunchCallsEventHandler() { + @Test + fun testLaunchCallsEventHandler() { val activity = Robolectric.buildActivity(Activity::class.java) val eventHandler = RecordingEventHandler() val webAuthenticationProvider = DefaultWebAuthenticationProvider(EventCoordinator(eventHandler)) @@ -71,7 +73,8 @@ class DefaultWebAuthenticationProviderTest { assertThat((eventHandler[1] as CustomizeBrowserEvent).queryIntentServicesFlags).isEqualTo(0) } - @Test fun testLaunchWithEnabledBrowsers() { + @Test + fun testLaunchWithEnabledBrowsers() { installCustomTabsProvider("com.android.chrome.beta") installCustomTabsProvider("com.android.chrome") ShadowResolveInfo.newResolveInfo( @@ -88,7 +91,8 @@ class DefaultWebAuthenticationProviderTest { assertThat(cctActivity.`package`).isEqualTo("com.android.chrome") } - @Test fun testLaunchWithPreferredBrowsers() { + @Test + fun testLaunchWithPreferredBrowsers() { installCustomTabsProvider("com.android.chrome.beta") installCustomTabsProvider("my.custom.preferred.browser") installCustomTabsProvider("com.android.chrome") @@ -110,7 +114,8 @@ class DefaultWebAuthenticationProviderTest { assertThat(cctActivity.`package`).isEqualTo("my.custom.preferred.browser") } - @Test fun testLaunchCausesActivityNotFound() { + @Test + fun testLaunchCausesActivityNotFound() { shadowOf(RuntimeEnvironment.getApplication()).checkActivities(true) val activity = Robolectric.buildActivity(Activity::class.java) val webAuthenticationProvider = DefaultWebAuthenticationProvider(EventCoordinator(emptyList())) @@ -118,6 +123,21 @@ class DefaultWebAuthenticationProviderTest { assertThat(exception).isInstanceOf(ActivityNotFoundException::class.java) } + @Test + fun testLaunchWithCustomTabsProviderThatIsAlsoBrowser() { + installCustomTabsProvider("my.custom.simpleCustomTabProvider", false) + installCustomTabsProvider("my.custom.browserCustomTabProvider") + + val activity = Robolectric.buildActivity(Activity::class.java) + val webAuthenticationProvider = DefaultWebAuthenticationProvider(EventCoordinator(emptyList())) + + assertThat(webAuthenticationProvider.launch(activity.get(), "https://example.com/not_used".toHttpUrl())).isNull() + val activityShadow = shadowOf(activity.get()) + val cctActivity = activityShadow.nextStartedActivity + assertThat(cctActivity.action).isEqualTo("android.intent.action.VIEW") + assertThat(cctActivity.`package`).isEqualTo("my.custom.browserCustomTabProvider") + } + // Copyright 2019 Google Inc. All Rights Reserved. // https://chromium.googlesource.com/custom-tabs-client/+/refs/heads/main/customtabs/junit/src/android/support/customtabs/trusted/TwaProviderPickerTest.java private fun installBrowser(packageName: String) { @@ -126,6 +146,7 @@ class DefaultWebAuthenticationProviderTest { .setData(Uri.parse("http://")) .setAction(Intent.ACTION_VIEW) .addCategory(Intent.CATEGORY_BROWSABLE) + .setPackage(packageName) val resolveInfo = ResolveInfo() resolveInfo.activityInfo = ActivityInfo() resolveInfo.activityInfo.packageName = packageName @@ -134,8 +155,10 @@ class DefaultWebAuthenticationProviderTest { packageManager.addResolveInfoForIntent(intent, resolveInfo) } - private fun installCustomTabsProvider(packageName: String) { - installBrowser(packageName) + private fun installCustomTabsProvider(packageName: String, installAsBrowser: Boolean = true) { + if (installAsBrowser) { + installBrowser(packageName) + } val intent = Intent().setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION) val resolveInfo = ResolveInfo() resolveInfo.serviceInfo = ServiceInfo()