{@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/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
similarity index 91%
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 fd20fd175..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,13 +16,11 @@ 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;
import com.google.android.exoplayer2.PlaybackParameters;
-import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
@@ -49,19 +47,21 @@ 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;
- protected BasePlayer player;
+ protected Player player;
private boolean serviceBound;
private ServiceConnection serviceConnection;
private boolean seeking;
- private boolean redraw;
////////////////////////////////////////////////////////////////////////////
// Views
@@ -73,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
////////////////////////////////////////////////////////////////////////////
@@ -107,35 +89,32 @@ 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;
}
// 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);
}
@@ -167,14 +146,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);
@@ -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();
@@ -235,12 +217,14 @@ 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 {
buildComponents();
- startPlayerListener();
+ if (player != null) {
+ player.setActivityListener(PlayQueueActivity.this);
+ }
}
}
};
@@ -375,7 +359,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 +420,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()) {
@@ -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));
}
////////////////////////////////////////////////////////////////////////////
@@ -616,15 +598,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 +615,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 +632,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 +682,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/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}.
+ *
{@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/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 3fdab9a12..c235d7d67 100644
--- a/app/src/main/java/org/schabi/newpipe/util/Localization.java
+++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java
@@ -351,4 +351,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..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,10 +46,9 @@ import org.schabi.newpipe.local.history.StatisticsPlaylistFragment;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.local.subscription.SubscriptionFragment;
import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment;
-import org.schabi.newpipe.player.BackgroundPlayerActivity;
-import org.schabi.newpipe.player.BasePlayer;
+import org.schabi.newpipe.player.PlayQueueActivity;
+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);
@@ -531,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:ignore="HardcodedText,RtlHardcoded"
+ tools:text="720p" />
- use_external_audio_player
- use_oldplayervolume_gesture_controlbrightness_gesture_control
@@ -33,6 +32,10 @@
screen_brightness_timestamp_keyclear_queue_confirmation_key
+ popup_saved_width
+ popup_saved_x
+ popup_saved_y
+
seek_duration10000
@@ -70,7 +73,6 @@
@string/minimize_on_exit_popup_description
-
autoplay_key@string/autoplay_wifi_keyautoplay_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"/>