Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add non-browser URL launch mode for Android
  • Loading branch information
Mosc committed Jun 11, 2022
commit d14828d4c4607e3bc5634180b5985c5f97fac9ae
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface.
/// On iOS, this should be used in cases where sharing the cookies of the
/// user's browser is important, such as SSO flows, since Safari View
/// Controller does not share the browser's context.
/// - [LaunchMode.externalNonBrowserApplication] is supported on iOS 10+.
/// This setting is used to require universal links to open in a non-browser
/// application.
/// - [LaunchMode.externalNonBrowserApplication] is supported on iOS 10+ and
/// Android. This setting is used to require universal links to open in a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change will need to be a separate PR, since you need to update the minimum Android implementation package version to say this.

/// non-browser application.
///
/// For web, [webOnlyWindowName] specifies a target for the launch. This
/// supports the standard special link target names. For example:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,13 @@ private void onLaunch(MethodCall call, Result result, String url) {
final boolean useWebView = call.argument("useWebView");
final boolean enableJavaScript = call.argument("enableJavaScript");
final boolean enableDomStorage = call.argument("enableDomStorage");
final boolean universalLinksOnly = call.argument("universalLinksOnly");
final Map<String, String> headersMap = call.argument("headers");
final Bundle headersBundle = extractBundle(headersMap);

LaunchStatus launchStatus =
urlLauncher.launch(url, headersBundle, useWebView, enableJavaScript, enableDomStorage);
urlLauncher.launch(
url, headersBundle, useWebView, enableJavaScript, enableDomStorage, universalLinksOnly);

if (launchStatus == LaunchStatus.NO_ACTIVITY) {
result.error("NO_ACTIVITY", "Launching a URL requires a foreground activity.", null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@
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.Nullable;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/** Launches components for URLs. */
class UrlLauncher {
private static final String TAG = "UrlLauncher";
Expand Down Expand Up @@ -60,6 +67,9 @@ boolean canLaunch(String url) {
* @param useWebView when true, the URL is launched inside of {@link WebViewActivity}.
* @param enableJavaScript Only used if {@param useWebView} is true. Enables JS in the WebView.
* @param enableDomStorage Only used if {@param useWebView} is true. Enables DOM storage in the
* WebView.
* @param universalLinksOnly Only used if {@param useWebView} is false. When true, will only
* launch if an app is available that is not a browser.
* @return {@link LaunchStatus#NO_ACTIVITY} if there's no available {@code applicationContext}.
* {@link LaunchStatus#ACTIVITY_NOT_FOUND} if there's no activity found to handle {@code
* launchIntent}. {@link LaunchStatus#OK} otherwise.
Expand All @@ -69,7 +79,8 @@ LaunchStatus launch(
Bundle headersBundle,
boolean useWebView,
boolean enableJavaScript,
boolean enableDomStorage) {
boolean enableDomStorage,
boolean universalLinksOnly) {
if (activity == null) {
return LaunchStatus.NO_ACTIVITY;
}
Expand All @@ -84,6 +95,17 @@ LaunchStatus launch(
new Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(url))
.putExtra(Browser.EXTRA_HEADERS, headersBundle);

if (universalLinksOnly) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
launchIntent = launchIntent.addFlags(Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER);
} else {
Set<String> nonBrowserPackageNames = getNonBrowserPackageNames(launchIntent);
if (nonBrowserPackageNames.isEmpty()) {
return LaunchStatus.ACTIVITY_NOT_FOUND;
}
}
}
}

try {
Expand All @@ -95,6 +117,31 @@ LaunchStatus launch(
return LaunchStatus.OK;
}

private Set<String> getNonBrowserPackageNames(Intent specializedIntent) {
PackageManager packageManager = activity.getPackageManager();

// Get all apps that resolve the specific URL.
Set<String> specializedPackageNames = queryPackageNames(packageManager, specializedIntent);

// Get all apps that resolve a generic URL.
Intent genericIntent =
new Intent().setAction(Intent.ACTION_VIEW).setData(Uri.fromParts("https", "", null));
Set<String> genericPackageNames = queryPackageNames(packageManager, genericIntent);

// Keep only the apps that resolve the specific, but not the generic URLs.
specializedPackageNames.removeAll(genericPackageNames);
return specializedPackageNames;
}

private Set<String> queryPackageNames(PackageManager packageManager, Intent intent) {
List<ResolveInfo> intentActivities = packageManager.queryIntentActivities(intent, 0);
Set<String> packageNames = new HashSet<>();
for (ResolveInfo intentActivity : intentActivities) {
packageNames.add(intentActivity.activityInfo.packageName);
}
return packageNames;
}

/** Closes any activities started with {@link #launch} {@code useWebView=true}. */
void closeWebView() {
applicationContext.sendBroadcast(new Intent(WebViewActivity.ACTION_CLOSE));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,23 @@ public void onMethodCall_launchReturnsNoActivityError() {
boolean useWebView = false;
boolean enableJavaScript = false;
boolean enableDomStorage = false;
boolean universalLinksOnly = false;
// Setup arguments map send on the method channel
Map<String, Object> args = new HashMap<>();
args.put("url", url);
args.put("useWebView", useWebView);
args.put("enableJavaScript", enableJavaScript);
args.put("enableDomStorage", enableDomStorage);
args.put("universalLinksOnly", universalLinksOnly);
args.put("headers", new HashMap<>());
// Mock the launch method on the urlLauncher class
when(urlLauncher.launch(
eq(url), any(Bundle.class), eq(useWebView), eq(enableJavaScript), eq(enableDomStorage)))
eq(url),
any(Bundle.class),
eq(useWebView),
eq(enableJavaScript),
eq(enableDomStorage),
eq(universalLinksOnly)))
.thenReturn(UrlLauncher.LaunchStatus.NO_ACTIVITY);
// Act by calling the "launch" method on the method channel
methodCallHandler = new MethodCallHandlerImpl(urlLauncher);
Expand All @@ -149,16 +156,23 @@ public void onMethodCall_launchReturnsActivityNotFoundError() {
boolean useWebView = false;
boolean enableJavaScript = false;
boolean enableDomStorage = false;
boolean universalLinksOnly = false;
// Setup arguments map send on the method channel
Map<String, Object> args = new HashMap<>();
args.put("url", url);
args.put("useWebView", useWebView);
args.put("enableJavaScript", enableJavaScript);
args.put("enableDomStorage", enableDomStorage);
args.put("universalLinksOnly", universalLinksOnly);
args.put("headers", new HashMap<>());
// Mock the launch method on the urlLauncher class
when(urlLauncher.launch(
eq(url), any(Bundle.class), eq(useWebView), eq(enableJavaScript), eq(enableDomStorage)))
eq(url),
any(Bundle.class),
eq(useWebView),
eq(enableJavaScript),
eq(enableDomStorage),
eq(universalLinksOnly)))
.thenReturn(UrlLauncher.LaunchStatus.ACTIVITY_NOT_FOUND);
// Act by calling the "launch" method on the method channel
methodCallHandler = new MethodCallHandlerImpl(urlLauncher);
Expand All @@ -181,16 +195,23 @@ public void onMethodCall_launchReturnsTrue() {
boolean useWebView = false;
boolean enableJavaScript = false;
boolean enableDomStorage = false;
boolean universalLinksOnly = false;
// Setup arguments map send on the method channel
Map<String, Object> args = new HashMap<>();
args.put("url", url);
args.put("useWebView", useWebView);
args.put("enableJavaScript", enableJavaScript);
args.put("enableDomStorage", enableDomStorage);
args.put("universalLinksOnly", universalLinksOnly);
args.put("headers", new HashMap<>());
// Mock the launch method on the urlLauncher class
when(urlLauncher.launch(
eq(url), any(Bundle.class), eq(useWebView), eq(enableJavaScript), eq(enableDomStorage)))
eq(url),
any(Bundle.class),
eq(useWebView),
eq(enableJavaScript),
eq(enableDomStorage),
eq(universalLinksOnly)))
.thenReturn(UrlLauncher.LaunchStatus.OK);
// Act by calling the "launch" method on the method channel
methodCallHandler = new MethodCallHandlerImpl(urlLauncher);
Expand Down