Browse Source

Merge branch 'dev' into update-android-build-tools-gradle

pull/6974/head
litetex 5 months ago
committed by GitHub
parent
commit
fc52a6e871
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 30
      .github/workflows/ci.yml
  3. 9
      app/build.gradle
  4. 15
      app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
  5. 70
      app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
  6. 2
      app/src/main/java/org/schabi/newpipe/about/License.kt
  7. 2
      app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt
  8. 2
      app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.kt
  9. 2
      app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
  10. 2
      app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
  11. 116
      app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt
  12. 104
      app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
  13. 2
      app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
  14. 6
      app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt
  15. 2
      app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java
  16. 12
      app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt
  17. 3
      app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java
  18. 24
      app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt
  19. 34
      app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java
  20. 22
      app/src/main/java/org/schabi/newpipe/player/MainPlayer.java
  21. 64
      app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
  22. 56
      app/src/main/java/org/schabi/newpipe/player/Player.java
  23. 16
      app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java
  24. 12
      app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java
  25. 2
      app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
  26. 21
      app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
  27. 9
      app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
  28. 2
      app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java
  29. 2
      app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java
  30. 11
      app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java
  31. 10
      app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java
  32. 10
      app/src/main/java/org/schabi/newpipe/util/Localization.java
  33. 4
      app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
  34. 2
      app/src/main/java/org/schabi/newpipe/util/SavedState.kt
  35. 1
      app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java
  36. 2
      app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt
  37. 12
      app/src/main/java/us/shandian/giga/service/DownloadManagerService.java
  38. 5
      app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
  39. BIN
      app/src/main/res/drawable-nodpi/not_available_monkey.png
  40. 27
      app/src/main/res/drawable/not_available_monkey.xml
  41. 2
      app/src/main/res/layout-large-land/fragment_video_detail.xml
  42. 2
      app/src/main/res/layout/dialog_feed_group_reorder.xml
  43. 24
      app/src/main/res/layout/error_panel.xml
  44. 32
      app/src/main/res/layout/fragment_choose_tabs.xml
  45. 2
      app/src/main/res/layout/fragment_video_detail.xml
  46. 2
      app/src/main/res/menu/dialog_url.xml
  47. 16
      app/src/main/res/menu/menu_play_queue_item.xml
  48. 14
      app/src/main/res/values-ar/strings.xml
  49. 6
      app/src/main/res/values-b+ast/strings.xml
  50. 6
      app/src/main/res/values-b+uz+Latn/strings.xml
  51. 14
      app/src/main/res/values-b+zh+HANS+CN/strings.xml
  52. 6
      app/src/main/res/values-be/strings.xml
  53. 2
      app/src/main/res/values-ber/strings.xml
  54. 3
      app/src/main/res/values-bg/strings.xml
  55. 4
      app/src/main/res/values-bn-rBD/strings.xml
  56. 4
      app/src/main/res/values-bn-rIN/strings.xml
  57. 6
      app/src/main/res/values-bn/strings.xml
  58. 76
      app/src/main/res/values-ca/strings.xml
  59. 24
      app/src/main/res/values-ckb/strings.xml
  60. 6
      app/src/main/res/values-cs/strings.xml
  61. 5
      app/src/main/res/values-da/strings.xml
  62. 16
      app/src/main/res/values-de/strings.xml
  63. 14
      app/src/main/res/values-el/strings.xml
  64. 6
      app/src/main/res/values-eo/strings.xml
  65. 14
      app/src/main/res/values-es/strings.xml
  66. 14
      app/src/main/res/values-et/strings.xml
  67. 6
      app/src/main/res/values-eu/strings.xml
  68. 6
      app/src/main/res/values-fa/strings.xml
  69. 42
      app/src/main/res/values-fi/strings.xml
  70. 54
      app/src/main/res/values-fr/strings.xml
  71. 6
      app/src/main/res/values-gl/strings.xml
  72. 14
      app/src/main/res/values-he/strings.xml
  73. 6
      app/src/main/res/values-hi/strings.xml
  74. 6
      app/src/main/res/values-hr/strings.xml
  75. 6
      app/src/main/res/values-hu/strings.xml
  76. 4
      app/src/main/res/values-hy/strings.xml
  77. 6
      app/src/main/res/values-in/strings.xml
  78. 14
      app/src/main/res/values-it/strings.xml
  79. 12
      app/src/main/res/values-ja/strings.xml
  80. 5
      app/src/main/res/values-kab/strings.xml
  81. 6
      app/src/main/res/values-kmr/strings.xml
  82. 10
      app/src/main/res/values-ko/strings.xml
  83. 6
      app/src/main/res/values-ku/strings.xml
  84. 20
      app/src/main/res/values-lt/strings.xml
  85. 6
      app/src/main/res/values-lv/strings.xml
  86. 5
      app/src/main/res/values-mk/strings.xml
  87. 105
      app/src/main/res/values-ml/strings.xml
  88. 101
      app/src/main/res/values-ms/strings.xml
  89. 6
      app/src/main/res/values-nb-rNO/strings.xml
  90. 6
      app/src/main/res/values-ne/strings.xml
  91. 6
      app/src/main/res/values-nl-rBE/strings.xml
  92. 33
      app/src/main/res/values-nl/strings.xml
  93. 6
      app/src/main/res/values-pa/strings.xml
  94. 26
      app/src/main/res/values-pl/strings.xml
  95. 6
      app/src/main/res/values-pt-rBR/strings.xml
  96. 14
      app/src/main/res/values-pt-rPT/strings.xml
  97. 38
      app/src/main/res/values-pt/strings.xml
  98. 6
      app/src/main/res/values-ro/strings.xml
  99. 16
      app/src/main/res/values-ru/strings.xml
  100. 14
      app/src/main/res/values-sc/strings.xml

2
.github/ISSUE_TEMPLATE/bug_report.md

@ -57,7 +57,7 @@ Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it
<!-- Please fill this out when you do not provide a log generate by NewPipe -->
<!-- Please fill this section if you did not provide a log generated by NewPipe -->
### Device info

30
.github/workflows/ci.yml

@ -37,14 +37,8 @@ jobs:
uses: actions/setup-java@v2
with:
java-version: 11
distribution: "adopt"
- name: Cache Gradle dependencies
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
distribution: "temurin"
cache: 'gradle'
- name: Build debug APK and run jvm tests
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
@ -69,14 +63,8 @@ jobs:
uses: actions/setup-java@v2
with:
java-version: 11
distribution: "adopt"
- name: Cache Gradle dependencies
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
distribution: "temurin"
cache: 'gradle'
- name: Run android tests
uses: reactivecircus/android-emulator-runner@v2
@ -97,7 +85,8 @@ jobs:
# uses: actions/setup-java@v2
# with:
# java-version: 11 # Sonar requires JDK 11
# distribution: "adopt"
# distribution: "temurin"
# cache: 'gradle'
# - name: Cache SonarCloud packages
# uses: actions/cache@v2
@ -106,13 +95,6 @@ jobs:
# key: ${{ runner.os }}-sonar
# restore-keys: ${{ runner.os }}-sonar
# - name: Cache Gradle packages
# uses: actions/cache@v2
# with:
# path: ~/.gradle/caches
# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
# restore-keys: ${{ runner.os }}-gradle
# - name: Build and analyze
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any

9
app/build.gradle

@ -4,7 +4,7 @@ plugins {
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'checkstyle'
@ -84,11 +84,6 @@ android {
jvmTarget = JavaVersion.VERSION_1_8
}
// Required and used only by groupie
androidExtensions {
experimental = true
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
@ -189,7 +184,7 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.9'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:68f1fa994af78d2cd0f354f9226d5dbe3dc03d54'
/** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"

15
app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java

@ -4,7 +4,6 @@ import android.app.Application;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.net.ConnectivityManager;
@ -16,6 +15,7 @@ import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.pm.PackageInfoCompat;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonObject;
@ -34,6 +34,7 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Maybe;
@ -58,20 +59,22 @@ public final class CheckForNewAppVersion {
*/
@NonNull
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
final PackageInfo packageInfo;
final List<Signature> signatures;
try {
packageInfo = application.getPackageManager().getPackageInfo(
application.getPackageName(), PackageManager.GET_SIGNATURES);
signatures = PackageInfoCompat.getSignatures(application.getPackageManager(),
application.getPackageName());
} catch (final PackageManager.NameNotFoundException e) {
ErrorActivity.reportError(application, new ErrorInfo(e,
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
return "";
}
if (signatures.isEmpty()) {
return "";
}
final X509Certificate c;
try {
final Signature[] signatures = packageInfo.signatures;
final byte[] cert = signatures[0].toByteArray();
final byte[] cert = signatures.get(0).toByteArray();
final InputStream input = new ByteArrayInputStream(cert);
final CertificateFactory cf = CertificateFactory.getInstance("X509");
c = (X509Certificate) cf.generateCertificate(input);

70
app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java

@ -0,0 +1,70 @@
package org.schabi.newpipe;
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
import android.content.Context;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.widget.PopupMenu;
import androidx.fragment.app.FragmentManager;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.Collections;
public final class QueueItemMenuUtil {
public static void openPopupMenu(final PlayQueue playQueue,
final PlayQueueItem item,
final View view,
final boolean hideDetails,
final FragmentManager fragmentManager,
final Context context) {
final ContextThemeWrapper themeWrapper =
new ContextThemeWrapper(context, R.style.DarkPopupMenu);
final PopupMenu popupMenu = new PopupMenu(themeWrapper, view);
popupMenu.inflate(R.menu.menu_play_queue_item);
if (hideDetails) {
popupMenu.getMenu().findItem(R.id.menu_item_details).setVisible(false);
}
popupMenu.setOnMenuItemClickListener(menuItem -> {
switch (menuItem.getItemId()) {
case R.id.menu_item_remove:
final int index = playQueue.indexOf(item);
playQueue.remove(index);
return true;
case R.id.menu_item_details:
// playQueue is null since we don't want any queue change
NavigationHelper.openVideoDetail(context, item.getServiceId(),
item.getUrl(), item.getTitle(), null,
false);
return true;
case R.id.menu_item_append_playlist:
final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(
Collections.singletonList(item)
);
PlaylistAppendDialog.onPlaylistFound(context,
() -> d.show(fragmentManager, "QueueItemMenuUtil@append_playlist"),
() -> PlaylistCreationDialog.newInstance(d)
.show(fragmentManager, "QueueItemMenuUtil@append_playlist"));
return true;
case R.id.menu_item_share:
shareText(context, item.getTitle(), item.getUrl(),
item.getThumbnailUrl());
return true;
}
return false;
});
popupMenu.show();
}
private QueueItemMenuUtil() { }
}

2
app/src/main/java/org/schabi/newpipe/about/License.kt

@ -1,7 +1,7 @@
package org.schabi.newpipe.about
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
import java.io.Serializable
/**

2
app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt

@ -108,7 +108,7 @@ object LicenseFragmentHelper {
alert.setView(webView)
Localization.assureCorrectAppLanguage(context)
alert.setNegativeButton(
context.getString(R.string.finish)
context.getString(R.string.ok)
) { dialog, _ -> dialog.dismiss() }
alert.show()
}

2
app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.kt

@ -1,7 +1,7 @@
package org.schabi.newpipe.about
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
@Parcelize
class SoftwareComponent

2
app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java

@ -681,7 +681,7 @@ public class DownloadDialog extends DialogFragment
new AlertDialog.Builder(context)
.setTitle(R.string.general_error)
.setMessage(msg)
.setNegativeButton(getString(R.string.finish), null)
.setNegativeButton(getString(R.string.ok), null)
.create()
.show();
}

2
app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt

@ -2,7 +2,7 @@ package org.schabi.newpipe.error
import android.os.Parcelable
import androidx.annotation.StringRes
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Info
import org.schabi.newpipe.extractor.NewPipe

116
app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt

@ -6,6 +6,8 @@ import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.annotation.Nullable
import androidx.annotation.StringRes
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import com.jakewharton.rxbinding4.view.clicks
@ -37,22 +39,39 @@ class ErrorPanelHelper(
onRetry: Runnable
) {
private val context: Context = rootView.context!!
private val errorPanelRoot: View = rootView.findViewById(R.id.error_panel)
private val errorTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_view)
private val errorServiceInfoTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_info_view)
private val errorServiceExplenationTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_explenation_view)
private val errorButtonAction: Button = errorPanelRoot.findViewById(R.id.error_button_action)
private val errorButtonRetry: Button = errorPanelRoot.findViewById(R.id.error_button_retry)
// the only element that is visible by default
private val errorTextView: TextView =
errorPanelRoot.findViewById(R.id.error_message_view)
private val errorServiceInfoTextView: TextView =
errorPanelRoot.findViewById(R.id.error_message_service_info_view)
private val errorServiceExplanationTextView: TextView =
errorPanelRoot.findViewById(R.id.error_message_service_explanation_view)
private val errorActionButton: Button =
errorPanelRoot.findViewById(R.id.error_action_button)
private val errorRetryButton: Button =
errorPanelRoot.findViewById(R.id.error_retry_button)
private var errorDisposable: Disposable? = null
init {
errorDisposable = errorButtonRetry.clicks()
errorDisposable = errorRetryButton.clicks()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onRetry.run() }
}
private fun ensureDefaultVisibility() {
errorTextView.isVisible = true
errorServiceInfoTextView.isVisible = false
errorServiceExplanationTextView.isVisible = false
errorActionButton.isVisible = false
errorRetryButton.isVisible = false
}
fun showError(errorInfo: ErrorInfo) {
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
@ -62,10 +81,14 @@ class ErrorPanelHelper(
return
}
errorButtonAction.isVisible = true
ensureDefaultVisibility()
if (errorInfo.throwable is ReCaptchaException) {
errorButtonAction.setText(R.string.recaptcha_solve)
errorButtonAction.setOnClickListener {
errorTextView.setText(R.string.recaptcha_request_toast)
showAndSetErrorButtonAction(
R.string.recaptcha_solve
) {
// Starting ReCaptcha Challenge Activity
val intent = Intent(context, ReCaptchaActivity::class.java)
intent.putExtra(
@ -73,45 +96,31 @@ class ErrorPanelHelper(
(errorInfo.throwable as ReCaptchaException).url
)
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
errorButtonAction.setOnClickListener(null)
errorActionButton.setOnClickListener(null)
}
errorTextView.setText(R.string.recaptcha_request_toast)
// additional info is only provided by AccountTerminatedException
errorServiceInfoTextView.isVisible = false
errorServiceExplenationTextView.isVisible = false
errorButtonRetry.isVisible = true
errorRetryButton.isVisible = true
} else if (errorInfo.throwable is AccountTerminatedException) {
errorButtonRetry.isVisible = false
errorButtonAction.isVisible = false
errorTextView.setText(R.string.account_terminated)
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
errorServiceInfoTextView.setText(
context.resources.getString(
R.string.service_provides_reason,
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
)
)
errorServiceExplenationTextView.setText(
(errorInfo.throwable as AccountTerminatedException).message
errorServiceInfoTextView.text = context.resources.getString(
R.string.service_provides_reason,
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
)
errorServiceInfoTextView.isVisible = true
errorServiceExplenationTextView.isVisible = true
} else {
errorServiceInfoTextView.isVisible = false
errorServiceExplenationTextView.isVisible = false
errorServiceExplanationTextView.text =
(errorInfo.throwable as AccountTerminatedException).message
errorServiceExplanationTextView.isVisible = true
}
} else {
errorButtonAction.setText(R.string.error_snackbar_action)
errorButtonAction.setOnClickListener {
showAndSetErrorButtonAction(
R.string.error_snackbar_action
) {
ErrorActivity.reportError(context, errorInfo)
}
// additional info is only provided by AccountTerminatedException
errorServiceInfoTextView.isVisible = false
errorServiceExplenationTextView.isVisible = false
// hide retry button by default, then show only if not unavailable/unsupported content
errorButtonRetry.isVisible = false
errorTextView.setText(
when (errorInfo.throwable) {
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
@ -124,7 +133,7 @@ class ErrorPanelHelper(
is ContentNotSupportedException -> R.string.content_not_supported
else -> {
// show retry button only for content which is not unavailable or unsupported
errorButtonRetry.isVisible = true
errorRetryButton.isVisible = true
if (errorInfo.throwable != null && errorInfo.throwable!!.isNetworkRelated) {
R.string.network_error
} else {
@ -134,17 +143,36 @@ class ErrorPanelHelper(
}
)
}
errorPanelRoot.animate(true, 300)
setRootVisible()
}
/**
* Shows the errorButtonAction, sets a text into it and sets the click listener.
*/
private fun showAndSetErrorButtonAction(
@StringRes resid: Int,
@Nullable listener: View.OnClickListener
) {
errorActionButton.isVisible = true
errorActionButton.setText(resid)
errorActionButton.setOnClickListener(listener)
}
fun showTextError(errorString: String) {
errorButtonAction.isVisible = false
errorButtonRetry.isVisible = false
ensureDefaultVisibility()
errorTextView.text = errorString
setRootVisible()
}
private fun setRootVisible() {
errorPanelRoot.animate(true, 300)
}
fun hide() {
errorButtonAction.setOnClickListener(null)
errorActionButton.setOnClickListener(null)
errorPanelRoot.animate(false, 150)
}
@ -153,8 +181,8 @@ class ErrorPanelHelper(
}
fun dispose() {
errorButtonAction.setOnClickListener(null)
errorButtonRetry.setOnClickListener(null)
errorActionButton.setOnClickListener(null)
errorRetryButton.setOnClickListener(null)
errorDisposable?.dispose()
}

104
app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java

@ -201,7 +201,7 @@ public final class VideoDetailFragment
@Nullable
private MainPlayer playerService;
private Player player;
private PlayerHolder playerHolder = PlayerHolder.getInstance();
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
/*//////////////////////////////////////////////////////////////////////////
// Service management
@ -220,7 +220,7 @@ public final class VideoDetailFragment
return;
}
if (isLandscape()) {
if (DeviceUtils.isLandscape(requireContext())) {
// If the video is playing but orientation changed
// let's make the video in fullscreen again
checkLandscape();
@ -241,7 +241,7 @@ public final class VideoDetailFragment
&& isAutoplayEnabled()
&& player.getParentActivity() == null)) {
autoPlayEnabled = true; // forcefully start playing
openVideoPlayer();
openVideoPlayerAutoFullscreen();
}
}
@ -499,7 +499,7 @@ public final class VideoDetailFragment
break;
case R.id.detail_thumbnail_root_layout:
autoPlayEnabled = true; // forcefully start playing
openVideoPlayer();
openVideoPlayerAutoFullscreen();
break;
case R.id.detail_title_root_layout:
toggleTitleAndSecondaryControls();
@ -516,7 +516,7 @@ public final class VideoDetailFragment
showSystemUi();
} else {
autoPlayEnabled = true; // forcefully start playing
openVideoPlayer();
openVideoPlayer(false);
}
setOverlayPlayPauseImage(isPlayerAvailable() && player.isPlaying());
@ -762,7 +762,7 @@ public final class VideoDetailFragment
private void setupFromHistoryItem(final StackItem item) {
setAutoPlay(false);
hideMainPlayer();
hideMainPlayerOnLoadingNewStream();
setInitialData(item.getServiceId(), item.getUrl(),
item.getTitle() == null ? "" : item.getTitle(), item.getPlayQueue());
@ -882,7 +882,7 @@ public final class VideoDetailFragment
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
isLoading.set(false);
hideMainPlayer();
hideMainPlayerOnLoadingNewStream();
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
getString(R.string.show_age_restricted_content), false)) {
hideAgeRestrictedContent();
@ -897,8 +897,9 @@ public final class VideoDetailFragment
stack.push(new StackItem(serviceId, url, title, playQueue));
}
}
if (isAutoplayEnabled()) {
openVideoPlayer();
openVideoPlayerAutoFullscreen();
}
}
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
@ -1103,7 +1104,29 @@ public final class VideoDetailFragment
}
}
public void openVideoPlayer() {
/**
* Opens the video player, in fullscreen if needed. In order to open fullscreen, the activity
* is toggled to landscape orientation (which will then cause fullscreen mode).
*
* @param directlyFullscreenIfApplicable whether to open fullscreen if we are not already
* in landscape and screen orientation is locked
*/
public void openVideoPlayer(final boolean directlyFullscreenIfApplicable) {
if (directlyFullscreenIfApplicable
&& !DeviceUtils.isLandscape(requireContext())
&& PlayerHelper.globalScreenOrientationLocked(requireContext())) {
// Make sure the bottom sheet turns out expanded. When this code kicks in the bottom
// sheet could not have fully expanded yet, and thus be in the STATE_SETTLING state.
// When the activity is rotated, and its state is saved and then restored, the bottom
// sheet would forget what it was doing, since even if STATE_SETTLING is restored, it
// doesn't tell which state it was settling to, and thus the bottom sheet settles to
// STATE_COLLAPSED. This can be solved by manually setting the state that will be
// restored (i.e. bottomSheetState) to STATE_EXPANDED.
bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
// toggle landscape in order to open directly in fullscreen
onScreenRotationButtonClicked();
}
if (PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
showExternalPlaybackDialog();
@ -1112,6 +1135,18 @@ public final class VideoDetailFragment
}
}
/**
* If the option to start directly fullscreen is enabled, calls
* {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable = true}, so that
* if the user is not already in landscape and he has screen orientation locked the activity
* rotates and fullscreen starts. Otherwise, if the option to start directly fullscreen is
* disabled, calls {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable
* = false}, hence preventing it from going directly fullscreen.
*/
public void openVideoPlayerAutoFullscreen() {
openVideoPlayer(PlayerHelper.isStartMainPlayerFullscreenEnabled(requireContext()));
}
private void openNormalBackgroundPlayer(final boolean append) {
// See UI changes while remote playQueue changes
if (!isPlayerAvailable()) {
@ -1145,12 +1180,19 @@ public final class VideoDetailFragment
}
addVideoPlayerView();
final Intent playerIntent = NavigationHelper
.getPlayerIntent(requireContext(), MainPlayer.class, queue, true, autoPlayEnabled);
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
MainPlayer.class, queue, true, autoPlayEnabled);
ContextCompat.startForegroundService(activity, playerIntent);
}
private void hideMainPlayer() {
/**
* When the video detail fragment is already showing details for a video and the user opens a
* new one, the video detail fragment changes all of its old data to the new stream, so if there
* is a video player currently open it should be hidden. This method does exactly that. If
* autoplay is enabled, the underlying player is not stopped completely, since it is going to
* be reused in a few milliseconds and the flickering would be annoying.
*/
private void hideMainPlayerOnLoadingNewStream() {
if (!isPlayerServiceAvailable()
|| playerService.getView() == null
|| !player.videoPlayerSelected()) {
@ -1158,8 +1200,12 @@ public final class VideoDetailFragment
}
removeVideoPlayerView();
playerService.stop(isAutoplayEnabled());
playerService.getView().setVisibility(View.GONE);
if (isAutoplayEnabled()) {
playerService.stopForImmediateReusing();
playerService.getView().setVisibility(View.GONE);
} else {
playerHolder.stopService();
}
}
private PlayQueue setupPlayQueueForIntent(final boolean append) {
@ -1252,7 +1298,7 @@ public final class VideoDetailFragment
final DisplayMetrics metrics = getResources().getDisplayMetrics();
if (getView() != null) {
final int height = (isInMultiWindow()
final int height = (DeviceUtils.isInMultiWindow(activity)
? requireView()
: activity.getWindow().getDecorView()).getHeight();
setHeightThumbnail(height, metrics);
@ -1275,7 +1321,7 @@ public final class VideoDetailFragment
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
if (isPlayerAvailable() && player.isFullscreen()) {
final int height = (isInMultiWindow()
final int height = (DeviceUtils.isInMultiWindow(activity)
? requireView()
: activity.getWindow().getDecorView()).getHeight();
// Height is zero when the view is not yet displayed like after orientation change
@ -1808,7 +1854,7 @@ public final class VideoDetailFragment
|| error.type == ExoPlaybackException.TYPE_UNEXPECTED) {
// Properly exit from fullscreen
toggleFullscreenIfInFullscreenMode();
hideMainPlayer();
hideMainPlayerOnLoadingNewStream();
}
}
@ -1864,13 +1910,14 @@ public final class VideoDetailFragment
// from landscape to portrait every time.
// Just turn on fullscreen mode in landscape orientation
// or portrait & unlocked global orientation
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
if (DeviceUtils.isTablet(activity)
&& (!globalScreenOrientationLocked(activity) || isLandscape())) {
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
player.toggleFullscreen();
return;
}
final int newOrientation = isLandscape()
final int newOrientation = isLandscape
? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
@ -1942,15 +1989,17 @@ public final class VideoDetailFragment
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
// In multiWindow mode status bar is not transparent for devices with cutout
// if I include this flag. So without it is better in this case
if (!isInMultiWindow()) {
final boolean isInMultiWindow = DeviceUtils.isInMultiWindow(activity);
if (!isInMultiWindow) {
visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
}
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& (isInMultiWindow() || (isPlayerAvailable() && player.isFullscreen()))) {
&& (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) {
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
}
@ -2022,15 +2071,6 @@ public final class VideoDetailFragment
}
}
private boolean isLandscape() {
return getResources().getDisplayMetrics().heightPixels < getResources()
.getDisplayMetrics().widthPixels;
}
private boolean isInMultiWindow() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
}
/*
* Means that the player fragment was swiped away via BottomSheetLayout
* and is empty but ready for any new actions. See cleanUp()
@ -2071,7 +2111,7 @@ public final class VideoDetailFragment
new AlertDialog.Builder(activity)
.setTitle(R.string.clear_queue_confirmation_description)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
.setPositiveButton(R.string.ok, (dialog, which) -> {
onAllow.run();
dialog.dismiss();
}).show();
@ -2213,7 +2253,7 @@ public final class VideoDetailFragment
setOverlayElementsClickable(false);
hideSystemUiIfNeeded();
// Conditions when the player should be expanded to fullscreen
if (isLandscape()
if (DeviceUtils.isLandscape(requireContext())
&& isPlayerAvailable()
&& player.isPlaying()
&& !player.isFullscreen()

2
app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt

@ -206,7 +206,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod)
}
}
.setPositiveButton(resources.getString(R.string.finish), null)
.setPositiveButton(resources.getString(R.string.ok), null)
.create()
.show()
return true

6
app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt

@ -300,6 +300,12 @@ class FeedLoadService : Service() {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { _, throwable ->
// There seems to be a bug in the kotlin plugin as it tells you when
// building that this can't be null:
// "Condition 'throwable != null' is always 'true'"
// However it can indeed be null
// The suppression may be removed in further versions
@Suppress("SENSELESS_COMPARISON")
if (throwable != null) {
Log.e(TAG, "Error while storing result", throwable)
handleError(throwable)

2
app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java

@ -40,7 +40,7 @@ public class ImportConfirmationDialog extends DialogFragment {
.setMessage(R.string.import_network_expensive_warning)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.finish, (dialogInterface, i) -> {
.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
if (resultServiceIntent != null && getContext() != null) {
getContext().startService(resultServiceIntent);
}

12
app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt

@ -179,7 +179,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
private fun onImportPreviousSelected() {
requestImportLauncher.launch(StoredFileHelper.getPicker(activity))
requestImportLauncher.launch(StoredFileHelper.getPicker(activity, JSON_MIME_TYPE))
}
private fun onExportSelected() {
@ -187,7 +187,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
val exportName = "newpipe_subscriptions_$date.json"
requestExportLauncher.launch(
StoredFileHelper.getNewPicker(activity, exportName, "application/json", null)
StoredFileHelper.getNewPicker(activity, exportName, JSON_MIME_TYPE, null)
)
}
@ -195,7 +195,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
FeedGroupReorderDialog().show(parentFragmentManager, null)
}
fun requestExportResult(result: ActivityResult) {
private fun requestExportResult(result: ActivityResult) {
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
activity.startService(
Intent(activity, SubscriptionsExportService::class.java)
@ -204,7 +204,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
}
fun requestImportResult(result: ActivityResult) {
private fun requestImportResult(result: ActivityResult) {
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
ImportConfirmationDialog.show(
this,
@ -407,4 +407,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
super.hideLoading()
binding.itemsList.animate(true, 200)
}
companion object {
const val JSON_MIME_TYPE = "application/json"
}
}

3
app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java

@ -177,7 +177,8 @@ public class SubscriptionsImportFragment extends BaseFragment {
}
public void onImportFile() {
requestImportFileLauncher.launch(StoredFileHelper.getPicker(activity));
// leave */* mime type to support all services with different mime types and file extensions
requestImportFileLauncher.launch(StoredFileHelper.getPicker(activity, "*/*"));
}
private void requestImportFileResult(final ActivityResult result) {

24
app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt

@ -143,21 +143,15 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
).get(FeedGroupDialogViewModel::class.java)
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
viewModel.subscriptionsLiveData.observe(
viewLifecycleOwner,
Observer {
setupSubscriptionPicker(it.first, it.second)
}
)
viewModel.dialogEventLiveData.observe(
viewLifecycleOwner,
Observer {
when (it) {
ProcessingEvent -> disableInput()
SuccessEvent -> dismiss()
}
viewModel.subscriptionsLiveData.observe(viewLifecycleOwner) {
setupSubscriptionPicker(it.first, it.second)
}
viewModel.dialogEventLiveData.observe(viewLifecycleOwner) {
when (it) {
ProcessingEvent -> disableInput()
SuccessEvent -> dismiss()
}
)
}
subscriptionGroupAdapter = GroupAdapter<GroupieViewHolder>().apply {
add(subscriptionMainSection)
@ -437,7 +431,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
feedGroupCreateBinding.confirmButton.setText(
when {
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
else -> android.R.string.ok
else -> R.string.ok
}
)

34
app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java

@ -19,6 +19,9 @@
package org.schabi.newpipe.local.subscription.services;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
@ -46,6 +49,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
@ -54,9 +58,6 @@ import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME;
public class SubscriptionsImportService extends BaseImportExportService {
public static final int CHANNEL_URL_MODE = 0;
public static final int INPUT_STREAM_MODE = 1;
@ -89,6 +90,8 @@ public class SubscriptionsImportService extends BaseImportExportService {
private String channelUrl;
@Nullable
private InputStream inputStream;
@Nullable
private String inputStreamType;
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
@ -111,8 +114,20 @@ public class SubscriptionsImportService extends BaseImportExportService {
}
try {
inputStream = new SharpInputStream(
new StoredFileHelper(this, uri, DEFAULT_MIME).getStream());
final StoredFileHelper fileHelper = new StoredFileHelper(this, uri, DEFAULT_MIME);
inputStream = new SharpInputStream(fileHelper.getStream());
inputStreamType = fileHelper.getType();
if (inputStreamType == null || inputStreamType.equals(DEFAULT_MIME)) {
// mime type could not be determined, just take file extension
final String name = fileHelper.getName();
final int pointIndex = name.lastIndexOf('.');
if (pointIndex == -1 || pointIndex >= name.length() - 1) {
inputStreamType = DEFAULT_MIME; // no extension, will fail in the extractor
} else {
inputStreamType = name.substring(pointIndex + 1);
}
}
} catch (final IOException e) {
handleError(e);
return START_NOT_STICKY;
@ -248,9 +263,9 @@ public class SubscriptionsImportService extends BaseImportExportService {
final Throwable error = notification.getError();
final Throwable cause = error.getCause();
if (error instanceof IOException) {
throw (IOException) error;
throw error;
} else if (cause instanceof IOException) {
throw (IOException) cause;
throw cause;
} else if (ExceptionUtils.isNetworkRelated(error)) {
throw new IOException(error);
}
@ -280,9 +295,12 @@ public class SubscriptionsImportService extends BaseImportExportService {
}
private Flowable<List<SubscriptionItem>> importFromInputStream() {
Objects.requireNonNull(inputStream);
Objects.requireNonNull(inputStreamType);
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
.getSubscriptionExtractor()
.fromInputStream(inputStream));
.fromInputStream(inputStream, inputStreamType));
}
private Flowable<List<SubscriptionItem>> importFromPreviousExport() {

22
app/src/main/java/org/schabi/newpipe/player/MainPlayer.java

@ -24,7 +24,6 @@ import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@ -36,6 +35,7 @@ import androidx.core.content.ContextCompat;
import org.schabi.newpipe.App;
import org.schabi.newpipe.databinding.PlayerBinding;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@ -133,32 +133,29 @@ public final class MainPlayer extends Service {
return START_NOT_STICKY;
}
public void stop(final boolean autoplayEnabled) {
public void stopForImmediateReusing() {
if (DEBUG) {
Log.d(TAG, "stop() called");
Log.d(TAG, "stopForImmediateReusing() called");
}
if (!player.exoPlayerIsNull()) {
player.saveWasPlaying();
// Releases wifi & cpu, disables keepScreenOn, etc.
if (!autoplayEnabled) {
player.pause();
}
// We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth
player.smoothStopPlayer();
player.setRecovery();
// Android TV will handle back button in case controls will be visible
// (one more additional unneeded click while the player is hidden)
player.hideControls(0, 0);
player.closeItemsList();
// Notification shows information about old stream but if a user selects
// a stream from backStack it's not actual anymore
// So we should hide the notification at all.
// When autoplay enabled such notification flashing is annoying so skip this case
if (!autoplayEnabled) {
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
}
}
}
@ -222,11 +219,8 @@ public final class MainPlayer extends Service {
boolean isLandscape() {
// DisplayMetrics from activity context knows about MultiWindow feature
// while DisplayMetrics from app context doesn't
final DisplayMetrics metrics = (player != null
&& player.getParentActivity() != null
? player.getParentActivity().getResources()
: getResources()).getDisplayMetrics();
return metrics.heightPixels < metrics.widthPixels;
return DeviceUtils.isLandscape(player != null && player.getParentActivity() != null
? player.getParentActivity() : this);
}
@Nullable

64
app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java

@ -1,9 +1,5 @@
package org.schabi.newpipe.player;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@ -16,7 +12,6 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupMenu;
import android.widget.SeekBar;
import androidx.annotation.Nullable;
@ -47,16 +42,18 @@ import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.Collections;
import java.util.List;
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public final class PlayQueueActivity extends AppCompatActivity
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, PlaybackParameterDialog.Callback {
private static final String TAG = PlayQueueActivity.class.getSimpleName();
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
protected Player player;
@ -279,49 +276,6 @@ public final class PlayQueueActivity extends AppCompatActivity
queueControlBinding.controlShuffle.setOnClickListener(this);
}
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
final PopupMenu popupMenu = new PopupMenu(this, view);
final MenuItem remove = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0,
Menu.NONE, R.string.play_queue_remove);
remove.setOnMenuItemClickListener(menuItem -> {
if (player == null) {
return false;
}
final int index = player.getPlayQueue().indexOf(item);
if (index != -1) {
player.getPlayQueue().remove(index);
}
return true;
});
final MenuItem detail = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1,
Menu.NONE, R.string.play_queue_stream_detail);
detail.setOnMenuItemClickListener(menuItem -> {
// playQueue is null since we don't want any queue change
NavigationHelper.openVideoDetail(this, item.getServiceId(), item.getUrl(),
item.getTitle(), null, false);
return true;
});
final MenuItem append = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 2,
Menu.NONE, R.string.append_playlist);
append.setOnMenuItemClickListener(menuItem -> {
openPlaylistAppendDialog(Collections.singletonList(item));
return true;
});
final MenuItem share = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 3,
Menu.NONE, R.string.share);
share.setOnMenuItemClickListener(menuItem -> {
shareText(getApplicationContext(), item.getTitle(), item.getUrl(),
item.getThumbnailUrl());
return true;
});
popupMenu.show();
}
////////////////////////////////////////////////////////////////////////////
// Component Helpers
////////////////////////////////////////////////////////////////////////////
@ -369,13 +323,9 @@ public final class PlayQueueActivity extends AppCompatActivity
@Override
public void held(final PlayQueueItem item, final View view) {
if (player == null) {
return;
}
final int index = player.getPlayQueue().indexOf(item);
if (index != -1) {
buildItemPopupMenu(item, view);
if (player != null && player.getPlayQueue().indexOf(item) != -1) {
openPopupMenu(player.getPlayQueue(), item, view, false,
getSupportFragmentManager(), PlayQueueActivity.this);
}
}

56
app/src/main/java/org/schabi/newpipe/player/Player.java

@ -159,6 +159,7 @@ import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static com.google.android.exoplayer2.Player.RepeatMode;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
@ -620,6 +621,9 @@ public final class Player implements
return;
}
// needed for tablets, check the function for a better explanation
directlyOpenFullscreenIfNeeded();
final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this);
final float playbackSpeed = savedParameters.speed;
final float playbackPitch = savedParameters.pitch;
@ -671,6 +675,7 @@ public final class Player implements
&& isPlaybackResumeEnabled(this)
&& !samePlayQueue
&& !newQueue.isEmpty()
&& newQueue.getItem() != null
&& newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem())
.observeOn(AndroidSchedulers.mainThread())
@ -742,6 +747,22 @@ public final class Player implements
NavigationHelper.sendPlayerStartedEvent(context);
}
/**
* Open fullscreen on tablets where the option to have the main player start automatically in
* fullscreen mode is on. Rotating the device to landscape is already done in {@link
* VideoDetailFragment#openVideoPlayer(boolean)} when the thumbnail is clicked, and that's
* enough for phones, but not for tablets since the mini player can be also shown in landscape.
*/
private void directlyOpenFullscreenIfNeeded() {
if (fragmentListener != null
&& PlayerHelper.isStartMainPlayerFullscreenEnabled(service)
&& DeviceUtils.isTablet(service)
&& videoPlayerSelected()
&& PlayerHelper.globalScreenOrientationLocked(service)) {
fragmentListener.onScreenRotationButtonClicked();
}
}
private void initPlayback(@NonNull final PlayQueue queue,
@RepeatMode final int repeatMode,
final float playbackSpeed,
@ -1572,8 +1593,7 @@ public final class Player implements
}
if (duration != binding.playbackSeekBar.getMax()) {
binding.playbackEndTime.setText(getTimeString(duration));
binding.playbackSeekBar.setMax(duration);
setVideoDurationToControls(duration);
}
if (currentState != STATE_PAUSED) {
if (currentState != STATE_PAUSED_SEEK) {
@ -2073,8 +2093,8 @@ public final class Player implements
Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
}
binding.playbackSeekBar.setMax((int) simpleExoPlayer.getDuration());
binding.playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration()));
setVideoDurationToControls((int) simpleExoPlayer.getDuration());
binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
if (playWhenReady) {
@ -2716,6 +2736,20 @@ public final class Player implements
simpleExoPlayer.seekToDefaultPosition();
}
}
/**
* Sets the video duration time into all control components (e.g. seekbar).
* @param duration
*/
private void setVideoDurationToControls(final int duration) {
binding.playbackEndTime.setText(getTimeString(duration));
binding.playbackSeekBar.setMax(duration);
// This is important for Android TVs otherwise it would apply the default from
// setMax/Min methods which is (max - min) / 20
binding.playbackSeekBar.setKeyProgressIncrement(
PlayerHelper.retrieveSeekDurationFromPreferences(this));
}
//endregion
@ -2765,7 +2799,9 @@ public final class Player implements
Log.d(TAG, "onPlayPause() called");
}
if (getPlayWhenReady()) {
if (getPlayWhenReady()
// When state is completed (replay button is shown) then (re)play and do not pause
&& currentState != STATE_COMPLETED) {
pause();
} else {
play();
@ -3198,9 +3234,9 @@ public final class Player implements
@Override
public void held(final PlayQueueItem item, final View view) {
final int index = playQueue.indexOf(item);
if (index != -1) {
playQueue.remove(index);
if (playQueue.indexOf(item) != -1) {
openPopupMenu(playQueue, item, view, true,
getParentActivity().getSupportFragmentManager(), context);
}
}