Skip to content

Commit 31fb1e6

Browse files
authored
Revert to CustomTabsIntent when needed (#181)
* Revert to CustomTabsIntent when needed * Update README * Update Options * Simplify intent wrapper
1 parent bd467a0 commit 31fb1e6

7 files changed

Lines changed: 213 additions & 71 deletions

File tree

flutter_web_auth_2/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ final accessToken = jsonDecode(response.body)['access_token'] as String;
8282

8383
**Note (Google multiple scopes):** To use multiple scopes with Google, you need to encode them as a single string, separated by spaces (`%20`). For example, `scope: 'email https://www.googleapis.com/auth/userinfo.profile'`. Here is [a list of all supported scopes](https://developers.google.com/identity/protocols/oauth2/scopes).
8484

85-
**Note (ephemeral auth):** Due to a [knownw Chrome bug](https://issuetracker.google.com/issues/444173718), when setting `preferEphemeral: true` on Android, the webview will crash on older Chrome version (minimum version required is [Chrome 141](https://developer.chrome.com/release-notes/141?hl=en)).
85+
**Note (ephemeral auth):** Due to a [known Chrome bug](https://issuetracker.google.com/issues/444173718), when setting `preferEphemeral: true` on Android with Chrome < 141, the package will launch an auth session with older implementation to avoid crashes.
8686

8787
## Migration
8888

@@ -327,7 +327,7 @@ When you use this package for the first time, you may experience some problems.
327327
+ android:exported="true"
328328
+ android:taskAffinity="">
329329
```
330-
330+
331331
- If you want to have a callback URL with `http` or `https` scheme, you also need to specify a host etc.
332332
See [c:geo](https://github.com/cgeo/cgeo/blob/d7ab67629ac4798adaae194e563afe7df134fcd0/main/AndroidManifest.xml#L164) as an example for this.
333333
- In older versions of this package, there was a known problem with task affinities and launch modes (see [#113](https://github.com/ThexXTURBOXx/flutter_web_auth_2/issues/113)). In order to ensure that `flutter_web_auth_2` works correctly on these versions (prior to `5.0.0-alpha.2`), set `android:launchMode="singleTop"` and remove any `android:taskAffinity` entries. This configuration is guaranteed to work. In newer versions, this should not cause problems!

flutter_web_auth_2/android/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,9 @@
99
<intent>
1010
<action android:name="android.support.customtabs.action.CustomTabsService" />
1111
</intent>
12+
<intent>
13+
<action android:name="android.intent.action.VIEW" />
14+
<data android:scheme="https" />
15+
</intent>
1216
</queries>
1317
</manifest>

flutter_web_auth_2/android/src/main/kotlin/com/linusu/flutter_web_auth_2/AuthenticationManagementActivity.kt

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.linusu.flutter_web_auth_2
22

3+
import android.annotation.SuppressLint
34
import android.content.Context
45
import android.content.Intent
56
import android.net.Uri
@@ -10,7 +11,9 @@ import androidx.activity.ComponentActivity
1011
import androidx.activity.result.ActivityResultLauncher
1112
import androidx.browser.auth.AuthTabIntent
1213
import androidx.browser.auth.AuthTabIntent.AuthResult
14+
import androidx.browser.customtabs.CustomTabsIntent
1315

16+
@SuppressLint("UnsafeOptInUsageError", "UnsafeOptInUsageWarning")
1417
class AuthenticationManagementActivity : ComponentActivity() {
1518
companion object {
1619
const val KEY_AUTH_STARTED: String = "authStarted"
@@ -32,7 +35,7 @@ class AuthenticationManagementActivity : ComponentActivity() {
3235
private var authStarted: Boolean = false
3336
private lateinit var authenticationUri: Uri
3437
private var intentFlags: Int = 0
35-
private var targetPackage: String? = null
38+
private lateinit var targetPackage: String
3639
private var preferEphemeral: Boolean = false
3740
private lateinit var callbackScheme: String
3841
private var callbackHost: String? = null
@@ -64,9 +67,11 @@ class AuthenticationManagementActivity : ComponentActivity() {
6467
AuthTabIntent.RESULT_OK -> {
6568
callback.success(result.resultUri!!.toString())
6669
}
70+
6771
AuthTabIntent.RESULT_CANCELED -> {
6872
callback.error("CANCELED", "User canceled authentication", null)
6973
}
74+
7075
else -> {
7176
callback.error("FAILED", "Authentication failed with code: ${result.resultCode}", null)
7277
}
@@ -80,32 +85,36 @@ class AuthenticationManagementActivity : ComponentActivity() {
8085
super.onResume()
8186

8287
if (!authStarted) {
83-
val intentBuilder = AuthTabIntent.Builder()
88+
89+
val intentBuilder = if (shouldUseLegacySystem()) {
90+
Log.d(LOG_TAG, "Using CustomTabsIntent")
91+
CtBuilderWrapper(CustomTabsIntent.Builder())
92+
} else {
93+
Log.d(LOG_TAG, "Using AuthTabIntent")
94+
AuthTabBuilderWrapper(AuthTabIntent.Builder())
95+
}
8496

8597
// Set ephemeral browsing if requested and supported
8698
if (preferEphemeral) {
8799
try {
88100
intentBuilder.setEphemeralBrowsingEnabled(true)
89-
Log.d("flutter_web_auth_2", "Ephemeral browsing enabled")
101+
Log.d(LOG_TAG, "Ephemeral browsing enabled")
90102
} catch (e: Exception) {
91-
Log.w("flutter_web_auth_2", "Failed to enable ephemeral browsing: ${e.message}")
103+
Log.w(LOG_TAG, "Failed to enable ephemeral browsing: ${e.message}")
92104
}
93105
}
94106

95107
val intent = intentBuilder.build()
96108

97109
intent.intent.addFlags(intentFlags)
98-
99-
if (targetPackage != null) {
100-
intent.intent.setPackage(targetPackage)
101-
}
110+
intent.intent.setPackage(targetPackage)
102111

103112
if (callbackScheme == "https" && callbackHost != null && callbackPath != null) {
104-
Log.d("flutter_web_auth_2", "Using https host and path: $callbackHost, $callbackPath")
105-
intent.launch(authLauncher, authenticationUri, callbackHost!!, callbackPath!!)
113+
Log.d(LOG_TAG, "Using https host and path: $callbackHost, $callbackPath")
114+
intent.launch(this, authLauncher, authenticationUri, callbackHost!!, callbackPath!!)
106115
} else {
107-
Log.d("flutter_web_auth_2", "Using custom scheme: $callbackScheme")
108-
intent.launch(authLauncher, authenticationUri, callbackScheme)
116+
Log.d(LOG_TAG, "Using custom scheme: $callbackScheme")
117+
intent.launch(this, authLauncher, authenticationUri, callbackScheme)
109118
}
110119

111120
authStarted = true
@@ -118,6 +127,31 @@ class AuthenticationManagementActivity : ComponentActivity() {
118127
finish()
119128
}
120129

130+
fun shouldUseLegacySystem(): Boolean {
131+
132+
if (!preferEphemeral) return false
133+
val packageMajorVersion = getInstalledVersion(targetPackage)?.substringBefore(".")?.toIntOrNull() ?: 0
134+
Log.d(LOG_TAG, "Chosen package: $targetPackage with version: $packageMajorVersion")
135+
136+
val chromePackages = setOf(
137+
PackageNames.CHROME_STABLE,
138+
PackageNames.CHROME_BETA,
139+
PackageNames.CHROME_DEV,
140+
)
141+
142+
if (chromePackages.contains(targetPackage)) {
143+
return packageMajorVersion < 141
144+
} else if (targetPackage == PackageNames.MICROSOFT_EDGE) {
145+
return packageMajorVersion < 141
146+
} else if (targetPackage == PackageNames.SAMSUNG_INTERNET) {
147+
return packageMajorVersion < 28
148+
} else if (targetPackage == PackageNames.FIREFOX) {
149+
return packageMajorVersion < 143
150+
}
151+
152+
return false
153+
}
154+
121155
override fun onSaveInstanceState(outState: Bundle) {
122156
super.onSaveInstanceState(outState)
123157
outState.putBoolean(KEY_AUTH_STARTED, authStarted)
@@ -146,7 +180,7 @@ class AuthenticationManagementActivity : ComponentActivity() {
146180
state.getParcelable(KEY_AUTH_URI)
147181
} ?: throw IllegalStateException("Authentication URI is null")
148182
intentFlags = state.getInt(KEY_AUTH_OPTION_INTENT_FLAGS, 0)
149-
targetPackage = state.getString(KEY_AUTH_OPTION_TARGET_PACKAGE)
183+
targetPackage = state.getString(KEY_AUTH_OPTION_TARGET_PACKAGE)!!
150184
preferEphemeral = state.getBoolean(KEY_AUTH_OPTION_PREFER_EPHEMERAL, false)
151185
callbackScheme = state.getString(KEY_AUTH_CALLBACK_SCHEME)!!
152186
callbackHost = state.getString(KEY_AUTH_CALLBACK_HOST)

flutter_web_auth_2/android/src/main/kotlin/com/linusu/flutter_web_auth_2/FlutterWebAuth2Plugin.kt

Lines changed: 48 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ package com.linusu.flutter_web_auth_2
33
import android.app.Activity
44
import android.content.Context
55
import android.content.Intent
6-
import android.content.pm.PackageManager
76
import android.net.Uri
8-
import android.os.Build
97
import androidx.browser.customtabs.CustomTabsClient
108

119
import io.flutter.embedding.engine.plugins.FlutterPlugin
@@ -49,8 +47,8 @@ class FlutterWebAuth2Plugin(
4947
val options = call.argument<Map<String, Any>>("options")!!
5048

5149
callbacks[callbackUrlScheme] = resultCallback
52-
activity?.startActivity(Intent(activity,AuthenticationManagementActivity::class.java).apply {
53-
putExtra(AuthenticationManagementActivity.KEY_AUTH_URI,url)
50+
activity?.startActivity(Intent(activity, AuthenticationManagementActivity::class.java).apply {
51+
putExtra(AuthenticationManagementActivity.KEY_AUTH_URI, url)
5452
putExtra(AuthenticationManagementActivity.KEY_AUTH_OPTION_INTENT_FLAGS, options["intentFlags"] as Int)
5553
putExtra(AuthenticationManagementActivity.KEY_AUTH_OPTION_TARGET_PACKAGE, findTargetBrowserPackageName(options))
5654
putExtra(AuthenticationManagementActivity.KEY_AUTH_CALLBACK_SCHEME, callbackUrlScheme)
@@ -92,66 +90,67 @@ class FlutterWebAuth2Plugin(
9290
* Find Support CustomTabs Browser.
9391
*
9492
* Priority:
95-
* 1. Chrome
96-
* 2. Custom Browser Order
97-
* 3. default Browser
98-
* 4. Installed Browser
93+
* 1. Custom Browser Order (if supported)
94+
* 2. default Browser
95+
* 3. Installed Browsers (if supported)
96+
* 4. Chrome (last fallback)
9997
*/
100-
private fun findTargetBrowserPackageName(options: Map<String, Any>): String? {
101-
@Suppress("UNCHECKED_CAST")
102-
val customTabsPackageOrder = (options["customTabsPackageOrder"] as Iterable<String>?) ?: emptyList()
103-
// Check target browser
104-
var targetPackage = customTabsPackageOrder.firstOrNull { isSupportCustomTabs(it) }
105-
if (targetPackage != null) {
106-
return targetPackage
98+
private fun findTargetBrowserPackageName(options: Map<String, Any>): String {
99+
val context = requireNotNull(context) { "Context is null" }
100+
101+
val selectedPackage = (options["customTabsPackageOrder"] as? Iterable<*>)
102+
?.mapNotNull { (it as? String)?.trim() }
103+
?.filter { it.isNotEmpty() }
104+
?.firstOrNull { isSupportCustomTabs(it) }
105+
106+
if (selectedPackage != null) {
107+
return selectedPackage
107108
}
108109

109110
// Check default browser
110-
val defaultBrowserSupported = CustomTabsClient.getPackageName(context!!, emptyList<String>()) != null
111-
if (defaultBrowserSupported) {
112-
return null;
111+
val defaultBrowserSupported = CustomTabsClient.getPackageName(context, emptyList<String>())
112+
if (defaultBrowserSupported != null) {
113+
return defaultBrowserSupported
113114
}
114115
// Check installed browser
115-
val allBrowsers = getInstalledBrowsers()
116-
targetPackage = allBrowsers.firstOrNull { isSupportCustomTabs(it) }
116+
val matchedBrowser = getInstalledBrowsers().firstOrNull { isSupportCustomTabs(it) }
117117

118118
// Safely fall back on Chrome just in case
119-
val chromePackage = "com.android.chrome"
120-
if (targetPackage == null && isSupportCustomTabs(chromePackage)) {
121-
return chromePackage
122-
}
123-
return targetPackage
119+
return matchedBrowser ?: PackageNames.CHROME_STABLE
124120
}
125121

126122
private fun getInstalledBrowsers(): List<String> {
123+
val context = requireNotNull(context) { "Context is null" }
124+
127125
// Get all apps that can handle VIEW intents
128-
val activityIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://"))
129-
val packageManager = context!!.packageManager
130-
val viewIntentHandlers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
131-
packageManager.queryIntentActivities(activityIntent, PackageManager.MATCH_ALL)
132-
} else {
133-
packageManager.queryIntentActivities(activityIntent, 0)
134-
}
126+
val activityIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://"))
127+
val viewIntentHandlers = context.getPackagesForIntent(activityIntent)
128+
129+
val preferredKnownBrowsers = listOf(
130+
PackageNames.CHROME_STABLE,
131+
PackageNames.CHROME_BETA,
132+
PackageNames.SAMSUNG_INTERNET,
133+
PackageNames.MICROSOFT_EDGE,
134+
PackageNames.FIREFOX,
135+
PackageNames.CHROME_DEV,
136+
)
135137

136-
val allBrowser = viewIntentHandlers.map { it.activityInfo.packageName }.sortedWith(compareBy {
137-
if (setOf(
138-
"com.android.chrome",
139-
"com.chrome.beta",
140-
"com.chrome.dev",
141-
"com.microsoft.emmx"
142-
).contains(it)
143-
) {
144-
return@compareBy -1
145-
}
138+
val preferred = mutableListOf<String>()
139+
val others = mutableListOf<String>()
140+
val pushToEnd = mutableListOf<String>() // for the least-favorite apps
146141

147-
// Firefox default is not enabled, must enable in the browser settings.
148-
if (setOf("org.mozilla.firefox").contains(it)) {
149-
return@compareBy 1
150-
}
151-
return@compareBy 0
152-
})
142+
for (pkg in viewIntentHandlers) {
143+
if (preferredKnownBrowsers.contains(pkg)) preferred += pkg
144+
else others += pkg
145+
}
153146

154-
return allBrowser
147+
preferred.sortBy { preferredKnownBrowsers.indexOf(it) }
148+
149+
return buildList {
150+
addAll(preferred)
151+
addAll(others)
152+
addAll(pushToEnd)
153+
}
155154
}
156155

157156
private fun isSupportCustomTabs(packageName: String): Boolean {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.linusu.flutter_web_auth_2
2+
3+
import android.annotation.SuppressLint
4+
import android.app.Activity
5+
import android.content.Intent
6+
import android.net.Uri
7+
import androidx.activity.result.ActivityResultLauncher
8+
import androidx.browser.auth.AuthTabIntent
9+
import androidx.browser.customtabs.CustomTabsIntent
10+
11+
interface TabBuilderWrapper {
12+
fun setEphemeralBrowsingEnabled(enabled: Boolean): TabBuilderWrapper
13+
fun build(): IntentWrapper
14+
}
15+
16+
interface IntentWrapper {
17+
val intent: Intent
18+
fun launch(activity: Activity, launcher: ActivityResultLauncher<Intent>, url: Uri, redirectHost: String, redirectPath: String)
19+
fun launch(activity: Activity, launcher: ActivityResultLauncher<Intent>, url: Uri, redirectScheme: String)
20+
}
21+
22+
@SuppressLint("UnsafeOptInUsageError", "UnsafeOptInUsageWarning")
23+
class CtBuilderWrapper(private val b: CustomTabsIntent.Builder) : TabBuilderWrapper {
24+
override fun setEphemeralBrowsingEnabled(enabled: Boolean) = apply { b.setEphemeralBrowsingEnabled(enabled) }
25+
26+
override fun build(): IntentWrapper {
27+
val intent = b.build()
28+
return object : IntentWrapper {
29+
30+
override val intent: Intent
31+
get() = intent.intent
32+
33+
override fun launch(activity: Activity, launcher: ActivityResultLauncher<Intent>, url: Uri, redirectHost: String, redirectPath: String) {
34+
intent.launchUrl(activity, url)
35+
}
36+
37+
override fun launch(activity: Activity, launcher: ActivityResultLauncher<Intent>, url: Uri, redirectScheme: String) {
38+
intent.launchUrl(activity, url)
39+
}
40+
}
41+
}
42+
}
43+
44+
@SuppressLint("UnsafeOptInUsageError", "UnsafeOptInUsageWarning")
45+
class AuthTabBuilderWrapper(private val b: AuthTabIntent.Builder) : TabBuilderWrapper {
46+
47+
override fun setEphemeralBrowsingEnabled(enabled: Boolean) = apply { b.setEphemeralBrowsingEnabled(enabled) }
48+
49+
override fun build(): IntentWrapper {
50+
val intent = b.build()
51+
return object : IntentWrapper {
52+
53+
override val intent: Intent
54+
get() = intent.intent
55+
56+
override fun launch(activity: Activity, launcher: ActivityResultLauncher<Intent>, url: Uri, redirectHost: String, redirectPath: String) {
57+
intent.launch(launcher, url, redirectHost, redirectPath)
58+
}
59+
60+
override fun launch(activity: Activity, launcher: ActivityResultLauncher<Intent>, url: Uri, redirectScheme: String) {
61+
intent.launch(launcher, url, redirectScheme)
62+
}
63+
}
64+
}
65+
}

0 commit comments

Comments
 (0)