Skip to content

Commit 963111a

Browse files
authored
Fixes Android when default browser is not support CustomTabs (#111)
* Fixes Android when default browser is not support CustomTabs * Fixes Android when default browser is not support CustomTabs * Update findTargetPackageName Priority: 1. Chrome 2. Custom Browser Order 3. default Browser 4. Installed Browser * Fix sometimes android:autoVerify="true" cannot works when it can't access Google after installation. * Remove CallbackActivity from RecentTask after finish. * Remove CallbackActivity from RecentTask after finish. * Refactor code. --------- Co-authored-by: kecson <kecson>
1 parent 67e1e9e commit 963111a

6 files changed

Lines changed: 192 additions & 51 deletions

File tree

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2-
package="com.linusu.flutter_web_auth_2">
2+
package="com.linusu.flutter_web_auth_2">
3+
4+
<queries>
5+
<intent>
6+
<action android:name="android.support.customtabs.action.CustomTabsService" />
7+
</intent>
8+
</queries>
39
</manifest>
Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,49 @@
11
package com.linusu.flutter_web_auth_2
22

3+
import android.annotation.SuppressLint
34
import android.app.Activity
5+
import android.content.Intent
46
import android.net.Uri
7+
import android.os.Build
58
import android.os.Bundle
69

7-
class CallbackActivity: Activity() {
8-
override fun onCreate(savedInstanceState: Bundle?) {
9-
super.onCreate(savedInstanceState)
10+
class CallbackActivity : Activity() {
1011

11-
val url = intent?.data
12-
val scheme = url?.scheme
12+
override fun onCreate(savedInstanceState: Bundle?) {
13+
super.onCreate(savedInstanceState)
1314

14-
if (scheme != null) {
15-
FlutterWebAuth2Plugin.callbacks.remove(scheme)?.success(url.toString())
15+
val url = intent?.data ?: fixAutoVerifyNotWorks(intent)
16+
val scheme = url?.scheme
17+
18+
if (scheme != null) {
19+
FlutterWebAuth2Plugin.callbacks.remove(scheme)?.success(url.toString())
20+
}
21+
finishAndRemoveTask()
22+
}
23+
24+
25+
/** Fix sometimes android:autoVerify="true" cannot works when it can't access Google after installation.
26+
* See https://stackoverflow.com/questions/76383106/auto-verify-not-always-working-in-app-links-using-android
27+
*
28+
* must register in AndroidManifest.xml :
29+
* <intent-filter>
30+
* <action android:name="android.intent.action.SEND" />
31+
* <category android:name="android.intent.category.DEFAULT" />
32+
* <data android:mimeType="text/plain" />
33+
*</intent-filter>
34+
*/
35+
private fun fixAutoVerifyNotWorks(intent: Intent?): Uri? {
36+
if (intent?.action == Intent.ACTION_SEND && "text/plain" == intent.type) {
37+
return intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
38+
try {
39+
//scheme://host/path#id_token=xxx
40+
return Uri.parse(it)
41+
} catch (e: Exception) {
42+
return null
43+
}
44+
}
45+
}
46+
return null
1647
}
1748

18-
finish()
19-
}
2049
}

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

Lines changed: 124 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package com.linusu.flutter_web_auth_2
22

33
import android.content.Context
44
import android.content.Intent
5+
import android.content.pm.PackageManager
56
import android.net.Uri
6-
7+
import android.os.Build
8+
import androidx.browser.customtabs.CustomTabsClient
79
import androidx.browser.customtabs.CustomTabsIntent
810

911
import io.flutter.embedding.engine.plugins.FlutterPlugin
@@ -13,51 +15,136 @@ import io.flutter.plugin.common.MethodChannel
1315
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
1416
import io.flutter.plugin.common.MethodChannel.Result
1517

16-
class FlutterWebAuth2Plugin(private var context: Context? = null, private var channel: MethodChannel? = null): MethodCallHandler, FlutterPlugin {
17-
companion object {
18-
val callbacks = mutableMapOf<String, Result>()
19-
}
18+
class FlutterWebAuth2Plugin(
19+
private var context: Context? = null,
20+
private var channel: MethodChannel? = null
21+
) : MethodCallHandler, FlutterPlugin {
22+
companion object {
23+
val callbacks = mutableMapOf<String, Result>()
24+
}
25+
26+
private fun initInstance(messenger: BinaryMessenger, context: Context) {
27+
this.context = context
28+
channel = MethodChannel(messenger, "flutter_web_auth_2")
29+
channel?.setMethodCallHandler(this)
30+
}
2031

21-
fun initInstance(messenger: BinaryMessenger, context: Context) {
22-
this.context = context
23-
channel = MethodChannel(messenger, "flutter_web_auth_2")
24-
channel?.setMethodCallHandler(this)
25-
}
32+
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
33+
initInstance(binding.binaryMessenger, binding.applicationContext)
34+
}
2635

27-
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
28-
initInstance(binding.binaryMessenger, binding.applicationContext)
29-
}
36+
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
37+
context = null
38+
channel = null
39+
}
3040

31-
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
32-
context = null
33-
channel = null
34-
}
41+
override fun onMethodCall(call: MethodCall, resultCallback: Result) {
42+
when (call.method) {
43+
"authenticate" -> {
44+
val url = Uri.parse(call.argument("url"))
45+
val callbackUrlScheme = call.argument<String>("callbackUrlScheme")!!
46+
val options = call.argument<Map<String, Any>>("options")!!
3547

36-
override fun onMethodCall(call: MethodCall, resultCallback: Result) {
37-
when (call.method) {
38-
"authenticate" -> {
39-
val url = Uri.parse(call.argument("url"))
40-
val callbackUrlScheme = call.argument<String>("callbackUrlScheme")!!
41-
val options = call.argument<Map<String, Any>>("options")!!
48+
callbacks[callbackUrlScheme] = resultCallback
49+
val intent = CustomTabsIntent.Builder().build()
50+
val keepAliveIntent = Intent(context, KeepAliveService::class.java)
4251

43-
callbacks[callbackUrlScheme] = resultCallback
52+
intent.intent.addFlags(options["intentFlags"] as Int)
53+
intent.intent.putExtra("android.support.customtabs.extra.KEEP_ALIVE", keepAliveIntent)
54+
55+
val targetPackage = findTargetBrowserPackageName(options)
56+
if (targetPackage != null) {
57+
intent.intent.setPackage(targetPackage)
58+
}
59+
intent.launchUrl(context!!, url)
60+
}
61+
62+
"cleanUpDanglingCalls" -> {
63+
callbacks.forEach { (_, danglingResultCallback) ->
64+
danglingResultCallback.error("CANCELED", "User canceled login", null)
65+
}
66+
callbacks.clear()
67+
resultCallback.success(null)
68+
}
69+
70+
else -> resultCallback.notImplemented()
71+
}
72+
}
4473

45-
val intent = CustomTabsIntent.Builder().build()
46-
val keepAliveIntent = Intent(context, KeepAliveService::class.java)
74+
/**
75+
* Find Support CustomTabs Browser.
76+
*
77+
* Priority:
78+
* 1. Chrome
79+
* 2. Custom Browser Order
80+
* 3. default Browser
81+
* 4. Installed Browser
82+
*/
83+
private fun findTargetBrowserPackageName(options: Map<String, Any>): String? {
84+
val chromePackage = "com.android.chrome"
85+
//if installed chrome, use chrome at first
86+
if (isSupportCustomTabs(chromePackage)) {
87+
return chromePackage
88+
}
4789

48-
intent.intent.addFlags(options["intentFlags"] as Int)
49-
intent.intent.putExtra("android.support.customtabs.extra.KEEP_ALIVE", keepAliveIntent)
90+
@Suppress("UNCHECKED_CAST")
91+
val customTabsPackageOrder = (options["customTabsPackageOrder"] as Iterable<String>?) ?: emptyList()
92+
//check target browser
93+
var targetPackage = customTabsPackageOrder.firstOrNull { isSupportCustomTabs(it) }
94+
if (targetPackage != null) {
95+
return targetPackage
96+
}
5097

51-
intent.launchUrl(context!!, url)
98+
//check default browser
99+
val defaultBrowserSupported = CustomTabsClient.getPackageName(context!!, emptyList<String>()) != null
100+
if (defaultBrowserSupported) {
101+
return null;
52102
}
53-
"cleanUpDanglingCalls" -> {
54-
callbacks.forEach{ (_, danglingResultCallback) ->
55-
danglingResultCallback.error("CANCELED", "User canceled login", null)
56-
}
57-
callbacks.clear()
58-
resultCallback.success(null)
103+
//check installed browser
104+
val allBrowsers = getInstalledBrowsers()
105+
targetPackage = allBrowsers.firstOrNull { isSupportCustomTabs(it) }
106+
107+
return targetPackage
108+
}
109+
110+
private fun getInstalledBrowsers(): List<String> {
111+
// Get all apps that can handle VIEW intents
112+
val activityIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://"))
113+
val packageManager = context!!.packageManager
114+
val viewIntentHandlers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
115+
packageManager.queryIntentActivities(activityIntent, PackageManager.MATCH_ALL)
116+
} else {
117+
packageManager.queryIntentActivities(activityIntent, 0)
59118
}
60-
else -> resultCallback.notImplemented()
119+
120+
val allBrowser = viewIntentHandlers.map { it.activityInfo.packageName }.sortedWith(compareBy {
121+
if (setOf(
122+
"com.android.chrome",
123+
"com.chrome.beta",
124+
"com.chrome.dev",
125+
"com.microsoft.emmx"
126+
).contains(it)
127+
) {
128+
return@compareBy -1
129+
}
130+
131+
//FireFox default is not enable ,must enable in the browser settings.
132+
if (setOf("org.mozilla.firefox").contains(it)) {
133+
return@compareBy 1
134+
}
135+
return@compareBy 0
136+
})
137+
138+
return allBrowser
61139
}
62-
}
140+
141+
private fun isSupportCustomTabs(packageName: String): Boolean {
142+
val value = CustomTabsClient.getPackageName(
143+
context!!,
144+
arrayListOf(packageName),
145+
true
146+
)
147+
return value == packageName
148+
}
149+
63150
}

flutter_web_auth_2/example/android/app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
2727
android:value="true" />
2828
<intent-filter>
29-
<action android:name="android.intent.action.MAIN"/>
30-
<category android:name="android.intent.category.LAUNCHER"/>
29+
<action android:name="android.intent.action.MAIN" />
30+
<category android:name="android.intent.category.LAUNCHER" />
3131
</intent-filter>
3232
</activity>
3333

@@ -36,12 +36,18 @@
3636
android:exported="true">
3737
<intent-filter android:label="flutter_web_auth_2">
3838
<action android:name="android.intent.action.VIEW" />
39+
3940
<category android:name="android.intent.category.DEFAULT" />
4041
<category android:name="android.intent.category.BROWSABLE" />
42+
4143
<data android:scheme="foobar" />
4244
</intent-filter>
45+
<intent-filter>
46+
<action android:name="android.intent.action.SEND" />
47+
<category android:name="android.intent.category.DEFAULT" />
48+
<data android:mimeType="text/plain" />
49+
</intent-filter>
4350
</activity>
44-
4551
<!-- Don't delete the meta-data below.
4652
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
4753
<meta-data

flutter_web_auth_2/example/lib/main.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ class MyAppState extends State<MyApp> {
126126
callbackUrlScheme: 'foobar',
127127
options: const FlutterWebAuth2Options(
128128
timeout: 5, // example: 5 seconds timeout
129+
//Set Android Browser priority
130+
// customTabsPackageOrder: ['com.android.chrome'],
129131
),
130132
);
131133
setState(() {
@@ -139,7 +141,8 @@ class MyAppState extends State<MyApp> {
139141
}
140142

141143
@override
142-
Widget build(BuildContext context) => MaterialApp(
144+
Widget build(BuildContext context) =>
145+
MaterialApp(
143146
home: Scaffold(
144147
appBar: AppBar(
145148
title: const Text('Web Auth 2 example'),

flutter_web_auth_2/lib/src/options.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class FlutterWebAuth2Options {
5656
String? landingPageHtml,
5757
bool? silentAuth,
5858
bool? useWebview,
59+
this.customTabsPackageOrder,
5960
}) : preferEphemeral = preferEphemeral ?? false,
6061
intentFlags = intentFlags ?? defaultIntentFlags,
6162
timeout = timeout ?? 5 * 60,
@@ -74,6 +75,7 @@ class FlutterWebAuth2Options {
7475
landingPageHtml: json['landingPageHtml'],
7576
silentAuth: json['silentAuth'],
7677
useWebview: json['useWebview'],
78+
customTabsPackageOrder: json['customTabsPackageOrder'],
7779
);
7880

7981
/// **Only has an effect on iOS and MacOS!**
@@ -135,6 +137,13 @@ class FlutterWebAuth2Options {
135137
/// described in https://github.com/ThexXTURBOXx/flutter_web_auth_2/issues/25
136138
final bool useWebview;
137139

140+
/// **Only has an effect on Android!**
141+
/// Sets the Android browser priority for opening custom tabs.
142+
/// Needs to be a list of packages providing a custom tabs
143+
/// service. If a browser is not installed, the next on the list
144+
/// is tested etc.
145+
final List<String>? customTabsPackageOrder;
146+
138147
/// Convert this instance to JSON format.
139148
Map<String, dynamic> toJson() => {
140149
'preferEphemeral': preferEphemeral,
@@ -145,5 +154,6 @@ class FlutterWebAuth2Options {
145154
'landingPageHtml': landingPageHtml,
146155
'silentAuth': silentAuth,
147156
'useWebview': useWebview,
157+
'customTabsPackageOrder': customTabsPackageOrder,
148158
};
149159
}

0 commit comments

Comments
 (0)