diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ad9f1f82f..de16c5cf0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.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 - + ### Device info diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b59c84487..b87219f4f 100644 --- a/.github/workflows/ci.yml +++ b/.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 diff --git a/app/build.gradle b/app/build.gradle index cf2823024..8e203db20 100644 --- a/app/build.gradle +++ b/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}" diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java index 37ca0e400..c8fa02186 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java +++ b/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 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); diff --git a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java new file mode 100644 index 000000000..9105ff992 --- /dev/null +++ b/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() { } +} diff --git a/app/src/main/java/org/schabi/newpipe/about/License.kt b/app/src/main/java/org/schabi/newpipe/about/License.kt index b2c5d44a2..117ff9bf5 100644 --- a/app/src/main/java/org/schabi/newpipe/about/License.kt +++ b/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 /** diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt index 7617ef451..a04de8abc 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt +++ b/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() } diff --git a/app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.kt b/app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.kt index 2e967ebe8..354e8fef7 100644 --- a/app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.kt +++ b/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 diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index c24c8f171..a7f5b938f 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/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(); } diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt index 487e7c7fb..6581b5752 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt +++ b/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 diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt index e790c5fc5..66d5e6831 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt +++ b/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() } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 7ee70fcc4..ecf235abc 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/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() diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index 1958ff8a4..42fb8915d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -206,7 +206,7 @@ class FeedFragment : BaseStateFragment() { 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 diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt index 3638b4c0e..98ff5914d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt +++ b/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) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java b/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java index 5ab0699eb..a3d8b0567 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportConfirmationDialog.java +++ b/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); } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index e2248e742..57e1effbe 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -179,7 +179,7 @@ class SubscriptionFragment : BaseStateFragment() { } 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() { 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() { 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() { } } - 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() { super.hideLoading() binding.itemsList.animate(true, 200) } + + companion object { + const val JSON_MIME_TYPE = "application/json" + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java index 4e667f2b9..c4d088e39 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java +++ b/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) { diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt index cb0c5fe35..69d4c8819 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt +++ b/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.subscriptionsLiveData.observe(viewLifecycleOwner) { + setupSubscriptionPicker(it.first, it.second) + } + viewModel.dialogEventLiveData.observe(viewLifecycleOwner) { + when (it) { + ProcessingEvent -> disableInput() + SuccessEvent -> dismiss() } - ) - viewModel.dialogEventLiveData.observe( - viewLifecycleOwner, - Observer { - when (it) { - ProcessingEvent -> disableInput() - SuccessEvent -> dismiss() - } - } - ) + } subscriptionGroupAdapter = GroupAdapter().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 } ) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java index a843ad77c..af598b106 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java +++ b/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> importFromInputStream() { + Objects.requireNonNull(inputStream); + Objects.requireNonNull(inputStreamType); + return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId) .getSubscriptionExtractor() - .fromInputStream(inputStream)); + .fromInputStream(inputStream, inputStreamType)); } private Flowable> importFromPreviousExport() { diff --git a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java index 7a04ec22e..a9b9f4c87 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java +++ b/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 diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java index ce7b82de4..0976aa4fb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java +++ b/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); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 83c567cb6..dd5468f69 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/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); } } @@ -3839,11 +3875,9 @@ public final class Player implements if (DEBUG) { Log.d(TAG, "toggleFullscreen() called"); } - if (popupPlayerSelected() || exoPlayerIsNull() || currentMetadata == null - || fragmentListener == null) { + if (popupPlayerSelected() || exoPlayerIsNull() || fragmentListener == null) { return; } - //changeState(STATE_BLOCKED); TODO check what this does isFullscreen = !isFullscreen; if (!isFullscreen) { diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 45b593328..2e2fda86c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -16,6 +16,7 @@ import androidx.media.AudioManagerCompat; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.analytics.AnalyticsListener; +import com.google.android.exoplayer2.decoder.DecoderCounters; public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener { @@ -50,6 +51,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An public void dispose() { abandonAudioFocus(); player.removeAnalyticsListener(this); + notifyAudioSessionUpdate(false, player.getAudioSessionId()); } /*////////////////////////////////////////////////////////////////////////// @@ -149,11 +151,21 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An @Override public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) { + notifyAudioSessionUpdate(true, audioSessionId); + } + + @Override + public void onAudioDisabled(final EventTime eventTime, final DecoderCounters counters) { + notifyAudioSessionUpdate(false, player.getAudioSessionId()); + } + + private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) { if (!PlayerHelper.isUsingDSP()) { return; } - - final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); + final Intent intent = new Intent(active + ? AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION + : AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId); intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName()); context.sendBroadcast(intent); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java index e4ae27750..71cfcc818 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java @@ -20,18 +20,16 @@ public class LoadController implements LoadControl { //////////////////////////////////////////////////////////////////////////*/ public LoadController() { - this(PlayerHelper.getPlaybackStartBufferMs(), - PlayerHelper.getPlaybackMinimumBufferMs(), - PlayerHelper.getPlaybackOptimalBufferMs()); + this(PlayerHelper.getPlaybackStartBufferMs()); } - private LoadController(final int initialPlaybackBufferMs, - final int minimumPlaybackBufferMs, - final int optimalPlaybackBufferMs) { + private LoadController(final int initialPlaybackBufferMs) { this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000; final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder(); - builder.setBufferDurationsMs(minimumPlaybackBufferMs, optimalPlaybackBufferMs, + builder.setBufferDurationsMs( + DefaultLoadControl.DEFAULT_MIN_BUFFER_MS, + DefaultLoadControl.DEFAULT_MAX_BUFFER_MS, initialPlaybackBufferMs, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); internalLoadControl = builder.build(); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index a9a36e2f5..bbe281921 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -164,7 +164,7 @@ public class PlaybackParameterDialog extends DialogFragment { setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence)) .setNeutralButton(R.string.playback_reset, (dialogInterface, i) -> setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE)) - .setPositiveButton(R.string.finish, (dialogInterface, i) -> + .setPositiveButton(R.string.ok, (dialogInterface, i) -> setCurrentPlaybackParameters()); return dialogBuilder.create(); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index d60a14381..828833a8d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -239,6 +239,11 @@ public final class PlayerHelper { .getBoolean(context.getString(R.string.brightness_gesture_control_key), true); } + public static boolean isStartMainPlayerFullscreenEnabled(@NonNull final Context context) { + return getPreferences(context) + .getBoolean(context.getString(R.string.start_main_player_fullscreen_key), false); + } + public static boolean isAutoQueueEnabled(@NonNull final Context context) { return getPreferences(context) .getBoolean(context.getString(R.string.auto_queue_key), false); @@ -307,22 +312,6 @@ public final class PlayerHelper { return 500; } - /** - * @return the minimum number of milliseconds the player always buffers to - * after starting playback. - */ - public static int getPlaybackMinimumBufferMs() { - return 25000; - } - - /** - * @return the maximum/optimal number of milliseconds the player will buffer to once the buffer - * hits the point of {@link #getPlaybackMinimumBufferMs()}. - */ - public static int getPlaybackOptimalBufferMs() { - return 60000; - } - public static TrackSelection.Factory getQualitySelector() { return new AdaptiveTrackSelection.Factory( 1000, diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index 1a86cf38b..6e7e75932 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -74,7 +74,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment { final Preference importDataPreference = requirePreference(R.string.import_data); importDataPreference.setOnPreferenceClickListener((Preference p) -> { requestImportPathLauncher.launch( - StoredFileHelper.getPicker(requireContext(), getImportExportDataUri())); + StoredFileHelper.getPicker(requireContext(), + ZIP_MIME_TYPE, getImportExportDataUri())); return true; }); @@ -183,7 +184,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment { new AlertDialog.Builder(requireActivity()) .setMessage(R.string.override_current_data) - .setPositiveButton(R.string.finish, (d, id) -> + .setPositiveButton(R.string.ok, (d, id) -> importDatabase(file, lastImportDataUri)) .setNegativeButton(R.string.cancel, (d, id) -> d.cancel()) @@ -231,11 +232,11 @@ public class ContentSettingsFragment extends BasePreferenceFragment { final AlertDialog.Builder alert = new AlertDialog.Builder(requireContext()); alert.setTitle(R.string.import_settings); - alert.setNegativeButton(android.R.string.no, (dialog, which) -> { + alert.setNegativeButton(R.string.cancel, (dialog, which) -> { dialog.dismiss(); finishImport(importDataUri); }); - alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> { + alert.setPositiveButton(R.string.ok, (dialog, which) -> { dialog.dismiss(); manager.loadSharedPreferences(PreferenceManager .getDefaultSharedPreferences(requireContext())); diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index b4af0e43b..dfd77f049 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -179,7 +179,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { final AlertDialog.Builder msg = new AlertDialog.Builder(ctx); msg.setTitle(title); msg.setMessage(message); - msg.setPositiveButton(getString(R.string.finish), null); + msg.setPositiveButton(getString(R.string.ok), null); msg.show(); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java index 207d1ffc6..5f388efb7 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java @@ -218,7 +218,7 @@ public class PeertubeInstanceListFragment extends Fragment { .setIcon(R.drawable.place_holder_peertube) .setView(dialogBinding.getRoot()) .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.finish, (dialog1, which) -> { + .setPositiveButton(R.string.ok, (dialog1, which) -> { final String url = dialogBinding.dialogEditText.getText().toString(); addInstance(url); }) diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java index dd379b730..9fe4a9340 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java @@ -459,11 +459,12 @@ public class StoredFileHelper implements Serializable { return !str1.equals(str2); } - public static Intent getPicker(@NonNull final Context ctx) { + public static Intent getPicker(@NonNull final Context ctx, + @NonNull final String mimeType) { if (NewPipeSettings.useStorageAccessFramework(ctx)) { return new Intent(Intent.ACTION_OPEN_DOCUMENT) .putExtra("android.content.extra.SHOW_ADVANCED", true) - .setType("*/*") + .setType(mimeType) .addCategory(Intent.CATEGORY_OPENABLE) .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); @@ -477,8 +478,10 @@ public class StoredFileHelper implements Serializable { } } - public static Intent getPicker(@NonNull final Context ctx, @Nullable final Uri initialPath) { - return applyInitialPathToPickerIntent(ctx, getPicker(ctx), initialPath, null); + public static Intent getPicker(@NonNull final Context ctx, + @NonNull final String mimeType, + @Nullable final Uri initialPath) { + return applyInitialPathToPickerIntent(ctx, getPicker(ctx, mimeType), initialPath, null); } public static Intent getNewPicker(@NonNull final Context ctx, diff --git a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java index 8d918c162..73bc4d6bb 100644 --- a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java @@ -11,6 +11,7 @@ import android.view.KeyEvent; import androidx.annotation.Dimension; import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; @@ -130,4 +131,13 @@ public final class DeviceUtils { && !HI3798MV200 && !CVT_MT5886_EU_1G; } + + public static boolean isLandscape(final Context context) { + return context.getResources().getDisplayMetrics().heightPixels < context.getResources() + .getDisplayMetrics().widthPixels; + } + + public static boolean isInMultiWindow(final AppCompatActivity activity) { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index 674c7844f..b222f6abf 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -226,6 +226,16 @@ public final class Localization { shortCount(context, subscriberCount)); } + public static String downloadCount(final Context context, final int downloadCount) { + return getQuantity(context, R.plurals.download_finished_notification, 0, + downloadCount, shortCount(context, downloadCount)); + } + + public static String deletedDownloadCount(final Context context, final int deletedCount) { + return getQuantity(context, R.plurals.deleted_downloads_toast, 0, + deletedCount, shortCount(context, deletedCount)); + } + private static String getQuantity(final Context context, @PluralsRes final int pluralId, @StringRes final int zeroCaseStringId, final long count, final String formattedCount) { diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index ad9654073..eba24020f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -366,7 +366,9 @@ public final class NavigationHelper { if (switchingPlayers) { // Situation when user switches from players to main player. All needed data is // here, we can start watching (assuming newQueue equals playQueue). - detailFragment.openVideoPlayer(); + // Starting directly in fullscreen if the previous player type was popup. + detailFragment.openVideoPlayer(playerType == MainPlayer.PlayerType.POPUP + || PlayerHelper.isStartMainPlayerFullscreenEnabled(context)); } else { detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue); } diff --git a/app/src/main/java/org/schabi/newpipe/util/SavedState.kt b/app/src/main/java/org/schabi/newpipe/util/SavedState.kt index 313d56192..c556b59ff 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SavedState.kt +++ b/app/src/main/java/org/schabi/newpipe/util/SavedState.kt @@ -1,7 +1,7 @@ package org.schabi.newpipe.util import android.os.Parcelable -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize /** * Information about the saved state on the disk. diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java index e49cd6ea2..22ab6cf2b 100644 --- a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java @@ -248,6 +248,7 @@ public final class ShareUtils { shareIntent.putExtra(Intent.EXTRA_TEXT, content); if (!title.isEmpty()) { shareIntent.putExtra(Intent.EXTRA_TITLE, title); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, title); } /* TODO: add the image of the content to Android share sheet with setClipData after diff --git a/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt index b2364f058..403eee0c7 100644 --- a/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt +++ b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt @@ -1,7 +1,7 @@ package us.shandian.giga.get import android.os.Parcelable -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize import org.schabi.newpipe.extractor.MediaFormat import org.schabi.newpipe.extractor.stream.AudioStream import org.schabi.newpipe.extractor.stream.Stream diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index 52c28828d..d96b4fc5b 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -49,6 +49,8 @@ import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.MissionRecoveryInfo; import org.schabi.newpipe.streams.io.StoredDirectoryHelper; import org.schabi.newpipe.streams.io.StoredFileHelper; +import org.schabi.newpipe.util.Localization; + import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.service.DownloadManager.NetworkState; @@ -467,7 +469,8 @@ public class DownloadManagerService extends Service { .setContentIntent(makePendingIntent(ACTION_OPEN_DOWNLOADS_FINISHED)); } - if (downloadDoneCount < 1) { + downloadDoneCount++; + if (downloadDoneCount == 1) { downloadDoneList.append(name); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { @@ -476,9 +479,9 @@ public class DownloadManagerService extends Service { downloadDoneNotification.setContentTitle(null); } - downloadDoneNotification.setContentText(getString(R.string.download_finished)); + downloadDoneNotification.setContentText(Localization.downloadCount(this, downloadDoneCount)); downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle() - .setBigContentTitle(getString(R.string.download_finished)) + .setBigContentTitle(Localization.downloadCount(this, downloadDoneCount)) .bigText(name) ); } else { @@ -486,12 +489,11 @@ public class DownloadManagerService extends Service { downloadDoneList.append(name); downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle().bigText(downloadDoneList)); - downloadDoneNotification.setContentTitle(getString(R.string.download_finished_more, String.valueOf(downloadDoneCount + 1))); + downloadDoneNotification.setContentTitle(Localization.downloadCount(this, downloadDoneCount)); downloadDoneNotification.setContentText(downloadDoneList); } mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); - downloadDoneCount++; } public void notifyFailedDownload(DownloadMission mission) { diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index e06485fdf..057b9cb09 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -43,6 +43,7 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.error.ErrorActivity; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.external_communication.ShareUtils; @@ -554,7 +555,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb ); } - builder.setNegativeButton(R.string.finish, (dialog, which) -> dialog.cancel()) + builder.setNegativeButton(R.string.ok, (dialog, which) -> dialog.cancel()) .setTitle(mission.storage.getName()) .create() .show(); @@ -596,7 +597,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb } applyChanges(); - String msg = String.format(mContext.getString(R.string.deleted_downloads), mHidden.size()); + String msg = Localization.deletedDownloadCount(mContext, mHidden.size()); mSnackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE); mSnackbar.setAction(R.string.undo, s -> { Iterator i = mHidden.iterator(); diff --git a/app/src/main/res/drawable-nodpi/not_available_monkey.png b/app/src/main/res/drawable-nodpi/not_available_monkey.png deleted file mode 100644 index 35b216c48..000000000 Binary files a/app/src/main/res/drawable-nodpi/not_available_monkey.png and /dev/null differ diff --git a/app/src/main/res/drawable/not_available_monkey.xml b/app/src/main/res/drawable/not_available_monkey.xml new file mode 100644 index 000000000..b15a381c5 --- /dev/null +++ b/app/src/main/res/drawable/not_available_monkey.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index d4f1ccc3d..7f664f5d4 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -52,7 +52,7 @@ android:id="@+id/detail_thumbnail_image_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@android:color/transparent" + android:background="?windowBackground" android:contentDescription="@string/detail_thumbnail_view_description" android:scaleType="fitCenter" tools:ignore="RtlHardcoded" diff --git a/app/src/main/res/layout/dialog_feed_group_reorder.xml b/app/src/main/res/layout/dialog_feed_group_reorder.xml index 82a9b1591..2e932f468 100644 --- a/app/src/main/res/layout/dialog_feed_group_reorder.xml +++ b/app/src/main/res/layout/dialog_feed_group_reorder.xml @@ -27,5 +27,5 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end" - android:text="@string/finish" /> + android:text="@string/ok" /> \ No newline at end of file diff --git a/app/src/main/res/layout/error_panel.xml b/app/src/main/res/layout/error_panel.xml index 355dd17e3..007349d44 100644 --- a/app/src/main/res/layout/error_panel.xml +++ b/app/src/main/res/layout/error_panel.xml @@ -21,14 +21,16 @@ android:id="@+id/error_message_service_info_view" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center" android:layout_marginTop="6dp" + android:gravity="center" android:text="@string/general_error" android:textSize="16sp" - tools:text="YouTube provides this reason:" /> + android:visibility="gone" + tools:text="YouTube provides this reason:" + tools:visibility="visible" /> + android:visibility="gone" + tools:text="This account has been terminated because we received multiple third-party claims of copyright infringement regarding material that the user posted." + tools:visibility="visible" />