Skip to content

Commit

Permalink
[url_launcher_android] Add support for Custom Tabs
Browse files Browse the repository at this point in the history
  • Loading branch information
rajveermalviya committed Aug 22, 2023
1 parent c8a0870 commit b753406
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 8 deletions.
14 changes: 13 additions & 1 deletion packages/url_launcher/url_launcher/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ class _MyHomePageState extends State<MyHomePage> {
}

Future<void> _launchInWebViewOrVC(Uri url) async {
if (!await launchUrl(url, mode: LaunchMode.inAppWebView)) {
throw Exception('Could not launch $url');
}
}

Future<void> _launchInWebViewOrVCWithCustomHeaders(Uri url) async {
if (!await launchUrl(
url,
mode: LaunchMode.inAppWebView,
Expand Down Expand Up @@ -171,6 +177,12 @@ class _MyHomePageState extends State<MyHomePage> {
}),
child: const Text('Launch in app'),
),
ElevatedButton(
onPressed: () => setState(() {
_launched = _launchInWebViewOrVCWithCustomHeaders(toLaunch);
}),
child: const Text('Launch in app (Custom Headers)'),
),
ElevatedButton(
onPressed: () => setState(() {
_launched = _launchInWebViewWithoutJavaScript(toLaunch);
Expand All @@ -194,7 +206,7 @@ class _MyHomePageState extends State<MyHomePage> {
const Padding(padding: EdgeInsets.all(16.0)),
ElevatedButton(
onPressed: () => setState(() {
_launched = _launchInWebViewOrVC(toLaunch);
_launched = _launchInWebViewOrVCWithCustomHeaders(toLaunch);
Timer(const Duration(seconds: 5), () {
closeInAppWebView();
});
Expand Down
3 changes: 2 additions & 1 deletion packages/url_launcher/url_launcher_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## NEXT
## 6.0.39

* Updates minimum supported SDK version to Flutter 3.7/Dart 2.19.
* Add support for Android Custom Tabs

## 6.0.38

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ dependencies {
// Java language implementation
implementation "androidx.core:core:1.10.1"
implementation 'androidx.annotation:annotation:1.6.0'
implementation 'androidx.browser:browser:1.5.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.1.1'
testImplementation 'androidx.test:core:1.0.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.browser.customtabs.CustomTabsIntent;
import io.flutter.plugins.urllauncher.Messages.UrlLauncherApi;
import io.flutter.plugins.urllauncher.Messages.WebViewOptions;
import java.util.Map;
Expand Down Expand Up @@ -46,12 +47,12 @@ interface IntentResolver {
}

UrlLauncher(@NonNull Context context) {
this(
context,
this.applicationContext = context;
this.intentResolver =
intent -> {
ComponentName componentName = intent.resolveActivity(context.getPackageManager());
return componentName == null ? null : componentName.toShortString();
});
};
}

void setActivity(@Nullable Activity activity) {
Expand Down Expand Up @@ -97,13 +98,23 @@ void setActivity(@Nullable Activity activity) {
ensureActivity();
assert activity != null;

Uri uri = Uri.parse(url);
Bundle headersBundle = extractBundle(options.getHeaders());
if (!containsRestrictedHeader(options.getHeaders())
&& options.getEnableJavaScript()
&& options.getEnableDomStorage()) {
if (openCustomTab(activity, uri, headersBundle)) {
return true;
}
}

Intent launchIntent =
WebViewActivity.createIntent(
activity,
url,
options.getEnableJavaScript(),
options.getEnableDomStorage(),
extractBundle(options.getHeaders()));
headersBundle);
try {
activity.startActivity(launchIntent);
} catch (ActivityNotFoundException e) {
Expand All @@ -118,6 +129,38 @@ public void closeWebView() {
applicationContext.sendBroadcast(new Intent(WebViewActivity.ACTION_CLOSE));
}

private static @NonNull 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 @NonNull Boolean containsRestrictedHeader(Map<String, String> headersMap) {
for (String key : headersMap.keySet()) {
switch (key.toLowerCase()) {
case "accept":
continue;
case "accept-language":
continue;
case "content-language":
continue;
case "content-type":
continue;
default:
return true;
}
}
return false;
}

private static @NonNull Bundle extractBundle(Map<String, String> headersMap) {
final Bundle headersBundle = new Bundle();
for (String key : headersMap.keySet()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
Expand Down Expand Up @@ -128,7 +130,7 @@ public void launch_returnsTrue() {
}

@Test
public void openWebView_opensUrl() {
public void openWebView_opensUrl_inWebView() {
Activity activity = mock(Activity.class);
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
api.setActivity(activity);
Expand Down Expand Up @@ -157,6 +159,61 @@ public void openWebView_opensUrl() {
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 enableJavaScript = true;
boolean enableDomStorage = true;

boolean result =
api.openUrlInWebView(
url,
new Messages.WebViewOptions.Builder()
.setEnableJavaScript(enableJavaScript)
.setEnableDomStorage(enableDomStorage)
.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";
boolean enableJavaScript = true;
boolean enableDomStorage = true;
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(enableJavaScript)
.setEnableDomStorage(enableDomStorage)
.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_handlesEnableJavaScript() {
Activity activity = mock(Activity.class);
Expand Down
20 changes: 20 additions & 0 deletions packages/url_launcher/url_launcher_android/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ class _MyHomePageState extends State<MyHomePage> {
}

Future<void> _launchInWebView(String url) async {
if (!await launcher.launch(
url,
useSafariVC: true,
useWebView: true,
enableJavaScript: true,
enableDomStorage: true,
universalLinksOnly: false,
headers: <String, String>{},
)) {
throw Exception('Could not launch $url');
}
}

Future<void> _launchInWebViewWithCustomHeaders(String url) async {
if (!await launcher.launch(
url,
useSafariVC: true,
Expand Down Expand Up @@ -185,6 +199,12 @@ class _MyHomePageState extends State<MyHomePage> {
}),
child: const Text('Launch in app'),
),
ElevatedButton(
onPressed: () => setState(() {
_launched = _launchInWebViewWithCustomHeaders(toLaunch);
}),
child: const Text('Launch in app (Custom headers)'),
),
ElevatedButton(
onPressed: () => setState(() {
_launched = _launchInWebViewWithJavaScript(toLaunch);
Expand Down
2 changes: 1 addition & 1 deletion packages/url_launcher/url_launcher_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.0.38
version: 6.0.39
environment:
sdk: ">=2.19.0 <4.0.0"
flutter: ">=3.7.0"
Expand Down

0 comments on commit b753406

Please sign in to comment.