1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-06-18 11:20:00 +00:00
NewPipe/app/src/main/java/org/schabi/newpipe/player/Player.java
TiA4f8R 79e98db3bd
Apply the requested changes and little improvements
Apply the requested changes, use ShareUtils.shareText to share an stream in the play queue and optimize imports for Java files, using Android Studio functionality.

Apply the requested changes and do little improvements
Apply the requested changes, use ShareUtils.shareText to share an stream in the play queue and optimize imports for Java files, using Android Studio functionality.
2021-01-16 13:23:42 +01:00

4075 lines
154 KiB
Java

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.StreamSegment;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.info_list.StreamSegmentAdapter;
import org.schabi.newpipe.ktx.AnimationType;
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.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.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
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.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;
private StreamSegmentAdapter segmentAdapter;
@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<VideoStream> availableStreams;
private int selectedStreamIndex;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private PlayerBinding binding;
private ValueAnimator controlViewAnimator;
private final Handler controlsVisibilityHandler = new Handler();
// fullscreen player
private boolean isQueueVisible = false;
private boolean areSegmentsVisible = 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<VideoStream> sortedVideos) {
return videoPlayerSelected()
? ListHelper.getDefaultResolutionIndex(context, sortedVideos)
: ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos);
}
@Override
public int getOverrideResolutionIndex(final List<VideoStream> 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.itemsList.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.segmentsButton.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.itemsListPanel, (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();
closeItemsList();
// 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);
segmentAdapter = new StreamSegmentAdapter(getStreamSegmentListener());
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.segmentsButton.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();
closeItemsList();
} 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
closeItemsList();
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).
* <p>
* If it's out of these boundaries, {@link #popupLayoutParams}' position is changed
* and {@code true} is returned to represent this change.
* </p>
*/
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);
if (areSegmentsVisible) {
segmentAdapter.selectSegmentAt(getNearestStreamSegmentPosition(currentProgress));
}
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);
animate(binding.currentDisplaySeek, true, DEFAULT_CONTROLS_DURATION,
AnimationType.SCALE_AND_ALPHA);
}
@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()));
animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
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);
animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION,
AnimationType.ALPHA, 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);
animate(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);
animate(binding.playbackControlRoot, false, duration, AnimationType.ALPHA,
0, this::hideSystemUIIfNeeded);
}, delay);
}
private void showHideShadow(final boolean show, final long duration) {
animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null);
animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 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();
boolean showSegment = false;
if (currentMetadata != null) {
showSegment = !currentMetadata.getMetadata().getStreamSegments().isEmpty()
&& !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);
binding.segmentsButton.setVisibility(showSegment ? View.VISIBLE : View.GONE);
binding.segmentsButton.setAlpha(showSegment ? 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);
animate(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);
animate(binding.loadingPanel, true, 0);
animate(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);
animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 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);
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 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");
}
animate(binding.playPauseButton, false, 0, AnimationType.SCALE_AND_ALPHA, 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);
animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
binding.loadingPanel.setVisibility(View.GONE);
animate(binding.surfaceForeground, true, 100);
}
private void animatePlayButtons(final boolean show, final int duration) {
animate(binding.playPauseButton, show, duration, AnimationType.SCALE_AND_ALPHA);
boolean showQueueButtons = show;
if (playQueue == null) {
showQueueButtons = false;
}
if (!showQueueButtons || playQueue.getIndex() > 0) {
animate(
binding.playPreviousButton,
showQueueButtons,
duration,
AnimationType.SCALE_AND_ALPHA);
}
if (!showQueueButtons || playQueue.getIndex() + 1 < playQueue.getStreams().size()) {
animate(
binding.playNextButton,
showQueueButtons,
duration,
AnimationType.SCALE_AND_ALPHA);
}
}
//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
animate(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION);
}
//endregion
/*//////////////////////////////////////////////////////////////////////////
// Errors
//////////////////////////////////////////////////////////////////////////*/
//region
/**
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
* <p>There are multiple types of errors:</p>
* <ul>
* <li>{@link ExoPlaybackException#TYPE_SOURCE TYPE_SOURCE}</li>
* <li>{@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.</li>
* <li>{@link ExoPlaybackException#TYPE_RENDERER TYPE_RENDERER}:
* If the renderer failed, treat the error as unrecoverable.</li>
* </ul>
*
* @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();
if (areSegmentsVisible) {
if (segmentAdapter.setItems(info)) {
final int adapterPosition = getNearestStreamSegmentPosition(
simpleExoPlayer.getCurrentPosition());
segmentAdapter.selectSegmentAt(adapterPosition);
binding.itemsList.scrollToPosition(adapterPosition);
} else {
closeItemsList();
}
}
}
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, segments 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();
binding.itemsListHeaderTitle.setVisibility(View.GONE);
binding.shuffleButton.setVisibility(View.VISIBLE);
binding.repeatButton.setVisibility(View.VISIBLE);
hideControls(0, 0);
binding.itemsListPanel.requestFocus();
animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION,
AnimationType.SLIDE_AND_ALPHA);
binding.itemsList.scrollToPosition(playQueue.getIndex());
}
private void buildQueue() {
binding.itemsList.setAdapter(playQueueAdapter);
binding.itemsList.setClickable(true);
binding.itemsList.setLongClickable(true);
binding.itemsList.clearOnScrollListeners();
binding.itemsList.addOnScrollListener(getQueueScrollListener());
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(binding.itemsList);
playQueueAdapter.setSelectedListener(getOnSelectedListener());
binding.itemsListClose.setOnClickListener(view -> closeItemsList());
}
private void onSegmentsClicked() {
areSegmentsVisible = true;
hideSystemUIIfNeeded();
buildSegments();
binding.itemsListHeaderTitle.setVisibility(View.VISIBLE);
binding.shuffleButton.setVisibility(View.GONE);
binding.repeatButton.setVisibility(View.GONE);
hideControls(0, 0);
binding.itemsListPanel.requestFocus();
animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION,
AnimationType.SLIDE_AND_ALPHA);
final int adapterPosition = getNearestStreamSegmentPosition(simpleExoPlayer
.getCurrentPosition());
segmentAdapter.selectSegmentAt(adapterPosition);
binding.itemsList.scrollToPosition(adapterPosition);
}
private void buildSegments() {
binding.itemsList.setAdapter(segmentAdapter);
binding.itemsList.setClickable(true);
binding.itemsList.setLongClickable(false);
binding.itemsList.clearOnScrollListeners();
if (itemTouchHelper != null) {
itemTouchHelper.attachToRecyclerView(null);
}
if (currentMetadata != null) {
segmentAdapter.setItems(currentMetadata.getMetadata());
}
binding.shuffleButton.setVisibility(View.GONE);
binding.repeatButton.setVisibility(View.GONE);
binding.itemsListClose.setOnClickListener(view -> closeItemsList());
}
public void closeItemsList() {
if (isQueueVisible || areSegmentsVisible) {
isQueueVisible = false;
areSegmentsVisible = false;
if (itemTouchHelper != null) {
itemTouchHelper.attachToRecyclerView(null);
}
animate(binding.itemsListPanel, false, DEFAULT_CONTROLS_DURATION,
AnimationType.SLIDE_AND_ALPHA, 0, () -> {
// Even when queueLayout is GONE it receives touch events
// and ruins normal behavior of the app. This line fixes it
binding.itemsListPanel.setTranslationY(
-binding.itemsListPanel.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.itemsList.clearOnScrollListeners();
}
}
};
}
private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
return (item, seconds) -> {
segmentAdapter.selectSegment(item);
seekTo(seconds * 1000);
triggerProgressUpdate();
};
}
private int getNearestStreamSegmentPosition(final long playbackPosition) {
int nearestPosition = 0;
final List<StreamSegment> segments = currentMetadata.getMetadata().getStreamSegments();
for (int i = 0; i < segments.size(); i++) {
if (segments.get(i).getStartTimeSeconds() * 1000 > playbackPosition) {
break;
}
nearestPosition++;
}
return Math.max(0, nearestPosition - 1);
}
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<String> 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<String> 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.segmentsButton.getId()) {
onSegmentsClicked();
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);
animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION,
AnimationType.ALPHA, 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);
animate(binding.secondaryControls, !isMoreControlsVisible, DEFAULT_CONTROLS_DURATION,
AnimationType.SLIDE_AND_ALPHA, 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.shareText(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.itemsListPanel.getLayoutParams().height
= height - binding.itemsListPanel.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.itemsListPanel.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
}