Skip to content
Merged
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
Expand All @@ -23,6 +22,7 @@

import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.RouterActivity;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
Expand Down Expand Up @@ -62,8 +62,9 @@ public static void installApp(@NonNull final Context context, final String packa
}

/**
* Open the url with the system default browser. If no browser is set as default, falls back to
* {@link #openAppChooser(Context, Intent, boolean)}.
* Open the url with the system default browser. If no browser is installed, falls back to
* {@link #openAppChooser(Context, Intent, boolean)} (for displaying that no apps are available
* to handle the action, or possible OEM-related edge cases).
* <p>
* This function selects the package to open based on which apps respond to the {@code http://}
* schema alone, which should exclude special non-browser apps that are can handle the url (e.g.
Expand All @@ -77,44 +78,26 @@ public static void installApp(@NonNull final Context context, final String packa
* @param url the url to browse
**/
public static void openUrlInBrowser(@NonNull final Context context, final String url) {
// Resolve using a generic http://, so we are sure to get a browser and not e.g. the yt app.
// Target a generic http://, so we are sure to get a browser and not e.g. the yt app.
// Note that this requires the `http` schema to be added to `<queries>` in the manifest.
final ResolveInfo defaultBrowserInfo;
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://"));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
defaultBrowserInfo = context.getPackageManager().resolveActivity(browserIntent,
PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY));
} else {
defaultBrowserInfo = context.getPackageManager().resolveActivity(browserIntent,
PackageManager.MATCH_DEFAULT_ONLY);
}

final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

if (defaultBrowserInfo == null) {
// No app installed to open a web URL, but it may be handled by other apps so try
// opening a system chooser for the link in this case (it could be bypassed by the
// system if there is only one app which can open the link or a default app associated
// with the link domain on Android 12 and higher)
openAppChooser(context, intent, true);
return;
}

final String defaultBrowserPackage = defaultBrowserInfo.activityInfo.packageName;

if (defaultBrowserPackage.equals("android")) {
// No browser set as default (doesn't work on some devices)
// See https://stackoverflow.com/a/58801285 and `setSelector` documentation
intent.setSelector(browserIntent);
try {
context.startActivity(intent);
} catch (final ActivityNotFoundException e) {
// No browser is available. This should, in the end, yield a nice AOSP error message
// indicating that no app is available to handle this action.
//
// Note: there are some situations where modified OEM ROMs have apps that appear
// to be browsers but are actually app choosers. If starting the Activity fails
// related to this, opening the system app chooser is still the correct behavior.
intent.setSelector(null);
openAppChooser(context, intent, true);
} else {
try {
intent.setPackage(defaultBrowserPackage);
context.startActivity(intent);
} catch (final ActivityNotFoundException e) {
// Not a browser but an app chooser because of OEMs changes
intent.setPackage(null);
openAppChooser(context, intent, true);
}
}
}

Expand Down Expand Up @@ -190,6 +173,18 @@ private static void openAppChooser(@NonNull final Context context,
chooserIntent.putExtra(Intent.EXTRA_TITLE, context.getString(R.string.open_with));
}

// Avoid opening in NewPipe
// (Implementation note: if the URL is one for which NewPipe itself
// is set as handler on Android >= 12, we actually remove the only eligible app
// for this link, and browsers will not be offered to the user. For that, use
// `openUrlInBrowser`.)
Comment on lines +177 to +180
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should (I tested this in the Android 14 Developer Preview 2 on March 2023) be able use Intent.EXTRA_ALTERNATE_INTENTS (Android API 23+) to show all apps that can open a given intent even with web restrictions on Android >= 12. However, we need to have access to queries of all intents we use (so http, https, market, as we target Kodi's package directly):

final List<ResolveInfo> resolveInfoList;

// Use PackageManager.MATCH_ALL, as we want the list of apps which can handle a web intent
// and not only the default app
// This flag doesn't include disabled or uninstalled apps with data kept (they would be not
// accessible anyway, as this requires the QUERY_ALL_PACKAGES permission)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    resolveInfoList = context.getPackageManager()
            .queryIntentActivities(intent,
                    PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL));
} else {
    resolveInfoList = context.getPackageManager()
            .queryIntentActivities(intent, PackageManager.MATCH_ALL);
}

final Intent[] intents = resolveInfoList.stream()
        .map(resolveInfo -> new Intent(Intent.ACTION_VIEW, urlUri)
                .setPackage(resolveInfo.activityInfo.packageName))
        .toArray(Intent[]::new);

// Passing an Intent which doesn't match any activity, as we don't want any other app in the
// system chooser than the ones we got earlier
// In this example an ACTION_VIEW intent with an empty URI is used
final Intent chooserIntent = createChooserIntent(
        context, new Intent(Intent.ACTION_VIEW, Uri.EMPTY), true);
// Intent.EXTRA_ALTERNATE_INTENTS extra allows to pass an unlimited amount of intents
chooserIntent.putExtra(Intent.EXTRA_ALTERNATE_INTENTS, intents);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh yeah, I see how this could work, we're basically recreating the chooser dialog. You propose adding this just for the case where openUrlInBrowser fails, right? Not for the general openAppChooser implementation, right? Because openAppChooser is also used in shareText which is not an URL.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
chooserIntent.putExtra(
Intent.EXTRA_EXCLUDE_COMPONENTS,
new ComponentName[]{new ComponentName(context, RouterActivity.class)}
);
}

// Migrate any clip data and flags from the original intent.
final int permFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
Expand Down