mirror of
https://github.com/flutter/packages.git
synced 2025-06-27 21:28:33 +08:00
[url_launcher_android] Add support for Custom Tabs (#4739)
Implement support for [Android Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs/). Custom Tabs will only be used if *__all__* of the following conditions are true: - `launchMode` == `LaunchMode.inAppWebView` (or `LaunchMode.platformDefault`; only if url is web url) - `WebViewConfiguration.headers` == `{}` (or if it only contains [CORS-safelisted headers](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header)) Fixes flutter/flutter#18589
This commit is contained in:
@ -1,3 +1,7 @@
|
|||||||
|
## 6.1.14
|
||||||
|
|
||||||
|
* Updates documentation to mention support for Android Custom Tabs.
|
||||||
|
|
||||||
## 6.1.13
|
## 6.1.13
|
||||||
|
|
||||||
* Adds pub topics to package metadata.
|
* Adds pub topics to package metadata.
|
||||||
|
@ -63,6 +63,12 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _launchInWebViewOrVC(Uri url) async {
|
Future<void> _launchInWebViewOrVC(Uri url) async {
|
||||||
|
if (!await launchUrl(url, mode: LaunchMode.inAppWebView)) {
|
||||||
|
throw Exception('Could not launch $url');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _launchAsInAppWebViewWithCustomHeaders(Uri url) async {
|
||||||
if (!await launchUrl(
|
if (!await launchUrl(
|
||||||
url,
|
url,
|
||||||
mode: LaunchMode.inAppWebView,
|
mode: LaunchMode.inAppWebView,
|
||||||
@ -171,6 +177,12 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
}),
|
}),
|
||||||
child: const Text('Launch in app'),
|
child: const Text('Launch in app'),
|
||||||
),
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => setState(() {
|
||||||
|
_launched = _launchAsInAppWebViewWithCustomHeaders(toLaunch);
|
||||||
|
}),
|
||||||
|
child: const Text('Launch in app (Custom Headers)'),
|
||||||
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => setState(() {
|
onPressed: () => setState(() {
|
||||||
_launched = _launchInWebViewWithoutJavaScript(toLaunch);
|
_launched = _launchInWebViewWithoutJavaScript(toLaunch);
|
||||||
|
@ -14,7 +14,7 @@ enum LaunchMode {
|
|||||||
/// implementation.
|
/// implementation.
|
||||||
platformDefault,
|
platformDefault,
|
||||||
|
|
||||||
/// Loads the URL in an in-app web view (e.g., Safari View Controller).
|
/// Loads the URL in an in-app web view (e.g., Android Custom Tabs, Safari View Controller).
|
||||||
inAppWebView,
|
inAppWebView,
|
||||||
|
|
||||||
/// Passes the URL to the OS to be handled by another application.
|
/// Passes the URL to the OS to be handled by another application.
|
||||||
|
@ -3,7 +3,7 @@ description: Flutter plugin for launching a URL. Supports
|
|||||||
web, phone, SMS, and email schemes.
|
web, phone, SMS, and email schemes.
|
||||||
repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher
|
repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher
|
||||||
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
|
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
|
||||||
version: 6.1.13
|
version: 6.1.14
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
## 6.1.0
|
||||||
|
|
||||||
|
* Adds support for Android Custom Tabs.
|
||||||
|
|
||||||
## 6.0.39
|
## 6.0.39
|
||||||
|
|
||||||
* Adds pub topics to package metadata.
|
* Adds pub topics to package metadata.
|
||||||
|
@ -66,6 +66,7 @@ dependencies {
|
|||||||
// Java language implementation
|
// Java language implementation
|
||||||
implementation "androidx.core:core:1.10.1"
|
implementation "androidx.core:core:1.10.1"
|
||||||
implementation 'androidx.annotation:annotation:1.6.0'
|
implementation 'androidx.annotation:annotation:1.6.0'
|
||||||
|
implementation 'androidx.browser:browser:1.5.0'
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.mockito:mockito-core:5.1.1'
|
testImplementation 'org.mockito:mockito-core:5.1.1'
|
||||||
testImplementation 'androidx.test:core:1.0.0'
|
testImplementation 'androidx.test:core:1.0.0'
|
||||||
|
@ -16,8 +16,10 @@ import android.util.Log;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent;
|
||||||
import io.flutter.plugins.urllauncher.Messages.UrlLauncherApi;
|
import io.flutter.plugins.urllauncher.Messages.UrlLauncherApi;
|
||||||
import io.flutter.plugins.urllauncher.Messages.WebViewOptions;
|
import io.flutter.plugins.urllauncher.Messages.WebViewOptions;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/** Implements the Pigeon-defined interface for calls from Dart. */
|
/** Implements the Pigeon-defined interface for calls from Dart. */
|
||||||
@ -97,13 +99,24 @@ final class UrlLauncher implements UrlLauncherApi {
|
|||||||
ensureActivity();
|
ensureActivity();
|
||||||
assert activity != null;
|
assert activity != null;
|
||||||
|
|
||||||
|
Bundle headersBundle = extractBundle(options.getHeaders());
|
||||||
|
|
||||||
|
// Try to launch using Custom Tabs if they have the necessary functionality.
|
||||||
|
if (!containsRestrictedHeader(options.getHeaders())) {
|
||||||
|
Uri uri = Uri.parse(url);
|
||||||
|
if (openCustomTab(activity, uri, headersBundle)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to a web view if necessary.
|
||||||
Intent launchIntent =
|
Intent launchIntent =
|
||||||
WebViewActivity.createIntent(
|
WebViewActivity.createIntent(
|
||||||
activity,
|
activity,
|
||||||
url,
|
url,
|
||||||
options.getEnableJavaScript(),
|
options.getEnableJavaScript(),
|
||||||
options.getEnableDomStorage(),
|
options.getEnableDomStorage(),
|
||||||
extractBundle(options.getHeaders()));
|
headersBundle);
|
||||||
try {
|
try {
|
||||||
activity.startActivity(launchIntent);
|
activity.startActivity(launchIntent);
|
||||||
} catch (ActivityNotFoundException e) {
|
} catch (ActivityNotFoundException e) {
|
||||||
@ -118,6 +131,35 @@ final class UrlLauncher implements UrlLauncherApi {
|
|||||||
applicationContext.sendBroadcast(new Intent(WebViewActivity.ACTION_CLOSE));
|
applicationContext.sendBroadcast(new Intent(WebViewActivity.ACTION_CLOSE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean openCustomTab(
|
||||||
|
@NonNull Context context, @NonNull Uri uri, @NonNull Bundle headersBundle) {
|
||||||
|
CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build();
|
||||||
|
customTabsIntent.intent.putExtra(Browser.EXTRA_HEADERS, headersBundle);
|
||||||
|
try {
|
||||||
|
customTabsIntent.launchUrl(context, uri);
|
||||||
|
} catch (ActivityNotFoundException ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if headers contains a CORS restricted header.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header
|
||||||
|
private static boolean containsRestrictedHeader(Map<String, String> headersMap) {
|
||||||
|
for (String key : headersMap.keySet()) {
|
||||||
|
switch (key.toLowerCase(Locale.US)) {
|
||||||
|
case "accept":
|
||||||
|
case "accept-language":
|
||||||
|
case "content-language":
|
||||||
|
case "content-type":
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static @NonNull Bundle extractBundle(Map<String, String> headersMap) {
|
private static @NonNull Bundle extractBundle(Map<String, String> headersMap) {
|
||||||
final Bundle headersBundle = new Bundle();
|
final Bundle headersBundle = new Bundle();
|
||||||
for (String key : headersMap.keySet()) {
|
for (String key : headersMap.keySet()) {
|
||||||
|
@ -6,9 +6,11 @@ package io.flutter.plugins.urllauncher;
|
|||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.isNull;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@ -128,13 +130,15 @@ public class UrlLauncherTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void openWebView_opensUrl() {
|
public void openWebView_opensUrl_inWebView() {
|
||||||
Activity activity = mock(Activity.class);
|
Activity activity = mock(Activity.class);
|
||||||
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
|
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
|
||||||
api.setActivity(activity);
|
api.setActivity(activity);
|
||||||
String url = "https://flutter.dev";
|
String url = "https://flutter.dev";
|
||||||
boolean enableJavaScript = false;
|
boolean enableJavaScript = false;
|
||||||
boolean enableDomStorage = false;
|
boolean enableDomStorage = false;
|
||||||
|
HashMap<String, String> headers = new HashMap<>();
|
||||||
|
headers.put("key", "value");
|
||||||
|
|
||||||
boolean result =
|
boolean result =
|
||||||
api.openUrlInWebView(
|
api.openUrlInWebView(
|
||||||
@ -142,7 +146,7 @@ public class UrlLauncherTest {
|
|||||||
new Messages.WebViewOptions.Builder()
|
new Messages.WebViewOptions.Builder()
|
||||||
.setEnableJavaScript(enableJavaScript)
|
.setEnableJavaScript(enableJavaScript)
|
||||||
.setEnableDomStorage(enableDomStorage)
|
.setEnableDomStorage(enableDomStorage)
|
||||||
.setHeaders(new HashMap<>())
|
.setHeaders(headers)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
|
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
|
||||||
@ -157,19 +161,102 @@ public class UrlLauncherTest {
|
|||||||
intentCaptor.getValue().getExtras().getBoolean(WebViewActivity.ENABLE_DOM_EXTRA));
|
intentCaptor.getValue().getExtras().getBoolean(WebViewActivity.ENABLE_DOM_EXTRA));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void openWebView_opensUrl_inCustomTabs() {
|
||||||
|
Activity activity = mock(Activity.class);
|
||||||
|
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
|
||||||
|
api.setActivity(activity);
|
||||||
|
String url = "https://flutter.dev";
|
||||||
|
|
||||||
|
boolean result =
|
||||||
|
api.openUrlInWebView(
|
||||||
|
url,
|
||||||
|
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(), isNull());
|
||||||
|
assertTrue(result);
|
||||||
|
assertEquals(Intent.ACTION_VIEW, intentCaptor.getValue().getAction());
|
||||||
|
assertNull(intentCaptor.getValue().getComponent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void openWebView_opensUrl_inCustomTabs_withCORSAllowedHeader() {
|
||||||
|
Activity activity = mock(Activity.class);
|
||||||
|
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
|
||||||
|
api.setActivity(activity);
|
||||||
|
String url = "https://flutter.dev";
|
||||||
|
HashMap<String, String> headers = new HashMap<>();
|
||||||
|
String headerKey = "Content-Type";
|
||||||
|
headers.put(headerKey, "text/plain");
|
||||||
|
|
||||||
|
boolean result =
|
||||||
|
api.openUrlInWebView(
|
||||||
|
url,
|
||||||
|
new Messages.WebViewOptions.Builder()
|
||||||
|
.setEnableJavaScript(false)
|
||||||
|
.setEnableDomStorage(false)
|
||||||
|
.setHeaders(headers)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
|
||||||
|
verify(activity).startActivity(intentCaptor.capture(), isNull());
|
||||||
|
assertTrue(result);
|
||||||
|
assertEquals(Intent.ACTION_VIEW, intentCaptor.getValue().getAction());
|
||||||
|
assertNull(intentCaptor.getValue().getComponent());
|
||||||
|
final Bundle passedHeaders =
|
||||||
|
intentCaptor.getValue().getExtras().getBundle(Browser.EXTRA_HEADERS);
|
||||||
|
assertEquals(headers.get(headerKey), passedHeaders.getString(headerKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void openWebView_fallsbackTo_inWebView() {
|
||||||
|
Activity activity = mock(Activity.class);
|
||||||
|
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
|
||||||
|
api.setActivity(activity);
|
||||||
|
String url = "https://flutter.dev";
|
||||||
|
doThrow(new ActivityNotFoundException())
|
||||||
|
.when(activity)
|
||||||
|
.startActivity(any(), isNull()); // for custom tabs intent
|
||||||
|
|
||||||
|
boolean result =
|
||||||
|
api.openUrlInWebView(
|
||||||
|
url,
|
||||||
|
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));
|
||||||
|
assertEquals(
|
||||||
|
false, intentCaptor.getValue().getExtras().getBoolean(WebViewActivity.ENABLE_JS_EXTRA));
|
||||||
|
assertEquals(
|
||||||
|
false, intentCaptor.getValue().getExtras().getBoolean(WebViewActivity.ENABLE_DOM_EXTRA));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void openWebView_handlesEnableJavaScript() {
|
public void openWebView_handlesEnableJavaScript() {
|
||||||
Activity activity = mock(Activity.class);
|
Activity activity = mock(Activity.class);
|
||||||
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
|
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
|
||||||
api.setActivity(activity);
|
api.setActivity(activity);
|
||||||
boolean enableJavaScript = true;
|
boolean enableJavaScript = true;
|
||||||
|
HashMap<String, String> headers = new HashMap<>();
|
||||||
|
headers.put("key", "value");
|
||||||
|
|
||||||
api.openUrlInWebView(
|
api.openUrlInWebView(
|
||||||
"https://flutter.dev",
|
"https://flutter.dev",
|
||||||
new Messages.WebViewOptions.Builder()
|
new Messages.WebViewOptions.Builder()
|
||||||
.setEnableJavaScript(enableJavaScript)
|
.setEnableJavaScript(enableJavaScript)
|
||||||
.setEnableDomStorage(false)
|
.setEnableDomStorage(false)
|
||||||
.setHeaders(new HashMap<>())
|
.setHeaders(headers)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
|
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
|
||||||
@ -213,13 +300,15 @@ public class UrlLauncherTest {
|
|||||||
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
|
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
|
||||||
api.setActivity(activity);
|
api.setActivity(activity);
|
||||||
boolean enableDomStorage = true;
|
boolean enableDomStorage = true;
|
||||||
|
HashMap<String, String> headers = new HashMap<>();
|
||||||
|
headers.put("key", "value");
|
||||||
|
|
||||||
api.openUrlInWebView(
|
api.openUrlInWebView(
|
||||||
"https://flutter.dev",
|
"https://flutter.dev",
|
||||||
new Messages.WebViewOptions.Builder()
|
new Messages.WebViewOptions.Builder()
|
||||||
.setEnableJavaScript(false)
|
.setEnableJavaScript(false)
|
||||||
.setEnableDomStorage(enableDomStorage)
|
.setEnableDomStorage(enableDomStorage)
|
||||||
.setHeaders(new HashMap<>())
|
.setHeaders(headers)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
|
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
|
||||||
@ -253,7 +342,12 @@ public class UrlLauncherTest {
|
|||||||
Activity activity = mock(Activity.class);
|
Activity activity = mock(Activity.class);
|
||||||
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
|
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
|
||||||
api.setActivity(activity);
|
api.setActivity(activity);
|
||||||
doThrow(new ActivityNotFoundException()).when(activity).startActivity(any());
|
doThrow(new ActivityNotFoundException())
|
||||||
|
.when(activity)
|
||||||
|
.startActivity(any(), isNull()); // for custom tabs intent
|
||||||
|
doThrow(new ActivityNotFoundException())
|
||||||
|
.when(activity)
|
||||||
|
.startActivity(any()); // for webview intent
|
||||||
|
|
||||||
boolean result =
|
boolean result =
|
||||||
api.openUrlInWebView(
|
api.openUrlInWebView(
|
||||||
|
@ -68,6 +68,20 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _launchInWebView(String url) async {
|
Future<void> _launchInWebView(String url) async {
|
||||||
|
if (!await launcher.launch(
|
||||||
|
url,
|
||||||
|
useSafariVC: true,
|
||||||
|
useWebView: true,
|
||||||
|
enableJavaScript: false,
|
||||||
|
enableDomStorage: false,
|
||||||
|
universalLinksOnly: false,
|
||||||
|
headers: <String, String>{},
|
||||||
|
)) {
|
||||||
|
throw Exception('Could not launch $url');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _launchInWebViewWithCustomHeaders(String url) async {
|
||||||
if (!await launcher.launch(
|
if (!await launcher.launch(
|
||||||
url,
|
url,
|
||||||
useSafariVC: true,
|
useSafariVC: true,
|
||||||
@ -185,6 +199,12 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
}),
|
}),
|
||||||
child: const Text('Launch in app'),
|
child: const Text('Launch in app'),
|
||||||
),
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => setState(() {
|
||||||
|
_launched = _launchInWebViewWithCustomHeaders(toLaunch);
|
||||||
|
}),
|
||||||
|
child: const Text('Launch in app (Custom headers)'),
|
||||||
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => setState(() {
|
onPressed: () => setState(() {
|
||||||
_launched = _launchInWebViewWithJavaScript(toLaunch);
|
_launched = _launchInWebViewWithJavaScript(toLaunch);
|
||||||
|
@ -2,7 +2,7 @@ name: url_launcher_android
|
|||||||
description: Android implementation of the url_launcher plugin.
|
description: Android implementation of the url_launcher plugin.
|
||||||
repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_android
|
repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_android
|
||||||
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
|
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
|
||||||
version: 6.0.39
|
version: 6.1.0
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.19.0 <4.0.0"
|
sdk: ">=2.19.0 <4.0.0"
|
||||||
flutter: ">=3.7.0"
|
flutter: ">=3.7.0"
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
## 2.1.5
|
||||||
|
|
||||||
|
* Updates documentation to mention support for Android Custom Tabs.
|
||||||
|
|
||||||
## 2.1.4
|
## 2.1.4
|
||||||
|
|
||||||
* Adds pub topics to package metadata.
|
* Adds pub topics to package metadata.
|
||||||
|
@ -13,7 +13,7 @@ enum PreferredLaunchMode {
|
|||||||
/// implementation.
|
/// implementation.
|
||||||
platformDefault,
|
platformDefault,
|
||||||
|
|
||||||
/// Loads the URL in an in-app web view (e.g., Safari View Controller).
|
/// Loads the URL in an in-app web view (e.g., Android Custom Tabs, Safari View Controller).
|
||||||
inAppWebView,
|
inAppWebView,
|
||||||
|
|
||||||
/// Passes the URL to the OS to be handled by another application.
|
/// Passes the URL to the OS to be handled by another application.
|
||||||
|
@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/
|
|||||||
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
|
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
|
||||||
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
|
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
|
||||||
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
|
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
|
||||||
version: 2.1.4
|
version: 2.1.5
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.19.0 <4.0.0"
|
sdk: ">=2.19.0 <4.0.0"
|
||||||
|
Reference in New Issue
Block a user