/* * Copyright 2017 Mauricio Colli * Copyright 2019 Eltex ltd * MainVideoPlayer.java is part of NewPipe * * License: GPL-3.0+ * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.schabi.newpipe.player; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.database.ContentObserver; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.media.AudioManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.DisplayCutout; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; 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.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; import androidx.core.app.ActivityCompat; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.SubtitleView; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.player.helper.PlayerHelper; 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.MediaSourceTag; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.util.AndroidTvUtils; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.KoreUtil; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.FocusOverlayView; import java.util.List; import java.util.Queue; import java.util.UUID; import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION; import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME; import static org.schabi.newpipe.player.VideoPlayer.DPAD_CONTROLS_HIDE_TIME; import static org.schabi.newpipe.util.AnimationUtils.Type.SCALE_AND_ALPHA; import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA; import static org.schabi.newpipe.util.AnimationUtils.animateRotation; import static org.schabi.newpipe.util.AnimationUtils.animateView; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE; /** * Activity Player implementing {@link VideoPlayer}. * * @author mauriciocolli */ public final class MainVideoPlayer extends AppCompatActivity implements StateSaver.WriteRead, PlaybackParameterDialog.Callback { private static final String TAG = ".MainVideoPlayer"; private static final boolean DEBUG = BasePlayer.DEBUG; private GestureDetector gestureDetector; private VideoPlayerImpl playerImpl; private SharedPreferences defaultPreferences; @Nullable private PlayerState playerState; private boolean isInMultiWindow; private boolean isBackPressed; private ContentObserver rotationObserver; /*////////////////////////////////////////////////////////////////////////// // Activity LifeCycle //////////////////////////////////////////////////////////////////////////*/ @Override protected void onCreate(@Nullable final Bundle savedInstanceState) { assureCorrectAppLanguage(this); super.onCreate(savedInstanceState); if (DEBUG) { Log.d(TAG, "onCreate() called with: " + "savedInstanceState = [" + savedInstanceState + "]"); } defaultPreferences = PreferenceManager.getDefaultSharedPreferences(this); ThemeHelper.setTheme(this); getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { getWindow().setStatusBarColor(Color.BLACK); } setVolumeControlStream(AudioManager.STREAM_MUSIC); WindowManager.LayoutParams lp = getWindow().getAttributes(); lp.screenBrightness = PlayerHelper.getScreenBrightness(getApplicationContext()); getWindow().setAttributes(lp); hideSystemUi(); setContentView(R.layout.activity_main_player); playerImpl = new VideoPlayerImpl(this); playerImpl.setup(findViewById(android.R.id.content)); if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) { return; // We have saved states, stop here to restore it } final Intent intent = getIntent(); if (intent != null) { playerImpl.handleIntent(intent); } else { Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show(); finish(); } rotationObserver = new ContentObserver(new Handler()) { @Override public void onChange(final boolean selfChange) { super.onChange(selfChange); if (globalScreenOrientationLocked()) { final String orientKey = getString(R.string.last_orientation_landscape_key); final boolean lastOrientationWasLandscape = defaultPreferences .getBoolean(orientKey, AndroidTvUtils.isTv(getApplicationContext())); setLandscape(lastOrientationWasLandscape); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } } }; getContentResolver().registerContentObserver( Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false, rotationObserver); if (AndroidTvUtils.isTv(this)) { FocusOverlayView.setupFocusObserver(this); } } @Override protected void onRestoreInstanceState(@NonNull final Bundle bundle) { if (DEBUG) { Log.d(TAG, "onRestoreInstanceState() called"); } super.onRestoreInstanceState(bundle); StateSaver.tryToRestore(bundle, this); } @Override protected void onNewIntent(final Intent intent) { if (DEBUG) { Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]"); } super.onNewIntent(intent); if (intent != null) { playerState = null; playerImpl.handleIntent(intent); } } @Override public boolean onKeyDown(final int keyCode, final KeyEvent event) { switch (event.getKeyCode()) { default: break; case KeyEvent.KEYCODE_BACK: if (AndroidTvUtils.isTv(getApplicationContext()) && playerImpl.isControlsVisible()) { playerImpl.hideControls(0, 0); hideSystemUi(); 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: View playerRoot = playerImpl.getRootView(); View controls = playerImpl.getControlsRoot(); if (playerRoot.hasFocus() && !controls.hasFocus()) { // do not interfere with focus in playlist etc. return super.onKeyDown(keyCode, event); } if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) { return true; } if (!playerImpl.isControlsVisible()) { playerImpl.playPauseButton.requestFocus(); playerImpl.showControlsThenHide(); showSystemUi(); return true; } else { playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME); } break; } return super.onKeyDown(keyCode, event); } @Override protected void onResume() { if (DEBUG) { Log.d(TAG, "onResume() called"); } assureCorrectAppLanguage(this); super.onResume(); if (globalScreenOrientationLocked()) { final String orientKey = getString(R.string.last_orientation_landscape_key); boolean lastOrientationWasLandscape = defaultPreferences .getBoolean(orientKey, AndroidTvUtils.isTv(getApplicationContext())); setLandscape(lastOrientationWasLandscape); } final int lastResizeMode = defaultPreferences.getInt( getString(R.string.last_resize_mode), AspectRatioFrameLayout.RESIZE_MODE_FIT); playerImpl.setResizeMode(lastResizeMode); // Upon going in or out of multiwindow mode, isInMultiWindow will always be false, // since the first onResume needs to restore the player. // Subsequent onResume calls while multiwindow mode remains the same and the player is // prepared should be ignored. if (isInMultiWindow) { return; } isInMultiWindow = isInMultiWindow(); if (playerState != null) { playerImpl.setPlaybackQuality(playerState.getPlaybackQuality()); playerImpl.initPlayback(playerState.getPlayQueue(), playerState.getRepeatMode(), playerState.getPlaybackSpeed(), playerState.getPlaybackPitch(), playerState.isPlaybackSkipSilence(), playerState.wasPlaying(), playerImpl.isMuted()); } } @Override public void onConfigurationChanged(final Configuration newConfig) { super.onConfigurationChanged(newConfig); assureCorrectAppLanguage(this); if (playerImpl.isSomePopupMenuVisible()) { playerImpl.getQualityPopupMenu().dismiss(); playerImpl.getPlaybackSpeedPopupMenu().dismiss(); } } @Override public void onBackPressed() { super.onBackPressed(); isBackPressed = true; } @Override protected void onSaveInstanceState(final Bundle outState) { if (DEBUG) { Log.d(TAG, "onSaveInstanceState() called"); } super.onSaveInstanceState(outState); if (playerImpl == null) { return; } playerImpl.setRecovery(); if (!playerImpl.gotDestroyed()) { playerState = createPlayerState(); } StateSaver.tryToSave(isChangingConfigurations(), null, outState, this); } @Override protected void onStop() { if (DEBUG) { Log.d(TAG, "onStop() called"); } super.onStop(); PlayerHelper.setScreenBrightness(getApplicationContext(), getWindow().getAttributes().screenBrightness); if (playerImpl == null) { return; } if (!isBackPressed) { playerImpl.minimize(); } playerState = createPlayerState(); playerImpl.destroy(); if (rotationObserver != null) { getContentResolver().unregisterContentObserver(rotationObserver); } isInMultiWindow = false; isBackPressed = false; } @Override protected void attachBaseContext(final Context newBase) { super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase)); } @Override protected void onPause() { playerImpl.savePlaybackState(); super.onPause(); } /*////////////////////////////////////////////////////////////////////////// // State Saving //////////////////////////////////////////////////////////////////////////*/ private PlayerState createPlayerState() { return new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(), playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(), playerImpl.getPlaybackQuality(), playerImpl.getPlaybackSkipSilence(), playerImpl.isPlaying()); } @Override public String generateSuffix() { return "." + UUID.randomUUID().toString() + ".player"; } @Override public void writeTo(final Queue objectsToSave) { if (objectsToSave == null) { return; } objectsToSave.add(playerState); } @Override @SuppressWarnings("unchecked") public void readFrom(@NonNull final Queue savedObjects) { playerState = (PlayerState) savedObjects.poll(); } /*////////////////////////////////////////////////////////////////////////// // View //////////////////////////////////////////////////////////////////////////*/ private void showSystemUi() { if (DEBUG) { Log.d(TAG, "showSystemUi() called"); } if (playerImpl != null && playerImpl.queueVisible) { return; } final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @ColorInt final int systenUiColor = ActivityCompat.getColor(getApplicationContext(), R.color.video_overlay_color); getWindow().setStatusBarColor(systenUiColor); getWindow().setNavigationBarColor(systenUiColor); } getWindow().getDecorView().setSystemUiVisibility(visibility); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } private void hideSystemUi() { if (DEBUG) { Log.d(TAG, "hideSystemUi() called"); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; } getWindow().getDecorView().setSystemUiVisibility(visibility); } getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } private void toggleOrientation() { setLandscape(!isLandscape()); defaultPreferences.edit() .putBoolean(getString(R.string.last_orientation_landscape_key), !isLandscape()) .apply(); } private boolean isLandscape() { return getResources().getDisplayMetrics().heightPixels < getResources().getDisplayMetrics().widthPixels; } private void setLandscape(final boolean v) { setRequestedOrientation(v ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); } private boolean globalScreenOrientationLocked() { // 1: Screen orientation changes using accelerometer // 0: Screen orientation is locked return !(android.provider.Settings.System .getInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 1); } protected void setRepeatModeButton(final ImageButton imageButton, final int repeatMode) { switch (repeatMode) { case Player.REPEAT_MODE_OFF: imageButton.setImageResource(R.drawable.exo_controls_repeat_off); break; case Player.REPEAT_MODE_ONE: imageButton.setImageResource(R.drawable.exo_controls_repeat_one); break; case Player.REPEAT_MODE_ALL: imageButton.setImageResource(R.drawable.exo_controls_repeat_all); break; } } protected void setShuffleButton(final ImageButton shuffleButton, final boolean shuffled) { final int shuffleAlpha = shuffled ? 255 : 77; shuffleButton.setImageAlpha(shuffleAlpha); } protected void setMuteButton(final ImageButton muteButton, final boolean isMuted) { muteButton.setImageDrawable(AppCompatResources.getDrawable(getApplicationContext(), isMuted ? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp)); } private boolean isInMultiWindow() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode(); } //////////////////////////////////////////////////////////////////////////// // Playback Parameters Listener //////////////////////////////////////////////////////////////////////////// @Override public void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch, final boolean playbackSkipSilence) { if (playerImpl != null) { playerImpl.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); } } /////////////////////////////////////////////////////////////////////////// @SuppressWarnings({"unused", "WeakerAccess"}) private class VideoPlayerImpl extends VideoPlayer { private static final float MAX_GESTURE_LENGTH = 0.75f; private TextView titleTextView; private TextView channelTextView; private RelativeLayout volumeRelativeLayout; private ProgressBar volumeProgressBar; private ImageView volumeImageView; private RelativeLayout brightnessRelativeLayout; private ProgressBar brightnessProgressBar; private ImageView brightnessImageView; private ImageButton queueButton; private ImageButton repeatButton; private ImageButton shuffleButton; private ImageButton playPauseButton; private ImageButton playPreviousButton; private ImageButton playNextButton; private Button closeButton; private RelativeLayout queueLayout; private ImageButton itemsListCloseButton; private RecyclerView itemsList; private ItemTouchHelper itemTouchHelper; private boolean queueVisible; private ImageButton moreOptionsButton; private ImageButton kodiButton; private ImageButton shareButton; private ImageButton toggleOrientationButton; private ImageButton switchPopupButton; private ImageButton switchBackgroundButton; private ImageButton muteButton; private RelativeLayout windowRootLayout; private View secondaryControls; private int maxGestureLength; VideoPlayerImpl(final Context context) { super("VideoPlayerImpl" + MainVideoPlayer.TAG, context); } @Override public void initViews(final View view) { super.initViews(view); this.titleTextView = view.findViewById(R.id.titleTextView); this.channelTextView = view.findViewById(R.id.channelTextView); this.volumeRelativeLayout = view.findViewById(R.id.volumeRelativeLayout); this.volumeProgressBar = view.findViewById(R.id.volumeProgressBar); this.volumeImageView = view.findViewById(R.id.volumeImageView); this.brightnessRelativeLayout = view.findViewById(R.id.brightnessRelativeLayout); this.brightnessProgressBar = view.findViewById(R.id.brightnessProgressBar); this.brightnessImageView = view.findViewById(R.id.brightnessImageView); this.queueButton = view.findViewById(R.id.queueButton); this.repeatButton = view.findViewById(R.id.repeatButton); this.shuffleButton = view.findViewById(R.id.shuffleButton); this.playPauseButton = view.findViewById(R.id.playPauseButton); this.playPreviousButton = view.findViewById(R.id.playPreviousButton); this.playNextButton = view.findViewById(R.id.playNextButton); this.closeButton = view.findViewById(R.id.closeButton); this.moreOptionsButton = view.findViewById(R.id.moreOptionsButton); this.secondaryControls = view.findViewById(R.id.secondaryControls); this.kodiButton = view.findViewById(R.id.kodi); this.shareButton = view.findViewById(R.id.share); this.toggleOrientationButton = view.findViewById(R.id.toggleOrientation); this.switchBackgroundButton = view.findViewById(R.id.switchBackground); this.muteButton = view.findViewById(R.id.switchMute); this.switchPopupButton = view.findViewById(R.id.switchPopup); this.queueLayout = findViewById(R.id.playQueuePanel); this.itemsListCloseButton = findViewById(R.id.playQueueClose); this.itemsList = findViewById(R.id.playQueue); titleTextView.setSelected(true); channelTextView.setSelected(true); getRootView().setKeepScreenOn(true); } @Override protected void setupSubtitleView(@NonNull final SubtitleView view, final float captionScale, @NonNull final CaptionStyleCompat captionStyle) { final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels); final float captionRatioInverse = 20f + 4f * (1f - captionScale); view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX, (float) minimumLength / captionRatioInverse); view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT)); view.setStyle(captionStyle); } @Override public void initListeners() { super.initListeners(); PlayerGestureListener listener = new PlayerGestureListener(); gestureDetector = new GestureDetector(context, listener); gestureDetector.setIsLongpressEnabled(false); getRootView().setOnTouchListener(listener); queueButton.setOnClickListener(this); repeatButton.setOnClickListener(this); shuffleButton.setOnClickListener(this); playPauseButton.setOnClickListener(this); playPreviousButton.setOnClickListener(this); playNextButton.setOnClickListener(this); closeButton.setOnClickListener(this); moreOptionsButton.setOnClickListener(this); kodiButton.setOnClickListener(this); shareButton.setOnClickListener(this); toggleOrientationButton.setOnClickListener(this); switchBackgroundButton.setOnClickListener(this); muteButton.setOnClickListener(this); switchPopupButton.setOnClickListener(this); getRootView().addOnLayoutChangeListener((view, l, t, r, b, ol, ot, or, ob) -> { if (l != ol || t != ot || r != or || b != ob) { // Use smaller value to be consistent between screen orientations // (and to make usage easier) int width = r - l; int height = b - t; maxGestureLength = (int) (Math.min(width, height) * MAX_GESTURE_LENGTH); if (DEBUG) { Log.d(TAG, "maxGestureLength = " + maxGestureLength); } volumeProgressBar.setMax(maxGestureLength); brightnessProgressBar.setMax(maxGestureLength); setInitialGestureValues(); } }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { queueLayout.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { @Override public WindowInsets onApplyWindowInsets(final View view, final WindowInsets windowInsets) { final DisplayCutout cutout = windowInsets.getDisplayCutout(); if (cutout != null) { view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); } return windowInsets; } }); } } public void minimize() { switch (PlayerHelper.getMinimizeOnExitAction(context)) { case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND: onPlayBackgroundButtonClicked(); break; case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP: onFullScreenButtonClicked(); break; case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE: default: // No action break; } } /*////////////////////////////////////////////////////////////////////////// // ExoPlayer Video Listener //////////////////////////////////////////////////////////////////////////*/ @Override public void onRepeatModeChanged(final int i) { super.onRepeatModeChanged(i); updatePlaybackButtons(); } @Override public void onShuffleClicked() { super.onShuffleClicked(); updatePlaybackButtons(); } /*////////////////////////////////////////////////////////////////////////// // Playback Listener //////////////////////////////////////////////////////////////////////////*/ protected void onMetadataChanged(@NonNull final MediaSourceTag tag) { super.onMetadataChanged(tag); // show kodi button if it supports the current service and it is enabled in settings final boolean showKodiButton = KoreUtil.isServiceSupportedByKore(tag.getMetadata().getServiceId()) && PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(context.getString(R.string.show_play_with_kodi_key), false); kodiButton.setVisibility(showKodiButton ? View.VISIBLE : View.GONE); titleTextView.setText(tag.getMetadata().getName()); channelTextView.setText(tag.getMetadata().getUploaderName()); } @Override public void onPlaybackShutdown() { super.onPlaybackShutdown(); finish(); } public void onKodiShare() { onPause(); try { NavigationHelper.playWithKore(context, Uri.parse(playerImpl.getVideoUrl())); } catch (Exception e) { if (DEBUG) { Log.i(TAG, "Failed to start kore", e); } KoreUtil.showInstallKoreDialog(context); } } /*////////////////////////////////////////////////////////////////////////// // Player Overrides //////////////////////////////////////////////////////////////////////////*/ @Override public void onFullScreenButtonClicked() { super.onFullScreenButtonClicked(); if (DEBUG) { Log.d(TAG, "onFullScreenButtonClicked() called"); } if (simpleExoPlayer == null) { return; } if (!PermissionHelper.isPopupEnabled(context)) { PermissionHelper.showPopupEnablementToast(context); return; } setRecovery(); final Intent intent = NavigationHelper.getPlayerIntent( context, PopupVideoPlayer.class, this.getPlayQueue(), this.getRepeatMode(), this.getPlaybackSpeed(), this.getPlaybackPitch(), this.getPlaybackSkipSilence(), this.getPlaybackQuality(), false, !isPlaying(), isMuted() ); context.startService(intent); ((View) getControlAnimationView().getParent()).setVisibility(View.GONE); destroy(); finish(); } public void onPlayBackgroundButtonClicked() { if (DEBUG) { Log.d(TAG, "onPlayBackgroundButtonClicked() called"); } if (playerImpl.getPlayer() == null) { return; } setRecovery(); final Intent intent = NavigationHelper.getPlayerIntent( context, BackgroundPlayer.class, this.getPlayQueue(), this.getRepeatMode(), this.getPlaybackSpeed(), this.getPlaybackPitch(), this.getPlaybackSkipSilence(), this.getPlaybackQuality(), false, !isPlaying(), isMuted() ); context.startService(intent); ((View) getControlAnimationView().getParent()).setVisibility(View.GONE); destroy(); finish(); } @Override public void onMuteUnmuteButtonClicked() { super.onMuteUnmuteButtonClicked(); setMuteButton(muteButton, playerImpl.isMuted()); } @Override public void onClick(final View v) { super.onClick(v); if (v.getId() == playPauseButton.getId()) { onPlayPause(); } else if (v.getId() == playPreviousButton.getId()) { onPlayPrevious(); } else if (v.getId() == playNextButton.getId()) { onPlayNext(); } else if (v.getId() == queueButton.getId()) { onQueueClicked(); return; } else if (v.getId() == repeatButton.getId()) { onRepeatClicked(); return; } else if (v.getId() == shuffleButton.getId()) { onShuffleClicked(); return; } else if (v.getId() == moreOptionsButton.getId()) { onMoreOptionsClicked(); } else if (v.getId() == shareButton.getId()) { onShareClicked(); } else if (v.getId() == toggleOrientationButton.getId()) { onScreenRotationClicked(); } else if (v.getId() == switchPopupButton.getId()) { onFullScreenButtonClicked(); } else if (v.getId() == switchBackgroundButton.getId()) { onPlayBackgroundButtonClicked(); } else if (v.getId() == muteButton.getId()) { onMuteUnmuteButtonClicked(); } else if (v.getId() == closeButton.getId()) { onPlaybackShutdown(); return; } else if (v.getId() == kodiButton.getId()) { onKodiShare(); } if (getCurrentState() != STATE_COMPLETED) { getControlsVisibilityHandler().removeCallbacksAndMessages(null); animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> { if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) { safeHideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } }); } } private void onQueueClicked() { queueVisible = true; hideSystemUi(); buildQueue(); updatePlaybackButtons(); getControlsRoot().setVisibility(View.INVISIBLE); animateView(queueLayout, SLIDE_AND_ALPHA, true, DEFAULT_CONTROLS_DURATION); itemsList.scrollToPosition(playQueue.getIndex()); } private void onQueueClosed() { animateView(queueLayout, SLIDE_AND_ALPHA, false, DEFAULT_CONTROLS_DURATION); queueVisible = false; } private void onMoreOptionsClicked() { if (DEBUG) { Log.d(TAG, "onMoreOptionsClicked() called"); } final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE; animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION, isMoreControlsVisible ? 0 : 180); animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible, DEFAULT_CONTROLS_DURATION); showControls(DEFAULT_CONTROLS_DURATION); setMuteButton(muteButton, playerImpl.isMuted()); } private void onShareClicked() { // share video at the current time (youtube.com/watch?v=ID&t=SECONDS) ShareUtils.shareUrl(MainVideoPlayer.this, playerImpl.getVideoTitle(), playerImpl.getVideoUrl() + "&t=" + playerImpl.getPlaybackSeekBar().getProgress() / 1000); } private void onScreenRotationClicked() { if (DEBUG) { Log.d(TAG, "onScreenRotationClicked() called"); } toggleOrientation(); showControlsThenHide(); } @Override public void onPlaybackSpeedClicked() { PlaybackParameterDialog .newInstance(getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence()) .show(getSupportFragmentManager(), TAG); } @Override public void onStopTrackingTouch(final SeekBar seekBar) { super.onStopTrackingTouch(seekBar); if (wasPlaying()) { showControlsThenHide(); } } @Override public void onDismiss(final PopupMenu menu) { super.onDismiss(menu); if (isPlaying()) { hideControls(DEFAULT_CONTROLS_DURATION, 0); } hideSystemUi(); } @Override protected int nextResizeMode(final int currentResizeMode) { final int newResizeMode; switch (currentResizeMode) { case AspectRatioFrameLayout.RESIZE_MODE_FIT: newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL; break; case AspectRatioFrameLayout.RESIZE_MODE_FILL: newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; break; default: newResizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; break; } storeResizeMode(newResizeMode); return newResizeMode; } private void storeResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) { defaultPreferences.edit() .putInt(getString(R.string.last_resize_mode), resizeMode) .apply(); } @Override protected VideoPlaybackResolver.QualityResolver getQualityResolver() { return new VideoPlaybackResolver.QualityResolver() { @Override public int getDefaultResolutionIndex(final List sortedVideos) { return ListHelper.getDefaultResolutionIndex(context, sortedVideos); } @Override public int getOverrideResolutionIndex(final List sortedVideos, final String playbackQuality) { return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality); } }; } /*////////////////////////////////////////////////////////////////////////// // States //////////////////////////////////////////////////////////////////////////*/ private void animatePlayButtons(final boolean show, final int duration) { animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); animateView(playPreviousButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); animateView(playNextButton, AnimationUtils.Type.SCALE_AND_ALPHA, show, duration); } @Override public void onBlocked() { super.onBlocked(); playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); animatePlayButtons(false, 100); animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); getRootView().setKeepScreenOn(true); } @Override public void onBuffering() { super.onBuffering(); getRootView().setKeepScreenOn(true); } @Override public void onPlaying() { super.onPlaying(); animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { playPauseButton.setImageResource(R.drawable.ic_pause_white_24dp); animatePlayButtons(true, 200); playPauseButton.requestFocus(); animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); }); getRootView().setKeepScreenOn(true); } @Override public void onPaused() { super.onPaused(); animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 80, 0, () -> { playPauseButton.setImageResource(R.drawable.ic_play_arrow_white_24dp); animatePlayButtons(true, 200); playPauseButton.requestFocus(); animateView(closeButton, false, DEFAULT_CONTROLS_DURATION); }); showSystemUi(); getRootView().setKeepScreenOn(false); } @Override public void onPausedSeek() { super.onPausedSeek(); animatePlayButtons(false, 100); getRootView().setKeepScreenOn(true); } @Override public void onCompleted() { animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> { playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp); animatePlayButtons(true, DEFAULT_CONTROLS_DURATION); animateView(closeButton, true, DEFAULT_CONTROLS_DURATION); }); getRootView().setKeepScreenOn(false); super.onCompleted(); } /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ private void setInitialGestureValues() { if (getAudioReactor() != null) { final float currentVolumeNormalized = (float) getAudioReactor().getVolume() / getAudioReactor().getMaxVolume(); volumeProgressBar.setProgress( (int) (volumeProgressBar.getMax() * currentVolumeNormalized)); } float screenBrightness = getWindow().getAttributes().screenBrightness; if (screenBrightness < 0) { screenBrightness = Settings.System.getInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, 0) / 255.0f; } brightnessProgressBar.setProgress( (int) (brightnessProgressBar.getMax() * screenBrightness)); if (DEBUG) { Log.d(TAG, "setInitialGestureValues: volumeProgressBar.getProgress() [" + volumeProgressBar.getProgress() + "] " + "brightnessProgressBar.getProgress() [" + brightnessProgressBar.getProgress() + "]"); } } @Override public void showControlsThenHide() { if (queueVisible) { return; } super.showControlsThenHide(); } @Override public void showControls(final long duration) { if (queueVisible) { return; } super.showControls(duration); } @Override public void safeHideControls(final long duration, final long delay) { if (DEBUG) { Log.d(TAG, "safeHideControls() called with: delay = [" + delay + "]"); } View controlsRoot = getControlsRoot(); if (controlsRoot.isInTouchMode()) { getControlsVisibilityHandler().removeCallbacksAndMessages(null); getControlsVisibilityHandler().postDelayed(() -> animateView(controlsRoot, false, duration, 0, MainVideoPlayer.this::hideSystemUi), delay); } } @Override public void hideControls(final long duration, final long delay) { if (DEBUG) { Log.d(TAG, "hideControls() called with: delay = [" + delay + "]"); } getControlsVisibilityHandler().removeCallbacksAndMessages(null); getControlsVisibilityHandler().postDelayed(() -> animateView(getControlsRoot(), false, duration, 0, MainVideoPlayer.this::hideSystemUi), /*delayMillis=*/delay ); } private void updatePlaybackButtons() { if (repeatButton == null || shuffleButton == null || simpleExoPlayer == null || playQueue == null) { return; } setRepeatModeButton(repeatButton, getRepeatMode()); setShuffleButton(shuffleButton, playQueue.isShuffled()); } private void buildQueue() { itemsList.setAdapter(playQueueAdapter); itemsList.setClickable(true); itemsList.setLongClickable(true); itemsList.clearOnScrollListeners(); itemsList.addOnScrollListener(getQueueScrollListener()); itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper.attachToRecyclerView(itemsList); playQueueAdapter.setSelectedListener(getOnSelectedListener()); itemsListCloseButton.setOnClickListener(view -> onQueueClosed()); } private OnScrollBelowItemsListener getQueueScrollListener() { return new OnScrollBelowItemsListener() { @Override public void onScrolledDown(final RecyclerView recyclerView) { if (playQueue != null && !playQueue.isComplete()) { playQueue.fetch(); } else if (itemsList != null) { itemsList.clearOnScrollListeners(); } } }; } private ItemTouchHelper.SimpleCallback getItemTouchCallback() { return new PlayQueueItemTouchCallback() { @Override public void onMove(final int sourceIndex, final int targetIndex) { if (playQueue != null) { playQueue.move(sourceIndex, targetIndex); } } @Override public void onSwiped(final int index) { if (index != -1) { playQueue.remove(index); } } }; } private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() { return new PlayQueueItemBuilder.OnSelectedListener() { @Override public void selected(final PlayQueueItem item, final View view) { onSelected(item); } @Override public void held(final PlayQueueItem item, final View view) { final int index = playQueue.indexOf(item); if (index != -1) { playQueue.remove(index); } } @Override public void onStartDrag(final PlayQueueItemHolder viewHolder) { if (itemTouchHelper != null) { itemTouchHelper.startDrag(viewHolder); } } }; } /////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////// public TextView getTitleTextView() { return titleTextView; } public TextView getChannelTextView() { return channelTextView; } public RelativeLayout getVolumeRelativeLayout() { return volumeRelativeLayout; } public ProgressBar getVolumeProgressBar() { return volumeProgressBar; } public ImageView getVolumeImageView() { return volumeImageView; } public RelativeLayout getBrightnessRelativeLayout() { return brightnessRelativeLayout; } public ProgressBar getBrightnessProgressBar() { return brightnessProgressBar; } public ImageView getBrightnessImageView() { return brightnessImageView; } public ImageButton getRepeatButton() { return repeatButton; } public ImageButton getMuteButton() { return muteButton; } public ImageButton getPlayPauseButton() { return playPauseButton; } public int getMaxGestureLength() { return maxGestureLength; } } private class PlayerGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { private static final int MOVEMENT_THRESHOLD = 40; private final boolean isVolumeGestureEnabled = PlayerHelper .isVolumeGestureEnabled(getApplicationContext()); private final boolean isBrightnessGestureEnabled = PlayerHelper .isBrightnessGestureEnabled(getApplicationContext()); private final int maxVolume = playerImpl.getAudioReactor().getMaxVolume(); private boolean isMoving; @Override public boolean onDoubleTap(final MotionEvent e) { if (DEBUG) { Log.d(TAG, "onDoubleTap() called with: " + "e = [" + e + "], " + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", " + "xy = " + e.getX() + ", " + e.getY()); } if (e.getX() > playerImpl.getRootView().getWidth() * 2 / 3) { playerImpl.onFastForward(); } else if (e.getX() < playerImpl.getRootView().getWidth() / 3) { playerImpl.onFastRewind(); } else { playerImpl.getPlayPauseButton().performClick(); } return true; } @Override public boolean onSingleTapConfirmed(final MotionEvent e) { if (DEBUG) { Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]"); } if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) { return true; } if (playerImpl.isControlsVisible()) { playerImpl.hideControls(150, 0); } else { playerImpl.playPauseButton.requestFocus(); playerImpl.showControlsThenHide(); showSystemUi(); } return true; } @Override public boolean onDown(final MotionEvent e) { if (DEBUG) { Log.d(TAG, "onDown() called with: e = [" + e + "]"); } return super.onDown(e); } @Override public boolean onScroll(final MotionEvent initialEvent, final MotionEvent movingEvent, final float distanceX, final float distanceY) { if (!isVolumeGestureEnabled && !isBrightnessGestureEnabled) { return false; } final boolean isTouchingStatusBar = initialEvent.getY() < getStatusBarHeight(); final boolean isTouchingNavigationBar = initialEvent.getY() > playerImpl.getRootView().getHeight() - getNavigationBarHeight(); if (isTouchingStatusBar || isTouchingNavigationBar) { return false; } // if (DEBUG) { // Log.d(TAG, "MainVideoPlayer.onScroll = " + // "e1.getRaw = [" + initialEvent.getRawX() + ", " // + initialEvent.getRawY() + "], " + // "e2.getRaw = [" + movingEvent.getRawX() + ", " // + movingEvent.getRawY() + "], " + // "distanceXy = [" + distanceX + ", " + distanceY + "]"); // } final boolean insideThreshold = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD; if (!isMoving && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY)) || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) { return false; } isMoving = true; boolean acceptAnyArea = isVolumeGestureEnabled != isBrightnessGestureEnabled; boolean acceptVolumeArea = acceptAnyArea || initialEvent.getX() > playerImpl.getRootView().getWidth() / 2; boolean acceptBrightnessArea = acceptAnyArea || !acceptVolumeArea; if (isVolumeGestureEnabled && acceptVolumeArea) { playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY); float currentProgressPercent = (float) playerImpl.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength(); int currentVolume = (int) (maxVolume * currentProgressPercent); playerImpl.getAudioReactor().setVolume(currentVolume); if (DEBUG) { Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume); } final int resId = currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_24dp : currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_24dp : currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_24dp : R.drawable.ic_volume_up_white_24dp; playerImpl.getVolumeImageView().setImageDrawable( AppCompatResources.getDrawable(getApplicationContext(), resId) ); if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200); } if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE); } } else if (isBrightnessGestureEnabled && acceptBrightnessArea) { playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY); float currentProgressPercent = (float) playerImpl.getBrightnessProgressBar().getProgress() / playerImpl.getMaxGestureLength(); WindowManager.LayoutParams layoutParams = getWindow().getAttributes(); layoutParams.screenBrightness = currentProgressPercent; getWindow().setAttributes(layoutParams); if (DEBUG) { Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentProgressPercent); } final int resId = currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_24dp : currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_24dp : R.drawable.ic_brightness_high_white_24dp; playerImpl.getBrightnessImageView().setImageDrawable( AppCompatResources.getDrawable(getApplicationContext(), resId) ); if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) { animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200); } if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE); } } return true; } private int getNavigationBarHeight() { int resId = getResources().getIdentifier("navigation_bar_height", "dimen", "android"); if (resId > 0) { return getResources().getDimensionPixelSize(resId); } return 0; } private int getStatusBarHeight() { int resId = getResources().getIdentifier("status_bar_height", "dimen", "android"); if (resId > 0) { return getResources().getDimensionPixelSize(resId); } return 0; } private void onScrollEnd() { if (DEBUG) { Log.d(TAG, "onScrollEnd() called"); } if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) { animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); } if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) { animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200); } if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) { playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); } } @Override public boolean onTouch(final View v, final MotionEvent event) { // if (DEBUG) { // Log.d(TAG, "onTouch() called with: v = [" + v + "], event = [" + event + "]"); // } gestureDetector.onTouchEvent(event); if (event.getAction() == MotionEvent.ACTION_UP && isMoving) { isMoving = false; onScrollEnd(); } return true; } } }