Skip to content

Commit

Permalink
[url_launcher] Add an inAppBrowserView mode in implementations (flu…
Browse files Browse the repository at this point in the history
…tter#5211)

Implementation package portion of flutter#5155

This adds:
- Android support for the new `inAppBrowserView` launch mode which is distinct from `inAppWebView`, so that use cases that require programatic close can specifically request `inAppWebView` instead.
  - The default for web links is the new `inAppBrowserView` since that gives better results in most cases.
  - `inAppBrowserView` will still automatically fall back to `inAppBrowserView` in cases where it's not supported. (In the future, we might want to tune that based on feedback. We could instead have three modes: the webview-only mode we now have, the dynamic mode we now have iff the user requested `platformDefault`, and a new Android Custom Tabs-only if it was explicitly requested which would fail if it doesn't work.)
- iOS support for treating `inAppBrowserView` as identical to `inAppWebView`, since in practice that's what its `inAppWebView` mode has always been.
- Support on all platforms for the new `supportsMode` and `supportsCloseForMode` support query methods.

Fixes flutter/flutter#134208
  • Loading branch information
stuartmorgan authored and HugoOlthof committed Dec 13, 2023
1 parent fdc19b8 commit 362d229
Show file tree
Hide file tree
Showing 36 changed files with 987 additions and 157 deletions.
7 changes: 7 additions & 0 deletions packages/url_launcher/url_launcher_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 6.2.0

* Adds support for `inAppBrowserView` as a separate launch mode option from
`inAppWebView` mode. `inAppBrowserView` is the preferred in-app mode for most uses,
but does not support `closeInAppWebView`.
* Implements `supportsMode` and `supportsCloseForMode`.

## 6.1.1

* Updates annotations lib to 1.7.0.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v10.0.0), do not edit directly.
// Autogenerated from Pigeon (v10.1.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon

package io.flutter.plugins.urllauncher;
Expand Down Expand Up @@ -190,9 +190,15 @@ public interface UrlLauncherApi {
/** Opens the URL externally, returning true if successful. */
@NonNull
Boolean launchUrl(@NonNull String url, @NonNull Map<String, String> headers);
/** Opens the URL in an in-app WebView, returning true if it opens successfully. */
/**
* Opens the URL in an in-app Custom Tab or WebView, returning true if it opens successfully.
*/
@NonNull
Boolean openUrlInWebView(@NonNull String url, @NonNull WebViewOptions options);
Boolean openUrlInApp(
@NonNull String url, @NonNull Boolean allowCustomTab, @NonNull WebViewOptions options);

@NonNull
Boolean supportsCustomTabs();
/** Closes the view opened by [openUrlInSafariViewController]. */
void closeWebView();

Expand All @@ -205,7 +211,9 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable UrlLaunche
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl", getCodec());
binaryMessenger,
"dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.canLaunchUrl",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
Expand All @@ -228,7 +236,9 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable UrlLaunche
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.UrlLauncherApi.launchUrl", getCodec());
binaryMessenger,
"dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.launchUrl",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
Expand All @@ -252,16 +262,42 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable UrlLaunche
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.UrlLauncherApi.openUrlInWebView", getCodec());
binaryMessenger,
"dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.openUrlInApp",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
String urlArg = (String) args.get(0);
WebViewOptions optionsArg = (WebViewOptions) args.get(1);
Boolean allowCustomTabArg = (Boolean) args.get(1);
WebViewOptions optionsArg = (WebViewOptions) args.get(2);
try {
Boolean output = api.openUrlInApp(urlArg, allowCustomTabArg, optionsArg);
wrapped.add(0, output);
} catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.supportsCustomTabs",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
try {
Boolean output = api.openUrlInWebView(urlArg, optionsArg);
Boolean output = api.supportsCustomTabs();
wrapped.add(0, output);
} catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
Expand All @@ -276,7 +312,9 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable UrlLaunche
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.UrlLauncherApi.closeWebView", getCodec());
binaryMessenger,
"dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.closeWebView",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsIntent;
import io.flutter.plugins.urllauncher.Messages.UrlLauncherApi;
import io.flutter.plugins.urllauncher.Messages.WebViewOptions;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;

Expand Down Expand Up @@ -95,14 +97,16 @@ void setActivity(@Nullable Activity activity) {
}

@Override
public @NonNull Boolean openUrlInWebView(@NonNull String url, @NonNull WebViewOptions options) {
public @NonNull Boolean openUrlInApp(
@NonNull String url, @NonNull Boolean allowCustomTab, @NonNull WebViewOptions options) {
ensureActivity();
assert activity != null;

Bundle headersBundle = extractBundle(options.getHeaders());

// Try to launch using Custom Tabs if they have the necessary functionality.
if (!containsRestrictedHeader(options.getHeaders())) {
// Try to launch using Custom Tabs if they have the necessary functionality, unless the caller
// specifically requested a web view.
if (allowCustomTab && !containsRestrictedHeader(options.getHeaders())) {
Uri uri = Uri.parse(url);
if (openCustomTab(activity, uri, headersBundle)) {
return true;
Expand Down Expand Up @@ -131,6 +135,11 @@ public void closeWebView() {
applicationContext.sendBroadcast(new Intent(WebViewActivity.ACTION_CLOSE));
}

@Override
public @NonNull Boolean supportsCustomTabs() {
return CustomTabsClient.getPackageName(applicationContext, Collections.emptyList()) != null;
}

private static boolean openCustomTab(
@NonNull Context context, @NonNull Uri uri, @NonNull Bundle headersBundle) {
CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public void launch_returnsTrue() {
}

@Test
public void openWebView_opensUrl_inWebView() {
public void openUrlInApp_opensUrlInWebViewIfNecessary() {
Activity activity = mock(Activity.class);
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
api.setActivity(activity);
Expand All @@ -141,8 +141,9 @@ public void openWebView_opensUrl_inWebView() {
headers.put("key", "value");

boolean result =
api.openUrlInWebView(
api.openUrlInApp(
url,
true,
new Messages.WebViewOptions.Builder()
.setEnableJavaScript(enableJavaScript)
.setEnableDomStorage(enableDomStorage)
Expand All @@ -162,15 +163,39 @@ public void openWebView_opensUrl_inWebView() {
}

@Test
public void openWebView_opensUrl_inCustomTabs() {
public void openWebView_opensUrlInWebViewIfRequested() {
Activity activity = mock(Activity.class);
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
api.setActivity(activity);
String url = "https://flutter.dev";

boolean result =
api.openUrlInWebView(
api.openUrlInApp(
url,
false,
new Messages.WebViewOptions.Builder()
.setEnableJavaScript(false)
.setEnableDomStorage(false)
.setHeaders(new HashMap<>())
.build());

final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(activity).startActivity(intentCaptor.capture());
assertTrue(result);
assertEquals(url, intentCaptor.getValue().getExtras().getString(WebViewActivity.URL_EXTRA));
}

@Test
public void openWebView_opensUrlInCustomTabs() {
Activity activity = mock(Activity.class);
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
api.setActivity(activity);
String url = "https://flutter.dev";

boolean result =
api.openUrlInApp(
url,
true,
new Messages.WebViewOptions.Builder()
.setEnableJavaScript(false)
.setEnableDomStorage(false)
Expand All @@ -185,7 +210,7 @@ public void openWebView_opensUrl_inCustomTabs() {
}

@Test
public void openWebView_opensUrl_inCustomTabs_withCORSAllowedHeader() {
public void openWebView_opensUrlInCustomTabsWithCORSAllowedHeader() {
Activity activity = mock(Activity.class);
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
api.setActivity(activity);
Expand All @@ -195,8 +220,9 @@ public void openWebView_opensUrl_inCustomTabs_withCORSAllowedHeader() {
headers.put(headerKey, "text/plain");

boolean result =
api.openUrlInWebView(
api.openUrlInApp(
url,
true,
new Messages.WebViewOptions.Builder()
.setEnableJavaScript(false)
.setEnableDomStorage(false)
Expand All @@ -214,7 +240,7 @@ public void openWebView_opensUrl_inCustomTabs_withCORSAllowedHeader() {
}

@Test
public void openWebView_fallsbackTo_inWebView() {
public void openWebView_fallsBackToWebViewIfCustomTabFails() {
Activity activity = mock(Activity.class);
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
api.setActivity(activity);
Expand All @@ -224,8 +250,9 @@ public void openWebView_fallsbackTo_inWebView() {
.startActivity(any(), isNull()); // for custom tabs intent

boolean result =
api.openUrlInWebView(
api.openUrlInApp(
url,
true,
new Messages.WebViewOptions.Builder()
.setEnableJavaScript(false)
.setEnableDomStorage(false)
Expand All @@ -251,8 +278,9 @@ public void openWebView_handlesEnableJavaScript() {
HashMap<String, String> headers = new HashMap<>();
headers.put("key", "value");

api.openUrlInWebView(
api.openUrlInApp(
"https://flutter.dev",
true,
new Messages.WebViewOptions.Builder()
.setEnableJavaScript(enableJavaScript)
.setEnableDomStorage(false)
Expand All @@ -277,8 +305,9 @@ public void openWebView_handlesHeaders() {
headers.put(key1, "value");
headers.put(key2, "value2");

api.openUrlInWebView(
api.openUrlInApp(
"https://flutter.dev",
true,
new Messages.WebViewOptions.Builder()
.setEnableJavaScript(false)
.setEnableDomStorage(false)
Expand All @@ -303,8 +332,9 @@ public void openWebView_handlesEnableDomStorage() {
HashMap<String, String> headers = new HashMap<>();
headers.put("key", "value");

api.openUrlInWebView(
api.openUrlInApp(
"https://flutter.dev",
true,
new Messages.WebViewOptions.Builder()
.setEnableJavaScript(false)
.setEnableDomStorage(enableDomStorage)
Expand All @@ -327,8 +357,9 @@ public void openWebView_throwsForNoCurrentActivity() {
assertThrows(
Messages.FlutterError.class,
() ->
api.openUrlInWebView(
api.openUrlInApp(
"https://flutter.dev",
true,
new Messages.WebViewOptions.Builder()
.setEnableJavaScript(false)
.setEnableDomStorage(false)
Expand All @@ -350,8 +381,9 @@ public void openWebView_returnsFalse() {
.startActivity(any()); // for webview intent

boolean result =
api.openUrlInWebView(
api.openUrlInApp(
"https://flutter.dev",
true,
new Messages.WebViewOptions.Builder()
.setEnableJavaScript(false)
.setEnableDomStorage(false)
Expand Down
Loading

0 comments on commit 362d229

Please sign in to comment.