implement open in app feature

This commit is contained in:
Fabian Freund
2026-01-25 17:09:48 +01:00
parent 8b844119c8
commit a1e599f0ab
12 changed files with 345 additions and 17 deletions
@@ -0,0 +1,68 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package eu.weblibre.flutter_mozilla_components.api
import android.content.Context
import android.content.Intent
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAppLinksApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
/**
* Implementation of GeckoAppLinksApi that detects and launches external applications
* that can handle URLs.
*
* This uses Mozilla Android Components' AppLinksUseCases to properly detect if a native
* app is available to handle a URL, matching the behavior in Firefox/Fenix.
*/
class GeckoAppLinksApiImpl(
private val context: Context
) : GeckoAppLinksApi {
companion object {
private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
}
private val components by lazy {
requireNotNull(GlobalComponents.components) { "Components not initialized" }
}
override fun hasExternalApp(url: String, callback: (Result<Boolean>) -> Unit) {
coroutineScope.launch {
try {
val redirect = components.useCases.appLinksUseCases.appLinkRedirect(url)
callback(Result.success(redirect.hasExternalApp()))
} catch (e: Exception) {
callback(Result.success(false))
}
}
}
override fun openAppLink(url: String, callback: (Result<Boolean>) -> Unit) {
coroutineScope.launch {
try {
val redirect = components.useCases.appLinksUseCases.appLinkRedirect(url)
if (!redirect.hasExternalApp()) {
callback(Result.success(false))
return@launch
}
// Set FLAG_ACTIVITY_NEW_TASK to launch in new task
// This prevents issues with app task stacks
redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
components.useCases.appLinksUseCases.openAppLink.invoke(redirect.appIntent)
callback(Result.success(true))
} catch (e: Exception) {
callback(Result.success(false))
}
}
}
}
@@ -21,6 +21,7 @@ import eu.weblibre.flutter_mozilla_components.pigeons.BrowserExtensionEvents
import eu.weblibre.flutter_mozilla_components.pigeons.ContentBlocking
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonsApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAppLinksApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoBookmarksApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoBrowserApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoBrowserExtensionApi
@@ -272,6 +273,7 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
GeckoSitePermissionsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSitePermissionsApiImpl())
GeckoPublicSuffixListApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPublicSuffixListApiImpl(profileApplicationContext))
GeckoTrackingProtectionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTrackingProtectionApiImpl())
GeckoAppLinksApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoAppLinksApiImpl(profileApplicationContext))
// Viewport API for dynamic toolbar and keyboard handling
val viewportEvents = GeckoViewportEvents(_flutterPluginBinding.binaryMessenger)
@@ -7112,3 +7112,85 @@ interface GeckoTrackingProtectionApi {
}
}
}
/**
* API for detecting and launching external applications that can handle URLs.
*
* This API wraps Mozilla Android Components' AppLinksUseCases to allow Flutter
* code to check if native apps can handle URLs and launch them directly.
*
* Generated interface from Pigeon that represents a handler of messages from Flutter.
*/
interface GeckoAppLinksApi {
/**
* Checks if an external application is available to handle the given URL.
*
* This method uses mozilla-components AppLinksUseCases to determine if
* a native app can handle the URL (e.g., YouTube app for youtube.com links).
*
* Returns true if an external app is available, false otherwise.
*/
fun hasExternalApp(url: String, callback: (Result<Boolean>) -> Unit)
/**
* Opens the URL in an external application if available.
*
* This method will:
* 1. Check if an external app can handle the URL
* 2. If available, launch the app directly with Intent.FLAG_ACTIVITY_NEW_TASK
* 3. Return true if successfully launched, false otherwise
*
* Returns true if URL was opened in external app, false if no app available.
*/
fun openAppLink(url: String, callback: (Result<Boolean>) -> Unit)
companion object {
/** The codec used by GeckoAppLinksApi. */
val codec: MessageCodec<Any?> by lazy {
GeckoPigeonCodec()
}
/** Sets up an instance of `GeckoAppLinksApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoAppLinksApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoAppLinksApi.hasExternalApp$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val urlArg = args[0] as String
api.hasExternalApp(urlArg) { result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(GeckoPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(GeckoPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoAppLinksApi.openAppLink$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val urlArg = args[0] as String
api.openAppLink(urlArg) { result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(GeckoPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(GeckoPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
@@ -8,6 +8,7 @@ export 'src/data/models/load_url_flags.dart';
export 'src/data/models/source.dart';
export 'src/domain/entities/default_selection_actions.dart';
export 'src/domain/services/gecko_addon.dart';
export 'src/domain/services/gecko_app_links.dart';
export 'src/domain/services/gecko_bookmarks.dart';
export 'src/domain/services/gecko_browser.dart';
export 'src/domain/services/gecko_browser_extension.dart';
@@ -0,0 +1,40 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
final _api = GeckoAppLinksApi();
/// Service for detecting and launching external applications that can handle URLs.
///
/// This service wraps Mozilla Android Components' AppLinksUseCases to allow
/// checking if native apps can handle URLs and launching them directly.
/// This matches the behavior in Firefox/Fenix for "Open in App" functionality.
class GeckoAppLinksService {
/// Checks if an external application is available to handle the given URL.
///
/// This method uses mozilla-components AppLinksUseCases to determine if
/// a native app can handle the URL (e.g., YouTube app for youtube.com links).
///
/// @param url The URL to check.
/// @return true if an external app is available, false otherwise.
Future<bool> hasExternalApp(Uri url) {
return _api.hasExternalApp(url.toString());
}
/// Opens the URL in an external application if available.
///
/// This method will:
/// 1. Check if an external app can handle the URL
/// 2. If available, launch the app directly with Intent.FLAG_ACTIVITY_NEW_TASK
/// 3. Return true if successfully launched, false otherwise
///
/// @param url The URL to open in external app.
/// @return true if URL was opened in external app, false if no app available.
Future<bool> openAppLink(Uri url) {
return _api.openAppLink(url.toString());
}
}
@@ -8272,3 +8272,89 @@ class GeckoTrackingProtectionApi {
}
}
}
/// API for detecting and launching external applications that can handle URLs.
///
/// This API wraps Mozilla Android Components' AppLinksUseCases to allow Flutter
/// code to check if native apps can handle URLs and launch them directly.
class GeckoAppLinksApi {
/// Constructor for [GeckoAppLinksApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
GeckoAppLinksApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
/// Checks if an external application is available to handle the given URL.
///
/// This method uses mozilla-components AppLinksUseCases to determine if
/// a native app can handle the URL (e.g., YouTube app for youtube.com links).
///
/// Returns true if an external app is available, false otherwise.
Future<bool> hasExternalApp(String url) async {
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAppLinksApi.hasExternalApp$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[url]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as bool?)!;
}
}
/// Opens the URL in an external application if available.
///
/// This method will:
/// 1. Check if an external app can handle the URL
/// 2. If available, launch the app directly with Intent.FLAG_ACTIVITY_NEW_TASK
/// 3. Return true if successfully launched, false otherwise
///
/// Returns true if URL was opened in external app, false if no app available.
Future<bool> openAppLink(String url) async {
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAppLinksApi.openAppLink$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[url]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as bool?)!;
}
}
}
@@ -1977,3 +1977,34 @@ abstract class GeckoTrackingProtectionApi {
@async
void removeAllExceptions();
}
// =============================================================================
// App Links API
// =============================================================================
/// API for detecting and launching external applications that can handle URLs.
///
/// This API wraps Mozilla Android Components' AppLinksUseCases to allow Flutter
/// code to check if native apps can handle URLs and launch them directly.
@HostApi()
abstract class GeckoAppLinksApi {
/// Checks if an external application is available to handle the given URL.
///
/// This method uses mozilla-components AppLinksUseCases to determine if
/// a native app can handle the URL (e.g., YouTube app for youtube.com links).
///
/// Returns true if an external app is available, false otherwise.
@async
bool hasExternalApp(String url);
/// Opens the URL in an external application if available.
///
/// This method will:
/// 1. Check if an external app can handle the URL
/// 2. If available, launch the app directly with Intent.FLAG_ACTIVITY_NEW_TASK
/// 3. Return true if successfully launched, false otherwise
///
/// Returns true if URL was opened in external app, false if no app available.
@async
bool openAppLink(String url);
}