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 18, 2023
1 parent 4c16f3e commit a40c741
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 14 deletions.
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 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 @@ -4,21 +4,29 @@

package io.flutter.plugins.urllauncher;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Browser;
import android.util.Log;
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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/** Implements the Pigeon-defined interface for calls from Dart. */
final class UrlLauncher implements UrlLauncherApi {
Expand Down Expand Up @@ -79,17 +87,20 @@ void setActivity(@Nullable Activity activity) {
ensureActivity();
assert activity != null;

Intent launchIntent =
new Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(url))
.putExtra(Browser.EXTRA_HEADERS, extractBundle(headers));
try {
activity.startActivity(launchIntent);
} catch (ActivityNotFoundException e) {
return false;
Uri uri = Uri.parse(url);
boolean nativeUrlLaunched =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
? launchNativeApi30(activity, uri)
: launchNativeBeforeApi30(activity, uri);
if (nativeUrlLaunched) {
return true;
}

return true;
Bundle headersBundle = extractBundle(headers);
if (openCustomTab(activity, uri, headersBundle)) {
return true;
}
return legacyLaunch(activity, uri, headersBundle);
}

@Override
Expand Down Expand Up @@ -118,6 +129,87 @@ public void closeWebView() {
applicationContext.sendBroadcast(new Intent(WebViewActivity.ACTION_CLOSE));
}

@TargetApi(Build.VERSION_CODES.R)
private static @NonNull Boolean launchNativeApi30(@NonNull Context context, @NonNull Uri uri) {
Intent nativeAppIntent =
new Intent(Intent.ACTION_VIEW, uri)
.addCategory(Intent.CATEGORY_BROWSABLE)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER);
try {
context.startActivity(nativeAppIntent);
} catch (ActivityNotFoundException ex) {
return false;
}
return true;
}

@SuppressWarnings({"deprecation"})
private static @NonNull Boolean launchNativeBeforeApi30(
@NonNull Context context, @NonNull Uri uri) {
PackageManager pm = context.getPackageManager();
if (pm == null) {
return false;
}

// Get a set of all the apps that can resolve a generic url,
// this will generate a list of all the browsers or browser-like apps.
Intent browserActivityIntent =
new Intent()
.setAction(Intent.ACTION_VIEW)
.addCategory(Intent.CATEGORY_BROWSABLE)
.setData(Uri.fromParts("http", "", null));
Set<String> genericResolvedList =
extractPackageNames(pm.queryIntentActivities(browserActivityIntent, 0));

// Get a set of all the apps that can resolve the specific url.
Intent specializedActivityIntent =
new Intent(Intent.ACTION_VIEW, uri).addCategory(Intent.CATEGORY_BROWSABLE);
Set<String> resolvedSpecializedList =
extractPackageNames(pm.queryIntentActivities(specializedActivityIntent, 0));

// Remove the apps that can resolve a generic url (probably browsers)
// from the list of apps that can resolve the specific url.
resolvedSpecializedList.removeAll(genericResolvedList);

// If the list is empty, no native app handlers were found for the specific url.
if (resolvedSpecializedList.isEmpty()) {
return false;
}

// We found native handlers. Launch the Intent.
specializedActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(specializedActivityIntent);
} catch (ActivityNotFoundException ex) {
return false;
}
return true;
}

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;
}

private static @NonNull Boolean legacyLaunch(
@NonNull Context context, @NonNull Uri uri, @NonNull Bundle headersBundle) {
Intent launchIntent =
new Intent(Intent.ACTION_VIEW).setData(uri).putExtra(Browser.EXTRA_HEADERS, headersBundle);
try {
context.startActivity(launchIntent);
} catch (ActivityNotFoundException ex) {
return false;
}
return true;
}

private static @NonNull Bundle extractBundle(Map<String, String> headersMap) {
final Bundle headersBundle = new Bundle();
for (String key : headersMap.keySet()) {
Expand All @@ -127,6 +219,14 @@ public void closeWebView() {
return headersBundle;
}

public static @NonNull Set<String> extractPackageNames(List<ResolveInfo> resolveInfos) {
Set<String> packageNames = new HashSet<>();
for (ResolveInfo ri : resolveInfos) {
packageNames.add(ri.activityInfo.packageName);
}
return packageNames;
}

private void ensureActivity() {
if (activity == null) {
throw new Messages.FlutterError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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 @@ -95,12 +96,15 @@ public void launch_createsIntentWithPassedUrl() {
String url = "https://flutter.dev";
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
api.setActivity(activity);
doThrow(new ActivityNotFoundException()).when(activity).startActivity(any());
doThrow(new ActivityNotFoundException())
.when(activity)
.startActivity(any(), isNull()); // for custom tabs
doThrow(new ActivityNotFoundException()).when(activity).startActivity(any()); // for legacy

api.launchUrl("https://flutter.dev", new HashMap<>());

final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(activity).startActivity(intentCaptor.capture());
verify(activity).startActivity(intentCaptor.capture(), isNull()); // only checks for custom tabs
assertEquals(url, intentCaptor.getValue().getData().toString());
}

Expand All @@ -109,7 +113,10 @@ public void launch_returnsFalse() {
Activity activity = mock(Activity.class);
UrlLauncher api = new UrlLauncher(ApplicationProvider.getApplicationContext());
api.setActivity(activity);
doThrow(new ActivityNotFoundException()).when(activity).startActivity(any());
doThrow(new ActivityNotFoundException())
.when(activity)
.startActivity(any(), isNull()); // for custom tabs
doThrow(new ActivityNotFoundException()).when(activity).startActivity(any()); // for legacy

boolean result = api.launchUrl("https://flutter.dev", new HashMap<>());

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 a40c741

Please sign in to comment.