Merge pull request #9850 from Stypox/fix-api33-links-again3

[Android 11+] Correctly open URLs in browser and fix opening downloads and external players
This commit is contained in:
Stypox 2023-02-25 14:33:41 +01:00 committed by GitHub
commit 4b050c0dd8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
83 changed files with 135 additions and 264 deletions

View file

@ -6,6 +6,7 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
@ -57,13 +58,9 @@ class AboutActivity : AppCompatActivity() {
* A placeholder fragment containing a simple view.
*/
class AboutFragment : Fragment() {
private fun Button.openLink(url: Int) {
private fun Button.openLink(@StringRes url: Int) {
setOnClickListener {
ShareUtils.openUrlInBrowser(
context,
requireContext().getString(url),
false
)
ShareUtils.openUrlInApp(context, requireContext().getString(url))
}
}

View file

@ -66,7 +66,7 @@ fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
dialog.dismiss()
}
setNeutralButton(R.string.open_website_license) { _, _ ->
ShareUtils.openUrlInBrowser(context!!, component.link)
ShareUtils.openUrlInApp(context!!, component.link)
}
}
}

View file

@ -160,7 +160,7 @@ public class ErrorActivity extends AppCompatActivity {
.setMessage(R.string.start_accept_privacy_policy)
.setCancelable(false)
.setNeutralButton(R.string.read_privacy_policy, (dialog, which) ->
ShareUtils.openUrlInBrowser(context,
ShareUtils.openUrlInApp(context,
context.getString(R.string.privacy_policy_url)))
.setPositiveButton(R.string.accept, (dialog, which) -> {
if (action.equals("EMAIL")) { // send on email
@ -171,9 +171,9 @@ public class ErrorActivity extends AppCompatActivity {
+ getString(R.string.app_name) + " "
+ BuildConfig.VERSION_NAME)
.putExtra(Intent.EXTRA_TEXT, buildJson());
ShareUtils.openIntentInApp(context, i, true);
ShareUtils.openIntentInApp(context, i);
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL, false);
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL);
}
})
.setNegativeButton(R.string.decline, (dialog, which) -> {

View file

@ -156,7 +156,7 @@ class ErrorPanelHelper(
) {
errorOpenInBrowserButton.isVisible = true
errorOpenInBrowserButton.setOnClickListener {
ShareUtils.openUrlInBrowser(context, errorInfo.request, true)
ShareUtils.openUrlInBrowser(context, errorInfo.request)
}
}

View file

@ -11,7 +11,6 @@ import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
import static org.schabi.newpipe.util.NavigationHelper.openPlayQueue;
import static org.schabi.newpipe.util.NavigationHelper.playWithKore;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
@ -485,16 +484,8 @@ public final class VideoDetailFragment
info.getThumbnailUrl())));
binding.detailControlsOpenInBrowser.setOnClickListener(makeOnClickListener(info ->
ShareUtils.openUrlInBrowser(requireContext(), info.getUrl())));
binding.detailControlsPlayWithKodi.setOnClickListener(makeOnClickListener(info -> {
try {
playWithKore(requireContext(), Uri.parse(info.getUrl()));
} catch (final Exception e) {
if (DEBUG) {
Log.i(TAG, "Failed to start kore", e);
}
KoreUtils.showInstallKoreDialog(requireContext());
}
}));
binding.detailControlsPlayWithKodi.setOnClickListener(makeOnClickListener(info ->
KoreUtils.playWithKore(requireContext(), Uri.parse(info.getUrl()))));
if (DEBUG) {
binding.detailControlsCrashThePlayer.setOnClickListener(v ->
VideoDetailPlayerCrasher.onCrashThePlayer(requireContext(), player));

View file

@ -204,8 +204,7 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
break;
case R.id.menu_item_rss:
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(
requireContext(), currentInfo.getFeedUrl(), false);
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
}
break;
case R.id.menu_item_openInBrowser:

View file

@ -99,14 +99,8 @@ public enum StreamDialogDefaultEntry {
)
),
PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragment, item) -> {
final Uri videoUrl = Uri.parse(item.getUrl());
try {
NavigationHelper.playWithKore(fragment.requireContext(), videoUrl);
} catch (final Exception e) {
KoreUtils.showInstallKoreDialog(fragment.requireActivity());
}
}),
PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragment, item) ->
KoreUtils.playWithKore(fragment.requireContext(), Uri.parse(item.getUrl()))),
SHARE(R.string.share, (fragment, item) ->
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),

View file

@ -1420,14 +1420,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
private void onPlayWithKodiClicked() {
if (player.getCurrentMetadata() != null) {
player.pause();
try {
NavigationHelper.playWithKore(context, Uri.parse(player.getVideoUrl()));
} catch (final Exception e) {
if (DEBUG) {
Log.i(TAG, "Failed to start kore", e);
}
KoreUtils.showInstallKoreDialog(player.getContext());
}
KoreUtils.playWithKore(context, Uri.parse(player.getVideoUrl()));
}
}

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 to use
*/
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 streamUrl the url to the stream to play
*/
public static void playWithKore(final Context context, final Uri streamUrl) {
final Intent intent = new Intent(Intent.ACTION_VIEW)
.setPackage(context.getString(R.string.kore_package))
.setData(streamUrl)
.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,67 @@ 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.
* Open the url with the system default browser. If no browser is set as default, falls back to
* {@link #openAppChooser(Context, Intent, boolean)}.
* <p>
* If no browser is set as default, fallbacks 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).
* <p>
* Therefore <b>please 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;
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);
}
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 +109,56 @@ 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.
* 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.
* <p>
* 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 +208,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);
}
}

View file

@ -1,6 +1,5 @@
package us.shandian.giga.ui.adapter;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST;
@ -345,16 +344,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
intent.setDataAndType(resolveShareableUri(mission), mimeType);
intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION);
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
}
if (intent.resolveActivity(mContext.getPackageManager()) != null) {
ShareUtils.openIntentInApp(mContext, intent, false);
} else {
Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG).show();
}
ShareUtils.openIntentInApp(mContext, intent);
}
private void shareFile(Mission mission) {