Skip to content

Commit

Permalink
Fixes Android when default browser is not support CustomTabs (#111)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
kecson authored Jun 6, 2024
1 parent 67e1e9e commit 963111a
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 51 deletions.
8 changes: 7 additions & 1 deletion flutter_web_auth_2/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.linusu.flutter_web_auth_2">
package="com.linusu.flutter_web_auth_2">

<queries>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,20 +1,49 @@
package com.linusu.flutter_web_auth_2

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle

class CallbackActivity: Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
class CallbackActivity : Activity() {

val url = intent?.data
val scheme = url?.scheme
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

if (scheme != null) {
FlutterWebAuth2Plugin.callbacks.remove(scheme)?.success(url.toString())
val url = intent?.data ?: fixAutoVerifyNotWorks(intent)
val scheme = url?.scheme

if (scheme != null) {
FlutterWebAuth2Plugin.callbacks.remove(scheme)?.success(url.toString())
}
finishAndRemoveTask()
}


/** Fix sometimes android:autoVerify="true" cannot works when it can't access Google after installation.
* See https://stackoverflow.com/questions/76383106/auto-verify-not-always-working-in-app-links-using-android
*
* must register in AndroidManifest.xml :
* <intent-filter>
* <action android:name="android.intent.action.SEND" />
* <category android:name="android.intent.category.DEFAULT" />
* <data android:mimeType="text/plain" />
*</intent-filter>
*/
private fun fixAutoVerifyNotWorks(intent: Intent?): Uri? {
if (intent?.action == Intent.ACTION_SEND && "text/plain" == intent.type) {
return intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
try {
//scheme://host/path#id_token=xxx
return Uri.parse(it)
} catch (e: Exception) {
return null
}
}
}
return null
}

finish()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package com.linusu.flutter_web_auth_2

import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri

import android.os.Build
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent

import io.flutter.embedding.engine.plugins.FlutterPlugin
Expand All @@ -13,51 +15,136 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result

class FlutterWebAuth2Plugin(private var context: Context? = null, private var channel: MethodChannel? = null): MethodCallHandler, FlutterPlugin {
companion object {
val callbacks = mutableMapOf<String, Result>()
}
class FlutterWebAuth2Plugin(
private var context: Context? = null,
private var channel: MethodChannel? = null
) : MethodCallHandler, FlutterPlugin {
companion object {
val callbacks = mutableMapOf<String, Result>()
}

private fun initInstance(messenger: BinaryMessenger, context: Context) {
this.context = context
channel = MethodChannel(messenger, "flutter_web_auth_2")
channel?.setMethodCallHandler(this)
}

fun initInstance(messenger: BinaryMessenger, context: Context) {
this.context = context
channel = MethodChannel(messenger, "flutter_web_auth_2")
channel?.setMethodCallHandler(this)
}
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
initInstance(binding.binaryMessenger, binding.applicationContext)
}

override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
initInstance(binding.binaryMessenger, binding.applicationContext)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
context = null
channel = null
}

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
context = null
channel = null
}
override fun onMethodCall(call: MethodCall, resultCallback: Result) {
when (call.method) {
"authenticate" -> {
val url = Uri.parse(call.argument("url"))
val callbackUrlScheme = call.argument<String>("callbackUrlScheme")!!
val options = call.argument<Map<String, Any>>("options")!!

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

callbacks[callbackUrlScheme] = resultCallback
intent.intent.addFlags(options["intentFlags"] as Int)
intent.intent.putExtra("android.support.customtabs.extra.KEEP_ALIVE", keepAliveIntent)

val targetPackage = findTargetBrowserPackageName(options)
if (targetPackage != null) {
intent.intent.setPackage(targetPackage)
}
intent.launchUrl(context!!, url)
}

"cleanUpDanglingCalls" -> {
callbacks.forEach { (_, danglingResultCallback) ->
danglingResultCallback.error("CANCELED", "User canceled login", null)
}
callbacks.clear()
resultCallback.success(null)
}

else -> resultCallback.notImplemented()
}
}

val intent = CustomTabsIntent.Builder().build()
val keepAliveIntent = Intent(context, KeepAliveService::class.java)
/**
* Find Support CustomTabs Browser.
*
* Priority:
* 1. Chrome
* 2. Custom Browser Order
* 3. default Browser
* 4. Installed Browser
*/
private fun findTargetBrowserPackageName(options: Map<String, Any>): String? {
val chromePackage = "com.android.chrome"
//if installed chrome, use chrome at first
if (isSupportCustomTabs(chromePackage)) {
return chromePackage
}

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

intent.launchUrl(context!!, url)
//check default browser
val defaultBrowserSupported = CustomTabsClient.getPackageName(context!!, emptyList<String>()) != null
if (defaultBrowserSupported) {
return null;
}
"cleanUpDanglingCalls" -> {
callbacks.forEach{ (_, danglingResultCallback) ->
danglingResultCallback.error("CANCELED", "User canceled login", null)
}
callbacks.clear()
resultCallback.success(null)
//check installed browser
val allBrowsers = getInstalledBrowsers()
targetPackage = allBrowsers.firstOrNull { isSupportCustomTabs(it) }

return targetPackage
}

private fun getInstalledBrowsers(): List<String> {
// Get all apps that can handle VIEW intents
val activityIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://"))
val packageManager = context!!.packageManager
val viewIntentHandlers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
packageManager.queryIntentActivities(activityIntent, PackageManager.MATCH_ALL)
} else {
packageManager.queryIntentActivities(activityIntent, 0)
}
else -> resultCallback.notImplemented()

val allBrowser = viewIntentHandlers.map { it.activityInfo.packageName }.sortedWith(compareBy {
if (setOf(
"com.android.chrome",
"com.chrome.beta",
"com.chrome.dev",
"com.microsoft.emmx"
).contains(it)
) {
return@compareBy -1
}

//FireFox default is not enable ,must enable in the browser settings.
if (setOf("org.mozilla.firefox").contains(it)) {
return@compareBy 1
}
return@compareBy 0
})

return allBrowser
}
}

private fun isSupportCustomTabs(packageName: String): Boolean {
val value = CustomTabsClient.getPackageName(
context!!,
arrayListOf(packageName),
true
)
return value == packageName
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

Expand All @@ -36,12 +36,18 @@
android:exported="true">
<intent-filter android:label="flutter_web_auth_2">
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="foobar" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>

<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
Expand Down
5 changes: 4 additions & 1 deletion flutter_web_auth_2/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ class MyAppState extends State<MyApp> {
callbackUrlScheme: 'foobar',
options: const FlutterWebAuth2Options(
timeout: 5, // example: 5 seconds timeout
//Set Android Browser priority
// customTabsPackageOrder: ['com.android.chrome'],
),
);
setState(() {
Expand All @@ -139,7 +141,8 @@ class MyAppState extends State<MyApp> {
}

@override
Widget build(BuildContext context) => MaterialApp(
Widget build(BuildContext context) =>
MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Web Auth 2 example'),
Expand Down
10 changes: 10 additions & 0 deletions flutter_web_auth_2/lib/src/options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class FlutterWebAuth2Options {
String? landingPageHtml,
bool? silentAuth,
bool? useWebview,
this.customTabsPackageOrder,
}) : preferEphemeral = preferEphemeral ?? false,
intentFlags = intentFlags ?? defaultIntentFlags,
timeout = timeout ?? 5 * 60,
Expand All @@ -74,6 +75,7 @@ class FlutterWebAuth2Options {
landingPageHtml: json['landingPageHtml'],
silentAuth: json['silentAuth'],
useWebview: json['useWebview'],
customTabsPackageOrder: json['customTabsPackageOrder'],
);

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

/// **Only has an effect on Android!**
/// Sets the Android browser priority for opening custom tabs.
/// Needs to be a list of packages providing a custom tabs
/// service. If a browser is not installed, the next on the list
/// is tested etc.
final List<String>? customTabsPackageOrder;

/// Convert this instance to JSON format.
Map<String, dynamic> toJson() => {
'preferEphemeral': preferEphemeral,
Expand All @@ -145,5 +154,6 @@ class FlutterWebAuth2Options {
'landingPageHtml': landingPageHtml,
'silentAuth': silentAuth,
'useWebview': useWebview,
'customTabsPackageOrder': customTabsPackageOrder,
};
}

0 comments on commit 963111a

Please sign in to comment.