From f6e2dd1480a71fa61d8a285912beb11c9169c069 Mon Sep 17 00:00:00 2001 From: Stypox Date: Fri, 8 Jan 2021 18:35:33 +0100 Subject: [PATCH 1/5] Merge player classes into a single one --- .../java/org/schabi/newpipe/MainActivity.java | 4 +- .../fragments/detail/VideoDetailFragment.java | 28 +- .../player/BackgroundPlayerActivity.java | 20 +- .../org/schabi/newpipe/player/BasePlayer.java | 1615 ------- .../org/schabi/newpipe/player/MainPlayer.java | 101 +- .../newpipe/player/NotificationUtil.java | 92 +- .../org/schabi/newpipe/player/Player.java | 3973 +++++++++++++++++ .../newpipe/player/PlayerServiceBinder.java | 10 +- .../newpipe/player/ServicePlayerActivity.java | 43 +- .../schabi/newpipe/player/VideoPlayer.java | 1036 ----- .../newpipe/player/VideoPlayerImpl.java | 2076 --------- .../player/event/BasePlayerGestureListener.kt | 116 +- .../player/event/PlayerGestureListener.java | 135 +- .../PlayerServiceExtendedEventListener.java | 4 +- .../helper/PlaybackParameterDialog.java | 2 +- .../newpipe/player/helper/PlayerHelper.java | 335 +- .../newpipe/player/helper/PlayerHolder.java | 4 +- .../mediasession/MediaSessionCallback.java | 10 +- .../mediasession/PlayQueueNavigator.java | 6 +- .../PlayQueuePlaybackController.java | 4 +- ...iaSession.java => PlayerMediaSession.java} | 35 +- .../org/schabi/newpipe/util/Localization.java | 15 + .../schabi/newpipe/util/NavigationHelper.java | 27 +- app/src/main/res/values/settings_keys.xml | 6 +- checkstyle-suppressions.xml | 2 +- 25 files changed, 4540 insertions(+), 5159 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/player/BasePlayer.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/Player.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java rename app/src/main/java/org/schabi/newpipe/player/playback/{BasePlayerMediaSession.java => PlayerMediaSession.java} (77%) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 0c784e9d5..b111af1fb 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -68,7 +68,7 @@ import org.schabi.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.MainFragment; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.fragments.list.search.SearchFragment; -import org.schabi.newpipe.player.VideoPlayer; +import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -763,7 +763,7 @@ public class MainActivity extends AppCompatActivity { switch (linkType) { case STREAM: final String intentCacheKey = intent.getStringExtra( - VideoPlayer.PLAY_QUEUE_KEY); + Player.PLAY_QUEUE_KEY); final PlayQueue playQueue = intentCacheKey != null ? SerializedCache.getInstance() .take(intentCacheKey, PlayQueue.class) 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 aeb51f63a..b9f4ef6a9 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 @@ -49,7 +49,6 @@ import androidx.viewpager.widget.ViewPager; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.tabs.TabLayout; @@ -80,9 +79,8 @@ import org.schabi.newpipe.fragments.list.videos.RelatedVideosFragment; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.dialog.PlaylistCreationDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.player.BasePlayer; +import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.MainPlayer; -import org.schabi.newpipe.player.VideoPlayerImpl; import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; import org.schabi.newpipe.player.helper.PlayerHelper; @@ -255,14 +253,14 @@ public final class VideoDetailFragment private ContentObserver settingsContentObserver; private MainPlayer playerService; - private VideoPlayerImpl player; + private Player player; /*////////////////////////////////////////////////////////////////////////// // Service management //////////////////////////////////////////////////////////////////////////*/ @Override - public void onServiceConnected(final VideoPlayerImpl connectedPlayer, + public void onServiceConnected(final Player connectedPlayer, final MainPlayer connectedPlayerService, final boolean playAfterConnect) { player = connectedPlayer; @@ -539,7 +537,7 @@ public final class VideoDetailFragment break; case R.id.overlay_play_pause_button: if (playerIsNotStopped()) { - player.onPlayPause(); + player.playPause(); player.hideControls(0, 0); showSystemUi(); } else { @@ -805,7 +803,7 @@ public final class VideoDetailFragment // If we are in fullscreen mode just exit from it via first back press if (player != null && player.isFullscreen()) { if (!DeviceUtils.isTablet(activity)) { - player.onPause(); + player.pause(); } restoreDefaultOrientation(); setAutoPlay(false); @@ -850,7 +848,7 @@ public final class VideoDetailFragment final PlayQueueItem playQueueItem = item.getPlayQueue().getItem(); // Update title, url, uploader from the last item in the stack (it's current now) - final boolean isPlayerStopped = player == null || player.isPlayerStopped(); + final boolean isPlayerStopped = player == null || player.isStopped(); if (playQueueItem != null && isPlayerStopped) { updateOverlayData(playQueueItem.getTitle(), playQueueItem.getUploader(), playQueueItem.getThumbnailUrl()); @@ -1569,7 +1567,7 @@ public final class VideoDetailFragment showMetaInfoInTextView(info.getMetaInfo(), detailMetaInfoTextView, detailMetaInfoSeparator); - if (player == null || player.isPlayerStopped()) { + if (player == null || player.isStopped()) { updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl()); } @@ -1797,7 +1795,7 @@ public final class VideoDetailFragment setOverlayPlayPauseImage(player != null && player.isPlaying()); switch (state) { - case BasePlayer.STATE_PLAYING: + case Player.STATE_PLAYING: if (positionView.getAlpha() != 1.0f && player.getPlayQueue() != null && player.getPlayQueue().getItem() != null @@ -1814,7 +1812,7 @@ public final class VideoDetailFragment final int duration, final int bufferPercent) { // Progress updates every second even if media is paused. It's useless until playing - if (!player.getPlayer().isPlaying() || playQueue == null) { + if (!player.isPlaying() || playQueue == null) { return; } @@ -2020,9 +2018,7 @@ public final class VideoDetailFragment } private boolean playerIsNotStopped() { - return player != null - && player.getPlayer() != null - && player.getPlayer().getPlaybackState() != Player.STATE_IDLE; + return player != null && !player.isStopped(); } private void restoreDefaultBrightness() { @@ -2073,7 +2069,7 @@ public final class VideoDetailFragment player.checkLandscape(); // Let's give a user time to look at video information page if video is not playing if (globalScreenOrientationLocked(activity) && !player.isPlaying()) { - player.onPlay(); + player.play(); } } @@ -2287,7 +2283,7 @@ public final class VideoDetailFragment // Re-enable clicks setOverlayElementsClickable(true); if (player != null) { - player.onQueueClosed(); + player.closeQueue(); } setOverlayLook(appBarLayout, behavior, 0); break; diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java index 2fc710fb0..eb1c74dad 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java @@ -26,15 +26,15 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity { @Override public void startPlayerListener() { - if (player instanceof VideoPlayerImpl) { - ((VideoPlayerImpl) player).setActivityListener(this); + if (player != null) { + player.setActivityListener(this); } } @Override public void stopPlayerListener() { - if (player instanceof VideoPlayerImpl) { - ((VideoPlayerImpl) player).removeActivityListener(this); + if (player != null) { + player.removeActivityListener(this); } } @@ -45,13 +45,11 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity { @Override public void setupMenu(final Menu menu) { - if (player == null) { - return; + if (player != null) { + menu.findItem(R.id.action_switch_popup) + .setVisible(!player.popupPlayerSelected()); + menu.findItem(R.id.action_switch_background) + .setVisible(!player.audioPlayerSelected()); } - - menu.findItem(R.id.action_switch_popup) - .setVisible(!((VideoPlayerImpl) player).popupPlayerSelected()); - menu.findItem(R.id.action_switch_background) - .setVisible(!((VideoPlayerImpl) player).audioPlayerSelected()); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java deleted file mode 100644 index ea16574e9..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ /dev/null @@ -1,1615 +0,0 @@ -/* - * Copyright 2017 Mauricio Colli - * BasePlayer.java is part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.schabi.newpipe.player; - -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; -import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; -import static java.util.concurrent.TimeUnit.MILLISECONDS; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.AudioManager; -import android.util.Log; -import android.view.View; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.PreferenceManager; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.BehindLiveWindowException; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.assist.FailReason; -import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.Observable; -import io.reactivex.rxjava3.disposables.CompositeDisposable; -import io.reactivex.rxjava3.disposables.Disposable; -import io.reactivex.rxjava3.disposables.SerialDisposable; -import java.io.IOException; -import org.schabi.newpipe.DownloaderImpl; -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.player.helper.AudioReactor; -import org.schabi.newpipe.player.helper.LoadController; -import org.schabi.newpipe.player.helper.MediaSessionManager; -import org.schabi.newpipe.player.helper.PlayerDataSource; -import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.playback.BasePlayerMediaSession; -import org.schabi.newpipe.player.playback.CustomTrackSelector; -import org.schabi.newpipe.player.playback.MediaSourceManager; -import org.schabi.newpipe.player.playback.PlaybackListener; -import org.schabi.newpipe.player.playqueue.PlayQueue; -import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.player.resolver.MediaSourceTag; -import org.schabi.newpipe.util.ImageDisplayConstants; -import org.schabi.newpipe.util.SerializedCache; - -/** - * Base for the players, joining the common properties. - * - * @author mauriciocolli - */ -@SuppressWarnings({"WeakerAccess"}) -public abstract class BasePlayer implements - Player.EventListener, PlaybackListener, ImageLoadingListener { - public static final boolean DEBUG = MainActivity.DEBUG; - @NonNull - public static final String TAG = "BasePlayer"; - - public static final int STATE_PREFLIGHT = -1; - public static final int STATE_BLOCKED = 123; - public static final int STATE_PLAYING = 124; - public static final int STATE_BUFFERING = 125; - public static final int STATE_PAUSED = 126; - public static final int STATE_PAUSED_SEEK = 127; - public static final int STATE_COMPLETED = 128; - - /*////////////////////////////////////////////////////////////////////////// - // Intent - //////////////////////////////////////////////////////////////////////////*/ - - @NonNull - public static final String REPEAT_MODE = "repeat_mode"; - @NonNull - public static final String PLAYBACK_QUALITY = "playback_quality"; - @NonNull - public static final String PLAY_QUEUE_KEY = "play_queue_key"; - @NonNull - public static final String APPEND_ONLY = "append_only"; - @NonNull - public static final String RESUME_PLAYBACK = "resume_playback"; - @NonNull - public static final String PLAY_WHEN_READY = "play_when_ready"; - @NonNull - public static final String SELECT_ON_APPEND = "select_on_append"; - @NonNull - public static final String PLAYER_TYPE = "player_type"; - @NonNull - public static final String IS_MUTED = "is_muted"; - - /*////////////////////////////////////////////////////////////////////////// - // Playback - //////////////////////////////////////////////////////////////////////////*/ - - protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f}; - - protected PlayQueue playQueue; - protected PlayQueueAdapter playQueueAdapter; - - @Nullable - protected MediaSourceManager playbackManager; - - @Nullable - private PlayQueueItem currentItem; - @Nullable - private MediaSourceTag currentMetadata; - @Nullable - private Bitmap currentThumbnail; - - @Nullable - protected Toast errorToast; - - /*////////////////////////////////////////////////////////////////////////// - // Player - //////////////////////////////////////////////////////////////////////////*/ - - protected static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds - protected static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500; - - public static final int PLAYER_TYPE_VIDEO = 0; - public static final int PLAYER_TYPE_AUDIO = 1; - public static final int PLAYER_TYPE_POPUP = 2; - - protected SimpleExoPlayer simpleExoPlayer; - protected AudioReactor audioReactor; - protected MediaSessionManager mediaSessionManager; - - - @NonNull - protected final Context context; - @NonNull - protected final BroadcastReceiver broadcastReceiver; - @NonNull - protected final IntentFilter intentFilter; - @NonNull - protected final HistoryRecordManager recordManager; - @NonNull - protected final SharedPreferences sharedPreferences; - @NonNull - protected final CustomTrackSelector trackSelector; - @NonNull - protected final PlayerDataSource dataSource; - @NonNull - private final LoadControl loadControl; - - @NonNull - private final RenderersFactory renderFactory; - @NonNull - private final SerialDisposable progressUpdateReactor; - @NonNull - private final CompositeDisposable databaseUpdateReactor; - - private boolean isPrepared = false; - private Disposable stateLoader; - - protected int currentState = STATE_PREFLIGHT; - - public BasePlayer(@NonNull final Context context) { - this.context = context; - - this.broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context ctx, final Intent intent) { - onBroadcastReceived(intent); - } - }; - this.intentFilter = new IntentFilter(); - setupBroadcastReceiver(intentFilter); - - this.recordManager = new HistoryRecordManager(context); - this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - - this.progressUpdateReactor = new SerialDisposable(); - this.databaseUpdateReactor = new CompositeDisposable(); - - final String userAgent = DownloaderImpl.USER_AGENT; - final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(context) - .build(); - this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter); - - final TrackSelection.Factory trackSelectionFactory = PlayerHelper - .getQualitySelector(); - this.trackSelector = new CustomTrackSelector(context, trackSelectionFactory); - - this.loadControl = new LoadController(); - this.renderFactory = new DefaultRenderersFactory(context); - } - - public void initPlayer(final boolean playOnReady) { - if (DEBUG) { - Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]"); - } - - simpleExoPlayer = new SimpleExoPlayer.Builder(context, renderFactory) - .setTrackSelector(trackSelector) - .setLoadControl(loadControl) - .build(); - simpleExoPlayer.addListener(this); - simpleExoPlayer.setPlayWhenReady(playOnReady); - simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); - simpleExoPlayer.setWakeMode(C.WAKE_MODE_NETWORK); - simpleExoPlayer.setHandleAudioBecomingNoisy(true); - - audioReactor = new AudioReactor(context, simpleExoPlayer); - mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer, - new BasePlayerMediaSession(this)); - - registerBroadcastReceiver(); - } - - public void initListeners() { - } - - public void handleIntent(final Intent intent) { - if (DEBUG) { - Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]"); - } - if (intent == null) { - return; - } - - // Resolve play queue - if (!intent.hasExtra(PLAY_QUEUE_KEY)) { - return; - } - final String intentCacheKey = intent.getStringExtra(PLAY_QUEUE_KEY); - final PlayQueue queue = SerializedCache.getInstance().take(intentCacheKey, PlayQueue.class); - if (queue == null) { - return; - } - - // Resolve append intents - if (intent.getBooleanExtra(APPEND_ONLY, false) && playQueue != null) { - final int sizeBeforeAppend = playQueue.size(); - playQueue.append(queue.getStreams()); - - if ((intent.getBooleanExtra(SELECT_ON_APPEND, false) - || getCurrentState() == STATE_COMPLETED) && queue.getStreams().size() > 0) { - playQueue.setIndex(sizeBeforeAppend); - } - - return; - } - - final PlaybackParameters savedParameters = retrievePlaybackParametersFromPreferences(); - final float playbackSpeed = savedParameters.speed; - final float playbackPitch = savedParameters.pitch; - final boolean playbackSkipSilence = savedParameters.skipSilence; - - final boolean samePlayQueue = playQueue != null && playQueue.equals(queue); - - final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); - final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true); - final boolean isMuted = intent - .getBooleanExtra(IS_MUTED, simpleExoPlayer != null && isMuted()); - - /* - * There are 3 situations when playback shouldn't be started from scratch (zero timestamp): - * 1. User pressed on a timestamp link and the same video should be rewound to the timestamp - * 2. User changed a player from, for example. main to popup, or from audio to main, etc - * 3. User chose to resume a video based on a saved timestamp from history of played videos - * In those cases time will be saved because re-init of the play queue is a not an instant - * task and requires network calls - * */ - // seek to timestamp if stream is already playing - if (simpleExoPlayer != null - && queue.size() == 1 - && playQueue != null - && playQueue.size() == 1 - && playQueue.getItem() != null - && queue.getItem().getUrl().equals(playQueue.getItem().getUrl()) - && queue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { - // Player can have state = IDLE when playback is stopped or failed - // and we should retry() in this case - if (simpleExoPlayer.getPlaybackState() == Player.STATE_IDLE) { - simpleExoPlayer.retry(); - } - simpleExoPlayer.seekTo(playQueue.getIndex(), queue.getItem().getRecoveryPosition()); - simpleExoPlayer.setPlayWhenReady(playWhenReady); - - } else if (simpleExoPlayer != null - && samePlayQueue - && playQueue != null - && !playQueue.isDisposed()) { - // Do not re-init the same PlayQueue. Save time - // Player can have state = IDLE when playback is stopped or failed - // and we should retry() in this case - if (simpleExoPlayer.getPlaybackState() == Player.STATE_IDLE) { - simpleExoPlayer.retry(); - } - simpleExoPlayer.setPlayWhenReady(playWhenReady); - - } else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) - && isPlaybackResumeEnabled() - && !samePlayQueue - && !queue.isEmpty() - && queue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { - stateLoader = recordManager.loadStreamState(queue.getItem()) - .observeOn(AndroidSchedulers.mainThread()) - // Do not place initPlayback() in doFinally() because - // it restarts playback after destroy() - //.doFinally() - .subscribe( - state -> { - queue.setRecovery(queue.getIndex(), state.getProgressTime()); - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted); - }, - error -> { - if (DEBUG) { - error.printStackTrace(); - } - // In case any error we can start playback without history - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted); - }, - () -> { - // Completed but not found in history - initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted); - } - ); - databaseUpdateReactor.add(stateLoader); - } else { - // Good to go... - // In a case of equal PlayQueues we can re-init old one but only when it is disposed - initPlayback(samePlayQueue ? playQueue : queue, repeatMode, playbackSpeed, - playbackPitch, playbackSkipSilence, playWhenReady, isMuted); - } - } - - private PlaybackParameters retrievePlaybackParametersFromPreferences() { - final SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(context); - - final float speed = preferences.getFloat( - context.getString(R.string.playback_speed_key), getPlaybackSpeed()); - final float pitch = preferences.getFloat( - context.getString(R.string.playback_pitch_key), getPlaybackPitch()); - final boolean skipSilence = preferences.getBoolean( - context.getString(R.string.playback_skip_silence_key), getPlaybackSkipSilence()); - return new PlaybackParameters(speed, pitch, skipSilence); - } - - protected void initPlayback(@NonNull final PlayQueue queue, - @Player.RepeatMode final int repeatMode, - final float playbackSpeed, - final float playbackPitch, - final boolean playbackSkipSilence, - final boolean playOnReady, - final boolean isMuted) { - destroyPlayer(); - initPlayer(playOnReady); - setRepeatMode(repeatMode); - setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence); - - playQueue = queue; - playQueue.init(); - if (playbackManager != null) { - playbackManager.dispose(); - } - playbackManager = new MediaSourceManager(this, playQueue); - - if (playQueueAdapter != null) { - playQueueAdapter.dispose(); - } - playQueueAdapter = new PlayQueueAdapter(context, playQueue); - - simpleExoPlayer.setVolume(isMuted ? 0 : 1); - } - - public void destroyPlayer() { - if (DEBUG) { - Log.d(TAG, "destroyPlayer() called"); - } - if (simpleExoPlayer != null) { - simpleExoPlayer.removeListener(this); - simpleExoPlayer.stop(); - simpleExoPlayer.release(); - } - if (isProgressLoopRunning()) { - stopProgressLoop(); - } - if (playQueue != null) { - playQueue.dispose(); - } - if (audioReactor != null) { - audioReactor.dispose(); - } - if (playbackManager != null) { - playbackManager.dispose(); - } - if (mediaSessionManager != null) { - mediaSessionManager.dispose(); - } - if (stateLoader != null) { - stateLoader.dispose(); - } - - if (playQueueAdapter != null) { - playQueueAdapter.unsetSelectedListener(); - playQueueAdapter.dispose(); - } - } - - public void destroy() { - if (DEBUG) { - Log.d(TAG, "destroy() called"); - } - destroyPlayer(); - unregisterBroadcastReceiver(); - - databaseUpdateReactor.clear(); - progressUpdateReactor.set(null); - ImageLoader.getInstance().stop(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Thumbnail Loading - //////////////////////////////////////////////////////////////////////////*/ - - private void initThumbnail(final String url) { - if (DEBUG) { - Log.d(TAG, "Thumbnail - initThumbnail() called"); - } - if (url == null || url.isEmpty()) { - return; - } - ImageLoader.getInstance().resume(); - ImageLoader.getInstance() - .loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, this); - } - - @Override - public void onLoadingStarted(final String imageUri, final View view) { - if (DEBUG) { - Log.d(TAG, "Thumbnail - onLoadingStarted() called on: " - + "imageUri = [" + imageUri + "], view = [" + view + "]"); - } - } - - @Override - public void onLoadingFailed(final String imageUri, final View view, - final FailReason failReason) { - Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]", - failReason.getCause()); - currentThumbnail = null; - } - - @Override - public void onLoadingComplete(final String imageUri, final View view, - final Bitmap loadedImage) { - final float width = Math.min( - context.getResources().getDimension(R.dimen.player_notification_thumbnail_width), - loadedImage.getWidth()); - currentThumbnail = Bitmap.createScaledBitmap(loadedImage, - (int) width, - (int) (loadedImage.getHeight() / (loadedImage.getWidth() / width)), true); - if (DEBUG) { - Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " - + "imageUri = [" + imageUri + "], view = [" + view + "], " - + "loadedImage = [" + loadedImage + "], " - + loadedImage.getWidth() + "x" + loadedImage.getHeight() - + ", scaled width = " + width); - } - } - - @Override - public void onLoadingCancelled(final String imageUri, final View view) { - if (DEBUG) { - Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " - + "imageUri = [" + imageUri + "], view = [" + view + "]"); - } - currentThumbnail = null; - } - - /*////////////////////////////////////////////////////////////////////////// - // Broadcast Receiver - //////////////////////////////////////////////////////////////////////////*/ - - /** - * Add your action in the intentFilter. - * - * @param intentFltr intent filter that will be used for register the receiver - */ - protected void setupBroadcastReceiver(final IntentFilter intentFltr) { - intentFltr.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); - } - - public void onBroadcastReceived(final Intent intent) { - if (intent == null || intent.getAction() == null) { - return; - } - switch (intent.getAction()) { - case AudioManager.ACTION_AUDIO_BECOMING_NOISY: - onPause(); - break; - } - } - - protected void registerBroadcastReceiver() { - // Try to unregister current first - unregisterBroadcastReceiver(); - context.registerReceiver(broadcastReceiver, intentFilter); - } - - protected void unregisterBroadcastReceiver() { - try { - context.unregisterReceiver(broadcastReceiver); - } catch (final IllegalArgumentException unregisteredException) { - Log.w(TAG, "Broadcast receiver already unregistered " - + "(" + unregisteredException.getMessage() + ")"); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // States Implementation - //////////////////////////////////////////////////////////////////////////*/ - - public void changeState(final int state) { - if (DEBUG) { - Log.d(TAG, "changeState() called with: state = [" + state + "]"); - } - currentState = state; - switch (state) { - case STATE_BLOCKED: - onBlocked(); - break; - case STATE_PLAYING: - onPlaying(); - break; - case STATE_BUFFERING: - onBuffering(); - break; - case STATE_PAUSED: - onPaused(); - break; - case STATE_PAUSED_SEEK: - onPausedSeek(); - break; - case STATE_COMPLETED: - onCompleted(); - break; - } - } - - public void onBlocked() { - if (DEBUG) { - Log.d(TAG, "onBlocked() called"); - } - if (!isProgressLoopRunning()) { - startProgressLoop(); - } - } - - public void onPlaying() { - if (DEBUG) { - Log.d(TAG, "onPlaying() called"); - } - if (!isProgressLoopRunning()) { - startProgressLoop(); - } - } - - public void onBuffering() { - } - - public void onPaused() { - if (isProgressLoopRunning()) { - stopProgressLoop(); - } - } - - public void onPausedSeek() { - } - - public void onCompleted() { - if (DEBUG) { - Log.d(TAG, "onCompleted() called"); - } - if (playQueue.getIndex() < playQueue.size() - 1) { - playQueue.offsetIndex(+1); - } - if (isProgressLoopRunning()) { - stopProgressLoop(); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Repeat and shuffle - //////////////////////////////////////////////////////////////////////////*/ - - public void onRepeatClicked() { - if (DEBUG) { - Log.d(TAG, "onRepeatClicked() called"); - } - - final int mode; - - switch (getRepeatMode()) { - case Player.REPEAT_MODE_OFF: - mode = Player.REPEAT_MODE_ONE; - break; - case Player.REPEAT_MODE_ONE: - mode = Player.REPEAT_MODE_ALL; - break; - case Player.REPEAT_MODE_ALL: - default: - mode = Player.REPEAT_MODE_OFF; - break; - } - - setRepeatMode(mode); - if (DEBUG) { - Log.d(TAG, "onRepeatClicked() currentRepeatMode = " + getRepeatMode()); - } - } - - public void onShuffleClicked() { - if (DEBUG) { - Log.d(TAG, "onShuffleClicked() called"); - } - - if (simpleExoPlayer == null) { - return; - } - simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled()); - } - /*////////////////////////////////////////////////////////////////////////// - // Mute / Unmute - //////////////////////////////////////////////////////////////////////////*/ - - public void onMuteUnmuteButtonClicked() { - if (DEBUG) { - Log.d(TAG, "onMuteUnmuteButtonClicked() called"); - } - simpleExoPlayer.setVolume(isMuted() ? 1 : 0); - } - - public boolean isMuted() { - return simpleExoPlayer.getVolume() == 0; - } - - /*////////////////////////////////////////////////////////////////////////// - // Progress Updates - //////////////////////////////////////////////////////////////////////////*/ - - public abstract void onUpdateProgress(int currentProgress, int duration, int bufferPercent); - - protected void startProgressLoop() { - progressUpdateReactor.set(getProgressReactor()); - } - - protected void stopProgressLoop() { - progressUpdateReactor.set(null); - } - - public void triggerProgressUpdate() { - if (simpleExoPlayer == null) { - return; - } - onUpdateProgress( - Math.max((int) simpleExoPlayer.getCurrentPosition(), 0), - (int) simpleExoPlayer.getDuration(), - simpleExoPlayer.getBufferedPercentage() - ); - } - - private Disposable getProgressReactor() { - return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS, - AndroidSchedulers.mainThread()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(ignored -> triggerProgressUpdate(), - error -> Log.e(TAG, "Progress update failure: ", error)); - } - - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onTimelineChanged(final Timeline timeline, final int reason) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onTimelineChanged() called with " - + "timeline size = [" + timeline.getWindowCount() + "], " - + "reason = [" + reason + "]"); - } - - maybeUpdateCurrentMetadata(); - } - - @Override - public void onTracksChanged(final TrackGroupArray trackGroups, - final TrackSelectionArray trackSelections) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onTracksChanged(), " - + "track group size = " + trackGroups.length); - } - - maybeUpdateCurrentMetadata(); - } - - @Override - public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - playbackParameters(), " - + "speed: " + playbackParameters.speed + ", " - + "pitch: " + playbackParameters.pitch); - } - } - - @Override - public void onLoadingChanged(final boolean isLoading) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onLoadingChanged() called with: " - + "isLoading = [" + isLoading + "]"); - } - - if (!isLoading && getCurrentState() == STATE_PAUSED && isProgressLoopRunning()) { - stopProgressLoop(); - } else if (isLoading && !isProgressLoopRunning()) { - startProgressLoop(); - } - - maybeUpdateCurrentMetadata(); - } - - @Override - public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: " - + "playWhenReady = [" + playWhenReady + "], " - + "playbackState = [" + playbackState + "]"); - } - - if (getCurrentState() == STATE_PAUSED_SEEK) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onPlayerStateChanged() is currently blocked"); - } - return; - } - - switch (playbackState) { - case Player.STATE_IDLE: // 1 - isPrepared = false; - break; - case Player.STATE_BUFFERING: // 2 - if (isPrepared) { - changeState(STATE_BUFFERING); - } - break; - case Player.STATE_READY: //3 - maybeUpdateCurrentMetadata(); - maybeCorrectSeekPosition(); - if (!isPrepared) { - isPrepared = true; - onPrepared(playWhenReady); - break; - } - changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); - break; - case Player.STATE_ENDED: // 4 - changeState(STATE_COMPLETED); - if (currentMetadata != null) { - resetPlaybackState(currentMetadata.getMetadata()); - } - isPrepared = false; - break; - } - } - - private void maybeCorrectSeekPosition() { - if (playQueue == null || simpleExoPlayer == null || currentMetadata == null) { - return; - } - - final PlayQueueItem currentSourceItem = playQueue.getItem(); - if (currentSourceItem == null) { - return; - } - - final StreamInfo currentInfo = currentMetadata.getMetadata(); - final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000; - if (presetStartPositionMillis > 0L) { - // Has another start position? - if (DEBUG) { - Log.d(TAG, "Playback - Seeking to preset start " - + "position=[" + presetStartPositionMillis + "]"); - } - seekTo(presetStartPositionMillis); - } - } - - /** - * Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. - *

There are multiple types of errors:

- *
    - *
  • {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}
  • - *
  • {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}: - * If a runtime error occurred, then we can try to recover it by restarting the playback - * after setting the timestamp recovery.
  • - *
  • {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}: - * If the renderer failed, treat the error as unrecoverable.
  • - *
- * - * @see #processSourceError(IOException) - * @see Player.EventListener#onPlayerError(ExoPlaybackException) - */ - @Override - public void onPlayerError(final ExoPlaybackException error) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onPlayerError() called with: " + "error = [" + error + "]"); - } - if (errorToast != null) { - errorToast.cancel(); - errorToast = null; - } - - savePlaybackState(); - - switch (error.type) { - case ExoPlaybackException.TYPE_SOURCE: - processSourceError(error.getSourceException()); - showStreamError(error); - break; - case ExoPlaybackException.TYPE_UNEXPECTED: - showRecoverableError(error); - setRecovery(); - reload(); - break; - default: - showUnrecoverableError(error); - onPlaybackShutdown(); - break; - } - } - - private void processSourceError(final IOException error) { - if (simpleExoPlayer == null || playQueue == null) { - return; - } - setRecovery(); - - if (error instanceof BehindLiveWindowException) { - reload(); - } else { - playQueue.error(); - } - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason final int reason) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " - + "reason = [" + reason + "]"); - } - if (playQueue == null) { - return; - } - - // Refresh the playback if there is a transition to the next video - final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); - switch (reason) { - case DISCONTINUITY_REASON_PERIOD_TRANSITION: - // When player is in single repeat mode and a period transition occurs, - // we need to register a view count here since no metadata has changed - if (getRepeatMode() == Player.REPEAT_MODE_ONE - && newWindowIndex == playQueue.getIndex()) { - registerView(); - break; - } - case DISCONTINUITY_REASON_SEEK: - case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: - case DISCONTINUITY_REASON_INTERNAL: - if (playQueue.getIndex() != newWindowIndex) { - resetPlaybackState(playQueue.getItem()); - playQueue.setIndex(newWindowIndex); - } - break; - } - - maybeUpdateCurrentMetadata(); - } - - @Override - public void onRepeatModeChanged(@Player.RepeatMode final int reason) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: " - + "mode = [" + reason + "]"); - } - } - - @Override - public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onShuffleModeEnabledChanged() called with: " - + "mode = [" + shuffleModeEnabled + "]"); - } - if (playQueue == null) { - return; - } - if (shuffleModeEnabled) { - playQueue.shuffle(); - } else { - playQueue.unshuffle(); - } - } - - @Override - public void onSeekProcessed() { - if (DEBUG) { - Log.d(TAG, "ExoPlayer - onSeekProcessed() called"); - } - if (isPrepared) { - savePlaybackState(); - } - } - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public boolean isApproachingPlaybackEdge(final long timeToEndMillis) { - // If live, then not near playback edge - // If not playing, then not approaching playback edge - if (simpleExoPlayer == null || isLive() || !isPlaying()) { - return false; - } - - final long currentPositionMillis = simpleExoPlayer.getCurrentPosition(); - final long currentDurationMillis = simpleExoPlayer.getDuration(); - return currentDurationMillis - currentPositionMillis < timeToEndMillis; - } - - @Override - public void onPlaybackBlock() { - if (simpleExoPlayer == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "Playback - onPlaybackBlock() called"); - } - - currentItem = null; - currentMetadata = null; - simpleExoPlayer.stop(); - isPrepared = false; - - changeState(STATE_BLOCKED); - } - - @Override - public void onPlaybackUnblock(final MediaSource mediaSource) { - if (simpleExoPlayer == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "Playback - onPlaybackUnblock() called"); - } - - if (getCurrentState() == STATE_BLOCKED) { - changeState(STATE_BUFFERING); - } - - simpleExoPlayer.prepare(mediaSource); - } - - public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) { - if (DEBUG) { - Log.d(TAG, "Playback - onPlaybackSynchronize() called with " - + "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); - } - if (simpleExoPlayer == null || playQueue == null) { - return; - } - - final boolean onPlaybackInitial = currentItem == null; - final boolean hasPlayQueueItemChanged = currentItem != item; - - final int currentPlayQueueIndex = playQueue.indexOf(item); - final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); - final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); - - // If nothing to synchronize - if (!hasPlayQueueItemChanged) { - return; - } - currentItem = item; - - // Check if on wrong window - if (currentPlayQueueIndex != playQueue.getIndex()) { - Log.e(TAG, "Playback - Play Queue may be desynchronized: item " - + "index=[" + currentPlayQueueIndex + "], " - + "queue index=[" + playQueue.getIndex() + "]"); - - // Check if bad seek position - } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize) - || currentPlayQueueIndex < 0) { - Log.e(TAG, "Playback - Trying to seek to invalid " - + "index=[" + currentPlayQueueIndex + "] with " - + "playlist length=[" + currentPlaylistSize + "]"); - - } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial - || !isPlaying()) { - if (DEBUG) { - Log.d(TAG, "Playback - Rewinding to correct " - + "index=[" + currentPlayQueueIndex + "], " - + "from=[" + currentPlaylistIndex + "], " - + "size=[" + currentPlaylistSize + "]."); - } - - if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { - simpleExoPlayer.seekTo(currentPlayQueueIndex, item.getRecoveryPosition()); - playQueue.unsetRecovery(currentPlayQueueIndex); - } else { - simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); - } - } - } - - protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { - final StreamInfo info = tag.getMetadata(); - if (DEBUG) { - Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName()); - } - - initThumbnail(info.getThumbnailUrl()); - registerView(); - } - - /*////////////////////////////////////////////////////////////////////////// - // General Player - //////////////////////////////////////////////////////////////////////////*/ - - public void showStreamError(final Exception exception) { - exception.printStackTrace(); - - if (errorToast == null) { - errorToast = Toast - .makeText(context, R.string.player_stream_failure, Toast.LENGTH_SHORT); - errorToast.show(); - } - } - - public void showRecoverableError(final Exception exception) { - exception.printStackTrace(); - - if (errorToast == null) { - errorToast = Toast - .makeText(context, R.string.player_recoverable_failure, Toast.LENGTH_SHORT); - errorToast.show(); - } - } - - public void showUnrecoverableError(final Exception exception) { - exception.printStackTrace(); - - if (errorToast != null) { - errorToast.cancel(); - } - errorToast = Toast - .makeText(context, R.string.player_unrecoverable_failure, Toast.LENGTH_SHORT); - errorToast.show(); - } - - public void onPrepared(final boolean playWhenReady) { - if (DEBUG) { - Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); - } - if (playWhenReady) { - audioReactor.requestAudioFocus(); - } - changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); - } - - public void onPlay() { - if (DEBUG) { - Log.d(TAG, "onPlay() called"); - } - if (audioReactor == null || playQueue == null || simpleExoPlayer == null) { - return; - } - - audioReactor.requestAudioFocus(); - - if (getCurrentState() == STATE_COMPLETED) { - if (playQueue.getIndex() == 0) { - seekToDefault(); - } else { - playQueue.setIndex(0); - } - } - - simpleExoPlayer.setPlayWhenReady(true); - savePlaybackState(); - } - - public void onPause() { - if (DEBUG) { - Log.d(TAG, "onPause() called"); - } - if (audioReactor == null || simpleExoPlayer == null) { - return; - } - - audioReactor.abandonAudioFocus(); - simpleExoPlayer.setPlayWhenReady(false); - savePlaybackState(); - } - - public void onPlayPause() { - if (DEBUG) { - Log.d(TAG, "onPlayPause() called"); - } - - if (isPlaying()) { - onPause(); - } else { - onPlay(); - } - } - - public void onFastRewind() { - if (DEBUG) { - Log.d(TAG, "onFastRewind() called"); - } - seekBy(-getSeekDuration()); - triggerProgressUpdate(); - } - - public void onFastForward() { - if (DEBUG) { - Log.d(TAG, "onFastForward() called"); - } - seekBy(getSeekDuration()); - triggerProgressUpdate(); - } - - private int getSeekDuration() { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - final String key = context.getString(R.string.seek_duration_key); - final String value = prefs - .getString(key, context.getString(R.string.seek_duration_default_value)); - return Integer.parseInt(value); - } - - public void onPlayPrevious() { - if (simpleExoPlayer == null || playQueue == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "onPlayPrevious() called"); - } - - /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT_MILLIS milliseconds, - * restart current track. Also restart the track if the current track - * is the first in a queue.*/ - if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT_MILLIS - || playQueue.getIndex() == 0) { - seekToDefault(); - playQueue.offsetIndex(0); - } else { - savePlaybackState(); - playQueue.offsetIndex(-1); - } - } - - public void onPlayNext() { - if (playQueue == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "onPlayNext() called"); - } - - savePlaybackState(); - playQueue.offsetIndex(+1); - } - - public void onSelected(final PlayQueueItem item) { - if (playQueue == null || simpleExoPlayer == null) { - return; - } - - final int index = playQueue.indexOf(item); - if (index == -1) { - return; - } - - if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { - seekToDefault(); - } else { - savePlaybackState(); - } - playQueue.setIndex(index); - } - - public void seekTo(final long positionMillis) { - if (DEBUG) { - Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); - } - if (simpleExoPlayer != null) { - // prevent invalid positions when fast-forwarding/-rewinding - long normalizedPositionMillis = positionMillis; - if (normalizedPositionMillis < 0) { - normalizedPositionMillis = 0; - } else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) { - normalizedPositionMillis = simpleExoPlayer.getDuration(); - } - - simpleExoPlayer.seekTo(normalizedPositionMillis); - } - } - - public void seekBy(final long offsetMillis) { - if (DEBUG) { - Log.d(TAG, "seekBy() called with: offsetMillis = [" + offsetMillis + "]"); - } - seekTo(simpleExoPlayer.getCurrentPosition() + offsetMillis); - } - - public boolean isCurrentWindowValid() { - return simpleExoPlayer != null && simpleExoPlayer.getDuration() >= 0 - && simpleExoPlayer.getCurrentPosition() >= 0; - } - - public void seekToDefault() { - if (simpleExoPlayer != null) { - simpleExoPlayer.seekToDefaultPosition(); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private void registerView() { - if (currentMetadata == null) { - return; - } - final StreamInfo currentInfo = currentMetadata.getMetadata(); - final Disposable viewRegister = recordManager.onViewed(currentInfo).onErrorComplete() - .subscribe( - ignored -> { /* successful */ }, - error -> Log.e(TAG, "Player onViewed() failure: ", error) - ); - databaseUpdateReactor.add(viewRegister); - } - - protected void reload() { - if (playbackManager != null) { - playbackManager.dispose(); - } - - if (playQueue != null) { - playbackManager = new MediaSourceManager(this, playQueue); - } - } - - private void savePlaybackState(final StreamInfo info, final long progress) { - if (info == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "savePlaybackState() called"); - } - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { - final Disposable stateSaver = recordManager.saveStreamState(info, progress) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError((e) -> { - if (DEBUG) { - e.printStackTrace(); - } - }) - .onErrorComplete() - .subscribe(); - databaseUpdateReactor.add(stateSaver); - } - } - - private void resetPlaybackState(final PlayQueueItem queueItem) { - if (queueItem == null) { - return; - } - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { - final Disposable stateSaver = queueItem.getStream() - .flatMapCompletable(info -> recordManager.saveStreamState(info, 0)) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError((e) -> { - if (DEBUG) { - e.printStackTrace(); - } - }) - .onErrorComplete() - .subscribe(); - databaseUpdateReactor.add(stateSaver); - } - } - - public void resetPlaybackState(final StreamInfo info) { - savePlaybackState(info, 0); - } - - public void savePlaybackState() { - if (simpleExoPlayer == null || currentMetadata == null) { - return; - } - final StreamInfo currentInfo = currentMetadata.getMetadata(); - if (playQueue != null) { - // Save current position. It will help to restore this position once a user - // wants to play prev or next stream from the queue - playQueue.setRecovery(playQueue.getIndex(), simpleExoPlayer.getContentPosition()); - } - savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); - } - - private void maybeUpdateCurrentMetadata() { - if (simpleExoPlayer == null) { - return; - } - - final MediaSourceTag metadata; - try { - metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag(); - } catch (IndexOutOfBoundsException | ClassCastException error) { - if (DEBUG) { - Log.d(TAG, "Could not update metadata: " + error.getMessage()); - error.printStackTrace(); - } - return; - } - - if (metadata == null) { - return; - } - maybeAutoQueueNextStream(metadata); - - if (currentMetadata == metadata) { - return; - } - currentMetadata = metadata; - onMetadataChanged(metadata); - } - - private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) { - if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1 - || getRepeatMode() != Player.REPEAT_MODE_OFF - || !PlayerHelper.isAutoQueueEnabled(context)) { - return; - } - // auto queue when starting playback on the last item when not repeating - final PlayQueue autoQueue = PlayerHelper.autoQueueOf(metadata.getMetadata(), - playQueue.getStreams()); - if (autoQueue != null) { - playQueue.append(autoQueue.getStreams()); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Getters and Setters - //////////////////////////////////////////////////////////////////////////*/ - - public SimpleExoPlayer getPlayer() { - return simpleExoPlayer; - } - - public AudioReactor getAudioReactor() { - return audioReactor; - } - - public int getCurrentState() { - return currentState; - } - - @Nullable - public MediaSourceTag getCurrentMetadata() { - return currentMetadata; - } - - @NonNull - public LoadController getLoadController() { - return (LoadController) loadControl; - } - - @NonNull - public String getVideoUrl() { - return currentMetadata == null - ? context.getString(R.string.unknown_content) - : currentMetadata.getMetadata().getUrl(); - } - - @NonNull - public String getVideoTitle() { - return currentMetadata == null - ? context.getString(R.string.unknown_content) - : currentMetadata.getMetadata().getName(); - } - - @NonNull - public String getUploaderName() { - return currentMetadata == null - ? context.getString(R.string.unknown_content) - : currentMetadata.getMetadata().getUploaderName(); - } - - @Nullable - public Bitmap getThumbnail() { - return currentThumbnail == null - ? BitmapFactory.decodeResource(context.getResources(), R.drawable.dummy_thumbnail) - : currentThumbnail; - } - - /** - * Checks if the current playback is a livestream AND is playing at or beyond the live edge. - * - * @return whether the livestream is playing at or beyond the edge - */ - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean isLiveEdge() { - if (simpleExoPlayer == null || !isLive()) { - return false; - } - - final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline(); - final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); - if (currentTimeline.isEmpty() || currentWindowIndex < 0 - || currentWindowIndex >= currentTimeline.getWindowCount()) { - return false; - } - - final Timeline.Window timelineWindow = new Timeline.Window(); - currentTimeline.getWindow(currentWindowIndex, timelineWindow); - return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition(); - } - - public boolean isLive() { - if (simpleExoPlayer == null) { - return false; - } - try { - return simpleExoPlayer.isCurrentWindowDynamic(); - } catch (@NonNull final IndexOutOfBoundsException e) { - // Why would this even happen =( - // But lets log it anyway. Save is save - if (DEBUG) { - Log.d(TAG, "Could not update metadata: " + e.getMessage()); - e.printStackTrace(); - } - return false; - } - } - - public boolean isPlaying() { - return simpleExoPlayer != null && simpleExoPlayer.isPlaying(); - } - - public boolean isLoading() { - return simpleExoPlayer != null && simpleExoPlayer.isLoading(); - } - - @Player.RepeatMode - public int getRepeatMode() { - return simpleExoPlayer == null - ? Player.REPEAT_MODE_OFF - : simpleExoPlayer.getRepeatMode(); - } - - public void setRepeatMode(@Player.RepeatMode final int repeatMode) { - if (simpleExoPlayer != null) { - simpleExoPlayer.setRepeatMode(repeatMode); - } - } - - public float getPlaybackSpeed() { - return getPlaybackParameters().speed; - } - - public void setPlaybackSpeed(final float speed) { - setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence()); - } - - public float getPlaybackPitch() { - return getPlaybackParameters().pitch; - } - - public boolean getPlaybackSkipSilence() { - return getPlaybackParameters().skipSilence; - } - - public PlaybackParameters getPlaybackParameters() { - if (simpleExoPlayer == null) { - return PlaybackParameters.DEFAULT; - } - return simpleExoPlayer.getPlaybackParameters(); - } - - /** - * Sets the playback parameters of the player, and also saves them to shared preferences. - * Speed and pitch are rounded up to 2 decimal places before being used or saved. - * - * @param speed the playback speed, will be rounded to up to 2 decimal places - * @param pitch the playback pitch, will be rounded to up to 2 decimal places - * @param skipSilence skip silence during playback - */ - public void setPlaybackParameters(final float speed, final float pitch, - final boolean skipSilence) { - final float roundedSpeed = Math.round(speed * 100.0f) / 100.0f; - final float roundedPitch = Math.round(pitch * 100.0f) / 100.0f; - - savePlaybackParametersToPreferences(roundedSpeed, roundedPitch, skipSilence); - simpleExoPlayer.setPlaybackParameters( - new PlaybackParameters(roundedSpeed, roundedPitch, skipSilence)); - } - - private void savePlaybackParametersToPreferences(final float speed, final float pitch, - final boolean skipSilence) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit() - .putFloat(context.getString(R.string.playback_speed_key), speed) - .putFloat(context.getString(R.string.playback_pitch_key), pitch) - .putBoolean(context.getString(R.string.playback_skip_silence_key), skipSilence) - .apply(); - } - - public PlayQueue getPlayQueue() { - return playQueue; - } - - public PlayQueueAdapter getPlayQueueAdapter() { - return playQueueAdapter; - } - - public boolean isPrepared() { - return isPrepared; - } - - public boolean isProgressLoopRunning() { - return progressUpdateReactor.get() != null; - } - - public void setRecovery() { - if (playQueue == null || simpleExoPlayer == null) { - return; - } - - final int queuePos = playQueue.getIndex(); - final long windowPos = simpleExoPlayer.getCurrentPosition(); - - if (windowPos > 0 && windowPos <= simpleExoPlayer.getDuration()) { - setRecovery(queuePos, windowPos); - } - } - - public void setRecovery(final int queuePos, final long windowPos) { - if (playQueue.size() <= queuePos) { - return; - } - - if (DEBUG) { - Log.d(TAG, "Setting recovery, queue: " + queuePos + ", pos: " + windowPos); - } - playQueue.setRecovery(queuePos, windowPos); - } - - public boolean gotDestroyed() { - return simpleExoPlayer == null; - } - - private boolean isPlaybackResumeEnabled() { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) - && prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true); - } -} 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 fc9f110e6..c4099e67e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java @@ -48,9 +48,9 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; */ public final class MainPlayer extends Service { private static final String TAG = "MainPlayer"; - private static final boolean DEBUG = BasePlayer.DEBUG; + private static final boolean DEBUG = Player.DEBUG; - private VideoPlayerImpl playerImpl; + private Player player; private WindowManager windowManager; private final IBinder mBinder = new MainPlayer.LocalBinder(); @@ -69,8 +69,6 @@ public final class MainPlayer extends Service { = App.PACKAGE_NAME + ".player.MainPlayer.CLOSE"; static final String ACTION_PLAY_PAUSE = App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE"; - static final String ACTION_OPEN_CONTROLS - = App.PACKAGE_NAME + ".player.MainPlayer.OPEN_CONTROLS"; static final String ACTION_REPEAT = App.PACKAGE_NAME + ".player.MainPlayer.REPEAT"; static final String ACTION_PLAY_NEXT @@ -105,11 +103,10 @@ public final class MainPlayer extends Service { private void createView() { final PlayerBinding binding = PlayerBinding.inflate(LayoutInflater.from(this)); - playerImpl = new VideoPlayerImpl(this); - playerImpl.setup(binding); - playerImpl.shouldUpdateOnProgress = true; + player = new Player(this); + player.setupFromView(binding); - NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this); + NotificationUtil.getInstance().createNotificationAndStartForeground(player, this); } @Override @@ -119,19 +116,19 @@ public final class MainPlayer extends Service { + "], flags = [" + flags + "], startId = [" + startId + "]"); } if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) - && playerImpl.playQueue == null) { + && player.getPlayQueue() == null) { // Player is not working, no need to process media button's action return START_NOT_STICKY; } if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) - || intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) != null) { - NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this); + || intent.getStringExtra(Player.PLAY_QUEUE_KEY) != null) { + NotificationUtil.getInstance().createNotificationAndStartForeground(player, this); } - playerImpl.handleIntent(intent); - if (playerImpl.mediaSessionManager != null) { - playerImpl.mediaSessionManager.handleMediaButtonIntent(intent); + player.handleIntent(intent); + if (player.getMediaSessionManager() != null) { + player.getMediaSessionManager().handleMediaButtonIntent(intent); } return START_NOT_STICKY; } @@ -141,20 +138,20 @@ public final class MainPlayer extends Service { Log.d(TAG, "stop() called"); } - if (playerImpl.getPlayer() != null) { - playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady(); + if (!player.exoPlayerIsNull()) { + player.saveWasPlaying(); // Releases wifi & cpu, disables keepScreenOn, etc. if (!autoplayEnabled) { - playerImpl.onPause(); + player.pause(); } // We can't just pause the player here because it will make transition // from one stream to a new stream not smooth - playerImpl.getPlayer().stop(false); - playerImpl.setRecovery(); + 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) - playerImpl.hideControls(0, 0); - playerImpl.onQueueClosed(); + player.hideControls(0, 0); + player.closeQueue(); // 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. @@ -168,7 +165,7 @@ public final class MainPlayer extends Service { @Override public void onTaskRemoved(final Intent rootIntent) { super.onTaskRemoved(rootIntent); - if (!playerImpl.videoPlayerSelected()) { + if (!player.videoPlayerSelected()) { return; } onDestroy(); @@ -181,7 +178,23 @@ public final class MainPlayer extends Service { if (DEBUG) { Log.d(TAG, "destroy() called"); } - onClose(); + + if (player != null) { + // Exit from fullscreen when user closes the player via notification + if (player.isFullscreen()) { + player.toggleFullscreen(); + } + removeViewFromParent(); + + player.saveStreamProgressState(); + player.setRecovery(); + player.stopActivityBinding(); + player.removePopupFromView(); + player.destroy(); + } + + NotificationUtil.getInstance().cancelNotificationAndStopForeground(this); + stopSelf(); } @Override @@ -194,32 +207,6 @@ public final class MainPlayer extends Service { return mBinder; } - /*////////////////////////////////////////////////////////////////////////// - // Actions - //////////////////////////////////////////////////////////////////////////*/ - private void onClose() { - if (DEBUG) { - Log.d(TAG, "onClose() called"); - } - - if (playerImpl != null) { - // Exit from fullscreen when user closes the player via notification - if (playerImpl.isFullscreen()) { - playerImpl.toggleFullscreen(); - } - removeViewFromParent(); - - playerImpl.setRecovery(); - playerImpl.savePlaybackState(); - playerImpl.stopActivityBinding(); - playerImpl.removePopupFromView(); - playerImpl.destroy(); - } - - NotificationUtil.getInstance().cancelNotificationAndStopForeground(this); - stopSelf(); - } - /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ @@ -227,25 +214,25 @@ 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 = (playerImpl != null - && playerImpl.getParentActivity() != null - ? playerImpl.getParentActivity().getResources() + final DisplayMetrics metrics = (player != null + && player.getParentActivity() != null + ? player.getParentActivity().getResources() : getResources()).getDisplayMetrics(); return metrics.heightPixels < metrics.widthPixels; } @Nullable public View getView() { - if (playerImpl == null) { + if (player == null) { return null; } - return playerImpl.getRootView(); + return player.getRootView(); } public void removeViewFromParent() { if (getView() != null && getView().getParent() != null) { - if (playerImpl.getParentActivity() != null) { + if (player.getParentActivity() != null) { // This means view was added to fragment final ViewGroup parent = (ViewGroup) getView().getParent(); parent.removeView(getView()); @@ -263,8 +250,8 @@ public final class MainPlayer extends Service { return MainPlayer.this; } - public VideoPlayerImpl getPlayer() { - return MainPlayer.this.playerImpl; + public Player getPlayer() { + return MainPlayer.this.player; } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java index c1c2e4eba..43c1b4405 100644 --- a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java +++ b/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java @@ -43,7 +43,7 @@ import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; */ public final class NotificationUtil { private static final String TAG = NotificationUtil.class.getSimpleName(); - private static final boolean DEBUG = BasePlayer.DEBUG; + private static final boolean DEBUG = Player.DEBUG; private static final int NOTIFICATION_ID = 123789; @Nullable private static NotificationUtil instance = null; @@ -76,7 +76,7 @@ public final class NotificationUtil { * @param forceRecreate whether to force the recreation of the notification even if it already * exists */ - synchronized void createNotificationIfNeededAndUpdate(final VideoPlayerImpl player, + synchronized void createNotificationIfNeededAndUpdate(final Player player, final boolean forceRecreate) { if (forceRecreate || notificationBuilder == null) { notificationBuilder = createNotification(player); @@ -85,14 +85,14 @@ public final class NotificationUtil { notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); } - private synchronized NotificationCompat.Builder createNotification( - final VideoPlayerImpl player) { + private synchronized NotificationCompat.Builder createNotification(final Player player) { if (DEBUG) { Log.d(TAG, "createNotification()"); } - notificationManager = NotificationManagerCompat.from(player.context); - final NotificationCompat.Builder builder = new NotificationCompat.Builder(player.context, - player.context.getString(R.string.notification_channel_id)); + notificationManager = NotificationManagerCompat.from(player.getContext()); + final NotificationCompat.Builder builder = + new NotificationCompat.Builder(player.getContext(), + player.getContext().getString(R.string.notification_channel_id)); initializeNotificationSlots(player); @@ -107,25 +107,25 @@ public final class NotificationUtil { // build the compact slot indices array (need code to convert from Integer... because Java) final List compactSlotList = NotificationConstants.getCompactSlotsFromPreferences( - player.context, player.sharedPreferences, nonNothingSlotCount); + player.getContext(), player.getPrefs(), nonNothingSlotCount); final int[] compactSlots = new int[compactSlotList.size()]; for (int i = 0; i < compactSlotList.size(); i++) { compactSlots[i] = compactSlotList.get(i); } builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle() - .setMediaSession(player.mediaSessionManager.getSessionToken()) + .setMediaSession(player.getMediaSessionManager().getSessionToken()) .setShowActionsInCompactView(compactSlots)) .setPriority(NotificationCompat.PRIORITY_HIGH) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCategory(NotificationCompat.CATEGORY_TRANSPORT) .setShowWhen(false) .setSmallIcon(R.drawable.ic_newpipe_triangle_white) - .setColor(ContextCompat.getColor(player.context, R.color.dark_background_color)) - .setColorized(player.sharedPreferences.getBoolean( - player.context.getString(R.string.notification_colorize_key), - true)) - .setDeleteIntent(PendingIntent.getBroadcast(player.context, NOTIFICATION_ID, + .setColor(ContextCompat.getColor(player.getContext(), + R.color.dark_background_color)) + .setColorized(player.getPrefs().getBoolean( + player.getContext().getString(R.string.notification_colorize_key), true)) + .setDeleteIntent(PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID, new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT)); return builder; @@ -135,20 +135,20 @@ public final class NotificationUtil { * Updates the notification builder and the button icons depending on the playback state. * @param player the player currently open, to take data from */ - private synchronized void updateNotification(final VideoPlayerImpl player) { + private synchronized void updateNotification(final Player player) { if (DEBUG) { Log.d(TAG, "updateNotification()"); } // also update content intent, in case the user switched players - notificationBuilder.setContentIntent(PendingIntent.getActivity(player.context, + notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(), NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT)); notificationBuilder.setContentTitle(player.getVideoTitle()); notificationBuilder.setContentText(player.getUploaderName()); notificationBuilder.setTicker(player.getVideoTitle()); updateActions(notificationBuilder, player); - final boolean showThumbnail = player.sharedPreferences.getBoolean( - player.context.getString(R.string.show_thumbnail_key), true); + final boolean showThumbnail = player.getPrefs().getBoolean( + player.getContext().getString(R.string.show_thumbnail_key), true); if (showThumbnail) { setLargeIcon(notificationBuilder, player); } @@ -174,7 +174,7 @@ public final class NotificationUtil { } - void createNotificationAndStartForeground(final VideoPlayerImpl player, final Service service) { + void createNotificationAndStartForeground(final Player player, final Service service) { if (notificationBuilder == null) { notificationBuilder = createNotification(player); } @@ -203,17 +203,16 @@ public final class NotificationUtil { // ACTIONS ///////////////////////////////////////////////////// - private void initializeNotificationSlots(final VideoPlayerImpl player) { + private void initializeNotificationSlots(final Player player) { for (int i = 0; i < 5; ++i) { - notificationSlots[i] = player.sharedPreferences.getInt( - player.context.getString(NotificationConstants.SLOT_PREF_KEYS[i]), + notificationSlots[i] = player.getPrefs().getInt( + player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]), NotificationConstants.SLOT_DEFAULTS[i]); } } @SuppressLint("RestrictedApi") - private void updateActions(final NotificationCompat.Builder builder, - final VideoPlayerImpl player) { + private void updateActions(final NotificationCompat.Builder builder, final Player player) { builder.mActions.clear(); for (int i = 0; i < 5; ++i) { addAction(builder, player, notificationSlots[i]); @@ -221,7 +220,7 @@ public final class NotificationUtil { } private void addAction(final NotificationCompat.Builder builder, - final VideoPlayerImpl player, + final Player player, @NotificationConstants.Action final int slot) { final NotificationCompat.Action action = getAction(player, slot); if (action != null) { @@ -231,7 +230,7 @@ public final class NotificationUtil { @Nullable private NotificationCompat.Action getAction( - final VideoPlayerImpl player, + final Player player, @NotificationConstants.Action final int selectedAction) { final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction]; switch (selectedAction) { @@ -252,7 +251,7 @@ public final class NotificationUtil { R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD); case NotificationConstants.SMART_REWIND_PREVIOUS: - if (player.playQueue != null && player.playQueue.size() > 1) { + if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) { return getAction(player, R.drawable.exo_notification_previous, R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS); } else { @@ -261,7 +260,7 @@ public final class NotificationUtil { } case NotificationConstants.SMART_FORWARD_NEXT: - if (player.playQueue != null && player.playQueue.size() > 1) { + if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) { return getAction(player, R.drawable.exo_notification_next, R.string.exo_controls_next_description, ACTION_PLAY_NEXT); } else { @@ -270,23 +269,23 @@ public final class NotificationUtil { } case NotificationConstants.PLAY_PAUSE_BUFFERING: - if (player.getCurrentState() == BasePlayer.STATE_PREFLIGHT - || player.getCurrentState() == BasePlayer.STATE_BLOCKED - || player.getCurrentState() == BasePlayer.STATE_BUFFERING) { + if (player.getCurrentState() == Player.STATE_PREFLIGHT + || player.getCurrentState() == Player.STATE_BLOCKED + || player.getCurrentState() == Player.STATE_BUFFERING) { // null intent -> show hourglass icon that does nothing when clicked return new NotificationCompat.Action(R.drawable.ic_hourglass_top_white_24dp_png, - player.context.getString(R.string.notification_action_buffering), + player.getContext().getString(R.string.notification_action_buffering), null); } case NotificationConstants.PLAY_PAUSE: - if (player.getCurrentState() == BasePlayer.STATE_COMPLETED) { + if (player.getCurrentState() == Player.STATE_COMPLETED) { return getAction(player, R.drawable.ic_replay_white_24dp_png, R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); } else if (player.isPlaying() - || player.getCurrentState() == BasePlayer.STATE_PREFLIGHT - || player.getCurrentState() == BasePlayer.STATE_BLOCKED - || player.getCurrentState() == BasePlayer.STATE_BUFFERING) { + || player.getCurrentState() == Player.STATE_PREFLIGHT + || player.getCurrentState() == Player.STATE_BLOCKED + || player.getCurrentState() == Player.STATE_BUFFERING) { return getAction(player, R.drawable.exo_notification_pause, R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); } else { @@ -307,7 +306,7 @@ public final class NotificationUtil { } case NotificationConstants.SHUFFLE: - if (player.playQueue != null && player.playQueue.isShuffled()) { + if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) { return getAction(player, R.drawable.exo_controls_shuffle_on, R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE); } else { @@ -326,23 +325,23 @@ public final class NotificationUtil { } } - private NotificationCompat.Action getAction(final VideoPlayerImpl player, + private NotificationCompat.Action getAction(final Player player, @DrawableRes final int drawable, @StringRes final int title, final String intentAction) { - return new NotificationCompat.Action(drawable, player.context.getString(title), - PendingIntent.getBroadcast(player.context, NOTIFICATION_ID, + return new NotificationCompat.Action(drawable, player.getContext().getString(title), + PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID, new Intent(intentAction), FLAG_UPDATE_CURRENT)); } - private Intent getIntentForNotification(final VideoPlayerImpl player) { + private Intent getIntentForNotification(final Player player) { if (player.audioPlayerSelected() || player.popupPlayerSelected()) { // Means we play in popup or audio only. Let's show the play queue - return NavigationHelper.getPlayQueueActivityIntent(player.context); + return NavigationHelper.getPlayQueueActivityIntent(player.getContext()); } else { // We are playing in fragment. Don't open another activity just show fragment. That's it final Intent intent = NavigationHelper.getPlayerIntent( - player.context, MainActivity.class, null, true); + player.getContext(), MainActivity.class, null, true); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setAction(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_LAUNCHER); @@ -355,10 +354,9 @@ public final class NotificationUtil { // BITMAP ///////////////////////////////////////////////////// - private void setLargeIcon(final NotificationCompat.Builder builder, - final VideoPlayerImpl player) { - final boolean scaleImageToSquareAspectRatio = player.sharedPreferences.getBoolean( - player.context.getString(R.string.scale_to_square_image_in_notifications_key), + private void setLargeIcon(final NotificationCompat.Builder builder, final Player player) { + final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean( + player.getContext().getString(R.string.scale_to_square_image_in_notifications_key), false); if (scaleImageToSquareAspectRatio) { builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail())); diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java new file mode 100644 index 000000000..490a0a693 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -0,0 +1,3973 @@ +package org.schabi.newpipe.player; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.animation.AnticipateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.PopupMenu; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.content.ContextCompat; +import androidx.core.view.DisplayCutoutCompat; +import androidx.core.view.ViewCompat; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.BehindLiveWindowException; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.text.CaptionStyleCompat; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.SubtitleView; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.video.VideoListener; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; + +import org.schabi.newpipe.DownloaderImpl; +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.PlayerBinding; +import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding; +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; +import org.schabi.newpipe.fragments.detail.VideoDetailFragment; +import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.player.MainPlayer.PlayerType; +import org.schabi.newpipe.player.event.PlayerEventListener; +import org.schabi.newpipe.player.event.PlayerGestureListener; +import org.schabi.newpipe.player.event.PlayerServiceEventListener; +import org.schabi.newpipe.player.helper.AudioReactor; +import org.schabi.newpipe.player.helper.LoadController; +import org.schabi.newpipe.player.helper.MediaSessionManager; +import org.schabi.newpipe.player.helper.PlaybackParameterDialog; +import org.schabi.newpipe.player.helper.PlayerDataSource; +import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.playback.CustomTrackSelector; +import org.schabi.newpipe.player.playback.MediaSourceManager; +import org.schabi.newpipe.player.playback.PlaybackListener; +import org.schabi.newpipe.player.playback.PlayerMediaSession; +import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; +import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; +import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; +import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; +import org.schabi.newpipe.player.resolver.MediaSourceTag; +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; +import org.schabi.newpipe.util.AnimationUtils; +import org.schabi.newpipe.util.DeviceUtils; +import org.schabi.newpipe.util.ImageDisplayConstants; +import org.schabi.newpipe.util.KoreUtil; +import org.schabi.newpipe.util.ListHelper; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.SerializedCache; +import org.schabi.newpipe.util.ShareUtils; +import org.schabi.newpipe.views.ExpandableSurfaceView; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.disposables.SerialDisposable; + +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AD_INSERTION; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK; +import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT; +import static com.google.android.exoplayer2.Player.DiscontinuityReason; +import static com.google.android.exoplayer2.Player.EventListener; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; +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.extractor.ServiceList.YouTube; +import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE; +import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD; +import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND; +import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT; +import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE; +import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS; +import static org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION; +import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; +import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE; +import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP; +import static org.schabi.newpipe.player.helper.PlayerHelper.buildCloseOverlayLayoutParams; +import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; +import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAction; +import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight; +import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; +import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; +import static org.schabi.newpipe.player.helper.PlayerHelper.isPlaybackResumeEnabled; +import static org.schabi.newpipe.player.helper.PlayerHelper.nextRepeatMode; +import static org.schabi.newpipe.player.helper.PlayerHelper.nextResizeModeAndSaveToPrefs; +import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs; +import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlayerTypeFromIntent; +import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePopupLayoutParamsFromPrefs; +import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences; +import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs; +import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; +import static org.schabi.newpipe.util.AnimationUtils.animateRotation; +import static org.schabi.newpipe.util.AnimationUtils.animateView; +import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex; +import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; +import static org.schabi.newpipe.util.Localization.containsCaseInsensitive; + +public final class Player implements + EventListener, + PlaybackListener, + ImageLoadingListener, + VideoListener, + SeekBar.OnSeekBarChangeListener, + View.OnClickListener, + PopupMenu.OnMenuItemClickListener, + PopupMenu.OnDismissListener, + View.OnLongClickListener { + public static final boolean DEBUG = MainActivity.DEBUG; + public static final String TAG = Player.class.getSimpleName(); + + /*////////////////////////////////////////////////////////////////////////// + // States + //////////////////////////////////////////////////////////////////////////*/ + + public static final int STATE_PREFLIGHT = -1; + public static final int STATE_BLOCKED = 123; + public static final int STATE_PLAYING = 124; + public static final int STATE_BUFFERING = 125; + public static final int STATE_PAUSED = 126; + public static final int STATE_PAUSED_SEEK = 127; + public static final int STATE_COMPLETED = 128; + + /*////////////////////////////////////////////////////////////////////////// + // Intent + //////////////////////////////////////////////////////////////////////////*/ + + public static final String REPEAT_MODE = "repeat_mode"; + public static final String PLAYBACK_QUALITY = "playback_quality"; + public static final String PLAY_QUEUE_KEY = "play_queue_key"; + public static final String APPEND_ONLY = "append_only"; + public static final String RESUME_PLAYBACK = "resume_playback"; + public static final String PLAY_WHEN_READY = "play_when_ready"; + public static final String SELECT_ON_APPEND = "select_on_append"; + public static final String PLAYER_TYPE = "player_type"; + public static final String IS_MUTED = "is_muted"; + + /*////////////////////////////////////////////////////////////////////////// + // Time constants + //////////////////////////////////////////////////////////////////////////*/ + + public static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds + public static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500; // 500 millis + public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis + public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds + public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds + + /*////////////////////////////////////////////////////////////////////////// + // Other constants + //////////////////////////////////////////////////////////////////////////*/ + + private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f}; + + private static final int RENDERER_UNAVAILABLE = -1; + + /*////////////////////////////////////////////////////////////////////////// + // Playback + //////////////////////////////////////////////////////////////////////////*/ + + private PlayQueue playQueue; + private PlayQueueAdapter playQueueAdapter; + + @Nullable private MediaSourceManager playQueueManager; + + @Nullable private PlayQueueItem currentItem; + @Nullable private MediaSourceTag currentMetadata; + @Nullable private Bitmap currentThumbnail; + + @Nullable private Toast errorToast; + + /*////////////////////////////////////////////////////////////////////////// + // Player + //////////////////////////////////////////////////////////////////////////*/ + + private SimpleExoPlayer simpleExoPlayer; + private AudioReactor audioReactor; + private MediaSessionManager mediaSessionManager; + + @NonNull private final CustomTrackSelector trackSelector; + @NonNull private final LoadController loadController; + @NonNull private final RenderersFactory renderFactory; + + @NonNull private final VideoPlaybackResolver videoResolver; + @NonNull private final AudioPlaybackResolver audioResolver; + + private final MainPlayer service; //TODO try to remove and replace everything with context + + /*////////////////////////////////////////////////////////////////////////// + // Player states + //////////////////////////////////////////////////////////////////////////*/ + + private PlayerType playerType = PlayerType.VIDEO; + private int currentState = STATE_PREFLIGHT; + + // audio only mode does not mean that player type is background, but that the player was + // minimized to background but will resume automatically to the original player type + private boolean isAudioOnly = false; + private boolean isPrepared = false; + private boolean wasPlaying = false; + private boolean isFullscreen = false; + private boolean isVerticalVideo = false; + private boolean fragmentIsVisible = false; + + private List availableStreams; + private int selectedStreamIndex; + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + private PlayerBinding binding; + + private ValueAnimator controlViewAnimator; + private final Handler controlsVisibilityHandler = new Handler(); + + // fullscreen player + private boolean isQueueVisible = false; + private ItemTouchHelper itemTouchHelper; + + /*////////////////////////////////////////////////////////////////////////// + // Popup menus ("popup" means that they pop up, not that they belong to the popup player) + //////////////////////////////////////////////////////////////////////////*/ + + private static final int POPUP_MENU_ID_QUALITY = 69; + private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79; + private static final int POPUP_MENU_ID_CAPTION = 89; + + private boolean isSomePopupMenuVisible = false; + private PopupMenu qualityPopupMenu; + private PopupMenu playbackSpeedPopupMenu; + private PopupMenu captionPopupMenu; + + /*////////////////////////////////////////////////////////////////////////// + // Popup player + //////////////////////////////////////////////////////////////////////////*/ + + private PlayerPopupCloseOverlayBinding closeOverlayBinding; + + private boolean isPopupClosing = false; + + private float screenWidth; + private float screenHeight; + + /*////////////////////////////////////////////////////////////////////////// + // Popup player window manager + //////////////////////////////////////////////////////////////////////////*/ + + public static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + public static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; + + @Nullable private WindowManager.LayoutParams popupLayoutParams; // null if player is not popup + @Nullable private final WindowManager windowManager; + + /*////////////////////////////////////////////////////////////////////////// + // Gestures + //////////////////////////////////////////////////////////////////////////*/ + + private static final float MAX_GESTURE_LENGTH = 0.75f; + + private int maxGestureLength; // scaled + private GestureDetector gestureDetector; + + /*////////////////////////////////////////////////////////////////////////// + // Listeners and disposables + //////////////////////////////////////////////////////////////////////////*/ + + private BroadcastReceiver broadcastReceiver; + private IntentFilter intentFilter; + private PlayerServiceEventListener fragmentListener; + private PlayerEventListener activityListener; + private ContentObserver settingsContentObserver; + + @NonNull private final SerialDisposable progressUpdateDisposable = new SerialDisposable(); + @NonNull private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable(); + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + @NonNull private final Context context; + @NonNull private final SharedPreferences prefs; + @NonNull private final HistoryRecordManager recordManager; + + + + /*////////////////////////////////////////////////////////////////////////// + // Constructor + //////////////////////////////////////////////////////////////////////////*/ + //region + + public Player(@NonNull final MainPlayer service) { + this.service = service; + context = service; + prefs = PreferenceManager.getDefaultSharedPreferences(context); + recordManager = new HistoryRecordManager(context); + + setupBroadcastReceiver(); + + trackSelector = new CustomTrackSelector(context, PlayerHelper.getQualitySelector()); + final PlayerDataSource dataSource = new PlayerDataSource(context, DownloaderImpl.USER_AGENT, + new DefaultBandwidthMeter.Builder(context).build()); + loadController = new LoadController(); + renderFactory = new DefaultRenderersFactory(context); + + videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver()); + audioResolver = new AudioPlaybackResolver(context, dataSource); + + windowManager = ContextCompat.getSystemService(context, WindowManager.class); + } + + private VideoPlaybackResolver.QualityResolver getQualityResolver() { + return new VideoPlaybackResolver.QualityResolver() { + @Override + public int getDefaultResolutionIndex(final List sortedVideos) { + return videoPlayerSelected() + ? ListHelper.getDefaultResolutionIndex(context, sortedVideos) + : ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); + } + + @Override + public int getOverrideResolutionIndex(final List sortedVideos, + final String playbackQuality) { + return videoPlayerSelected() + ? getResolutionIndex(context, sortedVideos, playbackQuality) + : getPopupResolutionIndex(context, sortedVideos, playbackQuality); + } + }; + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Setup and initialization + //////////////////////////////////////////////////////////////////////////*/ + //region + + public void setupFromView(@NonNull final PlayerBinding playerBinding) { + initViews(playerBinding); + if (exoPlayerIsNull()) { + initPlayer(true); + } + initListeners(); + } + + private void initViews(@NonNull final PlayerBinding playerBinding) { + binding = playerBinding; + setupSubtitleView(); + + binding.resizeTextView + .setText(PlayerHelper.resizeTypeOf(context, binding.surfaceView.getResizeMode())); + + binding.playbackSeekBar.getThumb() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); + binding.playbackSeekBar.getProgressDrawable() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)); + + qualityPopupMenu = new PopupMenu(context, binding.qualityTextView); + playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed); + captionPopupMenu = new PopupMenu(context, binding.captionTextView); + + binding.progressBarLoadingPanel.getIndeterminateDrawable() + .setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY)); + + binding.titleTextView.setSelected(true); + binding.channelTextView.setSelected(true); + + // Prevent hiding of bottom sheet via swipe inside queue + binding.playQueue.setNestedScrollingEnabled(false); + } + + private void initPlayer(final boolean playOnReady) { + if (DEBUG) { + Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]"); + } + + simpleExoPlayer = new SimpleExoPlayer.Builder(context, renderFactory) + .setTrackSelector(trackSelector) + .setLoadControl(loadController) + .build(); + simpleExoPlayer.addListener(this); + simpleExoPlayer.setPlayWhenReady(playOnReady); + simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context)); + simpleExoPlayer.setWakeMode(C.WAKE_MODE_NETWORK); + simpleExoPlayer.setHandleAudioBecomingNoisy(true); + + audioReactor = new AudioReactor(context, simpleExoPlayer); + mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer, + new PlayerMediaSession(this)); + + registerBroadcastReceiver(); + + // Setup video view + simpleExoPlayer.setVideoSurfaceView(binding.surfaceView); + simpleExoPlayer.addVideoListener(this); + + // Setup subtitle view + simpleExoPlayer.addTextOutput(binding.subtitleView); + + // Setup audio session with onboard equalizer + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + trackSelector.setParameters(trackSelector.buildUponParameters() + .setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context))); + } + } + + private void initListeners() { + binding.playbackSeekBar.setOnSeekBarChangeListener(this); + binding.playbackSpeed.setOnClickListener(this); + binding.qualityTextView.setOnClickListener(this); + binding.captionTextView.setOnClickListener(this); + binding.resizeTextView.setOnClickListener(this); + binding.playbackLiveSync.setOnClickListener(this); + + final PlayerGestureListener listener = new PlayerGestureListener(this, service); + gestureDetector = new GestureDetector(context, listener); + binding.getRoot().setOnTouchListener(listener); + + binding.queueButton.setOnClickListener(this); + binding.repeatButton.setOnClickListener(this); + binding.shuffleButton.setOnClickListener(this); + + binding.playPauseButton.setOnClickListener(this); + binding.playPreviousButton.setOnClickListener(this); + binding.playNextButton.setOnClickListener(this); + + binding.moreOptionsButton.setOnClickListener(this); + binding.moreOptionsButton.setOnLongClickListener(this); + binding.share.setOnClickListener(this); + binding.fullScreenButton.setOnClickListener(this); + binding.screenRotationButton.setOnClickListener(this); + binding.playWithKodi.setOnClickListener(this); + binding.openInBrowser.setOnClickListener(this); + binding.playerCloseButton.setOnClickListener(this); + binding.switchMute.setOnClickListener(this); + + settingsContentObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(final boolean selfChange) { + setupScreenRotationButton(); + } + }; + context.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, + settingsContentObserver); + binding.getRoot().addOnLayoutChangeListener(this::onLayoutChange); + + ViewCompat.setOnApplyWindowInsetsListener(binding.playQueuePanel, (view, windowInsets) -> { + final DisplayCutoutCompat cutout = windowInsets.getDisplayCutout(); + if (cutout != null) { + view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), + cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); + } + return windowInsets; + }); + + // PlaybackControlRoot already consumed window insets but we should pass them to + // player_overlays too. Without it they will be off-centered + binding.playbackControlRoot.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + binding.playerOverlays.setPadding( + v.getPaddingLeft(), + v.getPaddingTop(), + v.getPaddingRight(), + v.getPaddingBottom())); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Playback initialization via intent + //////////////////////////////////////////////////////////////////////////*/ + //region + + public void handleIntent(@NonNull final Intent intent) { + // fail fast if no play queue was provided + final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY); + if (queueCache == null) { + return; + } + final PlayQueue newQueue = SerializedCache.getInstance().take(queueCache, PlayQueue.class); + if (newQueue == null) { + return; + } + + final PlayerType oldPlayerType = playerType; + playerType = retrievePlayerTypeFromIntent(intent); + // We need to setup audioOnly before super(), see "sourceOf" + isAudioOnly = audioPlayerSelected(); + + if (intent.hasExtra(PLAYBACK_QUALITY)) { + setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY)); + } + + // Resolve append intents + if (intent.getBooleanExtra(APPEND_ONLY, false) && playQueue != null) { + final int sizeBeforeAppend = playQueue.size(); + playQueue.append(newQueue.getStreams()); + + if ((intent.getBooleanExtra(SELECT_ON_APPEND, false) + || currentState == STATE_COMPLETED) && newQueue.getStreams().size() > 0) { + playQueue.setIndex(sizeBeforeAppend); + } + + return; + } + + final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this); + final float playbackSpeed = savedParameters.speed; + final float playbackPitch = savedParameters.pitch; + final boolean playbackSkipSilence = savedParameters.skipSilence; + + final boolean samePlayQueue = playQueue != null && playQueue.equals(newQueue); + final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); + final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true); + final boolean isMuted = intent.getBooleanExtra(IS_MUTED, isMuted()); + + /* + * There are 3 situations when playback shouldn't be started from scratch (zero timestamp): + * 1. User pressed on a timestamp link and the same video should be rewound to the timestamp + * 2. User changed a player from, for example. main to popup, or from audio to main, etc + * 3. User chose to resume a video based on a saved timestamp from history of played videos + * In those cases time will be saved because re-init of the play queue is a not an instant + * task and requires network calls + * */ + // seek to timestamp if stream is already playing + if (!exoPlayerIsNull() + && newQueue.size() == 1 && newQueue.getItem() != null + && playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null + && newQueue.getItem().getUrl().equals(playQueue.getItem().getUrl()) + && newQueue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { + // Player can have state = IDLE when playback is stopped or failed + // and we should retry() in this case + if (simpleExoPlayer.getPlaybackState() + == com.google.android.exoplayer2.Player.STATE_IDLE) { + simpleExoPlayer.retry(); + } + simpleExoPlayer.seekTo(playQueue.getIndex(), newQueue.getItem().getRecoveryPosition()); + simpleExoPlayer.setPlayWhenReady(playWhenReady); + + } else if (!exoPlayerIsNull() + && samePlayQueue + && playQueue != null + && !playQueue.isDisposed()) { + // Do not re-init the same PlayQueue. Save time + // Player can have state = IDLE when playback is stopped or failed + // and we should retry() in this case + if (simpleExoPlayer.getPlaybackState() + == com.google.android.exoplayer2.Player.STATE_IDLE) { + simpleExoPlayer.retry(); + } + simpleExoPlayer.setPlayWhenReady(playWhenReady); + + } else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) + && isPlaybackResumeEnabled(this) + && !samePlayQueue + && !newQueue.isEmpty() + && newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { + databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem()) + .observeOn(AndroidSchedulers.mainThread()) + // Do not place initPlayback() in doFinally() because + // it restarts playback after destroy() + //.doFinally() + .subscribe( + state -> { + newQueue.setRecovery(newQueue.getIndex(), state.getProgressTime()); + initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, + playbackSkipSilence, playWhenReady, isMuted); + }, + error -> { + if (DEBUG) { + error.printStackTrace(); + } + // In case any error we can start playback without history + initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, + playbackSkipSilence, playWhenReady, isMuted); + }, + () -> { + // Completed but not found in history + initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, + playbackSkipSilence, playWhenReady, isMuted); + } + )); + } else { + // Good to go... + // In a case of equal PlayQueues we can re-init old one but only when it is disposed + initPlayback(samePlayQueue ? playQueue : newQueue, repeatMode, playbackSpeed, + playbackPitch, playbackSkipSilence, playWhenReady, isMuted); + } + + if (oldPlayerType != playerType && playQueue != null) { + // If playerType changes from one to another we should reload the player + // (to disable/enable video stream or to set quality) + setRecovery(); + reloadPlayQueueManager(); + } + + setupElementsVisibility(); + setupElementsSize(); + + if (audioPlayerSelected()) { + service.removeViewFromParent(); + } else if (popupPlayerSelected()) { + binding.getRoot().setVisibility(View.VISIBLE); + initPopup(); + initPopupCloseOverlay(); + binding.playPauseButton.requestFocus(); + } else { + binding.getRoot().setVisibility(View.VISIBLE); + initVideoPlayer(); + closeQueue(); + // Android TV: without it focus will frame the whole player + binding.playPauseButton.requestFocus(); + + if (simpleExoPlayer.getPlayWhenReady()) { + play(); + } else { + pause(); + } + } + NavigationHelper.sendPlayerStartedEvent(context); + } + + private void initPlayback(@NonNull final PlayQueue queue, + @RepeatMode final int repeatMode, + final float playbackSpeed, + final float playbackPitch, + final boolean playbackSkipSilence, + final boolean playOnReady, + final boolean isMuted) { + destroyPlayer(); + initPlayer(playOnReady); + setRepeatMode(repeatMode); + setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence); + + playQueue = queue; + playQueue.init(); + reloadPlayQueueManager(); + + if (playQueueAdapter != null) { + playQueueAdapter.dispose(); + } + playQueueAdapter = new PlayQueueAdapter(context, playQueue); + + simpleExoPlayer.setVolume(isMuted ? 0 : 1); + notifyQueueUpdateToListeners(); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Destroy and recovery + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void destroyPlayer() { + if (DEBUG) { + Log.d(TAG, "destroyPlayer() called"); + } + if (!exoPlayerIsNull()) { + simpleExoPlayer.removeListener(this); + simpleExoPlayer.stop(); + simpleExoPlayer.release(); + } + if (isProgressLoopRunning()) { + stopProgressLoop(); + } + if (playQueue != null) { + playQueue.dispose(); + } + if (audioReactor != null) { + audioReactor.dispose(); + } + if (playQueueManager != null) { + playQueueManager.dispose(); + } + if (mediaSessionManager != null) { + mediaSessionManager.dispose(); + } + + if (playQueueAdapter != null) { + playQueueAdapter.unsetSelectedListener(); + playQueueAdapter.dispose(); + } + } + + public void destroy() { + if (DEBUG) { + Log.d(TAG, "destroy() called"); + } + destroyPlayer(); + unregisterBroadcastReceiver(); + + databaseUpdateDisposable.clear(); + progressUpdateDisposable.set(null); + ImageLoader.getInstance().stop(); + + if (binding != null) { + binding.endScreen.setImageBitmap(null); + } + + context.getContentResolver().unregisterContentObserver(settingsContentObserver); + } + + public void setRecovery() { + if (playQueue == null || exoPlayerIsNull()) { + return; + } + + final int queuePos = playQueue.getIndex(); + final long windowPos = simpleExoPlayer.getCurrentPosition(); + + if (windowPos > 0 && windowPos <= simpleExoPlayer.getDuration()) { + setRecovery(queuePos, windowPos); + } + } + + private void setRecovery(final int queuePos, final long windowPos) { + if (playQueue.size() <= queuePos) { + return; + } + + if (DEBUG) { + Log.d(TAG, "Setting recovery, queue: " + queuePos + ", pos: " + windowPos); + } + playQueue.setRecovery(queuePos, windowPos); + } + + private void reloadPlayQueueManager() { + if (playQueueManager != null) { + playQueueManager.dispose(); + } + + if (playQueue != null) { + playQueueManager = new MediaSourceManager(this, playQueue); + } + } + + @Override // own playback listener + public void onPlaybackShutdown() { + if (DEBUG) { + Log.d(TAG, "onPlaybackShutdown() called"); + } + // destroys the service, which in turn will destroy the player + service.onDestroy(); + } + + public void smoothStopPlayer() { + // Pausing would make transition from one stream to a new stream not smooth, so only stop + simpleExoPlayer.stop(false); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Player type specific setup + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void initVideoPlayer() { + // restore last resize mode + setResizeMode(prefs.getInt(context.getString(R.string.last_resize_mode), + AspectRatioFrameLayout.RESIZE_MODE_FIT)); + binding.getRoot().setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + } + + @SuppressLint("RtlHardcoded") + private void initPopup() { + if (DEBUG) { + Log.d(TAG, "initPopup() called"); + } + + // Popup is already added to windowManager + if (popupHasParent()) { + return; + } + + updateScreenSize(); + + popupLayoutParams = retrievePopupLayoutParamsFromPrefs(this); + binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height); + + checkPopupPositionBounds(); + + binding.loadingPanel.setMinimumWidth(popupLayoutParams.width); + binding.loadingPanel.setMinimumHeight(popupLayoutParams.height); + + service.removeViewFromParent(); + Objects.requireNonNull(windowManager).addView(binding.getRoot(), popupLayoutParams); + + // Popup doesn't have aspectRatio selector, using FIT automatically + setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT); + } + + @SuppressLint("RtlHardcoded") + private void initPopupCloseOverlay() { + if (DEBUG) { + Log.d(TAG, "initPopupCloseOverlay() called"); + } + + // closeOverlayView is already added to windowManager + if (closeOverlayBinding != null) { + return; + } + + closeOverlayBinding = PlayerPopupCloseOverlayBinding.inflate(LayoutInflater.from(context)); + + final WindowManager.LayoutParams closeOverlayLayoutParams = buildCloseOverlayLayoutParams(); + closeOverlayBinding.closeButton.setVisibility(View.GONE); + Objects.requireNonNull(windowManager).addView( + closeOverlayBinding.getRoot(), closeOverlayLayoutParams); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Elements visibility and size: popup and main players have different look + //////////////////////////////////////////////////////////////////////////*/ + //region + + /** + * This method ensures that popup and main players have different look. + * We use one layout for both players and need to decide what to show and what to hide. + * Additional measuring should be done inside {@link #setupElementsSize}. + */ + private void setupElementsVisibility() { + if (popupPlayerSelected()) { + binding.fullScreenButton.setVisibility(View.VISIBLE); + binding.screenRotationButton.setVisibility(View.GONE); + binding.resizeTextView.setVisibility(View.GONE); + binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.GONE); + binding.queueButton.setVisibility(View.GONE); + binding.moreOptionsButton.setVisibility(View.GONE); + binding.topControls.setOrientation(LinearLayout.HORIZONTAL); + binding.primaryControls.getLayoutParams().width + = LinearLayout.LayoutParams.WRAP_CONTENT; + binding.secondaryControls.setAlpha(1.0f); + binding.secondaryControls.setVisibility(View.VISIBLE); + binding.secondaryControls.setTranslationY(0); + binding.share.setVisibility(View.GONE); + binding.playWithKodi.setVisibility(View.GONE); + binding.openInBrowser.setVisibility(View.GONE); + binding.switchMute.setVisibility(View.GONE); + binding.playerCloseButton.setVisibility(View.GONE); + binding.topControls.bringToFront(); + binding.topControls.setClickable(false); + binding.topControls.setFocusable(false); + binding.bottomControls.bringToFront(); + closeQueue(); + } else if (videoPlayerSelected()) { + binding.fullScreenButton.setVisibility(View.GONE); + setupScreenRotationButton(); + binding.resizeTextView.setVisibility(View.VISIBLE); + binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.VISIBLE); + binding.moreOptionsButton.setVisibility(View.VISIBLE); + binding.topControls.setOrientation(LinearLayout.VERTICAL); + binding.primaryControls.getLayoutParams().width + = LinearLayout.LayoutParams.MATCH_PARENT; + binding.secondaryControls.setVisibility(View.INVISIBLE); + binding.moreOptionsButton.setImageDrawable(AppCompatResources.getDrawable(context, + R.drawable.ic_expand_more_white_24dp)); + binding.share.setVisibility(View.VISIBLE); + binding.openInBrowser.setVisibility(View.VISIBLE); + binding.switchMute.setVisibility(View.VISIBLE); + binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE); + // Top controls have a large minHeight which is allows to drag the player + // down in fullscreen mode (just larger area to make easy to locate by finger) + binding.topControls.setClickable(true); + binding.topControls.setFocusable(true); + } + showHideKodiButton(); + + if (isFullscreen) { + binding.titleTextView.setVisibility(View.VISIBLE); + binding.channelTextView.setVisibility(View.VISIBLE); + } else { + binding.titleTextView.setVisibility(View.GONE); + binding.channelTextView.setVisibility(View.GONE); + } + setMuteButton(binding.switchMute, isMuted()); + + animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION, 0); + } + + /** + * Changes padding, size of elements based on player selected right now. + * Popup player has small padding in comparison with the main player + */ + private void setupElementsSize() { + final Resources res = context.getResources(); + final int buttonsMinWidth; + final int playerTopPad; + final int controlsPad; + final int buttonsPad; + + if (popupPlayerSelected()) { + buttonsMinWidth = 0; + playerTopPad = 0; + controlsPad = res.getDimensionPixelSize(R.dimen.player_popup_controls_padding); + buttonsPad = res.getDimensionPixelSize(R.dimen.player_popup_buttons_padding); + } else if (videoPlayerSelected()) { + buttonsMinWidth = res.getDimensionPixelSize(R.dimen.player_main_buttons_min_width); + playerTopPad = res.getDimensionPixelSize(R.dimen.player_main_top_padding); + controlsPad = res.getDimensionPixelSize(R.dimen.player_main_controls_padding); + buttonsPad = res.getDimensionPixelSize(R.dimen.player_main_buttons_padding); + } else { + return; + } + + binding.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0); + binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0); + binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); + binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); + binding.playbackSpeed.setMinimumWidth(buttonsMinWidth); + binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad); + } + + private void showHideKodiButton() { + // show kodi button if it supports the current service and it is enabled in settings + binding.playWithKodi.setVisibility(videoPlayerSelected() + && playQueue != null && playQueue.getItem() != null + && KoreUtil.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId()) + ? View.VISIBLE : View.GONE); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Broadcast receiver + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void setupBroadcastReceiver() { + if (DEBUG) { + Log.d(TAG, "setupBroadcastReceiver() called"); + } + + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context ctx, final Intent intent) { + onBroadcastReceived(intent); + } + }; + intentFilter = new IntentFilter(); + + intentFilter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); + + intentFilter.addAction(ACTION_CLOSE); + intentFilter.addAction(ACTION_PLAY_PAUSE); + intentFilter.addAction(ACTION_PLAY_PREVIOUS); + intentFilter.addAction(ACTION_PLAY_NEXT); + intentFilter.addAction(ACTION_FAST_REWIND); + intentFilter.addAction(ACTION_FAST_FORWARD); + intentFilter.addAction(ACTION_REPEAT); + intentFilter.addAction(ACTION_SHUFFLE); + intentFilter.addAction(ACTION_RECREATE_NOTIFICATION); + + intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED); + intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED); + + intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + intentFilter.addAction(Intent.ACTION_SCREEN_ON); + intentFilter.addAction(Intent.ACTION_SCREEN_OFF); + intentFilter.addAction(Intent.ACTION_HEADSET_PLUG); + } + + private void onBroadcastReceived(final Intent intent) { + if (intent == null || intent.getAction() == null) { + return; + } + + if (DEBUG) { + Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); + } + + switch (intent.getAction()) { + case AudioManager.ACTION_AUDIO_BECOMING_NOISY: + pause(); + break; + case ACTION_CLOSE: + service.onDestroy(); + break; + case ACTION_PLAY_PAUSE: + playPause(); + if (!fragmentIsVisible) { + // Ensure that we have audio-only stream playing when a user + // started to play from notification's play button from outside of the app + onFragmentStopped(); + } + break; + case ACTION_PLAY_PREVIOUS: + playPrevious(); + break; + case ACTION_PLAY_NEXT: + playNext(); + break; + case ACTION_FAST_REWIND: + fastRewind(); + break; + case ACTION_FAST_FORWARD: + fastForward(); + break; + case ACTION_REPEAT: + onRepeatClicked(); + break; + case ACTION_SHUFFLE: + onShuffleClicked(); + break; + case ACTION_RECREATE_NOTIFICATION: + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); + break; + case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED: + fragmentIsVisible = true; + useVideoSource(true); + break; + case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED: + fragmentIsVisible = false; + onFragmentStopped(); + break; + case Intent.ACTION_CONFIGURATION_CHANGED: + assureCorrectAppLanguage(service); + if (DEBUG) { + Log.d(TAG, "onConfigurationChanged() called"); + } + if (popupPlayerSelected()) { + updateScreenSize(); + changePopupSize(popupLayoutParams.width); + checkPopupPositionBounds(); + } + // Close it because when changing orientation from portrait + // (in fullscreen mode) the size of queue layout can be larger than the screen size + closeQueue(); + break; + case Intent.ACTION_SCREEN_ON: + // Interrupt playback only when screen turns on + // and user is watching video in popup player. + // Same actions for video player will be handled in ACTION_VIDEO_FRAGMENT_RESUMED + if (popupPlayerSelected() && (isPlaying() || isLoading())) { + useVideoSource(true); + } + break; + case Intent.ACTION_SCREEN_OFF: + // Interrupt playback only when screen turns off with popup player working + if (popupPlayerSelected() && (isPlaying() || isLoading())) { + useVideoSource(false); + } + break; + case Intent.ACTION_HEADSET_PLUG: //FIXME + /*notificationManager.cancel(NOTIFICATION_ID); + mediaSessionManager.dispose(); + mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/ + break; + } + } + + private void registerBroadcastReceiver() { + // Try to unregister current first + unregisterBroadcastReceiver(); + context.registerReceiver(broadcastReceiver, intentFilter); + } + + private void unregisterBroadcastReceiver() { + try { + context.unregisterReceiver(broadcastReceiver); + } catch (final IllegalArgumentException unregisteredException) { + Log.w(TAG, "Broadcast receiver already unregistered: " + + unregisteredException.getMessage()); + } + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Thumbnail loading + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void initThumbnail(final String url) { + if (DEBUG) { + Log.d(TAG, "Thumbnail - initThumbnail() called"); + } + if (url == null || url.isEmpty()) { + return; + } + ImageLoader.getInstance().resume(); + ImageLoader.getInstance() + .loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, this); + } + + @Override + public void onLoadingStarted(final String imageUri, final View view) { + if (DEBUG) { + Log.d(TAG, "Thumbnail - onLoadingStarted() called on: " + + "imageUri = [" + imageUri + "], view = [" + view + "]"); + } + } + + @Override + public void onLoadingFailed(final String imageUri, final View view, + final FailReason failReason) { + Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]", + failReason.getCause()); + currentThumbnail = null; + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + @Override + public void onLoadingComplete(final String imageUri, final View view, + final Bitmap loadedImage) { + final float width = Math.min( + context.getResources().getDimension(R.dimen.player_notification_thumbnail_width), + loadedImage.getWidth()); + + if (DEBUG) { + Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " + + "imageUri = [" + imageUri + "], view = [" + view + "], " + + "loadedImage = [" + loadedImage + "], " + + loadedImage.getWidth() + "x" + loadedImage.getHeight() + + ", scaled width = " + width); + } + + currentThumbnail = Bitmap.createScaledBitmap(loadedImage, + (int) width, + (int) (loadedImage.getHeight() / (loadedImage.getWidth() / width)), true); + binding.endScreen.setImageBitmap(loadedImage); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + @Override + public void onLoadingCancelled(final String imageUri, final View view) { + if (DEBUG) { + Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " + + "imageUri = [" + imageUri + "], view = [" + view + "]"); + } + currentThumbnail = null; + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Popup player utils + //////////////////////////////////////////////////////////////////////////*/ + //region + + /** + * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary + * that goes from (0, 0) to (screenWidth, screenHeight). + *

+ * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed + * and {@code true} is returned to represent this change. + *

+ */ + public void checkPopupPositionBounds() { + if (DEBUG) { + Log.d(TAG, "checkPopupPositionBounds() called with: " + + "screenWidth = [" + screenWidth + "], " + + "screenHeight = [" + screenHeight + "]"); + } + if (popupLayoutParams == null) { + return; + } + + if (popupLayoutParams.x < 0) { + popupLayoutParams.x = 0; + } else if (popupLayoutParams.x > screenWidth - popupLayoutParams.width) { + popupLayoutParams.x = (int) (screenWidth - popupLayoutParams.width); + } + + if (popupLayoutParams.y < 0) { + popupLayoutParams.y = 0; + } else if (popupLayoutParams.y > screenHeight - popupLayoutParams.height) { + popupLayoutParams.y = (int) (screenHeight - popupLayoutParams.height); + } + } + + public void updateScreenSize() { + if (windowManager != null) { + final DisplayMetrics metrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getMetrics(metrics); + + screenWidth = metrics.widthPixels; + screenHeight = metrics.heightPixels; + if (DEBUG) { + Log.d(TAG, "updateScreenSize() called: screenWidth = [" + + screenWidth + "], screenHeight = [" + screenHeight + "]"); + } + } + } + + /** + * Changes the size of the popup based on the width. + * @param width the new width, height is calculated with + * {@link PlayerHelper#getMinimumVideoHeight(float)} + */ + public void changePopupSize(final int width) { + if (DEBUG) { + Log.d(TAG, "changePopupSize() called with: width = [" + width + "]"); + } + + if (anyPopupViewIsNull()) { + return; + } + + final float minimumWidth = context.getResources().getDimension(R.dimen.popup_minimum_width); + final int actualWidth = (int) (width > screenWidth ? screenWidth + : (width < minimumWidth ? minimumWidth : width)); + final int actualHeight = (int) getMinimumVideoHeight(width); + if (DEBUG) { + Log.d(TAG, "updatePopupSize() updated values:" + + " width = [" + actualWidth + "], height = [" + actualHeight + "]"); + } + + popupLayoutParams.width = actualWidth; + popupLayoutParams.height = actualHeight; + binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height); + Objects.requireNonNull(windowManager) + .updateViewLayout(binding.getRoot(), popupLayoutParams); + } + + private void changePopupWindowFlags(final int flags) { + if (DEBUG) { + Log.d(TAG, "changePopupWindowFlags() called with: flags = [" + flags + "]"); + } + + if (!anyPopupViewIsNull()) { + popupLayoutParams.flags = flags; + Objects.requireNonNull(windowManager) + .updateViewLayout(binding.getRoot(), popupLayoutParams); + } + } + + public void closePopup() { + if (DEBUG) { + Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); + } + if (isPopupClosing) { + return; + } + isPopupClosing = true; + + saveStreamProgressState(); + Objects.requireNonNull(windowManager).removeView(binding.getRoot()); + + animatePopupOverlayAndFinishService(); + } + + public void removePopupFromView() { + if (windowManager != null) { + final boolean isCloseOverlayHasParent = closeOverlayBinding != null + && closeOverlayBinding.closeButton.getParent() != null; + if (popupHasParent()) { + windowManager.removeView(binding.getRoot()); + } + if (isCloseOverlayHasParent) { + windowManager.removeView(closeOverlayBinding.getRoot()); + } + } + } + + private void animatePopupOverlayAndFinishService() { + final int targetTranslationY = + (int) (closeOverlayBinding.closeButton.getRootView().getHeight() + - closeOverlayBinding.closeButton.getY()); + + closeOverlayBinding.closeButton.animate().setListener(null).cancel(); + closeOverlayBinding.closeButton.animate() + .setInterpolator(new AnticipateInterpolator()) + .translationY(targetTranslationY) + .setDuration(400) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(final Animator animation) { + end(); + } + + @Override + public void onAnimationEnd(final Animator animation) { + end(); + } + + private void end() { + Objects.requireNonNull(windowManager) + .removeView(closeOverlayBinding.getRoot()); + closeOverlayBinding = null; + service.onDestroy(); + } + }).start(); + } + + private boolean popupHasParent() { + return binding != null + && binding.getRoot().getLayoutParams() instanceof WindowManager.LayoutParams + && binding.getRoot().getParent() != null; + } + + private boolean anyPopupViewIsNull() { + // TODO understand why checking getParentActivity() != null + return popupLayoutParams == null || windowManager == null + || getParentActivity() != null || binding.getRoot().getParent() == null; + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Playback parameters + //////////////////////////////////////////////////////////////////////////*/ + //region + + public float getPlaybackSpeed() { + return getPlaybackParameters().speed; + } + + private void setPlaybackSpeed(final float speed) { + setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence()); + } + + public float getPlaybackPitch() { + return getPlaybackParameters().pitch; + } + + public boolean getPlaybackSkipSilence() { + return getPlaybackParameters().skipSilence; + } + + public PlaybackParameters getPlaybackParameters() { + if (exoPlayerIsNull()) { + return PlaybackParameters.DEFAULT; + } + return simpleExoPlayer.getPlaybackParameters(); + } + + /** + * Sets the playback parameters of the player, and also saves them to shared preferences. + * Speed and pitch are rounded up to 2 decimal places before being used or saved. + * + * @param speed the playback speed, will be rounded to up to 2 decimal places + * @param pitch the playback pitch, will be rounded to up to 2 decimal places + * @param skipSilence skip silence during playback + */ + public void setPlaybackParameters(final float speed, final float pitch, + final boolean skipSilence) { + final float roundedSpeed = Math.round(speed * 100.0f) / 100.0f; + final float roundedPitch = Math.round(pitch * 100.0f) / 100.0f; + + savePlaybackParametersToPrefs(this, roundedSpeed, roundedPitch, skipSilence); + simpleExoPlayer.setPlaybackParameters( + new PlaybackParameters(roundedSpeed, roundedPitch, skipSilence)); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Progress loop and updates + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void onUpdateProgress(final int currentProgress, + final int duration, + final int bufferPercent) { + if (!isPrepared) { + return; + } + + if (duration != binding.playbackSeekBar.getMax()) { + binding.playbackEndTime.setText(getTimeString(duration)); + binding.playbackSeekBar.setMax(duration); + } + if (currentState != STATE_PAUSED) { + if (currentState != STATE_PAUSED_SEEK) { + binding.playbackSeekBar.setProgress(currentProgress); + } + binding.playbackCurrentTime.setText(getTimeString(currentProgress)); + } + if (simpleExoPlayer.isLoading() || bufferPercent > 90) { + binding.playbackSeekBar.setSecondaryProgress( + (int) (binding.playbackSeekBar.getMax() * ((float) bufferPercent / 100))); + } + if (DEBUG && bufferPercent % 20 == 0) { //Limit log + Log.d(TAG, "notifyProgressUpdateToListeners() called with: " + + "isVisible = " + isControlsVisible() + ", " + + "currentProgress = [" + currentProgress + "], " + + "duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); + } + binding.playbackLiveSync.setClickable(!isLiveEdge()); + + notifyProgressUpdateToListeners(currentProgress, duration, bufferPercent); + + final boolean showThumbnail = prefs.getBoolean( + context.getString(R.string.show_thumbnail_key), true); + // setMetadata only updates the metadata when any of the metadata keys are null + mediaSessionManager.setMetadata(getVideoTitle(), getUploaderName(), + showThumbnail ? getThumbnail() : null, duration); + } + + private void startProgressLoop() { + progressUpdateDisposable.set(getProgressUpdateDisposable()); + } + + private void stopProgressLoop() { + progressUpdateDisposable.set(null); + } + + private boolean isProgressLoopRunning() { + return progressUpdateDisposable.get() != null; + } + + private void triggerProgressUpdate() { + if (exoPlayerIsNull()) { + return; + } + onUpdateProgress( + Math.max((int) simpleExoPlayer.getCurrentPosition(), 0), + (int) simpleExoPlayer.getDuration(), + simpleExoPlayer.getBufferedPercentage() + ); + } + + private Disposable getProgressUpdateDisposable() { + return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS, + AndroidSchedulers.mainThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(ignored -> triggerProgressUpdate(), + error -> Log.e(TAG, "Progress update failure: ", error)); + } + + @Override // seekbar listener + public void onProgressChanged(final SeekBar seekBar, final int progress, + final boolean fromUser) { + if (DEBUG && fromUser) { + Log.d(TAG, "onProgressChanged() called with: " + + "seekBar = [" + seekBar + "], progress = [" + progress + "]"); + } + if (fromUser) { + binding.currentDisplaySeek.setText(getTimeString(progress)); + } + } + + @Override // seekbar listener + public void onStartTrackingTouch(final SeekBar seekBar) { + if (DEBUG) { + Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]"); + } + if (currentState != STATE_PAUSED_SEEK) { + changeState(STATE_PAUSED_SEEK); + } + + saveWasPlaying(); + if (isPlaying()) { + simpleExoPlayer.setPlayWhenReady(false); + } + + showControls(0); + animateView(binding.currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true, + DEFAULT_CONTROLS_DURATION); + } + + @Override // seekbar listener + public void onStopTrackingTouch(final SeekBar seekBar) { + if (DEBUG) { + Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); + } + + seekTo(seekBar.getProgress()); + if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) { + simpleExoPlayer.setPlayWhenReady(true); + } + + binding.playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); + animateView(binding.currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); + + if (currentState == STATE_PAUSED_SEEK) { + changeState(STATE_BUFFERING); + } + if (!isProgressLoopRunning()) { + startProgressLoop(); + } + if (wasPlaying) { + showControlsThenHide(); + } + } + + public void saveWasPlaying() { + this.wasPlaying = simpleExoPlayer.getPlayWhenReady(); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Controls showing / hiding + //////////////////////////////////////////////////////////////////////////*/ + //region + + public boolean isControlsVisible() { + return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE; + } + + /** + * Show a animation, and depending on goneOnEnd, will stay on the screen or be gone. + * + * @param drawableId the drawable that will be used to animate, + * pass -1 to clear any animation that is visible + * @param goneOnEnd will set the animation view to GONE on the end of the animation + */ + public void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) { + if (DEBUG) { + Log.d(TAG, "showAndAnimateControl() called with: " + + "drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]"); + } + if (controlViewAnimator != null && controlViewAnimator.isRunning()) { + if (DEBUG) { + Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning"); + } + controlViewAnimator.end(); + } + + if (drawableId == -1) { + if (binding.controlAnimationView.getVisibility() == View.VISIBLE) { + controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder( + binding.controlAnimationView, + PropertyValuesHolder.ofFloat(View.ALPHA, 1.0f, 0.0f), + PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1.0f), + PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1.0f) + ).setDuration(DEFAULT_CONTROLS_DURATION); + controlViewAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(final Animator animation) { + binding.controlAnimationView.setVisibility(View.GONE); + } + }); + controlViewAnimator.start(); + } + return; + } + + final float scaleFrom = goneOnEnd ? 1f : 1f; + final float scaleTo = goneOnEnd ? 1.8f : 1.4f; + final float alphaFrom = goneOnEnd ? 1f : 0f; + final float alphaTo = goneOnEnd ? 0f : 1f; + + + controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder( + binding.controlAnimationView, + PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo), + PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo), + PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo) + ); + controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500); + controlViewAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(final Animator animation) { + binding.controlAnimationView.setVisibility(goneOnEnd ? View.GONE : View.VISIBLE); + } + }); + + + binding.controlAnimationView.setVisibility(View.VISIBLE); + binding.controlAnimationView.setImageDrawable( + AppCompatResources.getDrawable(context, drawableId)); + controlViewAnimator.start(); + } + + public void showControlsThenHide() { + if (DEBUG) { + Log.d(TAG, "showControlsThenHide() called"); + } + showOrHideButtons(); + showSystemUIPartially(); + + final int hideTime = binding.playbackControlRoot.isInTouchMode() + ? DEFAULT_CONTROLS_HIDE_TIME + : DPAD_CONTROLS_HIDE_TIME; + + showHideShadow(true, DEFAULT_CONTROLS_DURATION); + animateView(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION, 0, + () -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime)); + } + + public void showControls(final long duration) { + if (DEBUG) { + Log.d(TAG, "showControls() called"); + } + showOrHideButtons(); + showSystemUIPartially(); + controlsVisibilityHandler.removeCallbacksAndMessages(null); + showHideShadow(true, duration); + animateView(binding.playbackControlRoot, true, duration); + } + + public void hideControls(final long duration, final long delay) { + if (DEBUG) { + Log.d(TAG, "hideControls() called with: duration = [" + duration + + "], delay = [" + delay + "]"); + } + + showOrHideButtons(); + + controlsVisibilityHandler.removeCallbacksAndMessages(null); + controlsVisibilityHandler.postDelayed(() -> { + showHideShadow(false, duration); + animateView(binding.playbackControlRoot, false, duration, 0, + this::hideSystemUIIfNeeded); + }, delay); + } + + private void showHideShadow(final boolean show, final long duration) { + animateView(binding.playerTopShadow, show, duration, 0, null); + animateView(binding.playerBottomShadow, show, duration, 0, null); + } + + private void showOrHideButtons() { + if (playQueue == null) { + return; + } + + final boolean showPrev = playQueue.getIndex() != 0; + final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size(); + final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected(); + + binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE); + binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f); + binding.playNextButton.setVisibility(showNext ? View.VISIBLE : View.INVISIBLE); + binding.playNextButton.setAlpha(showNext ? 1.0f : 0.0f); + binding.queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE); + binding.queueButton.setAlpha(showQueue ? 1.0f : 0.0f); + } + + private void showSystemUIPartially() { + final AppCompatActivity activity = getParentActivity(); + if (isFullscreen && activity != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.getWindow().setStatusBarColor(Color.TRANSPARENT); + activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); + } + final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + activity.getWindow().getDecorView().setSystemUiVisibility(visibility); + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + } + + private void hideSystemUIIfNeeded() { + if (fragmentListener != null) { + fragmentListener.hideSystemUiIfNeeded(); + } + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Playback states + //////////////////////////////////////////////////////////////////////////*/ + //region + + @Override // exoplayer listener + public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: " + + "playWhenReady = [" + playWhenReady + "], " + + "playbackState = [" + playbackState + "]"); + } + + if (currentState == STATE_PAUSED_SEEK) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onPlayerStateChanged() is currently blocked"); + } + return; + } + + switch (playbackState) { + case com.google.android.exoplayer2.Player.STATE_IDLE: // 1 + isPrepared = false; + break; + case com.google.android.exoplayer2.Player.STATE_BUFFERING: // 2 + if (isPrepared) { + changeState(STATE_BUFFERING); + } + break; + case com.google.android.exoplayer2.Player.STATE_READY: //3 + maybeUpdateCurrentMetadata(); + maybeCorrectSeekPosition(); + if (!isPrepared) { + isPrepared = true; + onPrepared(playWhenReady); + } + changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED); + break; + case com.google.android.exoplayer2.Player.STATE_ENDED: // 4 + changeState(STATE_COMPLETED); + if (currentMetadata != null) { + resetStreamProgressState(currentMetadata.getMetadata()); + } + isPrepared = false; + break; + } + } + + @Override // exoplayer listener + public void onLoadingChanged(final boolean isLoading) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onLoadingChanged() called with: " + + "isLoading = [" + isLoading + "]"); + } + + if (!isLoading && currentState == STATE_PAUSED && isProgressLoopRunning()) { + stopProgressLoop(); + } else if (isLoading && !isProgressLoopRunning()) { + startProgressLoop(); + } + + maybeUpdateCurrentMetadata(); + } + + @Override // own playback listener + public void onPlaybackBlock() { + if (exoPlayerIsNull()) { + return; + } + if (DEBUG) { + Log.d(TAG, "Playback - onPlaybackBlock() called"); + } + + currentItem = null; + currentMetadata = null; + simpleExoPlayer.stop(); + isPrepared = false; + + changeState(STATE_BLOCKED); + } + + @Override // own playback listener + public void onPlaybackUnblock(final MediaSource mediaSource) { + if (DEBUG) { + Log.d(TAG, "Playback - onPlaybackUnblock() called"); + } + + if (exoPlayerIsNull()) { + return; + } + if (currentState == STATE_BLOCKED) { + changeState(STATE_BUFFERING); + } + simpleExoPlayer.prepare(mediaSource); + } + + public void changeState(final int state) { + if (DEBUG) { + Log.d(TAG, "changeState() called with: state = [" + state + "]"); + } + currentState = state; + switch (state) { + case STATE_BLOCKED: + onBlocked(); + break; + case STATE_PLAYING: + onPlaying(); + break; + case STATE_BUFFERING: + onBuffering(); + break; + case STATE_PAUSED: + onPaused(); + break; + case STATE_PAUSED_SEEK: + onPausedSeek(); + break; + case STATE_COMPLETED: + onCompleted(); + break; + } + notifyPlaybackUpdateToListeners(); + } + + private void onPrepared(final boolean playWhenReady) { + if (DEBUG) { + Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); + } + + binding.playbackSeekBar.setMax((int) simpleExoPlayer.getDuration()); + binding.playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration())); + binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed())); + + if (playWhenReady) { + audioReactor.requestAudioFocus(); + } + } + + private void onBlocked() { + if (DEBUG) { + Log.d(TAG, "onBlocked() called"); + } + if (!isProgressLoopRunning()) { + startProgressLoop(); + } + + controlsVisibilityHandler.removeCallbacksAndMessages(null); + animateView(binding.playbackControlRoot, false, DEFAULT_CONTROLS_DURATION); + + binding.playbackSeekBar.setEnabled(false); + binding.playbackSeekBar.getThumb() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); + + binding.loadingPanel.setBackgroundColor(Color.BLACK); + animateView(binding.loadingPanel, true, 0); + animateView(binding.surfaceForeground, true, 100); + + binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); + animatePlayButtons(false, 100); + binding.getRoot().setKeepScreenOn(false); + + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + private void onPlaying() { + if (DEBUG) { + Log.d(TAG, "onPlaying() called"); + } + if (!isProgressLoopRunning()) { + startProgressLoop(); + } + + updateStreamRelatedViews(); + + showAndAnimateControl(-1, true); + + binding.playbackSeekBar.setEnabled(true); + binding.playbackSeekBar.getThumb() + .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN)); + + binding.loadingPanel.setVisibility(View.GONE); + + animateView(binding.currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); + + animateView(binding.playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, + () -> { + binding.playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); + animatePlayButtons(true, 200); + if (!isQueueVisible) { + binding.playPauseButton.requestFocus(); + } + }); + + changePopupWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); + checkLandscape(); + binding.getRoot().setKeepScreenOn(true); + + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + private void onBuffering() { + if (DEBUG) { + Log.d(TAG, "onBuffering() called"); + } + binding.loadingPanel.setBackgroundColor(Color.TRANSPARENT); + + binding.getRoot().setKeepScreenOn(true); + + if (NotificationUtil.getInstance().shouldUpdateBufferingSlot()) { + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + } + + private void onPaused() { + if (DEBUG) { + Log.d(TAG, "onPaused() called"); + } + + if (isProgressLoopRunning()) { + stopProgressLoop(); + } + + showControls(400); + binding.loadingPanel.setVisibility(View.GONE); + + animateView(binding.playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, + () -> { + binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); + animatePlayButtons(true, 200); + if (!isQueueVisible) { + binding.playPauseButton.requestFocus(); + } + }); + + changePopupWindowFlags(IDLE_WINDOW_FLAGS); + + // Remove running notification when user does not want minimization to background or popup + if (PlayerHelper.isMinimizeOnExitDisabled(context) && videoPlayerSelected()) { + NotificationUtil.getInstance().cancelNotificationAndStopForeground(service); + } else { + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + binding.getRoot().setKeepScreenOn(false); + } + + private void onPausedSeek() { + if (DEBUG) { + Log.d(TAG, "onPausedSeek() called"); + } + showAndAnimateControl(-1, true); + + animatePlayButtons(false, 100); + binding.getRoot().setKeepScreenOn(true); + + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + private void onCompleted() { + if (DEBUG) { + Log.d(TAG, "onCompleted() called"); + } + + animateView(binding.playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, + () -> { + binding.playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); + animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); + }); + + binding.getRoot().setKeepScreenOn(false); + changePopupWindowFlags(IDLE_WINDOW_FLAGS); + + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + if (isFullscreen) { + toggleFullscreen(); + } + + if (playQueue.getIndex() < playQueue.size() - 1) { + playQueue.offsetIndex(+1); + } + if (isProgressLoopRunning()) { + stopProgressLoop(); + } + + showControls(500); + animateView(binding.currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200); + binding.loadingPanel.setVisibility(View.GONE); + animateView(binding.surfaceForeground, true, 100); + } + + private void animatePlayButtons(final boolean show, final int duration) { + animateView(binding.playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); + + boolean showQueueButtons = show; + if (playQueue == null) { + showQueueButtons = false; + } + + if (!showQueueButtons || playQueue.getIndex() > 0) { + animateView( + binding.playPreviousButton, + AnimationUtils.Type.SCALE_AND_ALPHA, + showQueueButtons, + duration); + } + if (!showQueueButtons || playQueue.getIndex() + 1 < playQueue.getStreams().size()) { + animateView( + binding.playNextButton, + AnimationUtils.Type.SCALE_AND_ALPHA, + showQueueButtons, + duration); + } + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Repeat and shuffle + //////////////////////////////////////////////////////////////////////////*/ + //region + + public void onRepeatClicked() { + if (DEBUG) { + Log.d(TAG, "onRepeatClicked() called"); + } + setRepeatMode(nextRepeatMode(getRepeatMode())); + } + + public void onShuffleClicked() { + if (DEBUG) { + Log.d(TAG, "onShuffleClicked() called"); + } + + if (exoPlayerIsNull()) { + return; + } + simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled()); + } + + @RepeatMode + public int getRepeatMode() { + return exoPlayerIsNull() ? REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode(); + } + + private void setRepeatMode(@RepeatMode final int repeatMode) { + if (!exoPlayerIsNull()) { + simpleExoPlayer.setRepeatMode(repeatMode); + } + } + + @Override + public void onRepeatModeChanged(@RepeatMode final int repeatMode) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: " + + "repeatMode = [" + repeatMode + "]"); + } + setRepeatModeButton(binding.repeatButton, repeatMode); + onShuffleOrRepeatModeChanged(); + } + + @Override + public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onShuffleModeEnabledChanged() called with: " + + "mode = [" + shuffleModeEnabled + "]"); + } + + if (playQueue != null) { + if (shuffleModeEnabled) { + playQueue.shuffle(); + } else { + playQueue.unshuffle(); + } + } + + setShuffleButton(binding.shuffleButton, shuffleModeEnabled); + onShuffleOrRepeatModeChanged(); + } + + private void onShuffleOrRepeatModeChanged() { + notifyPlaybackUpdateToListeners(); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + private void setRepeatModeButton(final ImageButton imageButton, final int repeatMode) { + switch (repeatMode) { + case REPEAT_MODE_OFF: + imageButton.setImageResource(R.drawable.exo_controls_repeat_off); + break; + case REPEAT_MODE_ONE: + imageButton.setImageResource(R.drawable.exo_controls_repeat_one); + break; + case REPEAT_MODE_ALL: + imageButton.setImageResource(R.drawable.exo_controls_repeat_all); + break; + } + } + + private void setShuffleButton(final ImageButton button, final boolean shuffled) { + button.setImageAlpha(shuffled ? 255 : 77); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Mute / Unmute + //////////////////////////////////////////////////////////////////////////*/ + //region + + public void onMuteUnmuteButtonClicked() { + if (DEBUG) { + Log.d(TAG, "onMuteUnmuteButtonClicked() called"); + } + simpleExoPlayer.setVolume(isMuted() ? 1 : 0); + notifyPlaybackUpdateToListeners(); + setMuteButton(binding.switchMute, isMuted()); + } + + boolean isMuted() { + return !exoPlayerIsNull() && simpleExoPlayer.getVolume() == 0; + } + + private void setMuteButton(final ImageButton button, final boolean isMuted) { + button.setImageDrawable(AppCompatResources.getDrawable(context, isMuted + ? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp)); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // ExoPlayer listeners (that didn't fit in other categories) + //////////////////////////////////////////////////////////////////////////*/ + //region + + @Override + public void onTimelineChanged(@NonNull final Timeline timeline, final int reason) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onTimelineChanged() called with " + + "timeline size = [" + timeline.getWindowCount() + "], " + + "reason = [" + reason + "]"); + } + + maybeUpdateCurrentMetadata(); + // force recreate notification to ensure seek bar is shown when preparation finishes + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); + } + + @Override + public void onTracksChanged(@NonNull final TrackGroupArray trackGroups, + @NonNull final TrackSelectionArray trackSelections) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onTracksChanged(), " + + "track group size = " + trackGroups.length); + } + maybeUpdateCurrentMetadata(); + onTextTracksChanged(); + } + + @Override + public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - playbackParameters(), speed = [" + playbackParameters.speed + + "], pitch = [" + playbackParameters.pitch + "]"); + } + binding.playbackSpeed.setText(formatSpeed(playbackParameters.speed)); + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason final int discontinuityReason) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " + + "discontinuityReason = [" + discontinuityReason + "]"); + } + if (playQueue == null) { + return; + } + + // Refresh the playback if there is a transition to the next video + final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); + switch (discontinuityReason) { + case DISCONTINUITY_REASON_PERIOD_TRANSITION: + // When player is in single repeat mode and a period transition occurs, + // we need to register a view count here since no metadata has changed + if (getRepeatMode() == REPEAT_MODE_ONE && newWindowIndex == playQueue.getIndex()) { + registerStreamViewed(); + break; + } + case DISCONTINUITY_REASON_SEEK: + case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: + case DISCONTINUITY_REASON_INTERNAL: + if (playQueue.getIndex() != newWindowIndex) { + resetStreamProgressState(playQueue.getItem()); + playQueue.setIndex(newWindowIndex); + } + break; + case DISCONTINUITY_REASON_AD_INSERTION: + break; // only makes Android Studio linter happy, as there are no ads + } + + maybeUpdateCurrentMetadata(); + } + + @Override + public void onRenderedFirstFrame() { + //TODO check if this causes black screen when switching to fullscreen + animateView(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Errors + //////////////////////////////////////////////////////////////////////////*/ + //region + /** + * Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. + *

There are multiple types of errors:

+ *
    + *
  • {@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}
  • + *
  • {@link ExoPlaybackException#TYPE_UNEXPECTED TYPE_UNEXPECTED}: + * If a runtime error occurred, then we can try to recover it by restarting the playback + * after setting the timestamp recovery.
  • + *
  • {@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}: + * If the renderer failed, treat the error as unrecoverable.
  • + *
+ * + * @see #processSourceError(IOException) + * @see com.google.android.exoplayer2.Player.EventListener#onPlayerError(ExoPlaybackException) + */ + @Override + public void onPlayerError(@NonNull final ExoPlaybackException error) { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onPlayerError() called with: " + "error = [" + error + "]"); + } + if (errorToast != null) { + errorToast.cancel(); + errorToast = null; + } + + saveStreamProgressState(); + + switch (error.type) { + case ExoPlaybackException.TYPE_SOURCE: + processSourceError(error.getSourceException()); + showStreamError(error); + break; + case ExoPlaybackException.TYPE_UNEXPECTED: + showRecoverableError(error); + setRecovery(); + reloadPlayQueueManager(); + break; + case ExoPlaybackException.TYPE_OUT_OF_MEMORY: + case ExoPlaybackException.TYPE_REMOTE: + case ExoPlaybackException.TYPE_RENDERER: + default: + showUnrecoverableError(error); + onPlaybackShutdown(); + break; + } + + if (fragmentListener != null) { + fragmentListener.onPlayerError(error); + } + } + + private void processSourceError(final IOException error) { + if (exoPlayerIsNull() || playQueue == null) { + return; + } + setRecovery(); + + if (error instanceof BehindLiveWindowException) { + reloadPlayQueueManager(); + } else { + playQueue.error(); + } + } + + private void showStreamError(final Exception exception) { + exception.printStackTrace(); + + if (errorToast == null) { + errorToast = Toast + .makeText(context, R.string.player_stream_failure, Toast.LENGTH_SHORT); + errorToast.show(); + } + } + + private void showRecoverableError(final Exception exception) { + exception.printStackTrace(); + + if (errorToast == null) { + errorToast = Toast + .makeText(context, R.string.player_recoverable_failure, Toast.LENGTH_SHORT); + errorToast.show(); + } + } + + private void showUnrecoverableError(final Exception exception) { + exception.printStackTrace(); + + if (errorToast != null) { + errorToast.cancel(); + } + errorToast = Toast + .makeText(context, R.string.player_unrecoverable_failure, Toast.LENGTH_SHORT); + errorToast.show(); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Playback position and seek + //////////////////////////////////////////////////////////////////////////*/ + //region + + @Override // own playback listener (this is a getter) + public boolean isApproachingPlaybackEdge(final long timeToEndMillis) { + // If live, then not near playback edge + // If not playing, then not approaching playback edge + if (exoPlayerIsNull() || isLive() || !isPlaying()) { + return false; + } + + final long currentPositionMillis = simpleExoPlayer.getCurrentPosition(); + final long currentDurationMillis = simpleExoPlayer.getDuration(); + return currentDurationMillis - currentPositionMillis < timeToEndMillis; + } + + /** + * Checks if the current playback is a livestream AND is playing at or beyond the live edge. + * + * @return whether the livestream is playing at or beyond the edge + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean isLiveEdge() { + if (exoPlayerIsNull() || !isLive()) { + return false; + } + + final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline(); + final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex(); + if (currentTimeline.isEmpty() || currentWindowIndex < 0 + || currentWindowIndex >= currentTimeline.getWindowCount()) { + return false; + } + + final Timeline.Window timelineWindow = new Timeline.Window(); + currentTimeline.getWindow(currentWindowIndex, timelineWindow); + return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition(); + } + + @Override // own playback listener + public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) { + if (DEBUG) { + Log.d(TAG, "Playback - onPlaybackSynchronize() called with " + + "item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]"); + } + if (exoPlayerIsNull() || playQueue == null) { + return; + } + + final boolean onPlaybackInitial = currentItem == null; + final boolean hasPlayQueueItemChanged = currentItem != item; + + final int currentPlayQueueIndex = playQueue.indexOf(item); + final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex(); + final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount(); + + // If nothing to synchronize + if (!hasPlayQueueItemChanged) { + return; + } + currentItem = item; + + // Check if on wrong window + if (currentPlayQueueIndex != playQueue.getIndex()) { + Log.e(TAG, "Playback - Play Queue may be desynchronized: item " + + "index=[" + currentPlayQueueIndex + "], " + + "queue index=[" + playQueue.getIndex() + "]"); + + // Check if bad seek position + } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize) + || currentPlayQueueIndex < 0) { + Log.e(TAG, "Playback - Trying to seek to invalid " + + "index=[" + currentPlayQueueIndex + "] with " + + "playlist length=[" + currentPlaylistSize + "]"); + + } else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial + || !isPlaying()) { + if (DEBUG) { + Log.d(TAG, "Playback - Rewinding to correct " + + "index=[" + currentPlayQueueIndex + "], " + + "from=[" + currentPlaylistIndex + "], " + + "size=[" + currentPlaylistSize + "]."); + } + + if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { + simpleExoPlayer.seekTo(currentPlayQueueIndex, item.getRecoveryPosition()); + playQueue.unsetRecovery(currentPlayQueueIndex); + } else { + simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex); + } + } + } + + private void maybeCorrectSeekPosition() { + if (playQueue == null || exoPlayerIsNull() || currentMetadata == null) { + return; + } + + final PlayQueueItem currentSourceItem = playQueue.getItem(); + if (currentSourceItem == null) { + return; + } + + final StreamInfo currentInfo = currentMetadata.getMetadata(); + final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000; + if (presetStartPositionMillis > 0L) { + // Has another start position? + if (DEBUG) { + Log.d(TAG, "Playback - Seeking to preset start " + + "position=[" + presetStartPositionMillis + "]"); + } + seekTo(presetStartPositionMillis); + } + } + + public void seekTo(final long positionMillis) { + if (DEBUG) { + Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]"); + } + if (!exoPlayerIsNull()) { + // prevent invalid positions when fast-forwarding/-rewinding + long normalizedPositionMillis = positionMillis; + if (normalizedPositionMillis < 0) { + normalizedPositionMillis = 0; + } else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) { + normalizedPositionMillis = simpleExoPlayer.getDuration(); + } + + simpleExoPlayer.seekTo(normalizedPositionMillis); + } + } + + private void seekBy(final long offsetMillis) { + if (DEBUG) { + Log.d(TAG, "seekBy() called with: offsetMillis = [" + offsetMillis + "]"); + } + seekTo(simpleExoPlayer.getCurrentPosition() + offsetMillis); + } + + public void seekToDefault() { + if (!exoPlayerIsNull()) { + simpleExoPlayer.seekToDefaultPosition(); + } + } + + @Override // exoplayer override + public void onSeekProcessed() { + if (DEBUG) { + Log.d(TAG, "ExoPlayer - onSeekProcessed() called"); + } + if (isPrepared) { + saveStreamProgressState(); + } + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Player actions (play, pause, previous, fast-forward, ...) + //////////////////////////////////////////////////////////////////////////*/ + //region + + public void play() { + if (DEBUG) { + Log.d(TAG, "play() called"); + } + if (audioReactor == null || playQueue == null || exoPlayerIsNull()) { + return; + } + + audioReactor.requestAudioFocus(); + + if (currentState == STATE_COMPLETED) { + if (playQueue.getIndex() == 0) { + seekToDefault(); + } else { + playQueue.setIndex(0); + } + } + + simpleExoPlayer.setPlayWhenReady(true); + saveStreamProgressState(); + } + + public void pause() { + if (DEBUG) { + Log.d(TAG, "pause() called"); + } + if (audioReactor == null || exoPlayerIsNull()) { + return; + } + + audioReactor.abandonAudioFocus(); + simpleExoPlayer.setPlayWhenReady(false); + saveStreamProgressState(); + } + + public void playPause() { + if (DEBUG) { + Log.d(TAG, "onPlayPause() called"); + } + + if (isPlaying()) { + pause(); + } else { + play(); + } + } + + public void playPrevious() { + if (DEBUG) { + Log.d(TAG, "onPlayPrevious() called"); + } + if (exoPlayerIsNull() || playQueue == null) { + return; + } + + /* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT_MILLIS milliseconds, + * restart current track. Also restart the track if the current track + * is the first in a queue.*/ + if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT_MILLIS + || playQueue.getIndex() == 0) { + seekToDefault(); + playQueue.offsetIndex(0); + } else { + saveStreamProgressState(); + playQueue.offsetIndex(-1); + } + triggerProgressUpdate(); + } + + public void playNext() { + if (DEBUG) { + Log.d(TAG, "onPlayNext() called"); + } + if (playQueue == null) { + return; + } + + saveStreamProgressState(); + playQueue.offsetIndex(+1); + triggerProgressUpdate(); + } + + public void fastForward() { + if (DEBUG) { + Log.d(TAG, "fastRewind() called"); + } + seekBy(retrieveSeekDurationFromPreferences(this)); + triggerProgressUpdate(); + showAndAnimateControl(R.drawable.ic_fast_forward_white_24dp, true); + } + + public void fastRewind() { + if (DEBUG) { + Log.d(TAG, "fastRewind() called"); + } + seekBy(-retrieveSeekDurationFromPreferences(this)); + triggerProgressUpdate(); + showAndAnimateControl(R.drawable.ic_fast_rewind_white_24dp, true); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // StreamInfo history: views and progress + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void registerStreamViewed() { + if (currentMetadata != null) { + databaseUpdateDisposable.add(recordManager.onViewed(currentMetadata.getMetadata()) + .onErrorComplete().subscribe()); + } + } + + private void saveStreamProgressState(final StreamInfo info, final long progress) { + if (info == null) { + return; + } + if (DEBUG) { + Log.d(TAG, "saveStreamProgressState() called"); + } + if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { + final Disposable stateSaver = recordManager.saveStreamState(info, progress) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError((e) -> { + if (DEBUG) { + e.printStackTrace(); + } + }) + .onErrorComplete() + .subscribe(); + databaseUpdateDisposable.add(stateSaver); + } + } + + private void resetStreamProgressState(final PlayQueueItem queueItem) { + if (queueItem == null) { + return; + } + if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { + final Disposable stateSaver = queueItem.getStream() + .flatMapCompletable(info -> recordManager.saveStreamState(info, 0)) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError((e) -> { + if (DEBUG) { + e.printStackTrace(); + } + }) + .onErrorComplete() + .subscribe(); + databaseUpdateDisposable.add(stateSaver); + } + } + + private void resetStreamProgressState(final StreamInfo info) { + saveStreamProgressState(info, 0); + } + + public void saveStreamProgressState() { + if (exoPlayerIsNull() || currentMetadata == null) { + return; + } + final StreamInfo currentInfo = currentMetadata.getMetadata(); + if (playQueue != null) { + // Save current position. It will help to restore this position once a user + // wants to play prev or next stream from the queue + playQueue.setRecovery(playQueue.getIndex(), simpleExoPlayer.getContentPosition()); + } + saveStreamProgressState(currentInfo, simpleExoPlayer.getCurrentPosition()); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Metadata + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void onMetadataChanged(@NonNull final MediaSourceTag tag) { + final StreamInfo info = tag.getMetadata(); + if (DEBUG) { + Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName()); + } + + initThumbnail(info.getThumbnailUrl()); + registerStreamViewed(); + updateStreamRelatedViews(); + showHideKodiButton(); + + binding.titleTextView.setText(tag.getMetadata().getName()); + binding.channelTextView.setText(tag.getMetadata().getUploaderName()); + + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + notifyMetadataUpdateToListeners(); + } + + private void maybeUpdateCurrentMetadata() { + if (exoPlayerIsNull()) { + return; + } + + final MediaSourceTag metadata; + try { + metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag(); + } catch (IndexOutOfBoundsException | ClassCastException error) { + if (DEBUG) { + Log.d(TAG, "Could not update metadata: " + error.getMessage()); + error.printStackTrace(); + } + return; + } + + if (metadata == null) { + return; + } + maybeAutoQueueNextStream(metadata); + + if (currentMetadata == metadata) { + return; + } + currentMetadata = metadata; + onMetadataChanged(metadata); + } + + @NonNull + private String getVideoUrl() { + return currentMetadata == null + ? context.getString(R.string.unknown_content) + : currentMetadata.getMetadata().getUrl(); + } + + @NonNull + public String getVideoTitle() { + return currentMetadata == null + ? context.getString(R.string.unknown_content) + : currentMetadata.getMetadata().getName(); + } + + @NonNull + public String getUploaderName() { + return currentMetadata == null + ? context.getString(R.string.unknown_content) + : currentMetadata.getMetadata().getUploaderName(); + } + + @Nullable + public Bitmap getThumbnail() { + return currentThumbnail == null + ? BitmapFactory.decodeResource(context.getResources(), R.drawable.dummy_thumbnail) + : currentThumbnail; + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Play queue and streams + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) { + if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1 + || getRepeatMode() != REPEAT_MODE_OFF + || !PlayerHelper.isAutoQueueEnabled(context)) { + return; + } + // auto queue when starting playback on the last item when not repeating + final PlayQueue autoQueue = PlayerHelper.autoQueueOf(metadata.getMetadata(), + playQueue.getStreams()); + if (autoQueue != null) { + playQueue.append(autoQueue.getStreams()); + } + } + + public void selectQueueItem(final PlayQueueItem item) { + if (playQueue == null || exoPlayerIsNull()) { + return; + } + + final int index = playQueue.indexOf(item); + if (index == -1) { + return; + } + + if (playQueue.getIndex() == index && simpleExoPlayer.getCurrentWindowIndex() == index) { + seekToDefault(); + } else { + saveStreamProgressState(); + } + playQueue.setIndex(index); + } + + @Override + public void onPlayQueueEdited() { + notifyPlaybackUpdateToListeners(); + showOrHideButtons(); + NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); + } + + private void onQueueClicked() { + isQueueVisible = true; + + hideSystemUIIfNeeded(); + buildQueue(); + //updatePlaybackButtons();//TODO verify this can be removed + + hideControls(0, 0); + binding.playQueuePanel.requestFocus(); + animateView(binding.playQueuePanel, SLIDE_AND_ALPHA, true, + DEFAULT_CONTROLS_DURATION); + + binding.playQueue.scrollToPosition(playQueue.getIndex()); + } + + private void buildQueue() { + binding.playQueue.setAdapter(playQueueAdapter); + binding.playQueue.setClickable(true); + binding.playQueue.setLongClickable(true); + + binding.playQueue.clearOnScrollListeners(); + binding.playQueue.addOnScrollListener(getQueueScrollListener()); + + itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + itemTouchHelper.attachToRecyclerView(binding.playQueue); + + playQueueAdapter.setSelectedListener(getOnSelectedListener()); + + binding.playQueueClose.setOnClickListener(view -> closeQueue()); + } + + public void closeQueue() { + if (isQueueVisible) { + isQueueVisible = false; + animateView(binding.playQueuePanel, SLIDE_AND_ALPHA, false, + DEFAULT_CONTROLS_DURATION, 0, () -> { + // Even when queueLayout is GONE it receives touch events + // and ruins normal behavior of the app. This line fixes it + binding.playQueuePanel.setTranslationY( + -binding.playQueuePanel.getHeight() * 5); + }); + binding.playPauseButton.requestFocus(); + } + } + + private OnScrollBelowItemsListener getQueueScrollListener() { + return new OnScrollBelowItemsListener() { + @Override + public void onScrolledDown(final RecyclerView recyclerView) { + if (playQueue != null && !playQueue.isComplete()) { + playQueue.fetch(); + } else if (binding != null) { + binding.playQueue.clearOnScrollListeners(); + } + } + }; + } + + private ItemTouchHelper.SimpleCallback getItemTouchCallback() { + return new PlayQueueItemTouchCallback() { + @Override + public void onMove(final int sourceIndex, final int targetIndex) { + if (playQueue != null) { + playQueue.move(sourceIndex, targetIndex); + } + } + + @Override + public void onSwiped(final int index) { + if (index != -1) { + playQueue.remove(index); + } + } + }; + } + + private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { + return new PlayQueueItemBuilder.OnSelectedListener() { + @Override + public void selected(final PlayQueueItem item, final View view) { + selectQueueItem(item); + } + + @Override + public void held(final PlayQueueItem item, final View view) { + final int index = playQueue.indexOf(item); + if (index != -1) { + playQueue.remove(index); + } + } + + @Override + public void onStartDrag(final PlayQueueItemHolder viewHolder) { + if (itemTouchHelper != null) { + itemTouchHelper.startDrag(viewHolder); + } + } + }; + } + + @Override // own playback listener + @Nullable + public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { + return (isAudioOnly ? audioResolver : videoResolver).resolve(info); + } + + public void disablePreloadingOfCurrentTrack() { + loadController.disablePreloadingOfCurrentTrack(); + } + + @Nullable + public VideoStream getSelectedVideoStream() { + return (selectedStreamIndex >= 0 && availableStreams != null + && availableStreams.size() > selectedStreamIndex) + ? availableStreams.get(selectedStreamIndex) : null; + } + + private void updateStreamRelatedViews() { + if (currentMetadata == null) { + return; + } + final StreamInfo info = currentMetadata.getMetadata(); + + binding.qualityTextView.setVisibility(View.GONE); + binding.playbackSpeed.setVisibility(View.GONE); + + binding.playbackEndTime.setVisibility(View.GONE); + binding.playbackLiveSync.setVisibility(View.GONE); + + switch (info.getStreamType()) { + case AUDIO_STREAM: + binding.surfaceView.setVisibility(View.GONE); + binding.endScreen.setVisibility(View.VISIBLE); + binding.playbackEndTime.setVisibility(View.VISIBLE); + break; + + case AUDIO_LIVE_STREAM: + binding.surfaceView.setVisibility(View.GONE); + binding.endScreen.setVisibility(View.VISIBLE); + binding.playbackLiveSync.setVisibility(View.VISIBLE); + break; + + case LIVE_STREAM: + binding.surfaceView.setVisibility(View.VISIBLE); + binding.endScreen.setVisibility(View.GONE); + binding.playbackLiveSync.setVisibility(View.VISIBLE); + break; + + case VIDEO_STREAM: + if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) { + break; + } + + availableStreams = currentMetadata.getSortedAvailableVideoStreams(); + selectedStreamIndex = currentMetadata.getSelectedVideoStreamIndex(); + buildQualityMenu(); + + binding.qualityTextView.setVisibility(View.VISIBLE); + binding.surfaceView.setVisibility(View.VISIBLE); + default: + binding.endScreen.setVisibility(View.GONE); + binding.playbackEndTime.setVisibility(View.VISIBLE); + break; + } + + buildPlaybackSpeedMenu(); + binding.playbackSpeed.setVisibility(View.VISIBLE); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Popup menus ("popup" means that they pop up, not that they belong to the popup player) + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void buildQualityMenu() { + if (qualityPopupMenu == null) { + return; + } + qualityPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_QUALITY); + + for (int i = 0; i < availableStreams.size(); i++) { + final VideoStream videoStream = availableStreams.get(i); + qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat + .getNameById(videoStream.getFormatId()) + " " + videoStream.resolution); + } + if (getSelectedVideoStream() != null) { + binding.qualityTextView.setText(getSelectedVideoStream().resolution); + } + qualityPopupMenu.setOnMenuItemClickListener(this); + qualityPopupMenu.setOnDismissListener(this); + } + + private void buildPlaybackSpeedMenu() { + if (playbackSpeedPopupMenu == null) { + return; + } + playbackSpeedPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_PLAYBACK_SPEED); + + for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) { + playbackSpeedPopupMenu.getMenu().add(POPUP_MENU_ID_PLAYBACK_SPEED, i, Menu.NONE, + formatSpeed(PLAYBACK_SPEEDS[i])); + } + binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed())); + playbackSpeedPopupMenu.setOnMenuItemClickListener(this); + playbackSpeedPopupMenu.setOnDismissListener(this); + } + + private void buildCaptionMenu(final List availableLanguages) { + if (captionPopupMenu == null) { + return; + } + captionPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_CAPTION); + + final String userPreferredLanguage = + prefs.getString(context.getString(R.string.caption_user_set_key), null); + /* + * only search for autogenerated cc as fallback + * if "(auto-generated)" was not already selected + * we are only looking for "(" instead of "(auto-generated)" to hopefully get all + * internationalized variants such as "(automatisch-erzeugt)" and so on + */ + boolean searchForAutogenerated = userPreferredLanguage != null + && !userPreferredLanguage.contains("("); + + // Add option for turning off caption + final MenuItem captionOffItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION, + 0, Menu.NONE, R.string.caption_none); + captionOffItem.setOnMenuItemClickListener(menuItem -> { + final int textRendererIndex = getCaptionRendererIndex(); + if (textRendererIndex != RENDERER_UNAVAILABLE) { + trackSelector.setParameters(trackSelector.buildUponParameters() + .setRendererDisabled(textRendererIndex, true)); + } + prefs.edit().remove(context.getString(R.string.caption_user_set_key)).apply(); + return true; + }); + + // Add all available captions + for (int i = 0; i < availableLanguages.size(); i++) { + final String captionLanguage = availableLanguages.get(i); + final MenuItem captionItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION, + i + 1, Menu.NONE, captionLanguage); + captionItem.setOnMenuItemClickListener(menuItem -> { + final int textRendererIndex = getCaptionRendererIndex(); + if (textRendererIndex != RENDERER_UNAVAILABLE) { + trackSelector.setPreferredTextLanguage(captionLanguage); + trackSelector.setParameters(trackSelector.buildUponParameters() + .setRendererDisabled(textRendererIndex, false)); + prefs.edit().putString(context.getString(R.string.caption_user_set_key), + captionLanguage).apply(); + } + return true; + }); + // apply caption language from previous user preference + if (userPreferredLanguage != null + && (captionLanguage.equals(userPreferredLanguage) + || (searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage)) + || (userPreferredLanguage.contains("(") && captionLanguage.startsWith( + userPreferredLanguage.substring(0, userPreferredLanguage.indexOf('(')))))) { + final int textRendererIndex = getCaptionRendererIndex(); + if (textRendererIndex != RENDERER_UNAVAILABLE) { + trackSelector.setPreferredTextLanguage(captionLanguage); + trackSelector.setParameters(trackSelector.buildUponParameters() + .setRendererDisabled(textRendererIndex, false)); + } + searchForAutogenerated = false; + } + } + captionPopupMenu.setOnDismissListener(this); + } + + /** + * Called when an item of the quality selector or the playback speed selector is selected. + */ + @Override + public boolean onMenuItemClick(final MenuItem menuItem) { + if (DEBUG) { + Log.d(TAG, "onMenuItemClick() called with: " + + "menuItem = [" + menuItem + "], " + + "menuItem.getItemId = [" + menuItem.getItemId() + "]"); + } + + if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) { + final int menuItemIndex = menuItem.getItemId(); + if (selectedStreamIndex == menuItemIndex || availableStreams == null + || availableStreams.size() <= menuItemIndex) { + return true; + } + + saveStreamProgressState(); //TODO added, check if good + final String newResolution = availableStreams.get(menuItemIndex).resolution; + setRecovery(); + setPlaybackQuality(newResolution); + reloadPlayQueueManager(); + + binding.qualityTextView.setText(menuItem.getTitle()); + return true; + } else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) { + final int speedIndex = menuItem.getItemId(); + final float speed = PLAYBACK_SPEEDS[speedIndex]; + + setPlaybackSpeed(speed); + binding.playbackSpeed.setText(formatSpeed(speed)); + } + + return false; + } + + /** + * Called when some popup menu is dismissed. + */ + @Override + public void onDismiss(final PopupMenu menu) { + if (DEBUG) { + Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]"); + } + isSomePopupMenuVisible = false; //TODO check if this works + if (getSelectedVideoStream() != null) { + binding.qualityTextView.setText(getSelectedVideoStream().resolution); + } + if (isPlaying()) { + hideControls(DEFAULT_CONTROLS_DURATION, 0); + hideSystemUIIfNeeded(); + } + } + + private void onQualitySelectorClicked() { + if (DEBUG) { + Log.d(TAG, "onQualitySelectorClicked() called"); + } + qualityPopupMenu.show(); + isSomePopupMenuVisible = true; + + final VideoStream videoStream = getSelectedVideoStream(); + if (videoStream != null) { + final String qualityText = MediaFormat.getNameById(videoStream.getFormatId()) + " " + + videoStream.resolution; + binding.qualityTextView.setText(qualityText); + } + + saveWasPlaying(); + } + + private void onPlaybackSpeedClicked() { + if (DEBUG) { + Log.d(TAG, "onPlaybackSpeedClicked() called"); + } + if (videoPlayerSelected()) { + PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch(), + getPlaybackSkipSilence(), this::setPlaybackParameters) + .show(getParentActivity().getSupportFragmentManager(), null); + } else { + playbackSpeedPopupMenu.show(); + isSomePopupMenuVisible = true; + } + } + + private void onCaptionClicked() { + if (DEBUG) { + Log.d(TAG, "onCaptionClicked() called"); + } + captionPopupMenu.show(); + isSomePopupMenuVisible = true; + } + + private void setPlaybackQuality(final String quality) { + videoResolver.setPlaybackQuality(quality); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Captions (text tracks) + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void setupSubtitleView() { + final float captionScale = PlayerHelper.getCaptionScale(context); + final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context); + if (popupPlayerSelected()) { + final float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f; + binding.subtitleView.setFractionalTextSize( + SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio); + } else { + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); + final float captionRatioInverse = 20f + 4f * (1.0f - captionScale); + binding.subtitleView.setFixedTextSize( + TypedValue.COMPLEX_UNIT_PX, (float) minimumLength / captionRatioInverse); + } + binding.subtitleView.setApplyEmbeddedStyles(captionStyle == CaptionStyleCompat.DEFAULT); + binding.subtitleView.setStyle(captionStyle); + } + + private void onTextTracksChanged() { + final int textRenderer = getCaptionRendererIndex(); + + if (binding == null) { + return; + } + if (trackSelector.getCurrentMappedTrackInfo() == null + || textRenderer == RENDERER_UNAVAILABLE) { + binding.captionTextView.setVisibility(View.GONE); + return; + } + + final TrackGroupArray textTracks = trackSelector.getCurrentMappedTrackInfo() + .getTrackGroups(textRenderer); + + // Extract all loaded languages + final List availableLanguages = new ArrayList<>(textTracks.length); + for (int i = 0; i < textTracks.length; i++) { + final TrackGroup textTrack = textTracks.get(i); + if (textTrack.length > 0 && textTrack.getFormat(0) != null) { + availableLanguages.add(textTrack.getFormat(0).language); + } + } + + // Normalize mismatching language strings + final String preferredLanguage = trackSelector.getPreferredTextLanguage(); + // Build UI + buildCaptionMenu(availableLanguages); + if (trackSelector.getParameters().getRendererDisabled(textRenderer) + || preferredLanguage == null || (!availableLanguages.contains(preferredLanguage) + && !containsCaseInsensitive(availableLanguages, preferredLanguage))) { + binding.captionTextView.setText(R.string.caption_none); + } else { + binding.captionTextView.setText(preferredLanguage); + } + binding.captionTextView.setVisibility( + availableLanguages.isEmpty() ? View.GONE : View.VISIBLE); + } + + private int getCaptionRendererIndex() { + if (exoPlayerIsNull()) { + return RENDERER_UNAVAILABLE; + } + + for (int t = 0; t < simpleExoPlayer.getRendererCount(); t++) { + if (simpleExoPlayer.getRendererType(t) == C.TRACK_TYPE_TEXT) { + return t; + } + } + + return RENDERER_UNAVAILABLE; + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Click listeners + //////////////////////////////////////////////////////////////////////////*/ + //region + + @Override + public void onClick(final View v) { + if (DEBUG) { + Log.d(TAG, "onClick() called with: v = [" + v + "]"); + } + if (v.getId() == binding.qualityTextView.getId()) { + onQualitySelectorClicked(); + } else if (v.getId() == binding.playbackSpeed.getId()) { + onPlaybackSpeedClicked(); + } else if (v.getId() == binding.resizeTextView.getId()) { + onResizeClicked(); + } else if (v.getId() == binding.captionTextView.getId()) { + onCaptionClicked(); + } else if (v.getId() == binding.playbackLiveSync.getId()) { + seekToDefault(); + } else if (v.getId() == binding.playPauseButton.getId()) { + playPause(); + } else if (v.getId() == binding.playPreviousButton.getId()) { + playPrevious(); + } else if (v.getId() == binding.playNextButton.getId()) { + playNext(); + } else if (v.getId() == binding.queueButton.getId()) { + onQueueClicked(); + return; + } else if (v.getId() == binding.repeatButton.getId()) { + onRepeatClicked(); + return; + } else if (v.getId() == binding.shuffleButton.getId()) { + onShuffleClicked(); + return; + } else if (v.getId() == binding.moreOptionsButton.getId()) { + onMoreOptionsClicked(); + } else if (v.getId() == binding.share.getId()) { + onShareClicked(); + } else if (v.getId() == binding.playWithKodi.getId()) { + onPlayWithKodiClicked(); + } else if (v.getId() == binding.openInBrowser.getId()) { + onOpenInBrowserClicked(); + } else if (v.getId() == binding.fullScreenButton.getId()) { + setRecovery(); + NavigationHelper.playOnMainPlayer(context, playQueue, true); + return; + } else if (v.getId() == binding.screenRotationButton.getId()) { + // Only if it's not a vertical video or vertical video but in landscape with locked + // orientation a screen orientation can be changed automatically + if (!isVerticalVideo + || (service.isLandscape() && globalScreenOrientationLocked(context))) { + fragmentListener.onScreenRotationButtonClicked(); + } else { + toggleFullscreen(); + } + } else if (v.getId() == binding.switchMute.getId()) { + onMuteUnmuteButtonClicked(); + } else if (v.getId() == binding.playerCloseButton.getId()) { + context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER)); + } + + if (currentState != STATE_COMPLETED) { + controlsVisibilityHandler.removeCallbacksAndMessages(null); + showHideShadow(true, DEFAULT_CONTROLS_DURATION); + animateView(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION, 0, () -> { + if (currentState == STATE_PLAYING && !isSomePopupMenuVisible) { + if (v.getId() == binding.playPauseButton.getId() + // Hide controls in fullscreen immediately + || (v.getId() == binding.screenRotationButton.getId() + && isFullscreen)) { + hideControls(0, 0); + } else { + hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + } + } + }); + } + } + + @Override + public boolean onLongClick(final View v) { + if (v.getId() == binding.moreOptionsButton.getId() && isFullscreen) { + fragmentListener.onMoreOptionsLongClicked(); + hideControls(0, 0); + hideSystemUIIfNeeded(); + } + return true; + } + + public boolean onKeyDown(final int keyCode) { + switch (keyCode) { + default: + break; + case KeyEvent.KEYCODE_SPACE: + if (isFullscreen) { + playPause(); + } + break; + case KeyEvent.KEYCODE_BACK: + if (DeviceUtils.isTv(context) && isControlsVisible()) { + hideControls(0, 0); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus()) { + // do not interfere with focus in playlist etc. + return false; + } + + if (currentState == Player.STATE_BLOCKED) { + return true; + } + + if (!isControlsVisible()) { + if (!isQueueVisible) { + binding.playPauseButton.requestFocus(); + } + showControlsThenHide(); + showSystemUIPartially(); + return true; + } else { + hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); + } + break; + } + + return false; + } + + private void onMoreOptionsClicked() { + if (DEBUG) { + Log.d(TAG, "onMoreOptionsClicked() called"); + } + + final boolean isMoreControlsVisible = + binding.secondaryControls.getVisibility() == View.VISIBLE; + + animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION, + isMoreControlsVisible ? 0 : 180); + animateView(binding.secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, + DEFAULT_CONTROLS_DURATION, 0, + () -> { + // Fix for a ripple effect on background drawable. + // When view returns from GONE state it takes more milliseconds than returning + // from INVISIBLE state. And the delay makes ripple background end to fast + if (isMoreControlsVisible) { + binding.secondaryControls.setVisibility(View.INVISIBLE); + } + }); + showControls(DEFAULT_CONTROLS_DURATION); + } + + private void onShareClicked() { + // share video at the current time (youtube.com/watch?v=ID&t=SECONDS) + // Timestamp doesn't make sense in a live stream so drop it + + final int ts = binding.playbackSeekBar.getProgress() / 1000; + String videoUrl = getVideoUrl(); + if (!isLive() && ts >= 0 && currentMetadata != null + && currentMetadata.getMetadata().getServiceId() == YouTube.getServiceId()) { + videoUrl += ("&t=" + ts); + } + ShareUtils.shareUrl(context, getVideoTitle(), videoUrl); + } + + private void onPlayWithKodiClicked() { + if (currentMetadata != null) { + pause(); + try { + NavigationHelper.playWithKore(context, Uri.parse(getVideoUrl())); + } catch (final Exception e) { + if (DEBUG) { + Log.i(TAG, "Failed to start kore", e); + } + KoreUtil.showInstallKoreDialog(getParentActivity()); + } + } + } + + private void onOpenInBrowserClicked() { + if (currentMetadata != null) { + ShareUtils.openUrlInBrowser(getParentActivity(), + currentMetadata.getMetadata().getOriginalUrl()); + } + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Video size, resize, orientation, fullscreen + //////////////////////////////////////////////////////////////////////////*/ + //region + + private void setupScreenRotationButton() { + binding.screenRotationButton.setVisibility(videoPlayerSelected() + && (globalScreenOrientationLocked(context) || isVerticalVideo + || DeviceUtils.isTablet(context)) + ? View.VISIBLE : View.GONE); + binding.screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(context, + isFullscreen ? R.drawable.ic_fullscreen_exit_white_24dp + : R.drawable.ic_fullscreen_white_24dp)); + } + + private void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) { + binding.surfaceView.setResizeMode(resizeMode); + binding.resizeTextView.setText(PlayerHelper.resizeTypeOf(context, resizeMode)); + } + + void onResizeClicked() { + if (binding != null) { + setResizeMode(nextResizeModeAndSaveToPrefs(this, binding.surfaceView.getResizeMode())); + } + } + + @Override // exoplayer listener + public void onVideoSizeChanged(final int width, final int height, + final int unappliedRotationDegrees, + final float pixelWidthHeightRatio) { + if (DEBUG) { + Log.d(TAG, "onVideoSizeChanged() called with: " + + "width / height = [" + width + " / " + height + + " = " + (((float) width) / height) + "], " + + "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], " + + "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]"); + } + + binding.surfaceView.setAspectRatio(((float) width) / height); + isVerticalVideo = width < height; + + if (globalScreenOrientationLocked(context) + && isFullscreen + && service.isLandscape() == isVerticalVideo + && !DeviceUtils.isTv(context) + && !DeviceUtils.isTablet(context) + && fragmentListener != null) { + // set correct orientation + fragmentListener.onScreenRotationButtonClicked(); + } + + setupScreenRotationButton(); + } + + public void toggleFullscreen() { + if (DEBUG) { + Log.d(TAG, "toggleFullscreen() called"); + } + if (popupPlayerSelected() || exoPlayerIsNull() || currentMetadata == null + || fragmentListener == null) { + return; + } + //changeState(STATE_BLOCKED); TODO check what this does + + isFullscreen = !isFullscreen; + if (!isFullscreen) { + // Apply window insets because Android will not do it when orientation changes + // from landscape to portrait (open vertical video to reproduce) + binding.playbackControlRoot.setPadding(0, 0, 0, 0); + } else { + // Android needs tens milliseconds to send new insets but a user is able to see + // how controls changes it's position from `0` to `nav bar height` padding. + // So just hide the controls to hide this visual inconsistency + hideControls(0, 0); + } + fragmentListener.onFullscreenStateChanged(isFullscreen); + + if (isFullscreen) { + binding.titleTextView.setVisibility(View.VISIBLE); + binding.channelTextView.setVisibility(View.VISIBLE); + binding.playerCloseButton.setVisibility(View.GONE); + } else { + binding.titleTextView.setVisibility(View.GONE); + binding.channelTextView.setVisibility(View.GONE); + binding.playerCloseButton.setVisibility( + videoPlayerSelected() ? View.VISIBLE : View.GONE); + } + setupScreenRotationButton(); + } + + public void checkLandscape() { + final AppCompatActivity parent = getParentActivity(); + final boolean videoInLandscapeButNotInFullscreen = + service.isLandscape() && !isFullscreen && videoPlayerSelected() && !isAudioOnly; + + final boolean notPaused = currentState != STATE_COMPLETED && currentState != STATE_PAUSED; + if (parent != null + && videoInLandscapeButNotInFullscreen + && notPaused + && !DeviceUtils.isTablet(context)) { + toggleFullscreen(); + } + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Gestures + //////////////////////////////////////////////////////////////////////////*/ + //region + + @SuppressWarnings("checkstyle:ParameterNumber") + private void onLayoutChange(final View view, final int l, final int t, final int r, final int b, + final int ol, final int ot, final int or, final int ob) { + if (l != ol || t != ot || r != or || b != ob) { + // Use smaller value to be consistent between screen orientations + // (and to make usage easier) + final int width = r - l; + final int height = b - t; + final int min = Math.min(width, height); + maxGestureLength = (int) (min * MAX_GESTURE_LENGTH); + + if (DEBUG) { + Log.d(TAG, "maxGestureLength = " + maxGestureLength); + } + + binding.volumeProgressBar.setMax(maxGestureLength); + binding.brightnessProgressBar.setMax(maxGestureLength); + + setInitialGestureValues(); + binding.playQueuePanel.getLayoutParams().height + = height - binding.playQueuePanel.getTop(); + } + } + + private void setInitialGestureValues() { + if (audioReactor != null) { + final float currentVolumeNormalized = + (float) audioReactor.getVolume() / audioReactor.getMaxVolume(); + binding.volumeProgressBar.setProgress( + (int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized)); + } + } + + private int distanceFromCloseButton(final MotionEvent popupMotionEvent) { + final int closeOverlayButtonX = closeOverlayBinding.closeButton.getLeft() + + closeOverlayBinding.closeButton.getWidth() / 2; + final int closeOverlayButtonY = closeOverlayBinding.closeButton.getTop() + + closeOverlayBinding.closeButton.getHeight() / 2; + + final float fingerX = popupLayoutParams.x + popupMotionEvent.getX(); + final float fingerY = popupLayoutParams.y + popupMotionEvent.getY(); + + return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) + + Math.pow(closeOverlayButtonY - fingerY, 2)); + } + + private float getClosingRadius() { + final int buttonRadius = closeOverlayBinding.closeButton.getWidth() / 2; + // 20% wider than the button itself + return buttonRadius * 1.2f; + } + + public boolean isInsideClosingRadius(final MotionEvent popupMotionEvent) { + return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); + } + //endregion + + + + /*////////////////////////////////////////////////////////////////////////// + // Activity / fragment binding + //////////////////////////////////////////////////////////////////////////*/ + //region + + public void setFragmentListener(final PlayerServiceEventListener listener) { + fragmentListener = listener; + fragmentIsVisible = true; + // Apply window insets because Android will not do it when orientation changes + // from landscape to portrait + if (!isFullscreen) { + binding.playbackControlRoot.setPadding(0, 0, 0, 0); + } + binding.playQueuePanel.setPadding(0, 0, 0, 0); + notifyQueueUpdateToListeners(); + notifyMetadataUpdateToListeners(); + notifyPlaybackUpdateToListeners(); + triggerProgressUpdate(); + } + + public void removeFragmentListener(final PlayerServiceEventListener listener) { + if (fragmentListener == listener) { + fragmentListener = null; + } + } + + void setActivityListener(final PlayerEventListener listener) { + activityListener = listener; + // TODO why not queue update? + notifyMetadataUpdateToListeners(); + notifyPlaybackUpdateToListeners(); + triggerProgressUpdate(); + } + + void removeActivityListener(final PlayerEventListener listener) { + if (activityListener == listener) { + activityListener = null; + } + } + + void stopActivityBinding() { + if (fragmentListener != null) { + fragmentListener.onServiceStopped(); + fragmentListener = null; + } + if (activityListener != null) { + activityListener.onServiceStopped(); + activityListener = null; + } + } + + /** + * This will be called when a user goes to another app/activity, turns off a screen. + * We don't want to interrupt playback and don't want to see notification so + * next lines of code will enable audio-only playback only if needed + */ + private void onFragmentStopped() { + if (videoPlayerSelected() && (isPlaying() || isLoading())) { + switch (getMinimizeOnExitAction(context)) { + case MINIMIZE_ON_EXIT_MODE_BACKGROUND: + useVideoSource(false); + case MINIMIZE_ON_EXIT_MODE_POPUP: + setRecovery(); + NavigationHelper.playOnPopupPlayer(getParentActivity(), playQueue, true); + case MINIMIZE_ON_EXIT_MODE_NONE: default: + pause(); + } + } + } + + private void notifyQueueUpdateToListeners() { + if (fragmentListener != null && playQueue != null) { + fragmentListener.onQueueUpdate(playQueue); + } + if (activityListener != null && playQueue != null) { + activityListener.onQueueUpdate(playQueue); + } + } + + private void notifyMetadataUpdateToListeners() { + if (fragmentListener != null && currentMetadata != null) { + fragmentListener.onMetadataUpdate(currentMetadata.getMetadata(), playQueue); + } + if (activityListener != null && currentMetadata != null) { + activityListener.onMetadataUpdate(currentMetadata.getMetadata(), playQueue); + } + } + + private void notifyPlaybackUpdateToListeners() { + if (fragmentListener != null && !exoPlayerIsNull() && playQueue != null) { + fragmentListener.onPlaybackUpdate(currentState, getRepeatMode(), + playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters()); + } + if (activityListener != null && !exoPlayerIsNull() && playQueue != null) { + activityListener.onPlaybackUpdate(currentState, getRepeatMode(), + playQueue.isShuffled(), getPlaybackParameters()); + } + } + + private void notifyProgressUpdateToListeners(final int currentProgress, + final int duration, + final int bufferPercent) { + if (fragmentListener != null) { + fragmentListener.onProgressUpdate(currentProgress, duration, bufferPercent); + } + if (activityListener != null) { + activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); + } + } + + public AppCompatActivity getParentActivity() { + // ! instanceof ViewGroup means that view was added via windowManager for Popup + if (binding == null || !(binding.getRoot().getParent() instanceof ViewGroup)) { + return null; + } + + return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext(); + } + + private void useVideoSource(final boolean video) { + if (playQueue == null || isAudioOnly == !video || audioPlayerSelected()) { + return; + } + + isAudioOnly = !video; + // When a user returns from background controls could be hidden + // but systemUI will be shown 100%. Hide it + if (!isAudioOnly && !isControlsVisible()) { + hideSystemUIIfNeeded(); + } + setRecovery(); + reloadPlayQueueManager(); + } + //endregion + + + /*////////////////////////////////////////////////////////////////////////// + // Getters + //////////////////////////////////////////////////////////////////////////*/ + //region + + public int getCurrentState() { + return currentState; + } + + public boolean exoPlayerIsNull() { + return simpleExoPlayer == null; + } + + public boolean isStopped() { + return exoPlayerIsNull() + || simpleExoPlayer.getPlaybackState() == SimpleExoPlayer.STATE_IDLE; + } + + public boolean isPlaying() { + return !exoPlayerIsNull() && simpleExoPlayer.isPlaying(); + } + + private boolean isLoading() { + return !exoPlayerIsNull() && simpleExoPlayer.isLoading(); + } + + private boolean isLive() { + try { + return !exoPlayerIsNull() && simpleExoPlayer.isCurrentWindowDynamic(); + } catch (@NonNull final IndexOutOfBoundsException e) { + // Why would this even happen =(... but lets log it anyway, better safe than sorry + if (DEBUG) { + Log.d(TAG, "player.isCurrentWindowDynamic() failed: " + e.getMessage()); + e.printStackTrace(); + } + return false; + } + } + + + @NonNull + public Context getContext() { + return context; + } + + @NonNull + public SharedPreferences getPrefs() { + return prefs; + } + + public MediaSessionManager getMediaSessionManager() { + return mediaSessionManager; + } + + + public PlayerType getPlayerType() { + return playerType; + } + + public boolean audioPlayerSelected() { + return playerType == PlayerType.AUDIO; + } + + public boolean videoPlayerSelected() { + return playerType == PlayerType.VIDEO; + } + + public boolean popupPlayerSelected() { + return playerType == PlayerType.POPUP; + } + + + public PlayQueue getPlayQueue() { + return playQueue; + } + + public AudioReactor getAudioReactor() { + return audioReactor; + } + + public GestureDetector getGestureDetector() { + return gestureDetector; + } + + public boolean isFullscreen() { + return isFullscreen; + } + + public boolean isVerticalVideo() { + return isVerticalVideo; + } + + public boolean isPopupClosing() { + return isPopupClosing; + } + + + public boolean isSomePopupMenuVisible() { + return isSomePopupMenuVisible; + } + + public ImageButton getPlayPauseButton() { + return binding.playPauseButton; + } + + public View getClosingOverlayView() { + return closeOverlayBinding.getRoot(); + } + + public ProgressBar getVolumeProgressBar() { + return binding.volumeProgressBar; + } + + public ProgressBar getBrightnessProgressBar() { + return binding.brightnessProgressBar; + } + + public int getMaxGestureLength() { + return maxGestureLength; + } + + public ImageView getVolumeImageView() { + return binding.volumeImageView; + } + + public RelativeLayout getVolumeRelativeLayout() { + return binding.volumeRelativeLayout; + } + + public ImageView getBrightnessImageView() { + return binding.brightnessImageView; + } + + public RelativeLayout getBrightnessRelativeLayout() { + return binding.brightnessRelativeLayout; + } + + public FloatingActionButton getCloseOverlayButton() { + return closeOverlayBinding.closeButton; + } + + public View getLoadingPanel() { + return binding.loadingPanel; + } + + public TextView getCurrentDisplaySeek() { + return binding.currentDisplaySeek; + } + + public TextView getResizingIndicator() { + return binding.resizingIndicator; + } + + @Nullable + public WindowManager.LayoutParams getPopupLayoutParams() { + return popupLayoutParams; + } + + @Nullable + public WindowManager getWindowManager() { + return windowManager; + } + + public float getScreenWidth() { + return screenWidth; + } + + public float getScreenHeight() { + return screenHeight; + } + + public View getRootView() { + return binding.getRoot(); + } + + public ExpandableSurfaceView getSurfaceView() { + return binding.surfaceView; + } + + public PlayQueueAdapter getPlayQueueAdapter() { + return playQueueAdapter; + } + + //endregion +} diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java b/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java index e8bd7dc85..5c28c6c7b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java @@ -5,13 +5,13 @@ import android.os.Binder; import androidx.annotation.NonNull; class PlayerServiceBinder extends Binder { - private final BasePlayer basePlayer; + private final Player player; - PlayerServiceBinder(@NonNull final BasePlayer basePlayer) { - this.basePlayer = basePlayer; + PlayerServiceBinder(@NonNull final Player player) { + this.player = player; } - BasePlayer getPlayerInstance() { - return basePlayer; + Player getPlayerInstance() { + return player; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index fd20fd175..283c25e4f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -22,7 +22,6 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; import org.schabi.newpipe.R; import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding; @@ -55,7 +54,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47; private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; - protected BasePlayer player; + protected Player player; private boolean serviceBound; private ServiceConnection serviceConnection; @@ -167,14 +166,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity case R.id.action_switch_popup: if (PermissionHelper.isPopupEnabled(this)) { this.player.setRecovery(); - NavigationHelper.playOnPopupPlayer(this, player.playQueue, true); + NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true); } else { PermissionHelper.showPopupEnablementToast(this); } return true; case R.id.action_switch_background: this.player.setRecovery(); - NavigationHelper.playOnBackgroundPlayer(this, player.playQueue, true); + NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true); return true; } return super.onOptionsItemSelected(item); @@ -235,7 +234,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } if (player == null || player.getPlayQueue() == null - || player.getPlayQueueAdapter() == null || player.getPlayer() == null) { + || player.getPlayQueueAdapter() == null || player.exoPlayerIsNull()) { unbind(); finish(); } else { @@ -375,7 +374,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity @Override public void selected(final PlayQueueItem item, final View view) { if (player != null) { - player.onSelected(item); + player.selectQueueItem(item); } } @@ -436,15 +435,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity if (view.getId() == queueControlBinding.controlRepeat.getId()) { player.onRepeatClicked(); } else if (view.getId() == queueControlBinding.controlBackward.getId()) { - player.onPlayPrevious(); + player.playPrevious(); } else if (view.getId() == queueControlBinding.controlFastRewind.getId()) { - player.onFastRewind(); + player.fastRewind(); } else if (view.getId() == queueControlBinding.controlPlayPause.getId()) { - player.onPlayPause(); + player.playPause(); } else if (view.getId() == queueControlBinding.controlFastForward.getId()) { - player.onFastForward(); + player.fastForward(); } else if (view.getId() == queueControlBinding.controlForward.getId()) { - player.onPlayNext(); + player.playNext(); } else if (view.getId() == queueControlBinding.controlShuffle.getId()) { player.onShuffleClicked(); } else if (view.getId() == queueControlBinding.metadata.getId()) { @@ -616,15 +615,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private void onStateChanged(final int state) { switch (state) { - case BasePlayer.STATE_PAUSED: + case Player.STATE_PAUSED: queueControlBinding.controlPlayPause .setImageResource(R.drawable.ic_play_arrow_white_24dp); break; - case BasePlayer.STATE_PLAYING: + case Player.STATE_PLAYING: queueControlBinding.controlPlayPause .setImageResource(R.drawable.ic_pause_white_24dp); break; - case BasePlayer.STATE_COMPLETED: + case Player.STATE_COMPLETED: queueControlBinding.controlPlayPause .setImageResource(R.drawable.ic_replay_white_24dp); break; @@ -633,9 +632,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity } switch (state) { - case BasePlayer.STATE_PAUSED: - case BasePlayer.STATE_PLAYING: - case BasePlayer.STATE_COMPLETED: + case Player.STATE_PAUSED: + case Player.STATE_PLAYING: + case Player.STATE_COMPLETED: queueControlBinding.controlPlayPause.setClickable(true); queueControlBinding.controlPlayPause.setVisibility(View.VISIBLE); queueControlBinding.controlProgressBar.setVisibility(View.GONE); @@ -650,15 +649,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private void onPlayModeChanged(final int repeatMode, final boolean shuffled) { switch (repeatMode) { - case Player.REPEAT_MODE_OFF: + case com.google.android.exoplayer2.Player.REPEAT_MODE_OFF: queueControlBinding.controlRepeat .setImageResource(R.drawable.exo_controls_repeat_off); break; - case Player.REPEAT_MODE_ONE: + case com.google.android.exoplayer2.Player.REPEAT_MODE_ONE: queueControlBinding.controlRepeat .setImageResource(R.drawable.exo_controls_repeat_one); break; - case Player.REPEAT_MODE_ALL: + case com.google.android.exoplayer2.Player.REPEAT_MODE_ALL: queueControlBinding.controlRepeat .setImageResource(R.drawable.exo_controls_repeat_all); break; @@ -700,9 +699,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity // using rootView.getContext() because getApplicationContext() didn't work final Context context = queueControlBinding.getRoot().getContext(); item.setIcon(ThemeHelper.resolveResourceIdFromAttr(context, - player.isMuted() - ? R.attr.ic_volume_off - : R.attr.ic_volume_up)); + player.isMuted() ? R.attr.ic_volume_off : R.attr.ic_volume_up)); } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java deleted file mode 100644 index 8894646c0..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ /dev/null @@ -1,1036 +0,0 @@ -/* - * Copyright 2017 Mauricio Colli - * VideoPlayer.java is part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.schabi.newpipe.player; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; -import android.animation.ValueAnimator; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.os.Build; -import android.os.Handler; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.PopupMenu; -import android.widget.SeekBar; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.preference.PreferenceManager; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.text.CaptionStyleCompat; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; -import com.google.android.exoplayer2.ui.SubtitleView; -import com.google.android.exoplayer2.video.VideoListener; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.databinding.PlayerBinding; -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.player.resolver.MediaSourceTag; -import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; -import org.schabi.newpipe.util.AnimationUtils; -import org.schabi.newpipe.views.ExpandableSurfaceView; - -import java.util.ArrayList; -import java.util.List; - -import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; -import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; -import static org.schabi.newpipe.util.AnimationUtils.animateView; - -/** - * Base for video players. - * - * @author mauriciocolli - */ -@SuppressWarnings({"WeakerAccess"}) -public abstract class VideoPlayer extends BasePlayer - implements VideoListener, - SeekBar.OnSeekBarChangeListener, - View.OnClickListener, - Player.EventListener, - PopupMenu.OnMenuItemClickListener, - PopupMenu.OnDismissListener { - public final String TAG; - public static final boolean DEBUG = BasePlayer.DEBUG; - - /*////////////////////////////////////////////////////////////////////////// - // Player - //////////////////////////////////////////////////////////////////////////*/ - - public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis - public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds - public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds - - protected static final int RENDERER_UNAVAILABLE = -1; - - @NonNull - private final VideoPlaybackResolver resolver; - - private List availableStreams; - private int selectedStreamIndex; - - protected boolean wasPlaying = false; - - /*////////////////////////////////////////////////////////////////////////// - // Views - //////////////////////////////////////////////////////////////////////////*/ - - protected PlayerBinding binding; - - protected SeekBar playbackSeekBar; - protected TextView qualityTextView; - protected TextView playbackSpeed; - - private ValueAnimator controlViewAnimator; - private final Handler controlsVisibilityHandler = new Handler(); - - boolean isSomePopupMenuVisible = false; - - private final int qualityPopupMenuGroupId = 69; - private PopupMenu qualityPopupMenu; - - private final int playbackSpeedPopupMenuGroupId = 79; - private PopupMenu playbackSpeedPopupMenu; - - private final int captionPopupMenuGroupId = 89; - private PopupMenu captionPopupMenu; - - /////////////////////////////////////////////////////////////////////////// - - protected VideoPlayer(final String debugTag, final Context context) { - super(context); - this.TAG = debugTag; - this.resolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver()); - } - - // workaround to match normalized captions like english to English or deutsch to Deutsch - private static boolean containsCaseInsensitive(final List list, final String toFind) { - for (final String i : list) { - if (i.equalsIgnoreCase(toFind)) { - return true; - } - } - return false; - } - - public void setup(@NonNull final PlayerBinding playerBinding) { - initViews(playerBinding); - if (simpleExoPlayer == null) { - initPlayer(true); - } - initListeners(); - } - - public void initViews(@NonNull final PlayerBinding playerBinding) { - binding = playerBinding; - playbackSeekBar = (SeekBar) binding.playbackSeekBar; - qualityTextView = (TextView) binding.qualityTextView; - playbackSpeed = (TextView) binding.playbackSpeed; - - final float captionScale = PlayerHelper.getCaptionScale(context); - final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context); - setupSubtitleView(binding.subtitleView, captionScale, captionStyle); - - ((TextView) binding.resizeTextView).setText(PlayerHelper.resizeTypeOf(context, - binding.surfaceView.getResizeMode())); - - playbackSeekBar.getThumb().setColorFilter(new PorterDuffColorFilter(Color.RED, - PorterDuff.Mode.SRC_IN)); - playbackSeekBar.getProgressDrawable().setColorFilter(new PorterDuffColorFilter(Color.RED, - PorterDuff.Mode.MULTIPLY)); - - qualityPopupMenu = new PopupMenu(context, qualityTextView); - playbackSpeedPopupMenu = new PopupMenu(context, playbackSpeed); - captionPopupMenu = new PopupMenu(context, binding.captionTextView); - - binding.progressBarLoadingPanel.getIndeterminateDrawable() - .setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY)); - } - - protected abstract void setupSubtitleView(@NonNull SubtitleView view, float captionScale, - @NonNull CaptionStyleCompat captionStyle); - - @Override - public void initListeners() { - playbackSeekBar.setOnSeekBarChangeListener(this); - binding.playbackSpeed.setOnClickListener(this); - binding.qualityTextView.setOnClickListener(this); - binding.captionTextView.setOnClickListener(this); - binding.resizeTextView.setOnClickListener(this); - binding.playbackLiveSync.setOnClickListener(this); - } - - @Override - public void initPlayer(final boolean playOnReady) { - super.initPlayer(playOnReady); - - // Setup video view - simpleExoPlayer.setVideoSurfaceView(binding.surfaceView); - simpleExoPlayer.addVideoListener(this); - - // Setup subtitle view - simpleExoPlayer.addTextOutput(binding.subtitleView); - - // Setup audio session with onboard equalizer - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - trackSelector.setParameters(trackSelector.buildUponParameters() - .setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context))); - } - } - - @Override - public void handleIntent(final Intent intent) { - if (intent == null) { - return; - } - - if (intent.hasExtra(PLAYBACK_QUALITY)) { - setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY)); - } - - super.handleIntent(intent); - } - - /*////////////////////////////////////////////////////////////////////////// - // UI Builders - //////////////////////////////////////////////////////////////////////////*/ - - public void buildQualityMenu() { - if (qualityPopupMenu == null) { - return; - } - - qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId); - for (int i = 0; i < availableStreams.size(); i++) { - final VideoStream videoStream = availableStreams.get(i); - qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat - .getNameById(videoStream.getFormatId()) + " " + videoStream.resolution); - } - if (getSelectedVideoStream() != null) { - qualityTextView.setText(getSelectedVideoStream().resolution); - } - qualityPopupMenu.setOnMenuItemClickListener(this); - qualityPopupMenu.setOnDismissListener(this); - } - - private void buildPlaybackSpeedMenu() { - if (playbackSpeedPopupMenu == null) { - return; - } - - playbackSpeedPopupMenu.getMenu().removeGroup(playbackSpeedPopupMenuGroupId); - for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) { - playbackSpeedPopupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE, - formatSpeed(PLAYBACK_SPEEDS[i])); - } - playbackSpeed.setText(formatSpeed(getPlaybackSpeed())); - playbackSpeedPopupMenu.setOnMenuItemClickListener(this); - playbackSpeedPopupMenu.setOnDismissListener(this); - } - - private void buildCaptionMenu(final List availableLanguages) { - if (captionPopupMenu == null) { - return; - } - captionPopupMenu.getMenu().removeGroup(captionPopupMenuGroupId); - - final String userPreferredLanguage = PreferenceManager.getDefaultSharedPreferences(context) - .getString(context.getString(R.string.caption_user_set_key), null); - /* - * only search for autogenerated cc as fallback - * if "(auto-generated)" was not already selected - * we are only looking for "(" instead of "(auto-generated)" to hopefully get all - * internationalized variants such as "(automatisch-erzeugt)" and so on - */ - boolean searchForAutogenerated = userPreferredLanguage != null - && !userPreferredLanguage.contains("("); - - // Add option for turning off caption - final MenuItem captionOffItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, - 0, Menu.NONE, R.string.caption_none); - captionOffItem.setOnMenuItemClickListener(menuItem -> { - final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); - if (textRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setParameters(trackSelector.buildUponParameters() - .setRendererDisabled(textRendererIndex, true)); - } - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - prefs.edit().remove(context.getString(R.string.caption_user_set_key)).commit(); - return true; - }); - - // Add all available captions - for (int i = 0; i < availableLanguages.size(); i++) { - final String captionLanguage = availableLanguages.get(i); - final MenuItem captionItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, - i + 1, Menu.NONE, captionLanguage); - captionItem.setOnMenuItemClickListener(menuItem -> { - final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); - if (textRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setPreferredTextLanguage(captionLanguage); - trackSelector.setParameters(trackSelector.buildUponParameters() - .setRendererDisabled(textRendererIndex, false)); - final SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(context); - prefs.edit().putString(context.getString(R.string.caption_user_set_key), - captionLanguage).commit(); - } - return true; - }); - // apply caption language from previous user preference - if (userPreferredLanguage != null - && (captionLanguage.equals(userPreferredLanguage) - || (searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage)) - || (userPreferredLanguage.contains("(") && captionLanguage.startsWith( - userPreferredLanguage.substring(0, userPreferredLanguage.indexOf('(')))))) { - final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT); - if (textRendererIndex != RENDERER_UNAVAILABLE) { - trackSelector.setPreferredTextLanguage(captionLanguage); - trackSelector.setParameters(trackSelector.buildUponParameters() - .setRendererDisabled(textRendererIndex, false)); - } - searchForAutogenerated = false; - } - } - captionPopupMenu.setOnDismissListener(this); - } - - private void updateStreamRelatedViews() { - if (getCurrentMetadata() == null) { - return; - } - - final MediaSourceTag tag = getCurrentMetadata(); - final StreamInfo metadata = tag.getMetadata(); - - binding.qualityTextView.setVisibility(View.GONE); - binding.playbackSpeed.setVisibility(View.GONE); - - binding.playbackEndTime.setVisibility(View.GONE); - binding.playbackLiveSync.setVisibility(View.GONE); - - switch (metadata.getStreamType()) { - case AUDIO_STREAM: - binding.surfaceView.setVisibility(View.GONE); - binding.endScreen.setVisibility(View.VISIBLE); - binding.playbackEndTime.setVisibility(View.VISIBLE); - break; - - case AUDIO_LIVE_STREAM: - binding.surfaceView.setVisibility(View.GONE); - binding.endScreen.setVisibility(View.VISIBLE); - binding.playbackLiveSync.setVisibility(View.VISIBLE); - break; - - case LIVE_STREAM: - binding.surfaceView.setVisibility(View.VISIBLE); - binding.endScreen.setVisibility(View.GONE); - binding.playbackLiveSync.setVisibility(View.VISIBLE); - break; - - case VIDEO_STREAM: - if (metadata.getVideoStreams().size() + metadata.getVideoOnlyStreams().size() - == 0) { - break; - } - - availableStreams = tag.getSortedAvailableVideoStreams(); - selectedStreamIndex = tag.getSelectedVideoStreamIndex(); - buildQualityMenu(); - - binding.qualityTextView.setVisibility(View.VISIBLE); - binding.surfaceView.setVisibility(View.VISIBLE); - default: - binding.endScreen.setVisibility(View.GONE); - binding.playbackEndTime.setVisibility(View.VISIBLE); - break; - } - - buildPlaybackSpeedMenu(); - binding.playbackSpeed.setVisibility(View.VISIBLE); - } - - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - - protected abstract VideoPlaybackResolver.QualityResolver getQualityResolver(); - - @Override - protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { - super.onMetadataChanged(tag); - updateStreamRelatedViews(); - } - - @Override - @Nullable - public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - return resolver.resolve(info); - } - - /*////////////////////////////////////////////////////////////////////////// - // States Implementation - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onBlocked() { - super.onBlocked(); - - controlsVisibilityHandler.removeCallbacksAndMessages(null); - animateView(binding.playbackControlRoot, false, DEFAULT_CONTROLS_DURATION); - - playbackSeekBar.setEnabled(false); - playbackSeekBar.getThumb().setColorFilter(new PorterDuffColorFilter(Color.RED, - PorterDuff.Mode.SRC_IN)); - - binding.loadingPanel.setBackgroundColor(Color.BLACK); - animateView(binding.loadingPanel, true, 0); - animateView(binding.surfaceForeground, true, 100); - } - - @Override - public void onPlaying() { - super.onPlaying(); - - updateStreamRelatedViews(); - - showAndAnimateControl(-1, true); - - playbackSeekBar.setEnabled(true); - playbackSeekBar.getThumb().setColorFilter(new PorterDuffColorFilter(Color.RED, - PorterDuff.Mode.SRC_IN)); - - binding.loadingPanel.setVisibility(View.GONE); - - animateView(binding.currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, - 200); - } - - @Override - public void onBuffering() { - if (DEBUG) { - Log.d(TAG, "onBuffering() called"); - } - binding.loadingPanel.setBackgroundColor(Color.TRANSPARENT); - } - - @Override - public void onPaused() { - if (DEBUG) { - Log.d(TAG, "onPaused() called"); - } - showControls(400); - binding.loadingPanel.setVisibility(View.GONE); - } - - @Override - public void onPausedSeek() { - if (DEBUG) { - Log.d(TAG, "onPausedSeek() called"); - } - showAndAnimateControl(-1, true); - } - - @Override - public void onCompleted() { - super.onCompleted(); - - showControls(500); - animateView(binding.currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, - 200); - binding.loadingPanel.setVisibility(View.GONE); - - animateView(binding.surfaceForeground, true, 100); - } - - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Video Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onTracksChanged(@NonNull final TrackGroupArray trackGroups, - @NonNull final TrackSelectionArray trackSelections) { - super.onTracksChanged(trackGroups, trackSelections); - onTextTrackUpdate(); - } - - @Override - public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) { - super.onPlaybackParametersChanged(playbackParameters); - playbackSpeed.setText(formatSpeed(playbackParameters.speed)); - } - - @Override - public void onVideoSizeChanged(final int width, final int height, - final int unappliedRotationDegrees, - final float pixelWidthHeightRatio) { - if (DEBUG) { - Log.d(TAG, "onVideoSizeChanged() called with: " - + "width / height = [" + width + " / " + height - + " = " + (((float) width) / height) + "], " - + "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], " - + "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]"); - } - binding.surfaceView.setAspectRatio(((float) width) / height); - } - - @Override - public void onRenderedFirstFrame() { - animateView(binding.surfaceForeground, false, 100); - } - - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Track Updates - //////////////////////////////////////////////////////////////////////////*/ - - private void onTextTrackUpdate() { - final int textRenderer = getRendererIndex(C.TRACK_TYPE_TEXT); - - if (binding == null) { - return; - } - if (trackSelector.getCurrentMappedTrackInfo() == null - || textRenderer == RENDERER_UNAVAILABLE) { - binding.captionTextView.setVisibility(View.GONE); - return; - } - - final TrackGroupArray textTracks = trackSelector.getCurrentMappedTrackInfo() - .getTrackGroups(textRenderer); - - // Extract all loaded languages - final List availableLanguages = new ArrayList<>(textTracks.length); - for (int i = 0; i < textTracks.length; i++) { - final TrackGroup textTrack = textTracks.get(i); - if (textTrack.length > 0 && textTrack.getFormat(0) != null) { - availableLanguages.add(textTrack.getFormat(0).language); - } - } - - // Normalize mismatching language strings - final String preferredLanguage = trackSelector.getPreferredTextLanguage(); - // Build UI - buildCaptionMenu(availableLanguages); - if (trackSelector.getParameters().getRendererDisabled(textRenderer) - || preferredLanguage == null || (!availableLanguages.contains(preferredLanguage) - && !containsCaseInsensitive(availableLanguages, preferredLanguage))) { - binding.captionTextView.setText(R.string.caption_none); - } else { - binding.captionTextView.setText(preferredLanguage); - } - binding.captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE - : View.VISIBLE); - } - - /*////////////////////////////////////////////////////////////////////////// - // General Player - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onPrepared(final boolean playWhenReady) { - if (DEBUG) { - Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); - } - - playbackSeekBar.setMax((int) simpleExoPlayer.getDuration()); - binding.playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration())); - playbackSpeed.setText(formatSpeed(getPlaybackSpeed())); - - super.onPrepared(playWhenReady); - } - - @Override - public void destroy() { - super.destroy(); - if (binding != null) { - binding.endScreen.setImageBitmap(null); - } - } - - @Override - public void onUpdateProgress(final int currentProgress, final int duration, - final int bufferPercent) { - if (!isPrepared()) { - return; - } - - if (duration != playbackSeekBar.getMax()) { - binding.playbackEndTime.setText(getTimeString(duration)); - playbackSeekBar.setMax(duration); - } - if (currentState != STATE_PAUSED) { - if (currentState != STATE_PAUSED_SEEK) { - playbackSeekBar.setProgress(currentProgress); - } - binding.playbackCurrentTime.setText(getTimeString(currentProgress)); - } - if (simpleExoPlayer.isLoading() || bufferPercent > 90) { - playbackSeekBar.setSecondaryProgress( - (int) (playbackSeekBar.getMax() * ((float) bufferPercent / 100))); - } - if (DEBUG && bufferPercent % 20 == 0) { //Limit log - Log.d(TAG, "updateProgress() called with: " - + "isVisible = " + isControlsVisible() + ", " - + "currentProgress = [" + currentProgress + "], " - + "duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]"); - } - binding.playbackLiveSync.setClickable(!isLiveEdge()); - } - - @Override - public void onLoadingComplete(final String imageUri, final View view, - final Bitmap loadedImage) { - super.onLoadingComplete(imageUri, view, loadedImage); - if (loadedImage != null) { - binding.endScreen.setImageBitmap(loadedImage); - } - } - - protected void toggleFullscreen() { - changeState(STATE_BLOCKED); - } - - @Override - public void onFastRewind() { - super.onFastRewind(); - showAndAnimateControl(R.drawable.ic_fast_rewind_white_24dp, true); - } - - @Override - public void onFastForward() { - super.onFastForward(); - showAndAnimateControl(R.drawable.ic_fast_forward_white_24dp, true); - } - - /*////////////////////////////////////////////////////////////////////////// - // OnClick related - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onClick(final View v) { - if (DEBUG) { - Log.d(TAG, "onClick() called with: v = [" + v + "]"); - } - if (v.getId() == binding.qualityTextView.getId()) { - onQualitySelectorClicked(); - } else if (v.getId() == binding.playbackSpeed.getId()) { - onPlaybackSpeedClicked(); - } else if (v.getId() == binding.resizeTextView.getId()) { - onResizeClicked(); - } else if (v.getId() == binding.captionTextView.getId()) { - onCaptionClicked(); - } else if (v.getId() == binding.playbackLiveSync.getId()) { - seekToDefault(); - } - } - - /** - * Called when an item of the quality selector or the playback speed selector is selected. - */ - @Override - public boolean onMenuItemClick(final MenuItem menuItem) { - if (DEBUG) { - Log.d(TAG, "onMenuItemClick() called with: " - + "menuItem = [" + menuItem + "], " - + "menuItem.getItemId = [" + menuItem.getItemId() + "]"); - } - - if (qualityPopupMenuGroupId == menuItem.getGroupId()) { - final int menuItemIndex = menuItem.getItemId(); - if (selectedStreamIndex == menuItemIndex || availableStreams == null - || availableStreams.size() <= menuItemIndex) { - return true; - } - - final String newResolution = availableStreams.get(menuItemIndex).resolution; - setRecovery(); - setPlaybackQuality(newResolution); - reload(); - - qualityTextView.setText(menuItem.getTitle()); - return true; - } else if (playbackSpeedPopupMenuGroupId == menuItem.getGroupId()) { - final int speedIndex = menuItem.getItemId(); - final float speed = PLAYBACK_SPEEDS[speedIndex]; - - setPlaybackSpeed(speed); - playbackSpeed.setText(formatSpeed(speed)); - } - - return false; - } - - /** - * Called when some popup menu is dismissed. - */ - @Override - public void onDismiss(final PopupMenu menu) { - if (DEBUG) { - Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]"); - } - isSomePopupMenuVisible = false; - if (getSelectedVideoStream() != null) { - qualityTextView.setText(getSelectedVideoStream().resolution); - } - } - - public void onQualitySelectorClicked() { - if (DEBUG) { - Log.d(TAG, "onQualitySelectorClicked() called"); - } - qualityPopupMenu.show(); - isSomePopupMenuVisible = true; - - final VideoStream videoStream = getSelectedVideoStream(); - if (videoStream != null) { - final String qualityText = MediaFormat.getNameById(videoStream.getFormatId()) + " " - + videoStream.resolution; - qualityTextView.setText(qualityText); - } - - wasPlaying = simpleExoPlayer.getPlayWhenReady(); - } - - public void onPlaybackSpeedClicked() { - if (DEBUG) { - Log.d(TAG, "onPlaybackSpeedClicked() called"); - } - playbackSpeedPopupMenu.show(); - isSomePopupMenuVisible = true; - } - - private void onCaptionClicked() { - if (DEBUG) { - Log.d(TAG, "onCaptionClicked() called"); - } - captionPopupMenu.show(); - isSomePopupMenuVisible = true; - } - - void onResizeClicked() { - if (binding != null) { - final int currentResizeMode = binding.surfaceView.getResizeMode(); - final int newResizeMode = nextResizeMode(currentResizeMode); - setResizeMode(newResizeMode); - } - } - - protected void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) { - binding.surfaceView.setResizeMode(resizeMode); - ((TextView) binding.resizeTextView).setText(PlayerHelper.resizeTypeOf(context, - resizeMode)); - } - - protected abstract int nextResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode); - - /*////////////////////////////////////////////////////////////////////////// - // SeekBar Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onProgressChanged(final SeekBar seekBar, final int progress, - final boolean fromUser) { - if (DEBUG && fromUser) { - Log.d(TAG, "onProgressChanged() called with: " - + "seekBar = [" + seekBar + "], progress = [" + progress + "]"); - } - if (fromUser) { - binding.currentDisplaySeek.setText(getTimeString(progress)); - } - } - - @Override - public void onStartTrackingTouch(final SeekBar seekBar) { - if (DEBUG) { - Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]"); - } - if (getCurrentState() != STATE_PAUSED_SEEK) { - changeState(STATE_PAUSED_SEEK); - } - - wasPlaying = simpleExoPlayer.getPlayWhenReady(); - if (isPlaying()) { - simpleExoPlayer.setPlayWhenReady(false); - } - - showControls(0); - animateView(binding.currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true, - DEFAULT_CONTROLS_DURATION); - } - - @Override - public void onStopTrackingTouch(final SeekBar seekBar) { - if (DEBUG) { - Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]"); - } - - seekTo(seekBar.getProgress()); - if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) { - simpleExoPlayer.setPlayWhenReady(true); - } - - binding.playbackCurrentTime.setText(getTimeString(seekBar.getProgress())); - animateView(binding.currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, - 200); - - if (getCurrentState() == STATE_PAUSED_SEEK) { - changeState(STATE_BUFFERING); - } - if (!isProgressLoopRunning()) { - startProgressLoop(); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - public int getRendererIndex(final int trackIndex) { - if (simpleExoPlayer == null) { - return RENDERER_UNAVAILABLE; - } - - for (int t = 0; t < simpleExoPlayer.getRendererCount(); t++) { - if (simpleExoPlayer.getRendererType(t) == trackIndex) { - return t; - } - } - - return RENDERER_UNAVAILABLE; - } - - public boolean isControlsVisible() { - return binding != null - && binding.playbackControlRoot.getVisibility() == View.VISIBLE; - } - - /** - * Show a animation, and depending on goneOnEnd, will stay on the screen or be gone. - * - * @param drawableId the drawable that will be used to animate, - * pass -1 to clear any animation that is visible - * @param goneOnEnd will set the animation view to GONE on the end of the animation - */ - public void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) { - if (DEBUG) { - Log.d(TAG, "showAndAnimateControl() called with: " - + "drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]"); - } - if (controlViewAnimator != null && controlViewAnimator.isRunning()) { - if (DEBUG) { - Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning"); - } - controlViewAnimator.end(); - } - - if (drawableId == -1) { - if (binding.controlAnimationView.getVisibility() == View.VISIBLE) { - controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder( - binding.controlAnimationView, - PropertyValuesHolder.ofFloat(View.ALPHA, 1.0f, 0.0f), - PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1.0f), - PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1.0f) - ).setDuration(DEFAULT_CONTROLS_DURATION); - controlViewAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(final Animator animation) { - binding.controlAnimationView.setVisibility(View.GONE); - } - }); - controlViewAnimator.start(); - } - return; - } - - final float scaleFrom = goneOnEnd ? 1f : 1f; - final float scaleTo = goneOnEnd ? 1.8f : 1.4f; - final float alphaFrom = goneOnEnd ? 1f : 0f; - final float alphaTo = goneOnEnd ? 0f : 1f; - - - controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder( - binding.controlAnimationView, - PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo), - PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo), - PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo) - ); - controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500); - controlViewAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(final Animator animation) { - binding.controlAnimationView.setVisibility(goneOnEnd ? View.GONE - : View.VISIBLE); - } - }); - - binding.controlAnimationView.setVisibility(View.VISIBLE); - binding.controlAnimationView.setImageDrawable(AppCompatResources.getDrawable(context, - drawableId)); - controlViewAnimator.start(); - } - - public boolean isSomePopupMenuVisible() { - return isSomePopupMenuVisible; - } - - public void showControlsThenHide() { - if (DEBUG) { - Log.d(TAG, "showControlsThenHide() called"); - } - - final int hideTime = binding.playbackControlRoot.isInTouchMode() - ? DEFAULT_CONTROLS_HIDE_TIME - : DPAD_CONTROLS_HIDE_TIME; - - showHideShadow(true, DEFAULT_CONTROLS_DURATION, 0); - animateView(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION, 0, - () -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime)); - } - - public void showControls(final long duration) { - if (DEBUG) { - Log.d(TAG, "showControls() called"); - } - controlsVisibilityHandler.removeCallbacksAndMessages(null); - showHideShadow(true, duration, 0); - animateView(binding.playbackControlRoot, true, duration); - } - - public void safeHideControls(final long duration, final long delay) { - if (DEBUG) { - Log.d(TAG, "safeHideControls() called with: delay = [" + delay + "]"); - } - if (binding.getRoot().isInTouchMode()) { - controlsVisibilityHandler.removeCallbacksAndMessages(null); - controlsVisibilityHandler.postDelayed( - () -> animateView(binding.playbackControlRoot, false, duration), - delay); - } - } - - public void hideControls(final long duration, final long delay) { - if (DEBUG) { - Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); - } - controlsVisibilityHandler.removeCallbacksAndMessages(null); - controlsVisibilityHandler.postDelayed(() -> { - showHideShadow(false, duration, 0); - animateView(binding.playbackControlRoot, false, duration); - }, delay); - } - - public void hideControlsAndButton(final long duration, final long delay, final View button) { - if (DEBUG) { - Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); - } - controlsVisibilityHandler.removeCallbacksAndMessages(null); - controlsVisibilityHandler - .postDelayed(hideControlsAndButtonHandler(duration, button), delay); - } - - private Runnable hideControlsAndButtonHandler(final long duration, final View videoPlayPause) { - return () -> { - videoPlayPause.setVisibility(View.INVISIBLE); - animateView(binding.playbackControlRoot, false, duration); - }; - } - - void showHideShadow(final boolean show, final long duration, final long delay) { - animateView(binding.playerTopShadow, show, duration, delay, null); - animateView(binding.playerBottomShadow, show, duration, delay, null); - } - - public abstract void hideSystemUIIfNeeded(); - - /*////////////////////////////////////////////////////////////////////////// - // Getters and Setters - //////////////////////////////////////////////////////////////////////////*/ - - @Nullable - public String getPlaybackQuality() { - return resolver.getPlaybackQuality(); - } - - public void setPlaybackQuality(final String quality) { - this.resolver.setPlaybackQuality(quality); - } - - public ExpandableSurfaceView getSurfaceView() { - return binding.surfaceView; - } - - public boolean wasPlaying() { - return wasPlaying; - } - - @Nullable - public VideoStream getSelectedVideoStream() { - return (selectedStreamIndex >= 0 && availableStreams != null - && availableStreams.size() > selectedStreamIndex) - ? availableStreams.get(selectedStreamIndex) : null; - } - - public Handler getControlsVisibilityHandler() { - return controlsVisibilityHandler; - } - - @NonNull - public View getRootView() { - return binding.getRoot(); - } - - @NonNull - public View getLoadingPanel() { - return binding.loadingPanel; - } - - @NonNull - public View getPlaybackControlRoot() { - return binding.playbackControlRoot; - } - - @NonNull - public TextView getCurrentDisplaySeek() { - return binding.currentDisplaySeek; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java deleted file mode 100644 index 949b11374..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java +++ /dev/null @@ -1,2076 +0,0 @@ -/* - * Copyright 2017 Mauricio Colli - * Part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.schabi.newpipe.player; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.annotation.SuppressLint; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.database.ContentObserver; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.PixelFormat; -import android.net.Uri; -import android.os.Build; -import android.os.Handler; -import android.provider.Settings; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.TypedValue; -import android.view.GestureDetector; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.animation.AnticipateInterpolator; -import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.PopupMenu; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import android.widget.SeekBar; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.content.ContextCompat; -import androidx.core.view.DisplayCutoutCompat; -import androidx.core.view.ViewCompat; -import androidx.preference.PreferenceManager; -import androidx.recyclerview.widget.ItemTouchHelper; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.text.CaptionStyleCompat; -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; -import com.google.android.exoplayer2.ui.SubtitleView; -import com.nostra13.universalimageloader.core.assist.FailReason; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.databinding.PlayerBinding; -import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.VideoStream; -import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; -import org.schabi.newpipe.fragments.detail.VideoDetailFragment; -import org.schabi.newpipe.player.event.PlayerEventListener; -import org.schabi.newpipe.player.event.PlayerGestureListener; -import org.schabi.newpipe.player.event.PlayerServiceEventListener; -import org.schabi.newpipe.player.helper.PlaybackParameterDialog; -import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.playqueue.PlayQueue; -import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder; -import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder; -import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; -import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; -import org.schabi.newpipe.player.resolver.MediaSourceTag; -import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; -import org.schabi.newpipe.util.AnimationUtils; -import org.schabi.newpipe.util.DeviceUtils; -import org.schabi.newpipe.util.KoreUtil; -import org.schabi.newpipe.util.ListHelper; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.ShareUtils; - -import java.util.List; - -import static org.schabi.newpipe.extractor.ServiceList.YouTube; -import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE; -import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD; -import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND; -import static org.schabi.newpipe.player.MainPlayer.ACTION_OPEN_CONTROLS; -import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT; -import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE; -import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS; -import static org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION; -import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; -import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; -import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND; -import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked; -import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; -import static org.schabi.newpipe.util.AnimationUtils.animateRotation; -import static org.schabi.newpipe.util.AnimationUtils.animateView; -import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex; -import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - -/** - * Unified UI for all players. - * - * @author mauriciocolli - */ - -public class VideoPlayerImpl extends VideoPlayer - implements View.OnLayoutChangeListener, - PlaybackParameterDialog.Callback, - View.OnLongClickListener { - private static final String TAG = ".VideoPlayerImpl"; - - static final String POPUP_SAVED_WIDTH = "popup_saved_width"; - static final String POPUP_SAVED_X = "popup_saved_x"; - static final String POPUP_SAVED_Y = "popup_saved_y"; - private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS - | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; - - private static final float MAX_GESTURE_LENGTH = 0.75f; - - private ItemTouchHelper itemTouchHelper; - - private boolean queueVisible; - private MainPlayer.PlayerType playerType = MainPlayer.PlayerType.VIDEO; - - private int maxGestureLength; - - private boolean audioOnly = false; - private boolean isFullscreen = false; - private boolean isVerticalVideo = false; - private boolean fragmentIsVisible = false; - boolean shouldUpdateOnProgress; - - private final MainPlayer service; - private PlayerServiceEventListener fragmentListener; - private PlayerEventListener activityListener; - private GestureDetector gestureDetector; - private final SharedPreferences defaultPreferences; - private ContentObserver settingsContentObserver; - @NonNull - private final AudioPlaybackResolver resolver; - - // Popup - private WindowManager.LayoutParams popupLayoutParams; - public WindowManager windowManager; - - private PlayerPopupCloseOverlayBinding closeOverlayBinding; - - public boolean isPopupClosing = false; - - private float screenWidth; - private float screenHeight; - private float popupWidth; - private float popupHeight; - private float minimumWidth; - private float minimumHeight; - private float maximumWidth; - private float maximumHeight; - // Popup end - - - @Override - public void handleIntent(final Intent intent) { - if (intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) == null) { - return; - } - - final MainPlayer.PlayerType oldPlayerType = playerType; - choosePlayerTypeFromIntent(intent); - audioOnly = audioPlayerSelected(); - - // We need to setup audioOnly before super(), see "sourceOf" - super.handleIntent(intent); - - if (oldPlayerType != playerType && playQueue != null) { - // If playerType changes from one to another we should reload the player - // (to disable/enable video stream or to set quality) - setRecovery(); - reload(); - } - - setupElementsVisibility(); - setupElementsSize(); - - if (audioPlayerSelected()) { - service.removeViewFromParent(); - } else if (popupPlayerSelected()) { - getRootView().setVisibility(View.VISIBLE); - initPopup(); - initPopupCloseOverlay(); - binding.playPauseButton.requestFocus(); - } else { - getRootView().setVisibility(View.VISIBLE); - initVideoPlayer(); - onQueueClosed(); - // Android TV: without it focus will frame the whole player - binding.playPauseButton.requestFocus(); - - if (simpleExoPlayer.getPlayWhenReady()) { - onPlay(); - } else { - onPause(); - } - } - NavigationHelper.sendPlayerStartedEvent(service); - } - - VideoPlayerImpl(final MainPlayer service) { - super("MainPlayer" + TAG, service); - this.service = service; - this.shouldUpdateOnProgress = true; - this.windowManager = ContextCompat.getSystemService(service, WindowManager.class); - this.defaultPreferences = PreferenceManager.getDefaultSharedPreferences(service); - this.resolver = new AudioPlaybackResolver(context, dataSource); - } - - @SuppressLint("ClickableViewAccessibility") - @Override - public void initViews(@NonNull final PlayerBinding binding) { - super.initViews(binding); - - binding.titleTextView.setSelected(true); - binding.channelTextView.setSelected(true); - - // Prevent hiding of bottom sheet via swipe inside queue - binding.playQueue.setNestedScrollingEnabled(false); - } - - @Override - protected void setupSubtitleView(final @NonNull SubtitleView view, - final float captionScale, - @NonNull final CaptionStyleCompat captionStyle) { - if (popupPlayerSelected()) { - final float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f; - view.setFractionalTextSize(SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio); - } else { - final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); - final float captionRatioInverse = 20f + 4f * (1.0f - captionScale); - view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, - (float) minimumLength / captionRatioInverse); - } - view.setApplyEmbeddedStyles(captionStyle == CaptionStyleCompat.DEFAULT); - view.setStyle(captionStyle); - } - - /** - * This method ensures that popup and main players have different look. - * We use one layout for both players and need to decide what to show and what to hide. - * Additional measuring should be done inside {@link #setupElementsSize}. - */ - private void setupElementsVisibility() { - if (popupPlayerSelected()) { - binding.fullScreenButton.setVisibility(View.VISIBLE); - binding.screenRotationButton.setVisibility(View.GONE); - binding.resizeTextView.setVisibility(View.GONE); - binding.metadataView.setVisibility(View.GONE); - binding.queueButton.setVisibility(View.GONE); - binding.moreOptionsButton.setVisibility(View.GONE); - binding.topControls.setOrientation(LinearLayout.HORIZONTAL); - binding.primaryControls.getLayoutParams().width = - LinearLayout.LayoutParams.WRAP_CONTENT; - binding.secondaryControls.setAlpha(1.0f); - binding.secondaryControls.setVisibility(View.VISIBLE); - binding.secondaryControls.setTranslationY(0); - binding.share.setVisibility(View.GONE); - binding.playWithKodi.setVisibility(View.GONE); - binding.openInBrowser.setVisibility(View.GONE); - binding.switchMute.setVisibility(View.GONE); - binding.playerCloseButton.setVisibility(View.GONE); - binding.topControls.bringToFront(); - binding.topControls.setClickable(false); - binding.topControls.setFocusable(false); - binding.bottomControls.bringToFront(); - onQueueClosed(); - } else { - binding.fullScreenButton.setVisibility(View.GONE); - setupScreenRotationButton(); - binding.resizeTextView.setVisibility(View.VISIBLE); - binding.metadataView.setVisibility(View.VISIBLE); - binding.moreOptionsButton.setVisibility(View.VISIBLE); - binding.topControls.setOrientation(LinearLayout.VERTICAL); - binding.primaryControls.getLayoutParams().width = - LinearLayout.LayoutParams.MATCH_PARENT; - binding.secondaryControls.setVisibility(View.INVISIBLE); - binding.moreOptionsButton.setImageDrawable(AppCompatResources.getDrawable(service, - R.drawable.ic_expand_more_white_24dp)); - binding.share.setVisibility(View.VISIBLE); - showHideKodiButton(); - binding.openInBrowser.setVisibility(View.VISIBLE); - binding.switchMute.setVisibility(View.VISIBLE); - binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE); - // Top controls have a large minHeight which is allows to drag the player - // down in fullscreen mode (just larger area to make easy to locate by finger) - binding.topControls.setClickable(true); - binding.topControls.setFocusable(true); - } - if (!isFullscreen()) { - binding.titleTextView.setVisibility(View.GONE); - binding.channelTextView.setVisibility(View.GONE); - } else { - binding.titleTextView.setVisibility(View.VISIBLE); - binding.channelTextView.setVisibility(View.VISIBLE); - } - setMuteButton(binding.switchMute, isMuted()); - - animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION, 0); - } - - /** - * Changes padding, size of elements based on player selected right now. - * Popup player has small padding in comparison with the main player - */ - private void setupElementsSize() { - if (popupPlayerSelected()) { - final int controlsPadding = service.getResources() - .getDimensionPixelSize(R.dimen.player_popup_controls_padding); - final int buttonsPadding = service.getResources() - .getDimensionPixelSize(R.dimen.player_popup_buttons_padding); - binding.topControls.setPaddingRelative(controlsPadding, 0, controlsPadding, 0); - binding.bottomControls.setPaddingRelative(controlsPadding, 0, controlsPadding, 0); - binding.qualityTextView.setPadding(buttonsPadding, buttonsPadding, buttonsPadding, - buttonsPadding); - binding.playbackSpeed.setPadding(buttonsPadding, buttonsPadding, buttonsPadding, - buttonsPadding); - binding.captionTextView.setPadding(buttonsPadding, buttonsPadding, buttonsPadding, - buttonsPadding); - binding.playbackSpeed.setMinimumWidth(0); - } else if (videoPlayerSelected()) { - final int buttonsMinWidth = service.getResources() - .getDimensionPixelSize(R.dimen.player_main_buttons_min_width); - final int playerTopPadding = service.getResources() - .getDimensionPixelSize(R.dimen.player_main_top_padding); - final int controlsPadding = service.getResources() - .getDimensionPixelSize(R.dimen.player_main_controls_padding); - final int buttonsPadding = service.getResources() - .getDimensionPixelSize(R.dimen.player_main_buttons_padding); - binding.topControls.setPaddingRelative(controlsPadding, playerTopPadding, - controlsPadding, 0); - binding.bottomControls.setPaddingRelative(controlsPadding, 0, controlsPadding, 0); - binding.qualityTextView.setPadding(buttonsPadding, buttonsPadding, buttonsPadding, - buttonsPadding); - binding.playbackSpeed.setPadding(buttonsPadding, buttonsPadding, buttonsPadding, - buttonsPadding); - binding.playbackSpeed.setMinimumWidth(buttonsMinWidth); - binding.captionTextView.setPadding(buttonsPadding, buttonsPadding, buttonsPadding, - buttonsPadding); - } - } - - @Override - public void initListeners() { - super.initListeners(); - - final PlayerGestureListener listener = new PlayerGestureListener(this, service); - gestureDetector = new GestureDetector(context, listener); - getRootView().setOnTouchListener(listener); - - binding.queueButton.setOnClickListener(this); - binding.repeatButton.setOnClickListener(this); - binding.shuffleButton.setOnClickListener(this); - - binding.playPauseButton.setOnClickListener(this); - binding.playPreviousButton.setOnClickListener(this); - binding.playNextButton.setOnClickListener(this); - - binding.moreOptionsButton.setOnClickListener(this); - binding.moreOptionsButton.setOnLongClickListener(this); - binding.share.setOnClickListener(this); - binding.fullScreenButton.setOnClickListener(this); - binding.screenRotationButton.setOnClickListener(this); - binding.playWithKodi.setOnClickListener(this); - binding.openInBrowser.setOnClickListener(this); - binding.playerCloseButton.setOnClickListener(this); - binding.switchMute.setOnClickListener(this); - - settingsContentObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(final boolean selfChange) { - setupScreenRotationButton(); - } - }; - service.getContentResolver().registerContentObserver( - Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, - settingsContentObserver); - getRootView().addOnLayoutChangeListener(this); - - ViewCompat.setOnApplyWindowInsetsListener(binding.playQueuePanel, - (view, windowInsets) -> { - final DisplayCutoutCompat cutout = windowInsets.getDisplayCutout(); - if (cutout != null) { - view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), - cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); - } - return windowInsets; - }); - - // PlaybackControlRoot already consumed window insets but we should pass them to - // player_overlays too. Without it they will be off-centered - binding.playbackControlRoot.addOnLayoutChangeListener( - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> - binding.playerOverlays.setPadding( - v.getPaddingLeft(), - v.getPaddingTop(), - v.getPaddingRight(), - v.getPaddingBottom())); - } - - public boolean onKeyDown(final int keyCode) { - switch (keyCode) { - default: - break; - case KeyEvent.KEYCODE_SPACE: - if (isFullscreen) { - onPlayPause(); - } - break; - case KeyEvent.KEYCODE_BACK: - if (DeviceUtils.isTv(service) && isControlsVisible()) { - hideControls(0, 0); - return true; - } - break; - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_RIGHT: - case KeyEvent.KEYCODE_DPAD_CENTER: - if (getRootView().hasFocus() && !binding.playbackControlRoot.hasFocus()) { - // do not interfere with focus in playlist etc. - return false; - } - - if (getCurrentState() == BasePlayer.STATE_BLOCKED) { - return true; - } - - if (!isControlsVisible()) { - if (!queueVisible) { - binding.playPauseButton.requestFocus(); - } - showControlsThenHide(); - showSystemUIPartially(); - return true; - } else { - hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); - } - break; - } - - return false; - } - - public AppCompatActivity getParentActivity() { - // ! instanceof ViewGroup means that view was added via windowManager for Popup - if (binding == null || binding.getRoot().getParent() == null - || !(binding.getRoot().getParent() instanceof ViewGroup)) { - return null; - } - - final ViewGroup parent = (ViewGroup) binding.getRoot().getParent(); - return (AppCompatActivity) parent.getContext(); - } - - /*////////////////////////////////////////////////////////////////////////// - // View - //////////////////////////////////////////////////////////////////////////*/ - - private void setRepeatModeButton(final ImageButton imageButton, final int repeatMode) { - switch (repeatMode) { - case Player.REPEAT_MODE_OFF: - imageButton.setImageResource(R.drawable.exo_controls_repeat_off); - break; - case Player.REPEAT_MODE_ONE: - imageButton.setImageResource(R.drawable.exo_controls_repeat_one); - break; - case Player.REPEAT_MODE_ALL: - imageButton.setImageResource(R.drawable.exo_controls_repeat_all); - break; - } - } - - private void setShuffleButton(final ImageButton button, final boolean shuffled) { - final int shuffleAlpha = shuffled ? 255 : 77; - button.setImageAlpha(shuffleAlpha); - } - - //////////////////////////////////////////////////////////////////////////// - // Playback Parameters Listener - //////////////////////////////////////////////////////////////////////////// - - @Override - public void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch, - final boolean playbackSkipSilence) { - setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); - } - - @Override - public void onVideoSizeChanged(final int width, final int height, - final int unappliedRotationDegrees, - final float pixelWidthHeightRatio) { - super.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); - isVerticalVideo = width < height; - prepareOrientation(); - setupScreenRotationButton(); - } - - /*////////////////////////////////////////////////////////////////////////// - // ExoPlayer Video Listener - //////////////////////////////////////////////////////////////////////////*/ - - void onShuffleOrRepeatModeChanged() { - updatePlaybackButtons(); - updatePlayback(); - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - @Override - public void onRepeatModeChanged(final int i) { - super.onRepeatModeChanged(i); - onShuffleOrRepeatModeChanged(); - } - - @Override - public void onShuffleClicked() { - super.onShuffleClicked(); - onShuffleOrRepeatModeChanged(); - - } - - /*////////////////////////////////////////////////////////////////////////// - // Playback Listener - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onPlayerError(final ExoPlaybackException error) { - super.onPlayerError(error); - - if (fragmentListener != null) { - fragmentListener.onPlayerError(error); - } - } - - @Override - public void onTimelineChanged(final Timeline timeline, final int reason) { - super.onTimelineChanged(timeline, reason); - // force recreate notification to ensure seek bar is shown when preparation finishes - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); - } - - @Override - protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { - super.onMetadataChanged(tag); - - showHideKodiButton(); - - binding.titleTextView.setText(tag.getMetadata().getName()); - binding.channelTextView.setText(tag.getMetadata().getUploaderName()); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - updateMetadata(); - } - - @Override - public void onPlaybackShutdown() { - if (DEBUG) { - Log.d(TAG, "onPlaybackShutdown() called"); - } - service.onDestroy(); - } - - @Override - public void onMuteUnmuteButtonClicked() { - super.onMuteUnmuteButtonClicked(); - updatePlayback(); - setMuteButton(binding.switchMute, isMuted()); - } - - @Override - public void onUpdateProgress(final int currentProgress, - final int duration, final int bufferPercent) { - super.onUpdateProgress(currentProgress, duration, bufferPercent); - updateProgress(currentProgress, duration, bufferPercent); - - final boolean showThumbnail = - sharedPreferences.getBoolean( - context.getString(R.string.show_thumbnail_key), - true); - // setMetadata only updates the metadata when any of the metadata keys are null - mediaSessionManager.setMetadata(getVideoTitle(), getUploaderName(), - showThumbnail ? getThumbnail() : null, duration); - } - - @Override - public void onPlayQueueEdited() { - updatePlayback(); - showOrHideButtons(); - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - @Override - @Nullable - public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { - // For LiveStream or video/popup players we can use super() method - // but not for audio player - if (!audioOnly) { - return super.sourceOf(item, info); - } else { - return resolver.resolve(info); - } - } - - @Override - public void onPlayPrevious() { - super.onPlayPrevious(); - triggerProgressUpdate(); - } - - @Override - public void onPlayNext() { - super.onPlayNext(); - triggerProgressUpdate(); - } - - @Override - protected void initPlayback(@NonNull final PlayQueue queue, final int repeatMode, - final float playbackSpeed, final float playbackPitch, - final boolean playbackSkipSilence, - final boolean playOnReady, final boolean isMuted) { - super.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playOnReady, isMuted); - updateQueue(); - } - - /*////////////////////////////////////////////////////////////////////////// - // Player Overrides - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void toggleFullscreen() { - if (DEBUG) { - Log.d(TAG, "toggleFullscreen() called"); - } - if (popupPlayerSelected() - || simpleExoPlayer == null - || getCurrentMetadata() == null - || fragmentListener == null) { - return; - } - - isFullscreen = !isFullscreen; - if (!isFullscreen) { - // Apply window insets because Android will not do it when orientation changes - // from landscape to portrait (open vertical video to reproduce) - getPlaybackControlRoot().setPadding(0, 0, 0, 0); - } else { - // Android needs tens milliseconds to send new insets but a user is able to see - // how controls changes it's position from `0` to `nav bar height` padding. - // So just hide the controls to hide this visual inconsistency - hideControls(0, 0); - } - fragmentListener.onFullscreenStateChanged(isFullscreen()); - - if (!isFullscreen()) { - binding.titleTextView.setVisibility(View.GONE); - binding.channelTextView.setVisibility(View.GONE); - binding.playerCloseButton.setVisibility(videoPlayerSelected() - ? View.VISIBLE : View.GONE); - } else { - binding.titleTextView.setVisibility(View.VISIBLE); - binding.channelTextView.setVisibility(View.VISIBLE); - binding.playerCloseButton.setVisibility(View.GONE); - } - setupScreenRotationButton(); - } - - @Override - public void onClick(final View v) { - super.onClick(v); - if (v.getId() == binding.playPauseButton.getId()) { - onPlayPause(); - } else if (v.getId() == binding.playPreviousButton.getId()) { - onPlayPrevious(); - } else if (v.getId() == binding.playNextButton.getId()) { - onPlayNext(); - } else if (v.getId() == binding.queueButton.getId()) { - onQueueClicked(); - return; - } else if (v.getId() == binding.repeatButton.getId()) { - onRepeatClicked(); - return; - } else if (v.getId() == binding.shuffleButton.getId()) { - onShuffleClicked(); - return; - } else if (v.getId() == binding.moreOptionsButton.getId()) { - onMoreOptionsClicked(); - } else if (v.getId() == binding.share.getId()) { - onShareClicked(); - } else if (v.getId() == binding.playWithKodi.getId()) { - onPlayWithKodiClicked(); - } else if (v.getId() == binding.openInBrowser.getId()) { - onOpenInBrowserClicked(); - } else if (v.getId() == binding.fullScreenButton.getId()) { - setRecovery(); - NavigationHelper.playOnMainPlayer(context, getPlayQueue(), true); - return; - } else if (v.getId() == binding.screenRotationButton.getId()) { - // Only if it's not a vertical video or vertical video but in landscape with locked - // orientation a screen orientation can be changed automatically - if (!isVerticalVideo - || (service.isLandscape() && globalScreenOrientationLocked(service))) { - fragmentListener.onScreenRotationButtonClicked(); - } else { - toggleFullscreen(); - } - } else if (v.getId() == binding.switchMute.getId()) { - onMuteUnmuteButtonClicked(); - } else if (v.getId() == binding.playerCloseButton.getId()) { - service.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER)); - } - - if (getCurrentState() != STATE_COMPLETED) { - getControlsVisibilityHandler().removeCallbacksAndMessages(null); - showHideShadow(true, DEFAULT_CONTROLS_DURATION, 0); - animateView(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION, 0, - () -> { - if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { - if (v.getId() == binding.playPauseButton.getId() - // Hide controls in fullscreen immediately - || (v.getId() == binding.screenRotationButton.getId() - && isFullscreen)) { - hideControls(0, 0); - } else { - hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); - } - } - }); - } - } - - @Override - public boolean onLongClick(final View v) { - if (v.getId() == binding.moreOptionsButton.getId() && isFullscreen()) { - fragmentListener.onMoreOptionsLongClicked(); - hideControls(0, 0); - hideSystemUIIfNeeded(); - } - return true; - } - - private void onQueueClicked() { - queueVisible = true; - - hideSystemUIIfNeeded(); - buildQueue(); - updatePlaybackButtons(); - - hideControls(0, 0); - binding.playQueuePanel.requestFocus(); - animateView(binding.playQueuePanel, SLIDE_AND_ALPHA, true, - DEFAULT_CONTROLS_DURATION); - - binding.playQueue.scrollToPosition(playQueue.getIndex()); - } - - public void onQueueClosed() { - if (!queueVisible) { - return; - } - - animateView(binding.playQueuePanel, SLIDE_AND_ALPHA, false, - DEFAULT_CONTROLS_DURATION, 0, () -> { - // Even when queueLayout is GONE it receives touch events - // and ruins normal behavior of the app. This line fixes it - binding.playQueuePanel - .setTranslationY(-binding.playQueuePanel.getHeight() * 5); - }); - queueVisible = false; - binding.playPauseButton.requestFocus(); - } - - private void onMoreOptionsClicked() { - if (DEBUG) { - Log.d(TAG, "onMoreOptionsClicked() called"); - } - - final boolean isMoreControlsVisible = - binding.secondaryControls.getVisibility() == View.VISIBLE; - - animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION, - isMoreControlsVisible ? 0 : 180); - animateView(binding.secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, - DEFAULT_CONTROLS_DURATION, 0, - () -> { - // Fix for a ripple effect on background drawable. - // When view returns from GONE state it takes more milliseconds than returning - // from INVISIBLE state. And the delay makes ripple background end to fast - if (isMoreControlsVisible) { - binding.secondaryControls.setVisibility(View.INVISIBLE); - } - }); - showControls(DEFAULT_CONTROLS_DURATION); - } - - private void onShareClicked() { - // share video at the current time (youtube.com/watch?v=ID&t=SECONDS) - // Timestamp doesn't make sense in a live stream so drop it - - final int ts = playbackSeekBar.getProgress() / 1000; - final MediaSourceTag metadata = getCurrentMetadata(); - String videoUrl = getVideoUrl(); - if (!isLive() && ts >= 0 && metadata != null - && metadata.getMetadata().getServiceId() == YouTube.getServiceId()) { - videoUrl += ("&t=" + ts); - } - ShareUtils.shareUrl(service, - getVideoTitle(), - videoUrl); - } - - private void onPlayWithKodiClicked() { - if (getCurrentMetadata() == null) { - return; - } - onPause(); - try { - NavigationHelper.playWithKore(getParentActivity(), Uri.parse(getVideoUrl())); - } catch (final Exception e) { - if (DEBUG) { - Log.i(TAG, "Failed to start kore", e); - } - KoreUtil.showInstallKoreDialog(getParentActivity()); - } - } - - private void onOpenInBrowserClicked() { - if (getCurrentMetadata() == null) { - return; - } - - ShareUtils.openUrlInBrowser(getParentActivity(), - getCurrentMetadata().getMetadata().getOriginalUrl()); - } - - private void showHideKodiButton() { - final boolean kodiEnabled = defaultPreferences.getBoolean( - service.getString(R.string.show_play_with_kodi_key), false); - // show kodi button if it supports the current service and it is enabled in settings - final boolean showKodiButton = playQueue != null && playQueue.getItem() != null - && KoreUtil.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId()); - binding.playWithKodi.setVisibility(videoPlayerSelected() && kodiEnabled - && showKodiButton ? View.VISIBLE : View.GONE); - } - - private void setupScreenRotationButton() { - final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(service); - final boolean showButton = videoPlayerSelected() - && (orientationLocked || isVerticalVideo || DeviceUtils.isTablet(service)); - binding.screenRotationButton.setVisibility(showButton ? View.VISIBLE : View.GONE); - binding.screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(service, - isFullscreen() ? R.drawable.ic_fullscreen_exit_white_24dp - : R.drawable.ic_fullscreen_white_24dp)); - } - - private void prepareOrientation() { - final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(service); - if (orientationLocked - && isFullscreen() - && service.isLandscape() == isVerticalVideo - && !DeviceUtils.isTv(service) - && !DeviceUtils.isTablet(service) - && fragmentListener != null) { - fragmentListener.onScreenRotationButtonClicked(); - } - } - - @Override - public void onPlaybackSpeedClicked() { - if (videoPlayerSelected()) { - PlaybackParameterDialog - .newInstance( - getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence(), this) - .show(getParentActivity().getSupportFragmentManager(), null); - } else { - super.onPlaybackSpeedClicked(); - } - } - - @Override - public void onStopTrackingTouch(final SeekBar seekBar) { - super.onStopTrackingTouch(seekBar); - if (wasPlaying()) { - showControlsThenHide(); - } - } - - @Override - public void onDismiss(final PopupMenu menu) { - super.onDismiss(menu); - if (isPlaying()) { - hideControls(DEFAULT_CONTROLS_DURATION, 0); - hideSystemUIIfNeeded(); - } - } - - @Override - @SuppressWarnings("checkstyle:ParameterNumber") - public void onLayoutChange(final View view, final int l, final int t, final int r, final int b, - final int ol, final int ot, final int or, final int ob) { - if (l != ol || t != ot || r != or || b != ob) { - // Use smaller value to be consistent between screen orientations - // (and to make usage easier) - final int width = r - l; - final int height = b - t; - final int min = Math.min(width, height); - maxGestureLength = (int) (min * MAX_GESTURE_LENGTH); - - if (DEBUG) { - Log.d(TAG, "maxGestureLength = " + maxGestureLength); - } - - binding.volumeProgressBar.setMax(maxGestureLength); - binding.brightnessProgressBar.setMax(maxGestureLength); - - setInitialGestureValues(); - binding.playQueuePanel.getLayoutParams().height = height - - binding.playQueuePanel.getTop(); - } - } - - @Override - protected int nextResizeMode(final int currentResizeMode) { - final int newResizeMode; - switch (currentResizeMode) { - case AspectRatioFrameLayout.RESIZE_MODE_FIT: - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; - break; - case AspectRatioFrameLayout.RESIZE_MODE_FILL: - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; - break; - default: - newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; - break; - } - - storeResizeMode(newResizeMode); - return newResizeMode; - } - - private void storeResizeMode(final @AspectRatioFrameLayout.ResizeMode int resizeMode) { - defaultPreferences.edit() - .putInt(service.getString(R.string.last_resize_mode), resizeMode) - .apply(); - } - - private void restoreResizeMode() { - setResizeMode(defaultPreferences.getInt( - service.getString(R.string.last_resize_mode), - AspectRatioFrameLayout.RESIZE_MODE_FIT)); - } - - @Override - protected VideoPlaybackResolver.QualityResolver getQualityResolver() { - return new VideoPlaybackResolver.QualityResolver() { - @Override - public int getDefaultResolutionIndex(final List sortedVideos) { - return videoPlayerSelected() - ? ListHelper.getDefaultResolutionIndex(context, sortedVideos) - : ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos); - } - - @Override - public int getOverrideResolutionIndex(final List sortedVideos, - final String playbackQuality) { - return videoPlayerSelected() - ? getResolutionIndex(context, sortedVideos, playbackQuality) - : getPopupResolutionIndex(context, sortedVideos, playbackQuality); - } - }; - } - - /*////////////////////////////////////////////////////////////////////////// - // States - //////////////////////////////////////////////////////////////////////////*/ - - private void animatePlayButtons(final boolean show, final int duration) { - animateView(binding.playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, - duration); - - boolean showQueueButtons = show; - if (playQueue == null) { - showQueueButtons = false; - } - - if (!showQueueButtons || playQueue.getIndex() > 0) { - animateView( - binding.playPreviousButton, - AnimationUtils.Type.SCALE_AND_ALPHA, - showQueueButtons, - duration); - } - if (!showQueueButtons || playQueue.getIndex() + 1 < playQueue.getStreams().size()) { - animateView( - binding.playNextButton, - AnimationUtils.Type.SCALE_AND_ALPHA, - showQueueButtons, - duration); - } - } - - @Override - public void changeState(final int state) { - super.changeState(state); - updatePlayback(); - } - - @Override - public void onBlocked() { - super.onBlocked(); - binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); - animatePlayButtons(false, 100); - getRootView().setKeepScreenOn(false); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - @Override - public void onBuffering() { - super.onBuffering(); - getRootView().setKeepScreenOn(true); - - if (NotificationUtil.getInstance().shouldUpdateBufferingSlot()) { - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - } - - @Override - public void onPlaying() { - super.onPlaying(); - animateView(binding.playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, - 80, 0, () -> { - binding.playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); - animatePlayButtons(true, 200); - if (!queueVisible) { - binding.playPauseButton.requestFocus(); - } - }); - - updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS); - checkLandscape(); - getRootView().setKeepScreenOn(true); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - @Override - public void onPaused() { - super.onPaused(); - animateView(binding.playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, - false, 80, 0, () -> { - binding.playPauseButton - .setImageResource(R.drawable.ic_play_arrow_white_24dp); - animatePlayButtons(true, 200); - if (!queueVisible) { - binding.playPauseButton.requestFocus(); - } - }); - - updateWindowFlags(IDLE_WINDOW_FLAGS); - - // Remove running notification when user don't want music (or video in popup) - // to be played in background - if (!minimizeOnPopupEnabled() && !backgroundPlaybackEnabled() && videoPlayerSelected()) { - NotificationUtil.getInstance().cancelNotificationAndStopForeground(service); - } else { - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - getRootView().setKeepScreenOn(false); - } - - @Override - public void onPausedSeek() { - super.onPausedSeek(); - animatePlayButtons(false, 100); - getRootView().setKeepScreenOn(true); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - - @Override - public void onCompleted() { - animateView(binding.playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, - 0, 0, () -> { - binding.playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); - animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); - }); - - getRootView().setKeepScreenOn(false); - updateWindowFlags(IDLE_WINDOW_FLAGS); - - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - if (isFullscreen) { - toggleFullscreen(); - } - super.onCompleted(); - } - - @Override - public void destroy() { - super.destroy(); - service.getContentResolver().unregisterContentObserver(settingsContentObserver); - } - - /*////////////////////////////////////////////////////////////////////////// - // Broadcast Receiver - //////////////////////////////////////////////////////////////////////////*/ - - @Override - protected void setupBroadcastReceiver(final IntentFilter intentFilter) { - super.setupBroadcastReceiver(intentFilter); - if (DEBUG) { - Log.d(TAG, "setupBroadcastReceiver() called with: " - + "intentFilter = [" + intentFilter + "]"); - } - - intentFilter.addAction(ACTION_CLOSE); - intentFilter.addAction(ACTION_PLAY_PAUSE); - intentFilter.addAction(ACTION_OPEN_CONTROLS); - intentFilter.addAction(ACTION_REPEAT); - intentFilter.addAction(ACTION_PLAY_PREVIOUS); - intentFilter.addAction(ACTION_PLAY_NEXT); - intentFilter.addAction(ACTION_FAST_REWIND); - intentFilter.addAction(ACTION_FAST_FORWARD); - intentFilter.addAction(ACTION_SHUFFLE); - intentFilter.addAction(ACTION_RECREATE_NOTIFICATION); - - intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED); - intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED); - - intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); - intentFilter.addAction(Intent.ACTION_SCREEN_ON); - intentFilter.addAction(Intent.ACTION_SCREEN_OFF); - - intentFilter.addAction(Intent.ACTION_HEADSET_PLUG); - } - - @Override - public void onBroadcastReceived(final Intent intent) { - super.onBroadcastReceived(intent); - if (intent == null || intent.getAction() == null) { - return; - } - - if (DEBUG) { - Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); - } - - switch (intent.getAction()) { - case ACTION_CLOSE: - service.onDestroy(); - break; - case ACTION_PLAY_NEXT: - onPlayNext(); - break; - case ACTION_PLAY_PREVIOUS: - onPlayPrevious(); - break; - case ACTION_FAST_FORWARD: - onFastForward(); - break; - case ACTION_FAST_REWIND: - onFastRewind(); - break; - case ACTION_PLAY_PAUSE: - onPlayPause(); - if (!fragmentIsVisible) { - // Ensure that we have audio-only stream playing when a user - // started to play from notification's play button from outside of the app - onFragmentStopped(); - } - break; - case ACTION_REPEAT: - onRepeatClicked(); - break; - case ACTION_SHUFFLE: - onShuffleClicked(); - break; - case ACTION_RECREATE_NOTIFICATION: - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true); - break; - case Intent.ACTION_HEADSET_PLUG: //FIXME - /*notificationManager.cancel(NOTIFICATION_ID); - mediaSessionManager.dispose(); - mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/ - break; - case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED: - fragmentIsVisible = true; - useVideoSource(true); - break; - case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED: - fragmentIsVisible = false; - onFragmentStopped(); - break; - case Intent.ACTION_CONFIGURATION_CHANGED: - assureCorrectAppLanguage(service); - if (DEBUG) { - Log.d(TAG, "onConfigurationChanged() called"); - } - if (popupPlayerSelected()) { - updateScreenSize(); - updatePopupSize(getPopupLayoutParams().width, -1); - checkPopupPositionBounds(); - } - // Close it because when changing orientation from portrait - // (in fullscreen mode) the size of queue layout can be larger than the screen size - onQueueClosed(); - break; - case Intent.ACTION_SCREEN_ON: - shouldUpdateOnProgress = true; - // Interrupt playback only when screen turns on - // and user is watching video in popup player. - // Same actions for video player will be handled in ACTION_VIDEO_FRAGMENT_RESUMED - if (popupPlayerSelected() && (isPlaying() || isLoading())) { - useVideoSource(true); - } - break; - case Intent.ACTION_SCREEN_OFF: - shouldUpdateOnProgress = false; - // Interrupt playback only when screen turns off with popup player working - if (popupPlayerSelected() && (isPlaying() || isLoading())) { - useVideoSource(false); - } - break; - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Thumbnail Loading - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onLoadingComplete(final String imageUri, - final View view, - final Bitmap loadedImage) { - super.onLoadingComplete(imageUri, view, loadedImage); - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - @Override - public void onLoadingFailed(final String imageUri, - final View view, - final FailReason failReason) { - super.onLoadingFailed(imageUri, view, failReason); - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - @Override - public void onLoadingCancelled(final String imageUri, final View view) { - super.onLoadingCancelled(imageUri, view); - NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false); - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private void setInitialGestureValues() { - if (getAudioReactor() != null) { - final float currentVolumeNormalized = (float) getAudioReactor() - .getVolume() / getAudioReactor().getMaxVolume(); - binding.volumeProgressBar.setProgress( - (int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized)); - } - } - - private void choosePlayerTypeFromIntent(final Intent intent) { - // If you want to open popup from the app just include Constants.POPUP_ONLY into an extra - if (intent.getIntExtra(PLAYER_TYPE, PLAYER_TYPE_VIDEO) == PLAYER_TYPE_AUDIO) { - playerType = MainPlayer.PlayerType.AUDIO; - } else if (intent.getIntExtra(PLAYER_TYPE, PLAYER_TYPE_VIDEO) == PLAYER_TYPE_POPUP) { - playerType = MainPlayer.PlayerType.POPUP; - } else { - playerType = MainPlayer.PlayerType.VIDEO; - } - } - - public boolean backgroundPlaybackEnabled() { - return PlayerHelper.getMinimizeOnExitAction(service) == MINIMIZE_ON_EXIT_MODE_BACKGROUND; - } - - public boolean minimizeOnPopupEnabled() { - return PlayerHelper.getMinimizeOnExitAction(service) - == PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP; - } - - public boolean audioPlayerSelected() { - return playerType == MainPlayer.PlayerType.AUDIO; - } - - public boolean videoPlayerSelected() { - return playerType == MainPlayer.PlayerType.VIDEO; - } - - public boolean popupPlayerSelected() { - return playerType == MainPlayer.PlayerType.POPUP; - } - - public boolean isPlayerStopped() { - return getPlayer() == null || getPlayer().getPlaybackState() == SimpleExoPlayer.STATE_IDLE; - } - - private int distanceFromCloseButton(final MotionEvent popupMotionEvent) { - final int closeOverlayButtonX = closeOverlayBinding.closeButton.getLeft() - + closeOverlayBinding.closeButton.getWidth() / 2; - final int closeOverlayButtonY = closeOverlayBinding.closeButton.getTop() - + closeOverlayBinding.closeButton.getHeight() / 2; - - final float fingerX = popupLayoutParams.x + popupMotionEvent.getX(); - final float fingerY = popupLayoutParams.y + popupMotionEvent.getY(); - - return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) - + Math.pow(closeOverlayButtonY - fingerY, 2)); - } - - private float getClosingRadius() { - final int buttonRadius = closeOverlayBinding.closeButton.getWidth() / 2; - // 20% wider than the button itself - return buttonRadius * 1.2f; - } - - public boolean isInsideClosingRadius(final MotionEvent popupMotionEvent) { - return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius(); - } - - public boolean isFullscreen() { - return isFullscreen; - } - - public void showControlsThenHide() { - if (DEBUG) { - Log.d(TAG, "showControlsThenHide() called"); - } - showOrHideButtons(); - showSystemUIPartially(); - super.showControlsThenHide(); - } - - @Override - public void showControls(final long duration) { - if (DEBUG) { - Log.d(TAG, "showControls() called with: duration = [" + duration + "]"); - } - showOrHideButtons(); - showSystemUIPartially(); - super.showControls(duration); - } - - @Override - public void hideControls(final long duration, final long delay) { - if (DEBUG) { - Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); - } - - showOrHideButtons(); - - getControlsVisibilityHandler().removeCallbacksAndMessages(null); - getControlsVisibilityHandler().postDelayed(() -> { - showHideShadow(false, duration, 0); - animateView(binding.playbackControlRoot, false, duration, 0, - this::hideSystemUIIfNeeded); - }, delay - ); - } - - @Override - public void safeHideControls(final long duration, final long delay) { - if (binding.playbackControlRoot.isInTouchMode()) { - hideControls(duration, delay); - } - } - - private void showOrHideButtons() { - if (playQueue == null) { - return; - } - - final boolean showPrev = playQueue.getIndex() != 0; - final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size(); - final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected(); - - binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE); - binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f); - binding.playNextButton.setVisibility(showNext ? View.VISIBLE : View.INVISIBLE); - binding.playNextButton.setAlpha(showNext ? 1.0f : 0.0f); - binding.queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE); - binding.queueButton.setAlpha(showQueue ? 1.0f : 0.0f); - } - - private void showSystemUIPartially() { - final AppCompatActivity activity = getParentActivity(); - if (isFullscreen() && activity != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - activity.getWindow().setStatusBarColor(Color.TRANSPARENT); - activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); - } - final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; - activity.getWindow().getDecorView().setSystemUiVisibility(visibility); - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - } - } - - @Override - public void hideSystemUIIfNeeded() { - if (fragmentListener != null) { - fragmentListener.hideSystemUiIfNeeded(); - } - } - - public void disablePreloadingOfCurrentTrack() { - getLoadController().disablePreloadingOfCurrentTrack(); - } - - protected void setMuteButton(final ImageButton button, final boolean isMuted) { - button.setImageDrawable(AppCompatResources.getDrawable(service, isMuted - ? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp)); - } - - /** - * @return true if main player is attached to activity and activity inside multiWindow mode - */ - private boolean isInMultiWindow() { - final AppCompatActivity parent = getParentActivity(); - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N - && parent != null - && parent.isInMultiWindowMode(); - } - - private void updatePlaybackButtons() { - if (binding == null || simpleExoPlayer == null || playQueue == null) { - return; - } - - setRepeatModeButton(binding.repeatButton, getRepeatMode()); - setShuffleButton(binding.shuffleButton, playQueue.isShuffled()); - } - - public void checkLandscape() { - final AppCompatActivity parent = getParentActivity(); - final boolean videoInLandscapeButNotInFullscreen = service.isLandscape() - && !isFullscreen() - && videoPlayerSelected() - && !audioOnly; - - final boolean playingState = getCurrentState() != STATE_COMPLETED - && getCurrentState() != STATE_PAUSED; - if (parent != null - && videoInLandscapeButNotInFullscreen - && playingState - && !DeviceUtils.isTablet(service)) { - toggleFullscreen(); - } - } - - private void buildQueue() { - binding.playQueue.setAdapter(playQueueAdapter); - binding.playQueue.setClickable(true); - binding.playQueue.setLongClickable(true); - - binding.playQueue.clearOnScrollListeners(); - binding.playQueue.addOnScrollListener(getQueueScrollListener()); - - itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); - itemTouchHelper.attachToRecyclerView(binding.playQueue); - - playQueueAdapter.setSelectedListener(getOnSelectedListener()); - - binding.playQueueClose.setOnClickListener(view -> onQueueClosed()); - } - - public void useVideoSource(final boolean video) { - if (playQueue == null || audioOnly == !video || audioPlayerSelected()) { - return; - } - - audioOnly = !video; - // When a user returns from background controls could be hidden - // but systemUI will be shown 100%. Hide it - if (!audioOnly && !isControlsVisible()) { - hideSystemUIIfNeeded(); - } - setRecovery(); - reload(); - } - - private OnScrollBelowItemsListener getQueueScrollListener() { - return new OnScrollBelowItemsListener() { - @Override - public void onScrolledDown(final RecyclerView recyclerView) { - if (playQueue != null && !playQueue.isComplete()) { - playQueue.fetch(); - } else if (binding != null) { - binding.playQueue.clearOnScrollListeners(); - } - } - }; - } - - private ItemTouchHelper.SimpleCallback getItemTouchCallback() { - return new PlayQueueItemTouchCallback() { - @Override - public void onMove(final int sourceIndex, final int targetIndex) { - if (playQueue != null) { - playQueue.move(sourceIndex, targetIndex); - } - } - - @Override - public void onSwiped(final int index) { - if (index != -1) { - playQueue.remove(index); - } - } - }; - } - - private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { - return new PlayQueueItemBuilder.OnSelectedListener() { - @Override - public void selected(final PlayQueueItem item, final View view) { - onSelected(item); - } - - @Override - public void held(final PlayQueueItem item, final View view) { - final int index = playQueue.indexOf(item); - if (index != -1) { - playQueue.remove(index); - } - } - - @Override - public void onStartDrag(final PlayQueueItemHolder viewHolder) { - if (itemTouchHelper != null) { - itemTouchHelper.startDrag(viewHolder); - } - } - }; - } - - /*////////////////////////////////////////////////////////////////////////// - // Init - //////////////////////////////////////////////////////////////////////////*/ - - @SuppressLint("RtlHardcoded") - private void initPopup() { - if (DEBUG) { - Log.d(TAG, "initPopup() called"); - } - - // Popup is already added to windowManager - if (popupHasParent()) { - return; - } - - updateScreenSize(); - - final boolean popupRememberSizeAndPos = PlayerHelper.isRememberingPopupDimensions(service); - final float defaultSize = service.getResources().getDimension(R.dimen.popup_default_width); - final SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(service); - popupWidth = popupRememberSizeAndPos - ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) - : defaultSize; - popupHeight = getMinimumVideoHeight(popupWidth); - - popupLayoutParams = new WindowManager.LayoutParams( - (int) popupWidth, (int) popupHeight, - popupLayoutParamType(), - IDLE_WINDOW_FLAGS, - PixelFormat.TRANSLUCENT); - popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; - popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; - getSurfaceView().setHeights((int) popupHeight, (int) popupHeight); - - final int centerX = (int) (screenWidth / 2f - popupWidth / 2f); - final int centerY = (int) (screenHeight / 2f - popupHeight / 2f); - popupLayoutParams.x = popupRememberSizeAndPos - ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX; - popupLayoutParams.y = popupRememberSizeAndPos - ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY; - - checkPopupPositionBounds(); - - binding.loadingPanel.setMinimumWidth(popupLayoutParams.width); - binding.loadingPanel.setMinimumHeight(popupLayoutParams.height); - - service.removeViewFromParent(); - windowManager.addView(getRootView(), popupLayoutParams); - - // Popup doesn't have aspectRatio selector, using FIT automatically - setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT); - } - - @SuppressLint("RtlHardcoded") - private void initPopupCloseOverlay() { - if (DEBUG) { - Log.d(TAG, "initPopupCloseOverlay() called"); - } - - // closeOverlayView is already added to windowManager - if (closeOverlayBinding != null) { - return; - } - - closeOverlayBinding = PlayerPopupCloseOverlayBinding.inflate(LayoutInflater.from(service)); - - final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - - final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, - popupLayoutParamType(), - flags, - PixelFormat.TRANSLUCENT); - closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; - closeOverlayLayoutParams.softInputMode = - WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; - - closeOverlayBinding.closeButton.setVisibility(View.GONE); - windowManager.addView(closeOverlayBinding.getRoot(), closeOverlayLayoutParams); - } - - private void initVideoPlayer() { - restoreResizeMode(); - getRootView().setLayoutParams(new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); - } - - /*////////////////////////////////////////////////////////////////////////// - // Popup utils - //////////////////////////////////////////////////////////////////////////*/ - - /** - * @return if the popup was out of bounds and have been moved back to it - * @see #checkPopupPositionBounds(float, float) - */ - @SuppressWarnings("UnusedReturnValue") - public boolean checkPopupPositionBounds() { - return checkPopupPositionBounds(screenWidth, screenHeight); - } - - /** - * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary - * that goes from (0, 0) to (boundaryWidth, boundaryHeight). - *

- * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed - * and {@code true} is returned to represent this change. - *

- * - * @param boundaryWidth width of the boundary - * @param boundaryHeight height of the boundary - * @return if the popup was out of bounds and have been moved back to it - */ - public boolean checkPopupPositionBounds(final float boundaryWidth, final float boundaryHeight) { - if (DEBUG) { - Log.d(TAG, "checkPopupPositionBounds() called with: " - + "boundaryWidth = [" + boundaryWidth + "], " - + "boundaryHeight = [" + boundaryHeight + "]"); - } - - if (popupLayoutParams.x < 0) { - popupLayoutParams.x = 0; - return true; - } else if (popupLayoutParams.x > boundaryWidth - popupLayoutParams.width) { - popupLayoutParams.x = (int) (boundaryWidth - popupLayoutParams.width); - return true; - } - - if (popupLayoutParams.y < 0) { - popupLayoutParams.y = 0; - return true; - } else if (popupLayoutParams.y > boundaryHeight - popupLayoutParams.height) { - popupLayoutParams.y = (int) (boundaryHeight - popupLayoutParams.height); - return true; - } - - return false; - } - - public void savePositionAndSize() { - final SharedPreferences sharedPreferences = PreferenceManager - .getDefaultSharedPreferences(service); - sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply(); - sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply(); - sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply(); - } - - private float getMinimumVideoHeight(final float width) { - final float height = width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have - /*if (DEBUG) { - Log.d(TAG, "getMinimumVideoHeight() called with: width = [" - + width + "], returned: " + height); - }*/ - return height; - } - - public void updateScreenSize() { - final DisplayMetrics metrics = new DisplayMetrics(); - windowManager.getDefaultDisplay().getMetrics(metrics); - - screenWidth = metrics.widthPixels; - screenHeight = metrics.heightPixels; - if (DEBUG) { - Log.d(TAG, "updateScreenSize() called > screenWidth = " - + screenWidth + ", screenHeight = " + screenHeight); - } - - popupWidth = service.getResources().getDimension(R.dimen.popup_default_width); - popupHeight = getMinimumVideoHeight(popupWidth); - - minimumWidth = service.getResources().getDimension(R.dimen.popup_minimum_width); - minimumHeight = getMinimumVideoHeight(minimumWidth); - - maximumWidth = screenWidth; - maximumHeight = screenHeight; - } - - public void updatePopupSize(final int width, final int height) { - if (DEBUG) { - Log.d(TAG, "updatePopupSize() called with: width = [" - + width + "], height = [" + height + "]"); - } - - if (popupLayoutParams == null - || windowManager == null - || getParentActivity() != null - || getRootView().getParent() == null) { - return; - } - - final int actualWidth = (int) (width > maximumWidth - ? maximumWidth : width < minimumWidth ? minimumWidth : width); - final int actualHeight; - if (height == -1) { - actualHeight = (int) getMinimumVideoHeight(width); - } else { - actualHeight = (int) (height > maximumHeight - ? maximumHeight : height < minimumHeight - ? minimumHeight : height); - } - - popupLayoutParams.width = actualWidth; - popupLayoutParams.height = actualHeight; - popupWidth = actualWidth; - popupHeight = actualHeight; - getSurfaceView().setHeights((int) popupHeight, (int) popupHeight); - - if (DEBUG) { - Log.d(TAG, "updatePopupSize() updated values:" - + " width = [" + actualWidth + "], height = [" + actualHeight + "]"); - } - windowManager.updateViewLayout(getRootView(), popupLayoutParams); - } - - private void updateWindowFlags(final int flags) { - if (popupLayoutParams == null - || windowManager == null - || getParentActivity() != null - || getRootView().getParent() == null) { - return; - } - - popupLayoutParams.flags = flags; - windowManager.updateViewLayout(getRootView(), popupLayoutParams); - } - - private int popupLayoutParamType() { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.O - ? WindowManager.LayoutParams.TYPE_PHONE - : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - } - - /*////////////////////////////////////////////////////////////////////////// - // Misc - //////////////////////////////////////////////////////////////////////////*/ - - public void closePopup() { - if (DEBUG) { - Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing); - } - if (isPopupClosing) { - return; - } - isPopupClosing = true; - - savePlaybackState(); - windowManager.removeView(getRootView()); - - animateOverlayAndFinishService(); - } - - public void removePopupFromView() { - final boolean isCloseOverlayHasParent = closeOverlayBinding != null - && closeOverlayBinding.getRoot().getParent() != null; - if (popupHasParent()) { - windowManager.removeView(getRootView()); - } - if (isCloseOverlayHasParent) { - windowManager.removeView(closeOverlayBinding.getRoot()); - } - } - - private void animateOverlayAndFinishService() { - final int targetTranslationY = - (int) (closeOverlayBinding.closeButton.getRootView().getHeight() - - closeOverlayBinding.closeButton.getY()); - - closeOverlayBinding.closeButton.animate().setListener(null).cancel(); - closeOverlayBinding.closeButton.animate() - .setInterpolator(new AnticipateInterpolator()) - .translationY(targetTranslationY) - .setDuration(400) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(final Animator animation) { - end(); - } - - @Override - public void onAnimationEnd(final Animator animation) { - end(); - } - - private void end() { - windowManager.removeView(closeOverlayBinding.getRoot()); - closeOverlayBinding = null; - - service.onDestroy(); - } - }).start(); - } - - private boolean popupHasParent() { - return binding != null - && binding.getRoot().getLayoutParams() instanceof WindowManager.LayoutParams - && binding.getRoot().getParent() != null; - } - - /////////////////////////////////////////////////////////////////////////// - // Manipulations with listener - /////////////////////////////////////////////////////////////////////////// - - public void setFragmentListener(final PlayerServiceEventListener listener) { - fragmentListener = listener; - fragmentIsVisible = true; - // Apply window insets because Android will not do it when orientation changes - // from landscape to portrait - if (!isFullscreen) { - binding.playbackControlRoot.setPadding(0, 0, 0, 0); - } - binding.playQueuePanel.setPadding(0, 0, 0, 0); - updateQueue(); - updateMetadata(); - updatePlayback(); - triggerProgressUpdate(); - } - - public void removeFragmentListener(final PlayerServiceEventListener listener) { - if (fragmentListener == listener) { - fragmentListener = null; - } - } - - void setActivityListener(final PlayerEventListener listener) { - activityListener = listener; - updateMetadata(); - updatePlayback(); - triggerProgressUpdate(); - } - - void removeActivityListener(final PlayerEventListener listener) { - if (activityListener == listener) { - activityListener = null; - } - } - - private void updateQueue() { - if (fragmentListener != null && playQueue != null) { - fragmentListener.onQueueUpdate(playQueue); - } - if (activityListener != null && playQueue != null) { - activityListener.onQueueUpdate(playQueue); - } - } - - private void updateMetadata() { - if (fragmentListener != null && getCurrentMetadata() != null) { - fragmentListener.onMetadataUpdate(getCurrentMetadata().getMetadata(), playQueue); - } - if (activityListener != null && getCurrentMetadata() != null) { - activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata(), playQueue); - } - } - - private void updatePlayback() { - if (fragmentListener != null && simpleExoPlayer != null && playQueue != null) { - fragmentListener.onPlaybackUpdate(currentState, getRepeatMode(), - playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters()); - } - if (activityListener != null && simpleExoPlayer != null && playQueue != null) { - activityListener.onPlaybackUpdate(currentState, getRepeatMode(), - playQueue.isShuffled(), getPlaybackParameters()); - } - } - - private void updateProgress(final int currentProgress, final int duration, - final int bufferPercent) { - if (fragmentListener != null) { - fragmentListener.onProgressUpdate(currentProgress, duration, bufferPercent); - } - if (activityListener != null) { - activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); - } - } - - void stopActivityBinding() { - if (fragmentListener != null) { - fragmentListener.onServiceStopped(); - fragmentListener = null; - } - if (activityListener != null) { - activityListener.onServiceStopped(); - activityListener = null; - } - } - - /** - * This will be called when a user goes to another app/activity, turns off a screen. - * We don't want to interrupt playback and don't want to see notification so - * next lines of code will enable audio-only playback only if needed - */ - private void onFragmentStopped() { - if (videoPlayerSelected() && (isPlaying() || isLoading())) { - if (backgroundPlaybackEnabled()) { - useVideoSource(false); - } else if (minimizeOnPopupEnabled()) { - setRecovery(); - NavigationHelper.playOnPopupPlayer(getParentActivity(), playQueue, true); - } else { - onPause(); - } - } - } - - /////////////////////////////////////////////////////////////////////////// - // Getters - /////////////////////////////////////////////////////////////////////////// - - public RelativeLayout getVolumeRelativeLayout() { - return binding.volumeRelativeLayout; - } - - public ProgressBar getVolumeProgressBar() { - return binding.volumeProgressBar; - } - - public ImageView getVolumeImageView() { - return binding.volumeImageView; - } - - public RelativeLayout getBrightnessRelativeLayout() { - return binding.brightnessRelativeLayout; - } - - public ProgressBar getBrightnessProgressBar() { - return binding.brightnessProgressBar; - } - - public ImageView getBrightnessImageView() { - return binding.brightnessImageView; - } - - public ImageButton getPlayPauseButton() { - return binding.playPauseButton; - } - - public int getMaxGestureLength() { - return maxGestureLength; - } - - public TextView getResizingIndicator() { - return binding.resizingIndicator; - } - - public GestureDetector getGestureDetector() { - return gestureDetector; - } - - public WindowManager.LayoutParams getPopupLayoutParams() { - return popupLayoutParams; - } - - public MainPlayer.PlayerType getPlayerType() { - return playerType; - } - - public float getScreenWidth() { - return screenWidth; - } - - public float getScreenHeight() { - return screenHeight; - } - - public float getPopupWidth() { - return popupWidth; - } - - public float getPopupHeight() { - return popupHeight; - } - - public void setPopupWidth(final float width) { - popupWidth = width; - } - - public void setPopupHeight(final float height) { - popupHeight = height; - } - - public View getCloseButton() { - return closeOverlayBinding.closeButton; - } - - public View getClosingOverlay() { - return binding.closingOverlay; - } - - public boolean isVerticalVideo() { - return isVerticalVideo; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt index d34746ca5..46502a270 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt +++ b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt @@ -7,10 +7,10 @@ import android.view.GestureDetector import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration -import org.schabi.newpipe.player.BasePlayer import org.schabi.newpipe.player.MainPlayer -import org.schabi.newpipe.player.VideoPlayerImpl +import org.schabi.newpipe.player.Player import org.schabi.newpipe.player.helper.PlayerHelper +import org.schabi.newpipe.player.helper.PlayerHelper.savePopupPositionAndSizeToPrefs import org.schabi.newpipe.util.AnimationUtils import kotlin.math.abs import kotlin.math.hypot @@ -18,14 +18,14 @@ import kotlin.math.max import kotlin.math.min /** - * Base gesture handling for [VideoPlayerImpl] + * Base gesture handling for [Player] * * This class contains the logic for the player gestures like View preparations * and provides some abstract methods to make it easier separating the logic from the UI. */ abstract class BasePlayerGestureListener( @JvmField - protected val playerImpl: VideoPlayerImpl, + protected val player: Player, @JvmField protected val service: MainPlayer ) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener { @@ -78,7 +78,7 @@ abstract class BasePlayerGestureListener( // /////////////////////////////////////////////////////////////////// override fun onTouch(v: View, event: MotionEvent): Boolean { - return if (playerImpl.popupPlayerSelected()) { + return if (player.popupPlayerSelected()) { onTouchInPopup(v, event) } else { onTouchInMain(v, event) @@ -86,14 +86,14 @@ abstract class BasePlayerGestureListener( } private fun onTouchInMain(v: View, event: MotionEvent): Boolean { - playerImpl.gestureDetector.onTouchEvent(event) + player.gestureDetector.onTouchEvent(event) if (event.action == MotionEvent.ACTION_UP && isMovingInMain) { isMovingInMain = false onScrollEnd(MainPlayer.PlayerType.VIDEO, event) } return when (event.action) { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { - v.parent.requestDisallowInterceptTouchEvent(playerImpl.isFullscreen) + v.parent.requestDisallowInterceptTouchEvent(player.isFullscreen) true } MotionEvent.ACTION_UP -> { @@ -105,7 +105,7 @@ abstract class BasePlayerGestureListener( } private fun onTouchInPopup(v: View, event: MotionEvent): Boolean { - playerImpl.gestureDetector.onTouchEvent(event) + player.gestureDetector.onTouchEvent(event) if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) { if (DEBUG) { Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.") @@ -157,10 +157,10 @@ abstract class BasePlayerGestureListener( initSecPointerY = (-1).toFloat() onPopupResizingEnd() - playerImpl.changeState(playerImpl.currentState) + player.changeState(player.currentState) } - if (!playerImpl.isPopupClosing) { - playerImpl.savePositionAndSize() + if (!player.isPopupClosing) { + savePopupPositionAndSizeToPrefs(player) } } @@ -190,19 +190,15 @@ abstract class BasePlayerGestureListener( event.getY(0) - event.getY(1).toDouble() ) - val popupWidth = playerImpl.popupWidth.toDouble() + val popupWidth = player.popupLayoutParams!!.width.toDouble() // change co-ordinates of popup so the center stays at the same position val newWidth = popupWidth * currentPointerDistance / initPointerDistance initPointerDistance = currentPointerDistance - playerImpl.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt() + player.popupLayoutParams!!.x += ((popupWidth - newWidth) / 2.0).toInt() - playerImpl.checkPopupPositionBounds() - playerImpl.updateScreenSize() - - playerImpl.updatePopupSize( - min(playerImpl.screenWidth.toDouble(), newWidth).toInt(), - -1 - ) + player.checkPopupPositionBounds() + player.updateScreenSize() + player.changePopupSize(min(player.screenWidth.toDouble(), newWidth).toInt()) return true } } @@ -222,7 +218,7 @@ abstract class BasePlayerGestureListener( return true } - return if (playerImpl.popupPlayerSelected()) + return if (player.popupPlayerSelected()) onDownInPopup(e) else true @@ -231,12 +227,10 @@ abstract class BasePlayerGestureListener( private fun onDownInPopup(e: MotionEvent): Boolean { // Fix popup position when the user touch it, it may have the wrong one // because the soft input is visible (the draggable area is currently resized). - playerImpl.updateScreenSize() - playerImpl.checkPopupPositionBounds() - initialPopupX = playerImpl.popupLayoutParams.x - initialPopupY = playerImpl.popupLayoutParams.y - playerImpl.popupWidth = playerImpl.popupLayoutParams.width.toFloat() - playerImpl.popupHeight = playerImpl.popupLayoutParams.height.toFloat() + player.updateScreenSize() + player.checkPopupPositionBounds() + initialPopupX = player.popupLayoutParams!!.x + initialPopupY = player.popupLayoutParams!!.y return super.onDown(e) } @@ -255,15 +249,15 @@ abstract class BasePlayerGestureListener( if (isDoubleTapping) return true - if (playerImpl.popupPlayerSelected()) { - if (playerImpl.player == null) + if (player.popupPlayerSelected()) { + if (player.exoPlayerIsNull()) return false onSingleTap(MainPlayer.PlayerType.POPUP) return true } else { super.onSingleTapConfirmed(e) - if (playerImpl.currentState == BasePlayer.STATE_BLOCKED) + if (player.currentState == Player.STATE_BLOCKED) return true onSingleTap(MainPlayer.PlayerType.VIDEO) @@ -272,10 +266,10 @@ abstract class BasePlayerGestureListener( } override fun onLongPress(e: MotionEvent?) { - if (playerImpl.popupPlayerSelected()) { - playerImpl.updateScreenSize() - playerImpl.checkPopupPositionBounds() - playerImpl.updatePopupSize(playerImpl.screenWidth.toInt(), -1) + if (player.popupPlayerSelected()) { + player.updateScreenSize() + player.checkPopupPositionBounds() + player.changePopupSize(player.screenWidth.toInt()) } } @@ -285,7 +279,7 @@ abstract class BasePlayerGestureListener( distanceX: Float, distanceY: Float ): Boolean { - return if (playerImpl.popupPlayerSelected()) { + return if (player.popupPlayerSelected()) { onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY) } else { onScrollInMain(initialEvent, movingEvent, distanceX, distanceY) @@ -298,19 +292,18 @@ abstract class BasePlayerGestureListener( velocityX: Float, velocityY: Float ): Boolean { - return if (playerImpl.popupPlayerSelected()) { + return if (player.popupPlayerSelected()) { val absVelocityX = abs(velocityX) val absVelocityY = abs(velocityY) if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) { if (absVelocityX > tossFlingVelocity) { - playerImpl.popupLayoutParams.x = velocityX.toInt() + player.popupLayoutParams!!.x = velocityX.toInt() } if (absVelocityY > tossFlingVelocity) { - playerImpl.popupLayoutParams.y = velocityY.toInt() + player.popupLayoutParams!!.y = velocityY.toInt() } - playerImpl.checkPopupPositionBounds() - playerImpl.windowManager - .updateViewLayout(playerImpl.rootView, playerImpl.popupLayoutParams) + player.checkPopupPositionBounds() + player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams) return true } return false @@ -326,13 +319,13 @@ abstract class BasePlayerGestureListener( distanceY: Float ): Boolean { - if (!playerImpl.isFullscreen) { + if (!player.isFullscreen) { return false } val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service) val isTouchingNavigationBar: Boolean = - initialEvent.y > (playerImpl.rootView.height - getNavigationBarHeight(service)) + initialEvent.y > (player.rootView.height - getNavigationBarHeight(service)) if (isTouchingStatusBar || isTouchingNavigationBar) { return false } @@ -340,7 +333,7 @@ abstract class BasePlayerGestureListener( val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD if ( !isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) || - playerImpl.currentState == BasePlayer.STATE_COMPLETED + player.currentState == Player.STATE_COMPLETED ) { return false } @@ -371,7 +364,7 @@ abstract class BasePlayerGestureListener( } if (!isMovingInPopup) { - AnimationUtils.animateView(playerImpl.closeButton, true, 200) + AnimationUtils.animateView(player.closeOverlayButton, true, 200) } isMovingInPopup = true @@ -381,20 +374,20 @@ abstract class BasePlayerGestureListener( val diffY: Float = (movingEvent.rawY - initialEvent.rawY) var posY: Float = (initialPopupY + diffY) - if (posX > playerImpl.screenWidth - playerImpl.popupWidth) { - posX = (playerImpl.screenWidth - playerImpl.popupWidth) + if (posX > player.screenWidth - player.popupLayoutParams!!.width) { + posX = (player.screenWidth - player.popupLayoutParams!!.width) } else if (posX < 0) { posX = 0f } - if (posY > playerImpl.screenHeight - playerImpl.popupHeight) { - posY = (playerImpl.screenHeight - playerImpl.popupHeight) + if (posY > player.screenHeight - player.popupLayoutParams!!.height) { + posY = (player.screenHeight - player.popupLayoutParams!!.height) } else if (posY < 0) { posY = 0f } - playerImpl.popupLayoutParams.x = posX.toInt() - playerImpl.popupLayoutParams.y = posY.toInt() + player.popupLayoutParams!!.x = posX.toInt() + player.popupLayoutParams!!.y = posY.toInt() onScroll( MainPlayer.PlayerType.POPUP, @@ -405,8 +398,7 @@ abstract class BasePlayerGestureListener( distanceY ) - playerImpl.windowManager - .updateViewLayout(playerImpl.rootView, playerImpl.popupLayoutParams) + player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams) return true } @@ -474,16 +466,16 @@ abstract class BasePlayerGestureListener( // /////////////////////////////////////////////////////////////////// private fun getDisplayPortion(e: MotionEvent): DisplayPortion { - return if (playerImpl.playerType == MainPlayer.PlayerType.POPUP) { + return if (player.playerType == MainPlayer.PlayerType.POPUP) { when { - e.x < playerImpl.popupWidth / 3.0 -> DisplayPortion.LEFT - e.x > playerImpl.popupWidth * 2.0 / 3.0 -> DisplayPortion.RIGHT + e.x < player.popupLayoutParams!!.width / 3.0 -> DisplayPortion.LEFT + e.x > player.popupLayoutParams!!.width * 2.0 / 3.0 -> DisplayPortion.RIGHT else -> DisplayPortion.MIDDLE } } else /* MainPlayer.PlayerType.VIDEO */ { when { - e.x < playerImpl.rootView.width / 3.0 -> DisplayPortion.LEFT - e.x > playerImpl.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT + e.x < player.rootView.width / 3.0 -> DisplayPortion.LEFT + e.x > player.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT else -> DisplayPortion.MIDDLE } } @@ -491,14 +483,14 @@ abstract class BasePlayerGestureListener( // Currently needed for scrolling since there is no action more the middle portion private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion { - return if (playerImpl.playerType == MainPlayer.PlayerType.POPUP) { + return if (player.playerType == MainPlayer.PlayerType.POPUP) { when { - e.x < playerImpl.popupWidth / 2.0 -> DisplayPortion.LEFT_HALF + e.x < player.popupLayoutParams!!.width / 2.0 -> DisplayPortion.LEFT_HALF else -> DisplayPortion.RIGHT_HALF } } else /* MainPlayer.PlayerType.VIDEO */ { when { - e.x < playerImpl.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF + e.x < player.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF else -> DisplayPortion.RIGHT_HALF } } @@ -522,7 +514,7 @@ abstract class BasePlayerGestureListener( companion object { private const val TAG = "BasePlayerGestListener" - private val DEBUG = BasePlayer.DEBUG + private val DEBUG = Player.DEBUG private const val DOUBLE_TAP_DELAY = 550L private const val MOVEMENT_THRESHOLD = 40 diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java index 8f9514781..887e32a23 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java @@ -11,15 +11,15 @@ import android.widget.ProgressBar; import androidx.appcompat.content.res.AppCompatResources; import org.jetbrains.annotations.NotNull; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; -import org.schabi.newpipe.player.BasePlayer; import org.schabi.newpipe.player.MainPlayer; -import org.schabi.newpipe.player.VideoPlayerImpl; +import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.helper.PlayerHelper; -import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; -import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; -import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; +import static org.schabi.newpipe.player.Player.STATE_PLAYING; +import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_DURATION; +import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_HIDE_TIME; import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -33,14 +33,14 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView; public class PlayerGestureListener extends BasePlayerGestureListener implements View.OnTouchListener { - private static final String TAG = ".PlayerGestureListener"; - private static final boolean DEBUG = BasePlayer.DEBUG; + private static final String TAG = PlayerGestureListener.class.getSimpleName(); + private static final boolean DEBUG = MainActivity.DEBUG; private final int maxVolume; - public PlayerGestureListener(final VideoPlayerImpl playerImpl, final MainPlayer service) { - super(playerImpl, service); - maxVolume = playerImpl.getAudioReactor().getMaxVolume(); + public PlayerGestureListener(final Player player, final MainPlayer service) { + super(player, service); + maxVolume = player.getAudioReactor().getMaxVolume(); } @Override @@ -48,46 +48,44 @@ public class PlayerGestureListener @NotNull final DisplayPortion portion) { if (DEBUG) { Log.d(TAG, "onDoubleTap called with playerType = [" - + playerImpl.getPlayerType() + "], portion = [" - + portion + "]"); + + player.getPlayerType() + "], portion = [" + portion + "]"); } - if (playerImpl.isSomePopupMenuVisible()) { - playerImpl.hideControls(0, 0); + if (player.isSomePopupMenuVisible()) { + player.hideControls(0, 0); } if (portion == DisplayPortion.LEFT) { - playerImpl.onFastRewind(); + player.fastRewind(); } else if (portion == DisplayPortion.MIDDLE) { - playerImpl.onPlayPause(); + player.playPause(); } else if (portion == DisplayPortion.RIGHT) { - playerImpl.onFastForward(); + player.fastForward(); } } @Override public void onSingleTap(@NotNull final MainPlayer.PlayerType playerType) { if (DEBUG) { - Log.d(TAG, "onSingleTap called with playerType = [" - + playerImpl.getPlayerType() + "]"); + Log.d(TAG, "onSingleTap called with playerType = [" + player.getPlayerType() + "]"); } if (playerType == MainPlayer.PlayerType.POPUP) { - if (playerImpl.isControlsVisible()) { - playerImpl.hideControls(100, 100); + if (player.isControlsVisible()) { + player.hideControls(100, 100); } else { - playerImpl.getPlayPauseButton().requestFocus(); - playerImpl.showControlsThenHide(); + player.getPlayPauseButton().requestFocus(); + player.showControlsThenHide(); } } else /* playerType == MainPlayer.PlayerType.VIDEO */ { - if (playerImpl.isControlsVisible()) { - playerImpl.hideControls(150, 0); + if (player.isControlsVisible()) { + player.hideControls(150, 0); } else { - if (playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { - playerImpl.showControls(0); + if (player.getCurrentState() == Player.STATE_COMPLETED) { + player.showControls(0); } else { - playerImpl.showControlsThenHide(); + player.showControlsThenHide(); } } } @@ -101,8 +99,7 @@ public class PlayerGestureListener final float distanceX, final float distanceY) { if (DEBUG) { Log.d(TAG, "onScroll called with playerType = [" - + playerImpl.getPlayerType() + "], portion = [" - + portion + "]"); + + player.getPlayerType() + "], portion = [" + portion + "]"); } if (playerType == MainPlayer.PlayerType.VIDEO) { final boolean isBrightnessGestureEnabled = @@ -123,8 +120,8 @@ public class PlayerGestureListener } } else /* MainPlayer.PlayerType.POPUP */ { - final View closingOverlayView = playerImpl.getClosingOverlay(); - if (playerImpl.isInsideClosingRadius(movingEvent)) { + final View closingOverlayView = player.getClosingOverlayView(); + if (player.isInsideClosingRadius(movingEvent)) { if (closingOverlayView.getVisibility() == View.GONE) { animateView(closingOverlayView, true, 250); } @@ -137,17 +134,17 @@ public class PlayerGestureListener } private void onScrollMainVolume(final float distanceX, final float distanceY) { - playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY); - final float currentProgressPercent = (float) playerImpl - .getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength(); + player.getVolumeProgressBar().incrementProgressBy((int) distanceY); + final float currentProgressPercent = (float) player + .getVolumeProgressBar().getProgress() / player.getMaxGestureLength(); final int currentVolume = (int) (maxVolume * currentProgressPercent); - playerImpl.getAudioReactor().setVolume(currentVolume); + player.getAudioReactor().setVolume(currentVolume); if (DEBUG) { Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); } - playerImpl.getVolumeImageView().setImageDrawable( + player.getVolumeImageView().setImageDrawable( AppCompatResources.getDrawable(service, currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_24dp : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_24dp @@ -155,23 +152,23 @@ public class PlayerGestureListener : R.drawable.ic_volume_up_white_24dp) ); - if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { - animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200); + if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { + animateView(player.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200); } - if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { - playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE); + if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { + player.getBrightnessRelativeLayout().setVisibility(View.GONE); } } private void onScrollMainBrightness(final float distanceX, final float distanceY) { - final Activity parent = playerImpl.getParentActivity(); + final Activity parent = player.getParentActivity(); if (parent == null) { return; } final Window window = parent.getWindow(); final WindowManager.LayoutParams layoutParams = window.getAttributes(); - final ProgressBar bar = playerImpl.getBrightnessProgressBar(); + final ProgressBar bar = player.getBrightnessProgressBar(); final float oldBrightness = layoutParams.screenBrightness; bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness)))); bar.incrementProgressBy((int) distanceY); @@ -188,7 +185,7 @@ public class PlayerGestureListener + "currentBrightness = " + currentProgressPercent); } - playerImpl.getBrightnessImageView().setImageDrawable( + player.getBrightnessImageView().setImageDrawable( AppCompatResources.getDrawable(service, currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_24dp @@ -197,11 +194,11 @@ public class PlayerGestureListener : R.drawable.ic_brightness_high_white_24dp) ); - if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { - animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200); + if (player.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { + animateView(player.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200); } - if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { - playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE); + if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { + player.getVolumeRelativeLayout().setVisibility(View.GONE); } } @@ -210,40 +207,40 @@ public class PlayerGestureListener @NotNull final MotionEvent event) { if (DEBUG) { Log.d(TAG, "onScrollEnd called with playerType = [" - + playerImpl.getPlayerType() + "]"); + + player.getPlayerType() + "]"); } if (playerType == MainPlayer.PlayerType.VIDEO) { if (DEBUG) { Log.d(TAG, "onScrollEnd() called"); } - if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { - animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, + if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { + animateView(player.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); } - if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { - animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, + if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { + animateView(player.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); } - if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { - playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) { + player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } } else { - if (playerImpl == null) { + if (player == null) { return; } - if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { - playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) { + player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } - if (playerImpl.isInsideClosingRadius(event)) { - playerImpl.closePopup(); + if (player.isInsideClosingRadius(event)) { + player.closePopup(); } else { - animateView(playerImpl.getClosingOverlay(), false, 0); + animateView(player.getClosingOverlayView(), false, 0); - if (!playerImpl.isPopupClosing) { - animateView(playerImpl.getCloseButton(), false, 200); + if (!player.isPopupClosing()) { + animateView(player.getCloseOverlayButton(), false, 200); } } } @@ -254,12 +251,12 @@ public class PlayerGestureListener if (DEBUG) { Log.d(TAG, "onPopupResizingStart called"); } - playerImpl.showAndAnimateControl(-1, true); - playerImpl.getLoadingPanel().setVisibility(View.GONE); + player.showAndAnimateControl(-1, true); + player.getLoadingPanel().setVisibility(View.GONE); - playerImpl.hideControls(0, 0); - animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0); - animateView(playerImpl.getResizingIndicator(), true, 200, 0); + player.hideControls(0, 0); + animateView(player.getCurrentDisplaySeek(), false, 0, 0); + animateView(player.getResizingIndicator(), true, 200, 0); } @Override @@ -267,7 +264,7 @@ public class PlayerGestureListener if (DEBUG) { Log.d(TAG, "onPopupResizingEnd called"); } - animateView(playerImpl.getResizingIndicator(), false, 100, 0); + animateView(player.getResizingIndicator(), false, 100, 0); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java index 93952a811..f774c90a0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java @@ -1,10 +1,10 @@ package org.schabi.newpipe.player.event; import org.schabi.newpipe.player.MainPlayer; -import org.schabi.newpipe.player.VideoPlayerImpl; +import org.schabi.newpipe.player.Player; public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener { - void onServiceConnected(VideoPlayerImpl player, + void onServiceConnected(Player player, MainPlayer playerService, boolean playAfterConnect); void onServiceDisconnected(); 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 8b2c0e925..253f0fbba 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 @@ -18,7 +18,7 @@ import androidx.fragment.app.DialogFragment; import org.schabi.newpipe.R; import org.schabi.newpipe.util.SliderStrategy; -import static org.schabi.newpipe.player.BasePlayer.DEBUG; +import static org.schabi.newpipe.player.Player.DEBUG; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; public class PlaybackParameterDialog extends DialogFragment { 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 d89b5dd19..54021b616 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 @@ -1,8 +1,15 @@ package org.schabi.newpipe.player.helper; +import android.annotation.SuppressLint; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.PixelFormat; +import android.os.Build; import android.provider.Settings; +import android.view.Gravity; +import android.view.ViewGroup; +import android.view.WindowManager; import android.view.accessibility.CaptioningManager; import androidx.annotation.IntDef; @@ -11,11 +18,14 @@ import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; import com.google.android.exoplayer2.util.MimeTypes; import org.schabi.newpipe.R; @@ -27,6 +37,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.utils.Utils; +import org.schabi.newpipe.player.MainPlayer; +import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; @@ -41,13 +53,16 @@ import java.util.Formatter; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; -import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL; -import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT; -import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; import static java.lang.annotation.RetentionPolicy.SOURCE; +import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS; +import static org.schabi.newpipe.player.Player.PLAYER_TYPE; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI; @@ -71,6 +86,15 @@ public final class PlayerHelper { int AUTOPLAY_TYPE_NEVER = 2; } + @Retention(SOURCE) + @IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND, + MINIMIZE_ON_EXIT_MODE_POPUP}) + public @interface MinimizeMode { + int MINIMIZE_ON_EXIT_MODE_NONE = 0; + int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1; + int MINIMIZE_ON_EXIT_MODE_POPUP = 2; + } + private PlayerHelper() { } //////////////////////////////////////////////////////////////////////////// @@ -121,14 +145,16 @@ public final class PlayerHelper { @NonNull public static String resizeTypeOf(@NonNull final Context context, - @AspectRatioFrameLayout.ResizeMode final int resizeMode) { + @ResizeMode final int resizeMode) { switch (resizeMode) { - case RESIZE_MODE_FIT: + case AspectRatioFrameLayout.RESIZE_MODE_FIT: return context.getResources().getString(R.string.resize_fit); - case RESIZE_MODE_FILL: + case AspectRatioFrameLayout.RESIZE_MODE_FILL: return context.getResources().getString(R.string.resize_fill); - case RESIZE_MODE_ZOOM: + case AspectRatioFrameLayout.RESIZE_MODE_ZOOM: return context.getResources().getString(R.string.resize_zoom); + case AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT: + case AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH: default: throw new IllegalArgumentException("Unrecognized resize mode: " + resizeMode); } @@ -199,23 +225,23 @@ public final class PlayerHelper { //////////////////////////////////////////////////////////////////////////// public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) { - return isResumeAfterAudioFocusGain(context, false); + return getPreferences(context) + .getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), false); } public static boolean isVolumeGestureEnabled(@NonNull final Context context) { - return isVolumeGestureEnabled(context, true); + return getPreferences(context) + .getBoolean(context.getString(R.string.volume_gesture_control_key), true); } public static boolean isBrightnessGestureEnabled(@NonNull final Context context) { - return isBrightnessGestureEnabled(context, true); - } - - public static boolean isRememberingPopupDimensions(@NonNull final Context context) { - return isRememberingPopupDimensions(context, true); + return getPreferences(context) + .getBoolean(context.getString(R.string.brightness_gesture_control_key), true); } public static boolean isAutoQueueEnabled(@NonNull final Context context) { - return isAutoQueueEnabled(context, false); + return getPreferences(context) + .getBoolean(context.getString(R.string.auto_queue_key), false); } public static boolean isClearingQueueConfirmationRequired(@NonNull final Context context) { @@ -229,7 +255,8 @@ public final class PlayerHelper { final String popupAction = context.getString(R.string.minimize_on_exit_popup_key); final String backgroundAction = context.getString(R.string.minimize_on_exit_background_key); - final String action = getMinimizeOnExitAction(context, defaultAction); + final String action = getPreferences(context) + .getString(context.getString(R.string.minimize_on_exit_key), defaultAction); if (action.equals(popupAction)) { return MINIMIZE_ON_EXIT_MODE_POPUP; } else if (action.equals(backgroundAction)) { @@ -239,9 +266,23 @@ public final class PlayerHelper { } } + public static boolean isMinimizeOnExitToPopup(@NonNull final Context context) { + return getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_POPUP; + } + + public static boolean isMinimizeOnExitToBackground(@NonNull final Context context) { + return getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_BACKGROUND; + } + + public static boolean isMinimizeOnExitDisabled(@NonNull final Context context) { + return getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_NONE; + } + @AutoplayType public static int getAutoplayType(@NonNull final Context context) { - final String type = getAutoplayType(context, context.getString(R.string.autoplay_wifi_key)); + final String type = getPreferences(context).getString( + context.getString(R.string.autoplay_key), + context.getString(R.string.autoplay_wifi_key)); if (type.equals(context.getString(R.string.autoplay_always_key))) { return AUTOPLAY_TYPE_ALWAYS; } else if (type.equals(context.getString(R.string.autoplay_never_key))) { @@ -350,14 +391,32 @@ public final class PlayerHelper { return captioningManager.getFontScale(); } + /** + * @param context the Android context + * @return the screen brightness to use. A value less than 0 (the default) means to use the + * preferred screen brightness + */ public static float getScreenBrightness(@NonNull final Context context) { - //a value of less than 0, the default, means to use the preferred screen brightness - return getScreenBrightness(context, -1); + final SharedPreferences sp = getPreferences(context); + final long timestamp = + sp.getLong(context.getString(R.string.screen_brightness_timestamp_key), 0); + // Hypothesis: 4h covers a viewing block, e.g. evening. + // External lightning conditions will change in the next + // viewing block so we fall back to the default brightness + if ((System.currentTimeMillis() - timestamp) > TimeUnit.HOURS.toMillis(4)) { + return -1; + } else { + return sp.getFloat(context.getString(R.string.screen_brightness_key), -1); + } } public static void setScreenBrightness(@NonNull final Context context, - final float setScreenBrightness) { - setScreenBrightness(context, setScreenBrightness, System.currentTimeMillis()); + final float screenBrightness) { + getPreferences(context).edit() + .putFloat(context.getString(R.string.screen_brightness_key), screenBrightness) + .putLong(context.getString(R.string.screen_brightness_timestamp_key), + System.currentTimeMillis()) + .apply(); } public static boolean globalScreenOrientationLocked(final Context context) { @@ -376,75 +435,11 @@ public final class PlayerHelper { return PreferenceManager.getDefaultSharedPreferences(context); } - private static boolean isResumeAfterAudioFocusGain(@NonNull final Context context, - final boolean b) { - return getPreferences(context) - .getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), b); - } - - private static boolean isVolumeGestureEnabled(@NonNull final Context context, - final boolean b) { - return getPreferences(context) - .getBoolean(context.getString(R.string.volume_gesture_control_key), b); - } - - private static boolean isBrightnessGestureEnabled(@NonNull final Context context, - final boolean b) { - return getPreferences(context) - .getBoolean(context.getString(R.string.brightness_gesture_control_key), b); - } - - private static boolean isRememberingPopupDimensions(@NonNull final Context context, - final boolean b) { - return getPreferences(context) - .getBoolean(context.getString(R.string.popup_remember_size_pos_key), b); - } - private static boolean isUsingInexactSeek(@NonNull final Context context) { return getPreferences(context) .getBoolean(context.getString(R.string.use_inexact_seek_key), false); } - private static boolean isAutoQueueEnabled(@NonNull final Context context, final boolean b) { - return getPreferences(context).getBoolean(context.getString(R.string.auto_queue_key), b); - } - - private static void setScreenBrightness(@NonNull final Context context, - final float screenBrightness, final long timestamp) { - final SharedPreferences.Editor editor = getPreferences(context).edit(); - editor.putFloat(context.getString(R.string.screen_brightness_key), screenBrightness); - editor.putLong(context.getString(R.string.screen_brightness_timestamp_key), timestamp); - editor.apply(); - } - - private static float getScreenBrightness(@NonNull final Context context, - final float screenBrightness) { - final SharedPreferences sp = getPreferences(context); - final long timestamp = sp - .getLong(context.getString(R.string.screen_brightness_timestamp_key), 0); - // Hypothesis: 4h covers a viewing block, e.g. evening. - // External lightning conditions will change in the next - // viewing block so we fall back to the default brightness - if ((System.currentTimeMillis() - timestamp) > TimeUnit.HOURS.toMillis(4)) { - return screenBrightness; - } else { - return sp - .getFloat(context.getString(R.string.screen_brightness_key), screenBrightness); - } - } - - private static String getMinimizeOnExitAction(@NonNull final Context context, - final String key) { - return getPreferences(context) - .getString(context.getString(R.string.minimize_on_exit_key), key); - } - - private static String getAutoplayType(@NonNull final Context context, - final String key) { - return getPreferences(context).getString(context.getString(R.string.autoplay_key), - key); - } - private static SinglePlayQueue getAutoQueuedSinglePlayQueue( final StreamInfoItem streamInfoItem) { final SinglePlayQueue singlePlayQueue = new SinglePlayQueue(streamInfoItem); @@ -452,12 +447,168 @@ public final class PlayerHelper { return singlePlayQueue; } - @Retention(SOURCE) - @IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND, - MINIMIZE_ON_EXIT_MODE_POPUP}) - public @interface MinimizeMode { - int MINIMIZE_ON_EXIT_MODE_NONE = 0; - int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1; - int MINIMIZE_ON_EXIT_MODE_POPUP = 2; + + //////////////////////////////////////////////////////////////////////////// + // Utils used by player + //////////////////////////////////////////////////////////////////////////// + + public static MainPlayer.PlayerType retrievePlayerTypeFromIntent(final Intent intent) { + // If you want to open popup from the app just include Constants.POPUP_ONLY into an extra + return MainPlayer.PlayerType.values()[ + intent.getIntExtra(PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal())]; + } + + public static boolean isPlaybackResumeEnabled(final Player player) { + return player.getPrefs().getBoolean( + player.getContext().getString(R.string.enable_watch_history_key), true) + && player.getPrefs().getBoolean( + player.getContext().getString(R.string.enable_playback_resume_key), true); + } + + @RepeatMode + public static int nextRepeatMode(@RepeatMode final int repeatMode) { + switch (repeatMode) { + case REPEAT_MODE_OFF: + return REPEAT_MODE_ONE; + case REPEAT_MODE_ONE: + return REPEAT_MODE_ALL; + case REPEAT_MODE_ALL: default: + return REPEAT_MODE_OFF; + } + } + + @ResizeMode + public static int retrieveResizeModeFromPrefs(final Player player) { + return player.getPrefs().getInt(player.getContext().getString(R.string.last_resize_mode), + AspectRatioFrameLayout.RESIZE_MODE_FIT); + } + + @SuppressLint("SwitchIntDef") // only fit, fill and zoom are supported by NewPipe + @ResizeMode + public static int nextResizeModeAndSaveToPrefs(final Player player, + @ResizeMode final int resizeMode) { + final int newResizeMode; + switch (resizeMode) { + case AspectRatioFrameLayout.RESIZE_MODE_FIT: + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; + break; + case AspectRatioFrameLayout.RESIZE_MODE_FILL: + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; + break; + case AspectRatioFrameLayout.RESIZE_MODE_ZOOM: + default: + newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + break; + } + + player.getPrefs().edit().putInt( + player.getContext().getString(R.string.last_resize_mode), resizeMode).apply(); + return newResizeMode; + } + + public static PlaybackParameters retrievePlaybackParametersFromPrefs(final Player player) { + final float speed = player.getPrefs().getFloat(player.getContext().getString( + R.string.playback_speed_key), player.getPlaybackSpeed()); + final float pitch = player.getPrefs().getFloat(player.getContext().getString( + R.string.playback_pitch_key), player.getPlaybackPitch()); + final boolean skipSilence = player.getPrefs().getBoolean(player.getContext().getString( + R.string.playback_skip_silence_key), player.getPlaybackSkipSilence()); + return new PlaybackParameters(speed, pitch, skipSilence); + } + + public static void savePlaybackParametersToPrefs(final Player player, + final float speed, + final float pitch, + final boolean skipSilence) { + player.getPrefs().edit() + .putFloat(player.getContext().getString(R.string.playback_speed_key), speed) + .putFloat(player.getContext().getString(R.string.playback_pitch_key), pitch) + .putBoolean(player.getContext().getString(R.string.playback_skip_silence_key), + skipSilence) + .apply(); + } + + /** + * @param player {@code screenWidth} and {@code screenHeight} must have been initialized + * @return the popup starting layout params + */ + @SuppressLint("RtlHardcoded") + public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs( + final Player player) { + final boolean popupRememberSizeAndPos = player.getPrefs().getBoolean( + player.getContext().getString(R.string.popup_remember_size_pos_key), true); + final float defaultSize = + player.getContext().getResources().getDimension(R.dimen.popup_default_width); + final float popupWidth = popupRememberSizeAndPos + ? player.getPrefs().getFloat(player.getContext().getString( + R.string.popup_saved_width_key), defaultSize) + : defaultSize; + final float popupHeight = getMinimumVideoHeight(popupWidth); + + final WindowManager.LayoutParams popupLayoutParams = new WindowManager.LayoutParams( + (int) popupWidth, (int) popupHeight, + popupLayoutParamType(), + IDLE_WINDOW_FLAGS, + PixelFormat.TRANSLUCENT); + popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; + popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + + final int centerX = (int) (player.getScreenWidth() / 2f - popupWidth / 2f); + final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f); + popupLayoutParams.x = popupRememberSizeAndPos + ? player.getPrefs().getInt(player.getContext().getString( + R.string.popup_saved_x_key), centerX) : centerX; + popupLayoutParams.y = popupRememberSizeAndPos + ? player.getPrefs().getInt(player.getContext().getString( + R.string.popup_saved_y_key), centerY) : centerY; + + return popupLayoutParams; + } + + public static void savePopupPositionAndSizeToPrefs(final Player player) { + if (player.getPopupLayoutParams() != null) { + player.getPrefs().edit() + .putFloat(player.getContext().getString(R.string.popup_saved_width_key), + player.getPopupLayoutParams().width) + .putInt(player.getContext().getString(R.string.popup_saved_x_key), + player.getPopupLayoutParams().x) + .putInt(player.getContext().getString(R.string.popup_saved_y_key), + player.getPopupLayoutParams().y) + .apply(); + } + } + + public static float getMinimumVideoHeight(final float width) { + return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have + } + + @SuppressLint("RtlHardcoded") + public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() { + final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + + final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, + popupLayoutParamType(), + flags, + PixelFormat.TRANSLUCENT); + + closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; + closeOverlayLayoutParams.softInputMode = + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + return closeOverlayLayoutParams; + } + + public static int popupLayoutParamType() { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.O + ? WindowManager.LayoutParams.TYPE_PHONE + : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } + + public static int retrieveSeekDurationFromPreferences(final Player player) { + return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString( + player.getContext().getString(R.string.seek_duration_key), + player.getContext().getString(R.string.seek_duration_default_value)))); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java index 854e3eb2b..da1238c81 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java @@ -16,7 +16,7 @@ import org.schabi.newpipe.App; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.MainPlayer; -import org.schabi.newpipe.player.VideoPlayerImpl; +import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.event.PlayerServiceEventListener; import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -33,7 +33,7 @@ public final class PlayerHolder { private static ServiceConnection serviceConnection; public static boolean bound; private static MainPlayer playerService; - private static VideoPlayerImpl player; + private static Player player; /** * Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service, diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java index 883d9bb4f..c4b02d985 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java @@ -3,11 +3,11 @@ package org.schabi.newpipe.player.mediasession; import android.support.v4.media.MediaDescriptionCompat; public interface MediaSessionCallback { - void onSkipToPrevious(); + void playPrevious(); - void onSkipToNext(); + void playNext(); - void onSkipToIndex(int index); + void playItemAtIndex(int index); int getCurrentPlayingIndex(); @@ -15,7 +15,7 @@ public interface MediaSessionCallback { MediaDescriptionCompat getQueueMetadata(int index); - void onPlay(); + void play(); - void onPause(); + void pause(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java index 764c375af..62664c827 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java @@ -65,18 +65,18 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator @Override public void onSkipToPrevious(final Player player, final ControlDispatcher controlDispatcher) { - callback.onSkipToPrevious(); + callback.playPrevious(); } @Override public void onSkipToQueueItem(final Player player, final ControlDispatcher controlDispatcher, final long id) { - callback.onSkipToIndex((int) id); + callback.playItemAtIndex((int) id); } @Override public void onSkipToNext(final Player player, final ControlDispatcher controlDispatcher) { - callback.onSkipToNext(); + callback.playNext(); } private void publishFloatingQueueWindow() { diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java index 21c99859c..8bfbcde6b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueuePlaybackController.java @@ -14,9 +14,9 @@ public class PlayQueuePlaybackController extends DefaultControlDispatcher { @Override public boolean dispatchSetPlayWhenReady(final Player player, final boolean playWhenReady) { if (playWhenReady) { - callback.onPlay(); + callback.play(); } else { - callback.onPause(); + callback.pause(); } return true; } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java similarity index 77% rename from app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java rename to app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java index 5b20077c3..9dcb12344 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/BasePlayerMediaSession.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java @@ -5,33 +5,33 @@ import android.os.Bundle; import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaMetadataCompat; -import org.schabi.newpipe.player.BasePlayer; +import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.mediasession.MediaSessionCallback; import org.schabi.newpipe.player.playqueue.PlayQueueItem; -public class BasePlayerMediaSession implements MediaSessionCallback { - private final BasePlayer player; +public class PlayerMediaSession implements MediaSessionCallback { + private final Player player; - public BasePlayerMediaSession(final BasePlayer player) { + public PlayerMediaSession(final Player player) { this.player = player; } @Override - public void onSkipToPrevious() { - player.onPlayPrevious(); + public void playPrevious() { + player.playPrevious(); } @Override - public void onSkipToNext() { - player.onPlayNext(); + public void playNext() { + player.playNext(); } @Override - public void onSkipToIndex(final int index) { + public void playItemAtIndex(final int index) { if (player.getPlayQueue() == null) { return; } - player.onSelected(player.getPlayQueue().getItem(index)); + player.selectQueueItem(player.getPlayQueue().getItem(index)); } @Override @@ -52,11 +52,14 @@ public class BasePlayerMediaSession implements MediaSessionCallback { @Override public MediaDescriptionCompat getQueueMetadata(final int index) { - if (player.getPlayQueue() == null || player.getPlayQueue().getItem(index) == null) { + if (player.getPlayQueue() == null) { + return null; + } + final PlayQueueItem item = player.getPlayQueue().getItem(index); + if (item == null) { return null; } - final PlayQueueItem item = player.getPlayQueue().getItem(index); final MediaDescriptionCompat.Builder descriptionBuilder = new MediaDescriptionCompat.Builder() .setMediaId(String.valueOf(index)) @@ -83,12 +86,12 @@ public class BasePlayerMediaSession implements MediaSessionCallback { } @Override - public void onPlay() { - player.onPlay(); + public void play() { + player.play(); } @Override - public void onPause() { - player.onPause(); + public void pause() { + player.pause(); } } 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 978f558c4..9493fbc92 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -354,4 +354,19 @@ public final class Localization { private static double round(final double value, final int places) { return new BigDecimal(value).setScale(places, RoundingMode.HALF_UP).doubleValue(); } + + /** + * Workaround to match normalized captions like english to English or deutsch to Deutsch. + * @param list the list to search into + * @param toFind the string to look for + * @return whether the string was found or not + */ + public static boolean containsCaseInsensitive(final List list, final String toFind) { + for (final String i : list) { + if (i.equalsIgnoreCase(toFind)) { + return true; + } + } + return false; + } } 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 b45a1e7b9..ea02e0f6b 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -47,9 +47,8 @@ import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment; import org.schabi.newpipe.player.BackgroundPlayerActivity; -import org.schabi.newpipe.player.BasePlayer; +import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.MainPlayer; -import org.schabi.newpipe.player.VideoPlayer; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -78,11 +77,11 @@ public final class NavigationHelper { if (playQueue != null) { final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); if (cacheKey != null) { - intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); + intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey); } } - intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback); - intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_VIDEO); + intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback); + intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal()); return intent; } @@ -94,7 +93,7 @@ public final class NavigationHelper { final boolean resumePlayback, final boolean playWhenReady) { return getPlayerIntent(context, targetClazz, playQueue, resumePlayback) - .putExtra(BasePlayer.PLAY_WHEN_READY, playWhenReady); + .putExtra(Player.PLAY_WHEN_READY, playWhenReady); } @NonNull @@ -104,8 +103,8 @@ public final class NavigationHelper { final boolean selectOnAppend, final boolean resumePlayback) { return getPlayerIntent(context, targetClazz, playQueue, resumePlayback) - .putExtra(BasePlayer.APPEND_ONLY, true) - .putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend); + .putExtra(Player.APPEND_ONLY, true) + .putExtra(Player.SELECT_ON_APPEND, selectOnAppend); } public static void playOnMainPlayer(final AppCompatActivity activity, @@ -135,7 +134,7 @@ public final class NavigationHelper { Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); - intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP); + intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.POPUP.ordinal()); ContextCompat.startForegroundService(context, intent); } @@ -145,7 +144,7 @@ public final class NavigationHelper { Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT) .show(); final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback); - intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO); + intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.AUDIO.ordinal()); ContextCompat.startForegroundService(context, intent); } @@ -162,7 +161,7 @@ public final class NavigationHelper { final Intent intent = getPlayerEnqueueIntent( context, MainPlayer.class, queue, selectOnAppend, resumePlayback); - intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_VIDEO); + intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal()); ContextCompat.startForegroundService(context, intent); } @@ -182,7 +181,7 @@ public final class NavigationHelper { Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show(); final Intent intent = getPlayerEnqueueIntent( context, MainPlayer.class, queue, selectOnAppend, resumePlayback); - intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_POPUP); + intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.POPUP.ordinal()); ContextCompat.startForegroundService(context, intent); } @@ -198,7 +197,7 @@ public final class NavigationHelper { Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show(); final Intent intent = getPlayerEnqueueIntent( context, MainPlayer.class, queue, selectOnAppend, resumePlayback); - intent.putExtra(VideoPlayer.PLAYER_TYPE, VideoPlayer.PLAYER_TYPE_AUDIO); + intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.AUDIO.ordinal()); ContextCompat.startForegroundService(context, intent); } @@ -493,7 +492,7 @@ public final class NavigationHelper { if (playQueue != null) { final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); if (cacheKey != null) { - intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); + intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey); } } context.startActivity(intent); diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index e9d951520..1e74f25a6 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -21,7 +21,6 @@ use_external_video_player use_external_audio_player - use_oldplayer volume_gesture_control brightness_gesture_control @@ -33,6 +32,10 @@ screen_brightness_timestamp_key clear_queue_confirmation_key + popup_saved_width + popup_saved_x + popup_saved_y + seek_duration 10000 @@ -70,7 +73,6 @@ @string/minimize_on_exit_popup_description - autoplay_key @string/autoplay_wifi_key autoplay_always_key diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index 0a5190b29..400a91a29 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -25,7 +25,7 @@ lines="156,158"/> + files="Player.java"/> From 4a12b0ab2d4069200f5608dc01924dfc030e1a41 Mon Sep 17 00:00:00 2001 From: Stypox Date: Mon, 11 Jan 2021 15:06:40 +0100 Subject: [PATCH 2/5] Revert hiding detail fragment tabs when in fullscreen --- .../schabi/newpipe/fragments/detail/VideoDetailFragment.java | 2 -- 1 file changed, 2 deletions(-) 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 b9f4ef6a9..33c125072 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 @@ -1889,10 +1889,8 @@ public final class VideoDetailFragment if (fullscreen) { hideSystemUiIfNeeded(); - viewPager.setVisibility(View.GONE); } else { showSystemUi(); - viewPager.setVisibility(View.VISIBLE); } if (relatedStreamsLayout != null) { From cece83328a882d9522bfbe8eb1c09fe3598181f9 Mon Sep 17 00:00:00 2001 From: Stypox Date: Mon, 11 Jan 2021 15:12:44 +0100 Subject: [PATCH 3/5] Fix wrong speed indicator in queue activity --- .../java/org/schabi/newpipe/player/ServicePlayerActivity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 283c25e4f..6e13554d2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -128,6 +128,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity getMenuInflater().inflate(R.menu.menu_play_queue, m); getMenuInflater().inflate(getPlayerOptionMenuResource(), m); onMaybeMuteChanged(); + onPlaybackParameterChanged(player.getPlaybackParameters()); return true; } From 059bb7622dbb04af314b0745cfd2e8cf7708ecf5 Mon Sep 17 00:00:00 2001 From: Stypox Date: Tue, 12 Jan 2021 21:15:06 +0100 Subject: [PATCH 4/5] Merge and rename into PlayQueueActivity --- app/src/main/AndroidManifest.xml | 2 +- .../player/BackgroundPlayerActivity.java | 55 --------------- ...erActivity.java => PlayQueueActivity.java} | 68 +++++++------------ .../schabi/newpipe/util/NavigationHelper.java | 4 +- .../activity_player_queue_control.xml | 2 +- .../layout/activity_player_queue_control.xml | 2 +- app/src/main/res/menu/menu_play_queue.xml | 2 +- 7 files changed, 31 insertions(+), 104 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java rename app/src/main/java/org/schabi/newpipe/player/{ServicePlayerActivity.java => PlayQueueActivity.java} (94%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3509f2d13..ac33a7f0f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -53,7 +53,7 @@ diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java deleted file mode 100644 index eb1c74dad..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.schabi.newpipe.player; - -import android.content.Intent; -import android.view.Menu; - -import org.schabi.newpipe.R; - -public final class BackgroundPlayerActivity extends ServicePlayerActivity { - - private static final String TAG = "BackgroundPlayerActivity"; - - @Override - public String getTag() { - return TAG; - } - - @Override - public String getSupportActionTitle() { - return getResources().getString(R.string.title_activity_play_queue); - } - - @Override - public Intent getBindIntent() { - return new Intent(this, MainPlayer.class); - } - - @Override - public void startPlayerListener() { - if (player != null) { - player.setActivityListener(this); - } - } - - @Override - public void stopPlayerListener() { - if (player != null) { - player.removeActivityListener(this); - } - } - - @Override - public int getPlayerOptionMenuResource() { - return R.menu.menu_play_queue_bg; - } - - @Override - public void setupMenu(final Menu menu) { - if (player != null) { - menu.findItem(R.id.action_switch_popup) - .setVisible(!player.popupPlayerSelected()); - menu.findItem(R.id.action_switch_background) - .setVisible(!player.audioPlayerSelected()); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java similarity index 94% rename from app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java rename to app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java index 6e13554d2..6ea7ecda3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java @@ -16,7 +16,6 @@ import android.widget.PopupMenu; import android.widget.SeekBar; import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ActivityCompat; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -48,9 +47,12 @@ import java.util.List; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; -public abstract class ServicePlayerActivity extends AppCompatActivity +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; @@ -60,7 +62,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private ServiceConnection serviceConnection; private boolean seeking; - private boolean redraw; //////////////////////////////////////////////////////////////////////////// // Views @@ -72,24 +73,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private Menu menu; - //////////////////////////////////////////////////////////////////////////// - // Abstracts - //////////////////////////////////////////////////////////////////////////// - - public abstract String getTag(); - - public abstract String getSupportActionTitle(); - - public abstract Intent getBindIntent(); - - public abstract void startPlayerListener(); - - public abstract void stopPlayerListener(); - - public abstract int getPlayerOptionMenuResource(); - - public abstract void setupMenu(Menu m); - //////////////////////////////////////////////////////////////////////////// // Activity Lifecycle //////////////////////////////////////////////////////////////////////////// @@ -106,27 +89,18 @@ public abstract class ServicePlayerActivity extends AppCompatActivity setSupportActionBar(queueControlBinding.toolbar); if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setTitle(getSupportActionTitle()); + getSupportActionBar().setTitle(R.string.title_activity_play_queue); } serviceConnection = getServiceConnection(); bind(); } - @Override - protected void onResume() { - super.onResume(); - if (redraw) { - ActivityCompat.recreate(this); - redraw = false; - } - } - @Override public boolean onCreateOptionsMenu(final Menu m) { this.menu = m; getMenuInflater().inflate(R.menu.menu_play_queue, m); - getMenuInflater().inflate(getPlayerOptionMenuResource(), m); + getMenuInflater().inflate(R.menu.menu_play_queue_bg, m); onMaybeMuteChanged(); onPlaybackParameterChanged(player.getPlaybackParameters()); return true; @@ -135,7 +109,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity // Allow to setup visibility of menuItems @Override public boolean onPrepareOptionsMenu(final Menu m) { - setupMenu(m); + if (player != null) { + menu.findItem(R.id.action_switch_popup) + .setVisible(!player.popupPlayerSelected()); + menu.findItem(R.id.action_switch_background) + .setVisible(!player.audioPlayerSelected()); + } return super.onPrepareOptionsMenu(m); } @@ -191,7 +170,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// private void bind() { - final boolean success = bindService(getBindIntent(), serviceConnection, BIND_AUTO_CREATE); + final Intent bindIntent = new Intent(this, MainPlayer.class); + final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE); if (!success) { unbindService(serviceConnection); } @@ -202,7 +182,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity if (serviceBound) { unbindService(serviceConnection); serviceBound = false; - stopPlayerListener(); + if (player != null) { + player.removeActivityListener(this); + } if (player != null && player.getPlayQueueAdapter() != null) { player.getPlayQueueAdapter().unsetSelectedListener(); @@ -221,12 +203,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return new ServiceConnection() { @Override public void onServiceDisconnected(final ComponentName name) { - Log.d(getTag(), "Player service is disconnected"); + Log.d(TAG, "Player service is disconnected"); } @Override public void onServiceConnected(final ComponentName name, final IBinder service) { - Log.d(getTag(), "Player service is connected"); + Log.d(TAG, "Player service is connected"); if (service instanceof PlayerServiceBinder) { player = ((PlayerServiceBinder) service).getPlayerInstance(); @@ -240,7 +222,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity finish(); } else { buildComponents(); - startPlayerListener(); + if (player != null) { + player.setActivityListener(PlayQueueActivity.this); + } } } }; @@ -463,7 +447,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity return; } PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(), - player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), getTag()); + player.getPlaybackSkipSilence(), this).show(getSupportFragmentManager(), TAG); } @Override @@ -517,10 +501,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(playlist); PlaylistAppendDialog.onPlaylistFound(getApplicationContext(), - () -> d.show(getSupportFragmentManager(), getTag()), - () -> PlaylistCreationDialog.newInstance(d) - .show(getSupportFragmentManager(), getTag() - )); + () -> d.show(getSupportFragmentManager(), TAG), + () -> PlaylistCreationDialog.newInstance(d).show(getSupportFragmentManager(), TAG)); } //////////////////////////////////////////////////////////////////////////// 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 ea02e0f6b..c90bb3025 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -46,7 +46,7 @@ import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment; -import org.schabi.newpipe.player.BackgroundPlayerActivity; +import org.schabi.newpipe.player.PlayQueueActivity; import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.helper.PlayerHelper; @@ -530,7 +530,7 @@ public final class NavigationHelper { } public static Intent getPlayQueueActivityIntent(final Context context) { - final Intent intent = new Intent(context, BackgroundPlayerActivity.class); + final Intent intent = new Intent(context, PlayQueueActivity.class); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml index 2adea9868..b106e7437 100644 --- a/app/src/main/res/layout-land/activity_player_queue_control.xml +++ b/app/src/main/res/layout-land/activity_player_queue_control.xml @@ -6,7 +6,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" - tools:context="org.schabi.newpipe.player.BackgroundPlayerActivity"> + tools:context="org.schabi.newpipe.player.PlayQueueActivity"> + tools:context="org.schabi.newpipe.player.PlayQueueActivity"> + tools:context=".player.PlayQueueActivity"> Date: Thu, 14 Jan 2021 10:11:44 +0100 Subject: [PATCH 5/5] Fix view binding types --- app/src/main/res/layout-large-land/player.xml | 12 +++++------- app/src/main/res/layout/player.xml | 7 +++---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/src/main/res/layout-large-land/player.xml b/app/src/main/res/layout-large-land/player.xml index 145d1e2f8..7213f4020 100644 --- a/app/src/main/res/layout-large-land/player.xml +++ b/app/src/main/res/layout-large-land/player.xml @@ -145,7 +145,7 @@ tools:text="The Video Artist LONG very LONG very Long" /> -