Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support launching a URL in an external browser #157

Merged
merged 4 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions flutter_custom_tabs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
[![pub package](https://img.shields.io/pub/v/flutter_custom_tabs.svg)](https://pub.dartlang.org/packages/flutter_custom_tabs)

A Flutter plugin for mobile apps to launch a URL in Custom Tabs.
This plugin allows you to add the browser experience that Custom Tabs provides to your mobile apps.
The plugin allows you to add the browser experience that Custom Tabs provides to your mobile apps.

In version 2.0, the plugin expands the support for launching a URL in mobile apps:
- Launch a URL in an external browser.
- Launch a deep link URL.

| | Android | iOS | Web |
|-------------|---------|-------|-------|
Expand All @@ -24,7 +28,7 @@ dependencies:
- Android Gradle Plugin v7.4.0 and above.
- Kotlin v1.7.0 and above.

```diff
```groovy
// your-project/android/build.gradle
buildscript {
ext.kotlin_version = '1.7.0' // and above if explicitly depending on Kotlin.
Expand Down Expand Up @@ -52,7 +56,7 @@ void _launchURL(BuildContext context) async {
final theme = Theme.of(context);
try {
await launchUrl(
Uri.parse('https://flutter.dev'),
Uri.parse('https://flutter.dev'),
customTabsOptions: CustomTabsOptions(
colorSchemes: CustomTabsColorSchemes.defaults(
toolbarColor: theme.colorScheme.surface,
Expand Down Expand Up @@ -85,8 +89,8 @@ See the example app for more complex examples.
This package supports a wide range of Custom Tabs customizations,
but we have experimentally introduced a lightweight URL launch for users who don't need as much in v2.0.0.

> [!TIP]
> On Android, **the lightweight version** prioritizes launching the default browser that supports Custom Tabs over Chrome.
> [!NOTE]
> On Android, **the lightweight version** prefers launching the default browser that supports Custom Tabs over Chrome.

```dart
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -136,12 +140,13 @@ Support status in `flutter_custom_tabs`:
## Advanced Usage

### Deep Linking
Supports launching deep link URLs.
Supports launching a deep link URL.
(If a native app that responds to the deep link URL is installed, it will directly launch it. otherwise, it will launch a custom tab.)

```dart
import 'package:flutter/material.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
// or import 'package:flutter_custom_tabs/flutter_custom_tabs_lite.dart';

Future<void> _launchDeepLinkURL(BuildContext context) async {
final theme = Theme.of(context);
Expand All @@ -161,8 +166,22 @@ Future<void> _launchDeepLinkURL(BuildContext context) async {
}
```

## Launch in an external browser
By default, if no mobile platform-specific options are specified, a URL will be launched in an external browser.

> [!TIP]
> Android: `CustomTabsOptions.externalBrowser` supports HTTP request headers.

```dart
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';

Future<void> _launchInExternalBrowser() async {
await launchUrl(Uri.parse('https://flutter.dev'));
}
```

### Show as a bottom sheet
You can launch URLs in Custom Tabs as a bottom sheet.
You can launch a URL in Custom Tabs as a bottom sheet.

Requirements:
- Android: Chrome v107 and above or [other browsers](https://developer.chrome.com/docs/android/custom-tabs/browser-support/#setinitialactivityheightpx)
Expand Down Expand Up @@ -204,14 +223,16 @@ Future<void> _launchURLInBottomSheet(BuildContext context) async {
```

### Prefer the default browser over Chrome
On Android, the default browser to launch is Chrome, which supports all Custom Tabs features.
On Android, the plugin defaults to launching Chrome, which supports all Custom Tabs features.
You can prioritize launching the default browser on the device that supports Custom Tabs over Chrome.

> [!NOTE]
> Some browsers may not support the options specified in CustomTabsOptions.
> - See: [Custom Tabs Browser Support](https://developer.chrome.com/docs/android/custom-tabs/browser-support/).

```dart
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';

Future<void> _launchURLInDefaultBrowserOnAndroid() async {
await launchUrl(
Uri.parse('https://flutter.dev'),
Expand Down
19 changes: 17 additions & 2 deletions flutter_custom_tabs/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class MyApp extends StatelessWidget {
child: const Text('Show flutter.dev (lite ver)'),
),
FilledButton(
onPressed: () => _launchDeepLinkingURL(context),
onPressed: () => _launchDeepLinkURL(context),
child: const Text('Deep link to platform maps'),
),
FilledButton(
Expand All @@ -71,6 +71,10 @@ class MyApp extends StatelessWidget {
onPressed: () => _launchAndCloseManually(context),
child: const Text('Show flutter.dev + close after 5 seconds'),
),
FilledButton(
onPressed: () => _launchInExternalBrowser(),
child: const Text('Show flutter.dev in external browser'),
),
],
),
),
Expand Down Expand Up @@ -145,7 +149,7 @@ Future<void> _launchUrlLite(BuildContext context) async {
}
}

Future<void> _launchDeepLinkingURL(BuildContext context) async {
Future<void> _launchDeepLinkURL(BuildContext context) async {
final theme = Theme.of(context);
final uri = Platform.isIOS
? 'https://maps.apple.com/?q=tokyo+station'
Expand Down Expand Up @@ -282,3 +286,14 @@ Future<void> _launchAndCloseManually(BuildContext context) async {
debugPrint(e.toString());
}
}

Future<void> _launchInExternalBrowser() async {
try {
await launchUrl(
Uri.parse('https://flutter.dev'),
prefersDeepLink: false,
);
} catch (e) {
debugPrint(e.toString());
}
}
14 changes: 13 additions & 1 deletion flutter_custom_tabs/lib/src/launcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import 'package:flutter_custom_tabs_platform_interface/flutter_custom_tabs_platf
/// - On iOS, the appearance and behavior of [SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller) can be customized using the [safariVCOptions] parameter.
/// - For web, customization options are not available.
///
/// Example:
/// If [customTabsOptions] or [safariVCOptions] are `null`, the URL will be launched in an external browser on mobile platforms.
///
/// Example of launching Custom Tabs:
///
/// ```dart
/// final theme = ...;
Expand Down Expand Up @@ -39,6 +41,16 @@ import 'package:flutter_custom_tabs_platform_interface/flutter_custom_tabs_platf
/// // An exception is thrown if browser app is not installed on Android device.
/// }
/// ```
///
/// Example of launching an external browser:
///
/// ```dart
/// try {
/// await launchUrl(Uri.parse('https://flutter.dev'));
/// } catch (e) {
/// // An exception is thrown if browser app is not installed on Android device.
/// }
/// ```
Future<void> launchUrl(
Uri url, {
bool prefersDeepLink = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
import static com.github.droibit.flutter.plugins.customtabs.ResourceFactory.resolveDrawableIdentifier;

import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Bundle;
import android.provider.Browser;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.browser.customtabs.CustomTabColorSchemeParams;
import androidx.browser.customtabs.CustomTabsIntent;
Expand All @@ -40,8 +42,28 @@ class CustomTabsFactory {
this.context = context;
}

@Nullable
Intent createExternalBrowserIntent(@Nullable CustomTabsOptionsMessage options) {
final Intent intent = new Intent(Intent.ACTION_VIEW);
if (options == null) {
return intent;
}

final CustomTabsBrowserConfigurationMessage browserOptions = options.getBrowser();
if (browserOptions == null || !browserOptions.getPrefersExternalBrowser()) {
return null;
}

final Map<String, String> headers = browserOptions.getHeaders();
if (headers != null) {
final Bundle bundleHeaders = extractBundle(headers);
intent.putExtra(Browser.EXTRA_HEADERS, bundleHeaders);
}
return intent;
}

@NonNull
CustomTabsIntent createIntent(@NonNull CustomTabsOptionsMessage options) {
CustomTabsIntent createCustomTabsIntent(@NonNull CustomTabsOptionsMessage options) {
final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
final CustomTabsColorSchemesMessage colorSchemes = options.getColorSchemes();
if (colorSchemes != null) {
Expand Down Expand Up @@ -92,7 +114,6 @@ CustomTabsIntent createIntent(@NonNull CustomTabsOptionsMessage options) {
}
applyBrowserConfiguration(customTabsIntent, browserConfiguration);
return customTabsIntent;

}

void applyColorSchemes(
Expand All @@ -101,6 +122,7 @@ void applyColorSchemes(
) {
final Long colorScheme = colorSchemes.getColorScheme();
if (colorScheme != null) {

builder.setColorScheme(colorScheme.intValue());
}

Expand Down Expand Up @@ -210,10 +232,7 @@ private void applyBrowserConfiguration(
) {
final Map<String, String> headers = options.getHeaders();
if (headers != null) {
final Bundle bundleHeaders = new Bundle();
for (Map.Entry<String, String> header : headers.entrySet()) {
bundleHeaders.putString(header.getKey(), header.getValue());
}
final Bundle bundleHeaders = extractBundle(headers);
customTabsIntent.intent.putExtra(Browser.EXTRA_HEADERS, bundleHeaders);
}

Expand All @@ -238,4 +257,12 @@ private void applyBrowserConfiguration(
ensureChromeCustomTabsPackage(customTabsIntent, context, fallback);
}
}

private @NonNull Bundle extractBundle(@NonNull Map<String, String> headers) {
final Bundle dest = new Bundle(headers.size());
for (Map.Entry<String, String> entry : headers.entrySet()) {
dest.putString(entry.getKey(), entry.getValue());
}
return dest;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_INITIAL_ACTIVITY_HEIGHT_PX;
import static androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION;
import static java.util.Objects.requireNonNull;

import android.app.Activity;
import android.app.ActivityManager;
Expand Down Expand Up @@ -36,10 +37,10 @@ void setActivity(@Nullable Activity activity) {
}

@Override
public void launchUrl(
public void launch(
@NonNull String urlString,
@NonNull Boolean prefersDeepLink,
@NonNull CustomTabsOptionsMessage options
@Nullable CustomTabsOptionsMessage options
) {
final Activity activity = this.activity;
if (activity == null) {
Expand All @@ -51,9 +52,16 @@ public void launchUrl(
return;
}

final CustomTabsFactory factory = new CustomTabsFactory(activity);
try {
final CustomTabsIntent customTabsIntent = factory.createIntent(options);
final CustomTabsFactory factory = new CustomTabsFactory(activity);
final Intent externalBrowserIntent = factory.createExternalBrowserIntent(options);
if (externalBrowserIntent != null) {
externalBrowserIntent.setData(uri);
activity.startActivity(externalBrowserIntent);
return;
}

final CustomTabsIntent customTabsIntent = factory.createCustomTabsIntent(requireNonNull(options));
if (customTabsIntent.intent.hasExtra(EXTRA_INITIAL_ACTIVITY_HEIGHT_PX)) {
customTabsIntent.intent.setData(uri);
activity.startActivityForResult(customTabsIntent.intent, REQUEST_CODE_CUSTOM_TABS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.github.droibit.flutter.plugins.customtabs.Messages.CustomTabsApi;

import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
Expand All @@ -13,7 +15,7 @@ public class CustomTabsPlugin implements FlutterPlugin, ActivityAware {
@Override
public void onAttachedToEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) {
api = new CustomTabsLauncher();
Messages.CustomTabsApi.setUp(binding.getBinaryMessenger(), api);
CustomTabsApi.setUp(binding.getBinaryMessenger(), api);
}

@Override
Expand All @@ -22,7 +24,7 @@ public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding bin
return;
}

Messages.CustomTabsApi.setUp(binding.getBinaryMessenger(), null);
CustomTabsApi.setUp(binding.getBinaryMessenger(), null);
api = null;
}

Expand Down
Loading