diff --git a/packages/url_launcher/url_launcher_android/CHANGELOG.md b/packages/url_launcher/url_launcher_android/CHANGELOG.md index fd8418f004d..5729cc1f086 100644 --- a/packages/url_launcher/url_launcher_android/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.3.22 + +* Adds support for `externalNonBrowserApplication` on API 30+. + ## 6.3.21 * Updates minimum supported SDK version to Flutter 3.35. diff --git a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/Messages.java b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/Messages.java index 2fa6e457673..f386f147d43 100644 --- a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/Messages.java +++ b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/Messages.java @@ -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 (v22.4.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.urllauncher; @@ -298,7 +298,10 @@ public interface UrlLauncherApi { Boolean canLaunchUrl(@NonNull String url); /** Opens the URL externally, returning true if successful. */ @NonNull - Boolean launchUrl(@NonNull String url, @NonNull Map headers); + Boolean launchUrl( + @NonNull String url, + @NonNull Map headers, + @NonNull Boolean requireNonBrowser); /** * Opens the URL in an in-app Custom Tab or WebView, returning true if it opens successfully. */ @@ -367,8 +370,9 @@ static void setUp( ArrayList args = (ArrayList) message; String urlArg = (String) args.get(0); Map headersArg = (Map) args.get(1); + Boolean requireNonBrowserArg = (Boolean) args.get(2); try { - Boolean output = api.launchUrl(urlArg, headersArg); + Boolean output = api.launchUrl(urlArg, headersArg, requireNonBrowserArg); wrapped.add(0, output); } catch (Throwable exception) { wrapped = wrapError(exception); diff --git a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java index 35094e79944..9b5d791dd49 100644 --- a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java +++ b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java @@ -10,6 +10,7 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.provider.Browser; import android.util.Log; @@ -80,7 +81,10 @@ void setActivity(@Nullable Activity activity) { } @Override - public @NonNull Boolean launchUrl(@NonNull String url, @NonNull Map headers) { + public @NonNull Boolean launchUrl( + @NonNull String url, + @NonNull Map headers, + @NonNull Boolean requireNonBrowser) { ensureActivity(); assert activity != null; @@ -88,6 +92,9 @@ void setActivity(@Nullable Activity activity) { new Intent(Intent.ACTION_VIEW) .setData(Uri.parse(url)) .putExtra(Browser.EXTRA_HEADERS, extractBundle(headers)); + if (requireNonBrowser && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER); + } try { activity.startActivity(launchIntent); } catch (ActivityNotFoundException e) { diff --git a/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/UrlLauncherTest.java b/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/UrlLauncherTest.java index 9a545a54b43..e1a92ce6b39 100644 --- a/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/UrlLauncherTest.java +++ b/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/UrlLauncherTest.java @@ -30,6 +30,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) public class UrlLauncherTest { @@ -88,7 +89,7 @@ public void launch_throwsForNoCurrentActivity() { Messages.FlutterError exception = assertThrows( Messages.FlutterError.class, - () -> api.launchUrl("https://flutter.dev", new HashMap<>())); + () -> api.launchUrl("https://flutter.dev", new HashMap<>(), false)); assertEquals("NO_ACTIVITY", exception.code); } @@ -100,11 +101,30 @@ public void launch_createsIntentWithPassedUrl() { api.setActivity(activity); doThrow(new ActivityNotFoundException()).when(activity).startActivity(any()); - api.launchUrl("https://flutter.dev", new HashMap<>()); + api.launchUrl("https://flutter.dev", new HashMap<>(), false); final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); verify(activity).startActivity(intentCaptor.capture()); assertEquals(url, intentCaptor.getValue().getData().toString()); + assertEquals(0, intentCaptor.getValue().getFlags() & Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER); + } + + @Config(minSdk = 30) + @Test + public void launch_setsRequireNonBrowserWhenRequested() { + Activity activity = mock(Activity.class); + String url = "https://flutter.dev"; + UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext()); + api.setActivity(activity); + doThrow(new ActivityNotFoundException()).when(activity).startActivity(any()); + + api.launchUrl("https://flutter.dev", new HashMap<>(), true); + + final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(activity).startActivity(intentCaptor.capture()); + assertEquals( + Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER, + intentCaptor.getValue().getFlags() & Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER); } @Test @@ -114,7 +134,7 @@ public void launch_returnsFalse() { api.setActivity(activity); doThrow(new ActivityNotFoundException()).when(activity).startActivity(any()); - boolean result = api.launchUrl("https://flutter.dev", new HashMap<>()); + boolean result = api.launchUrl("https://flutter.dev", new HashMap<>(), false); assertFalse(result); } @@ -125,7 +145,7 @@ public void launch_returnsTrue() { UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext()); api.setActivity(activity); - boolean result = api.launchUrl("https://flutter.dev", new HashMap<>()); + boolean result = api.launchUrl("https://flutter.dev", new HashMap<>(), false); assertTrue(result); } diff --git a/packages/url_launcher/url_launcher_android/example/lib/main.dart b/packages/url_launcher/url_launcher_android/example/lib/main.dart index 30bc2ae2893..6a41cff7e94 100644 --- a/packages/url_launcher/url_launcher_android/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_android/example/lib/main.dart @@ -69,6 +69,17 @@ class _MyHomePageState extends State { } } + Future _launchInNonBrowserExternalApp(String url) async { + if (!await launcher.launchUrl( + url, + const LaunchOptions( + mode: PreferredLaunchMode.externalNonBrowserApplication, + ), + )) { + throw Exception('Could not launch $url'); + } + } + Future _launchInCustomTab(String url) async { if (!await launcher.launchUrl( url, @@ -180,18 +191,24 @@ class _MyHomePageState extends State { child: Text(toLaunch), ), ElevatedButton( - onPressed: _hasCustomTabSupport - ? () => setState(() { - _launched = _launchInBrowser(toLaunch); - }) - : null, + onPressed: () => setState(() { + _launched = _launchInBrowser(toLaunch); + }), child: const Text('Launch in browser'), ), - const Padding(padding: EdgeInsets.all(16.0)), ElevatedButton( onPressed: () => setState(() { - _launched = _launchInCustomTab(toLaunch); + _launched = _launchInNonBrowserExternalApp(toLaunch); }), + child: const Text('Launch in non-browser app'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + ElevatedButton( + onPressed: _hasCustomTabSupport + ? () => setState(() { + _launched = _launchInCustomTab(toLaunch); + }) + : null, child: const Text('Launch in Android Custom Tab'), ), const Padding(padding: EdgeInsets.all(16.0)), diff --git a/packages/url_launcher/url_launcher_android/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_android/lib/src/messages.g.dart index 8de240486ce..ac87850f4bf 100644 --- a/packages/url_launcher/url_launcher_android/lib/src/messages.g.dart +++ b/packages/url_launcher/url_launcher_android/lib/src/messages.g.dart @@ -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 (v22.4.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -142,7 +142,11 @@ class UrlLauncherApi { } /// Opens the URL externally, returning true if successful. - Future launchUrl(String url, Map headers) async { + Future launchUrl( + String url, + Map headers, + bool requireNonBrowser, + ) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.url_launcher_android.UrlLauncherApi.launchUrl$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -152,7 +156,8 @@ class UrlLauncherApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send([url, headers]) as List?; + await pigeonVar_channel.send([url, headers, requireNonBrowser]) + as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { diff --git a/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart b/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart index 8e874c0bcc8..a6429690589 100644 --- a/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart +++ b/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart @@ -77,17 +77,16 @@ class UrlLauncherAndroid extends UrlLauncherPlatform { @override Future launchUrl(String url, LaunchOptions options) async { final bool inApp; + bool requireNonBrowser = false; switch (options.mode) { case PreferredLaunchMode.inAppWebView: case PreferredLaunchMode.inAppBrowserView: inApp = true; case PreferredLaunchMode.externalApplication: + inApp = false; case PreferredLaunchMode.externalNonBrowserApplication: - // TODO(stuartmorgan): Add full support for - // externalNonBrowsingApplication; see - // https://github.com/flutter/flutter/issues/66721. - // Currently it's treated the same as externalApplication. inApp = false; + requireNonBrowser = true; case PreferredLaunchMode.platformDefault: // Intentionally treat any new values as platformDefault; see comment in // supportsMode. @@ -114,6 +113,7 @@ class UrlLauncherAndroid extends UrlLauncherPlatform { succeeded = await _hostApi.launchUrl( url, options.webViewConfiguration.headers, + requireNonBrowser, ); } diff --git a/packages/url_launcher/url_launcher_android/pigeons/messages.dart b/packages/url_launcher/url_launcher_android/pigeons/messages.dart index 1d206c4815f..3f0ec46a812 100644 --- a/packages/url_launcher/url_launcher_android/pigeons/messages.dart +++ b/packages/url_launcher/url_launcher_android/pigeons/messages.dart @@ -40,7 +40,11 @@ abstract class UrlLauncherApi { bool canLaunchUrl(String url); /// Opens the URL externally, returning true if successful. - bool launchUrl(String url, Map headers); + bool launchUrl( + String url, + Map headers, + bool requireNonBrowser, + ); /// Opens the URL in an in-app Custom Tab or WebView, returning true if it /// opens successfully. diff --git a/packages/url_launcher/url_launcher_android/pubspec.yaml b/packages/url_launcher/url_launcher_android/pubspec.yaml index 7d254bc723d..55cbf146a34 100644 --- a/packages/url_launcher/url_launcher_android/pubspec.yaml +++ b/packages/url_launcher/url_launcher_android/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_android description: Android implementation of the url_launcher plugin. 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 -version: 6.3.21 +version: 6.3.22 environment: sdk: ^3.9.0 diff --git a/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart b/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart index 3dba890bfe3..324d7201a77 100644 --- a/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart +++ b/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart @@ -236,6 +236,7 @@ void main() { ); expect(launched, true); expect(api.usedWebView, false); + expect(api.requiredNonBrowser, false); expect(api.passedWebViewOptions?.headers, isEmpty); }); @@ -254,6 +255,19 @@ void main() { expect(api.passedWebViewOptions?.headers['key'], 'value'); }); + test('passes non-browser flag', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); + final bool launched = await launcher.launchUrl( + 'http://example.com/', + const LaunchOptions( + mode: PreferredLaunchMode.externalNonBrowserApplication, + ), + ); + expect(launched, true); + expect(api.usedWebView, false); + expect(api.requiredNonBrowser, true); + }); + test('passes through no-activity exception', () async { final UrlLauncherAndroid launcher = UrlLauncherAndroid(api: api); await expectLater( @@ -484,6 +498,7 @@ class _FakeUrlLauncherApi implements UrlLauncherApi { BrowserOptions? passedBrowserOptions; bool? usedWebView; bool? allowedCustomTab; + bool? requiredNonBrowser; bool? closed; /// A domain that will be treated as having no handler, even for http(s). @@ -495,13 +510,18 @@ class _FakeUrlLauncherApi implements UrlLauncherApi { } @override - Future launchUrl(String url, Map headers) async { + Future launchUrl( + String url, + Map headers, + bool requireNonBrowser, + ) async { passedWebViewOptions = WebViewOptions( enableJavaScript: false, enableDomStorage: false, headers: headers, ); + requiredNonBrowser = requireNonBrowser; usedWebView = false; return _launch(url); }