Correctly open urls in browser on Android 11+

- Fix misconfiguration in manifest ('http|https|market' is not valid)
- Split ShareUtils functions taking a boolean parameter into pairs of functions with better names and less runtime checks
- Move all Kore-related functions to KoreUtils
- Remove the toast_no_player string
This commit is contained in:
Stypox 2023-02-23 23:38:20 +01:00
parent a17c32fada
commit de067fe798
83 changed files with 125 additions and 267 deletions

View file

@ -1,6 +1,6 @@
package org.schabi.newpipe.util;
import static org.schabi.newpipe.util.external_communication.ShareUtils.installApp;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
import android.annotation.SuppressLint;
import android.app.Activity;
@ -50,9 +50,9 @@ import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.local.subscription.SubscriptionFragment;
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayQueueActivity;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder;
@ -63,8 +63,6 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.List;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
public final class NavigationHelper {
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
public static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag";
@ -323,15 +321,13 @@ public final class NavigationHelper {
public static void resolveActivityOrAskToInstall(@NonNull final Context context,
@NonNull final Intent intent) {
if (intent.resolveActivity(context.getPackageManager()) != null) {
ShareUtils.openIntentInApp(context, intent, false);
} else {
if (!ShareUtils.tryOpenIntentInApp(context, intent)) {
if (context instanceof Activity) {
new AlertDialog.Builder(context)
.setMessage(R.string.no_player_found)
.setPositiveButton(R.string.install,
(dialog, which) -> ShareUtils.openUrlInBrowser(context,
context.getString(R.string.fdroid_vlc_url), false))
(dialog, which) -> ShareUtils.installApp(context,
context.getString(R.string.vlc_package)))
.setNegativeButton(R.string.cancel, (dialog, which)
-> Log.i("NavigationHelper", "You unlocked a secret unicorn."))
.show();
@ -684,34 +680,6 @@ public final class NavigationHelper {
return getOpenIntent(context, url, serviceId, StreamingService.LinkType.CHANNEL);
}
/**
* Start an activity to install Kore.
*
* @param context the context
*/
public static void installKore(final Context context) {
installApp(context, context.getString(R.string.kore_package));
}
/**
* Start Kore app to show a video on Kodi.
* <p>
* For a list of supported urls see the
* <a href="https://github.com/xbmc/Kore/blob/master/app/src/main/AndroidManifest.xml">
* Kore source code
* </a>.
*
* @param context the context to use
* @param videoURL the url to the video
*/
public static void playWithKore(final Context context, final Uri videoURL) {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setPackage(context.getString(R.string.kore_package));
intent.setData(videoURL);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
/**
* Finish this <code>Activity</code> as well as all <code>Activities</code> running below it
* and then start <code>MainActivity</code>.

View file

@ -1,6 +1,11 @@
package org.schabi.newpipe.util.external_communication;
import static org.schabi.newpipe.util.external_communication.ShareUtils.installApp;
import static org.schabi.newpipe.util.external_communication.ShareUtils.tryOpenIntentInApp;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
@ -8,7 +13,6 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.util.NavigationHelper;
/**
* Util class that provides methods which are related to the Kodi Media Center and its Kore app.
@ -29,13 +33,39 @@ public final class KoreUtils {
.getBoolean(context.getString(R.string.show_play_with_kodi_key), false);
}
public static void showInstallKoreDialog(@NonNull final Context context) {
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(R.string.kore_not_found)
.setPositiveButton(R.string.install, (dialog, which) ->
NavigationHelper.installKore(context))
.setNegativeButton(R.string.cancel, (dialog, which) -> {
});
builder.create().show();
/**
* Start an activity to install Kore.
*
* @param context the context
*/
public static void installKore(final Context context) {
installApp(context, context.getString(R.string.kore_package));
}
/**
* Start Kore app to show a video on Kodi, and if the app is not installed ask the user to
* install it.
* <p>
* For a list of supported urls see the
* <a href="https://github.com/xbmc/Kore/blob/master/app/src/main/AndroidManifest.xml">
* Kore source code
* </a>.
*
* @param context the context to use
* @param videoURL the url to the video
*/
public static void playWithKore(final Context context, final Uri videoURL) {
final Intent intent = new Intent(Intent.ACTION_VIEW)
.setPackage(context.getString(R.string.kore_package))
.setData(videoURL)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (!tryOpenIntentInApp(context, intent)) {
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(R.string.kore_not_found)
.setPositiveButton(R.string.install, (dialog, which) -> installKore(context))
.setNegativeButton(R.string.cancel, (dialog, which) -> { });
builder.create().show();
}
}
}

View file

@ -41,60 +41,56 @@ public final class ShareUtils {
* second param (a system chooser will be opened if there are multiple markets and no default)
* and falls back to Google Play Store web URL if no app to handle the market scheme was found.
* <p>
* It uses {@link #openIntentInApp(Context, Intent, boolean)} to open market scheme
* and {@link #openUrlInBrowser(Context, String, boolean)} to open Google Play Store
* web URL with false for the boolean param.
* It uses {@link #openIntentInApp(Context, Intent)} to open market scheme and {@link
* #openUrlInBrowser(Context, String)} to open Google Play Store web URL.
*
* @param context the context to use
* @param packageId the package id of the app to be installed
*/
public static void installApp(@NonNull final Context context, final String packageId) {
// Try market scheme
final boolean marketSchemeResult = openIntentInApp(context, new Intent(Intent.ACTION_VIEW,
final Intent marketSchemeIntent = new Intent(Intent.ACTION_VIEW,
Uri.parse("market://details?id=" + packageId))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK), false);
if (!marketSchemeResult) {
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (!tryOpenIntentInApp(context, marketSchemeIntent)) {
// Fall back to Google Play Store Web URL (F-Droid can handle it)
openUrlInBrowser(context,
"https://play.google.com/store/apps/details?id=" + packageId, false);
openUrlInApp(context, "https://play.google.com/store/apps/details?id=" + packageId);
}
}
/**
* Open the url with the system default browser.
* <p>
* If no browser is set as default, fallbacks to
* {@link #openAppChooser(Context, Intent, boolean)}
* Open the url with the system default browser. If no browser is set as default, falls back to
* {@link #openAppChooser(Context, Intent, boolean)}. 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. the official YouTube app). Therefore
* please <b>prefer {@link #openUrlInApp(Context, String)}</b>, that handles package resolution
* in a standard way, unless this is the action of an explicit "Open in browser" button.
*
* @param context the context to use
* @param url the url to browse
* @param httpDefaultBrowserTest the boolean to set if the test for the default browser will be
* for HTTP protocol or for the created intent
* @return true if the URL can be opened or false if it cannot
*/
public static boolean openUrlInBrowser(@NonNull final Context context,
final String url,
final boolean httpDefaultBrowserTest) {
final String defaultPackageName;
* @param context the context to use
* @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.
// Note that this requires the `http` schema to be added to `<queries>` in the manifest.
final ResolveInfo defaultBrowserInfo = context.getPackageManager().resolveActivity(
new Intent(Intent.ACTION_VIEW, Uri.parse("http://")),
PackageManager.MATCH_DEFAULT_ONLY);
if (defaultBrowserInfo == null) {
// No app installed to open a web url
Toast.makeText(context, R.string.no_app_to_open_intent, Toast.LENGTH_LONG).show();
return;
}
final String defaultBrowserPackage = defaultBrowserInfo.activityInfo.packageName;
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (httpDefaultBrowserTest) {
defaultPackageName = getDefaultAppPackageName(context, new Intent(Intent.ACTION_VIEW,
Uri.parse("http://")).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} else {
defaultPackageName = getDefaultAppPackageName(context, intent);
}
if (defaultPackageName.equals("android")) {
if (defaultBrowserPackage.equals("android")) {
// No browser set as default (doesn't work on some devices)
openAppChooser(context, intent, true);
} else {
try {
// will be empty on Android 12+
if (!defaultPackageName.isEmpty()) {
intent.setPackage(defaultPackageName);
}
intent.setPackage(defaultBrowserPackage);
context.startActivity(intent);
} catch (final ActivityNotFoundException e) {
// Not a browser but an app chooser because of OEMs changes
@ -102,61 +98,54 @@ public final class ShareUtils {
openAppChooser(context, intent, true);
}
}
return true;
}
/**
* Open the url with the system default browser.
* <p>
* If no browser is set as default, fallbacks to
* {@link #openAppChooser(Context, Intent, boolean)}
* <p>
* This calls {@link #openUrlInBrowser(Context, String, boolean)} with true
* for the boolean parameter
* Open a url with the system default app using {@link Intent#ACTION_VIEW}, showing a toast in
* case of failure.
*
* @param context the context to use
* @param url the url to browse
* @return true if the URL can be opened or false if it cannot be
**/
public static boolean openUrlInBrowser(@NonNull final Context context, final String url) {
return openUrlInBrowser(context, url, true);
* @param url the url to open
*/
public static void openUrlInApp(@NonNull final Context context, final String url) {
openIntentInApp(context, new Intent(Intent.ACTION_VIEW, Uri.parse(url))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
/**
* Open an intent with the system default app.
* <p>
* The intent can be of every type, excepted a web intent for which
* {@link #openUrlInBrowser(Context, String, boolean)} should be used.
* <p>
* If no app can open the intent, a toast with the message {@code No app on your device can
* open this} is shown.
* Open an intent with the system default app. Use {@link #openIntentInApp(Context, Intent)} to
* show a toast in case of failure.
*
* @param context the context to use
* @param intent the intent to open
* @param showToast a boolean to set if a toast is displayed to user when no app is installed
* to open the intent (true) or not (false)
* @return true if the intent can be opened or false if it cannot be
* @param context the context to use
* @param intent the intent to open
* @return true if the intent could be opened successfully, false otherwise
*/
public static boolean openIntentInApp(@NonNull final Context context,
@NonNull final Intent intent,
final boolean showToast) {
final String defaultPackageName = getDefaultAppPackageName(context, intent);
if (defaultPackageName.isEmpty()) {
// No app installed to open the intent
if (showToast) {
Toast.makeText(context, R.string.no_app_to_open_intent, Toast.LENGTH_LONG)
.show();
}
return false;
} else {
public static boolean tryOpenIntentInApp(@NonNull final Context context,
@NonNull final Intent intent) {
try {
context.startActivity(intent);
} catch (final ActivityNotFoundException e) {
return false;
}
return true;
}
/**
* Open an intent with the system default app, showing a toast in case of failure. Use {@link
* #tryOpenIntentInApp(Context, Intent)} if you don't want the toast. Use {@link
* #openUrlInApp(Context, String)} as a shorthand for {@link Intent#ACTION_VIEW} with urls.
*
* @param context the context to use
* @param intent the intent to
*/
public static void openIntentInApp(@NonNull final Context context,
@NonNull final Intent intent) {
if (!tryOpenIntentInApp(context, intent)) {
Toast.makeText(context, R.string.no_app_to_open_intent, Toast.LENGTH_LONG)
.show();
}
}
/**
* Open the system chooser to launch an intent.
* <p>
@ -206,31 +195,6 @@ public final class ShareUtils {
context.startActivity(chooserIntent);
}
/**
* Get the default app package name.
* <p>
* If no app is set as default, it will return "android" (not on some devices because some
* OEMs changed the app chooser).
* <p>
* If no app is installed on user's device to handle the intent, it will return an empty string.
*
* @param context the context to use
* @param intent the intent to get default app
* @return the package name of the default app, an empty string if there's no app installed to
* handle the intent or the app chooser if there's no default
*/
private static String getDefaultAppPackageName(@NonNull final Context context,
@NonNull final Intent intent) {
final ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent,
PackageManager.MATCH_DEFAULT_ONLY);
if (resolveInfo == null) {
return "";
} else {
return resolveInfo.activityInfo.packageName;
}
}
/**
* Open the android share sheet to share a content.
*

View file

@ -30,7 +30,7 @@ final class UrlLongPressClickableSpan extends LongPressClickableSpan {
public void onClick(@NonNull final View view) {
if (!InternalUrlsHandler.handleUrlDescriptionTimestamp(
disposables, context, url)) {
ShareUtils.openUrlInBrowser(context, url, false);
ShareUtils.openUrlInApp(context, url);
}
}