NewPipe/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java

1315 lines
50 KiB
Java

/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* PopupVideoPlayer.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 <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.player;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.AnticipateInterpolator;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.RemoteViews;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.PlaybackParameters;
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 com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.nostra13.universalimageloader.core.assist.FailReason;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.List;
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.util.AnimationUtils.animateView;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
/**
* Service Popup Player implementing {@link VideoPlayer}.
*
* @author mauriciocolli
*/
public final class PopupVideoPlayer extends Service {
public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE";
public static final String ACTION_PLAY_PAUSE
= "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE";
public static final String ACTION_REPEAT = "org.schabi.newpipe.player.PopupVideoPlayer.REPEAT";
private static final String TAG = ".PopupVideoPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG;
private static final int NOTIFICATION_ID = 40028922;
private static final String POPUP_SAVED_WIDTH = "popup_saved_width";
private static final String POPUP_SAVED_X = "popup_saved_x";
private static final String POPUP_SAVED_Y = "popup_saved_y";
private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300;
private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
private WindowManager windowManager;
private WindowManager.LayoutParams popupLayoutParams;
private GestureDetector popupGestureDetector;
private View closeOverlayView;
private FloatingActionButton closeOverlayButton;
private int tossFlingVelocity;
private float screenWidth;
private float screenHeight;
private float popupWidth;
private float popupHeight;
private float minimumWidth;
private float minimumHeight;
private float maximumWidth;
private float maximumHeight;
private NotificationManager notificationManager;
private NotificationCompat.Builder notBuilder;
private RemoteViews notRemoteView;
private VideoPlayerImpl playerImpl;
private boolean isPopupClosing = false;
/*//////////////////////////////////////////////////////////////////////////
// Service-Activity Binder
//////////////////////////////////////////////////////////////////////////*/
private PlayerEventListener activityListener;
private IBinder mBinder;
/*//////////////////////////////////////////////////////////////////////////
// Service LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate() {
assureCorrectAppLanguage(this);
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
playerImpl = new VideoPlayerImpl(this);
ThemeHelper.setTheme(this);
mBinder = new PlayerServiceBinder(playerImpl);
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (DEBUG) {
Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], "
+ "flags = [" + flags + "], startId = [" + startId + "]");
}
if (playerImpl.getPlayer() == null) {
initPopup();
initPopupCloseOverlay();
}
if (!playerImpl.isPlaying()) {
playerImpl.getPlayer().setPlayWhenReady(true);
}
playerImpl.handleIntent(intent);
return START_NOT_STICKY;
}
@Override
public void onConfigurationChanged(final Configuration newConfig) {
assureCorrectAppLanguage(this);
if (DEBUG) {
Log.d(TAG, "onConfigurationChanged() called with: "
+ "newConfig = [" + newConfig + "]");
}
updateScreenSize();
updatePopupSize(popupLayoutParams.width, -1);
checkPopupPositionBounds();
}
@Override
public void onDestroy() {
if (DEBUG) {
Log.d(TAG, "onDestroy() called");
}
closePopup();
}
@Override
protected void attachBaseContext(final Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
}
@Override
public IBinder onBind(final Intent intent) {
return mBinder;
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
@SuppressLint("RtlHardcoded")
private void initPopup() {
if (DEBUG) {
Log.d(TAG, "initPopup() called");
}
View rootView = View.inflate(this, R.layout.player_popup, null);
playerImpl.setup(rootView);
tossFlingVelocity = PlayerHelper.getTossFlingVelocity(this);
updateScreenSize();
final boolean popupRememberSizeAndPos = PlayerHelper.isRememberingPopupDimensions(this);
final float defaultSize = getResources().getDimension(R.dimen.popup_default_width);
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
popupWidth = popupRememberSizeAndPos
? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize;
final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O
? WindowManager.LayoutParams.TYPE_PHONE
: WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
popupLayoutParams = new WindowManager.LayoutParams(
(int) popupWidth, (int) getMinimumVideoHeight(popupWidth),
layoutParamType,
IDLE_WINDOW_FLAGS,
PixelFormat.TRANSLUCENT);
popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
int centerX = (int) (screenWidth / 2f - popupWidth / 2f);
int centerY = (int) (screenHeight / 2f - popupHeight / 2f);
popupLayoutParams.x = popupRememberSizeAndPos
? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX;
popupLayoutParams.y = popupRememberSizeAndPos
? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY;
checkPopupPositionBounds();
PopupWindowGestureListener listener = new PopupWindowGestureListener();
popupGestureDetector = new GestureDetector(this, listener);
rootView.setOnTouchListener(listener);
playerImpl.getLoadingPanel().setMinimumWidth(popupLayoutParams.width);
playerImpl.getLoadingPanel().setMinimumHeight(popupLayoutParams.height);
windowManager.addView(rootView, popupLayoutParams);
}
@SuppressLint("RtlHardcoded")
private void initPopupCloseOverlay() {
if (DEBUG) {
Log.d(TAG, "initPopupCloseOverlay() called");
}
closeOverlayView = View.inflate(this, R.layout.player_popup_close_overlay, null);
closeOverlayButton = closeOverlayView.findViewById(R.id.closeButton);
final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O
? WindowManager.LayoutParams.TYPE_PHONE
: WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
layoutParamType,
flags,
PixelFormat.TRANSLUCENT);
closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
closeOverlayLayoutParams.softInputMode = WindowManager
.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
closeOverlayButton.setVisibility(View.GONE);
windowManager.addView(closeOverlayView, closeOverlayLayoutParams);
}
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
private void resetNotification() {
notBuilder = createNotification();
}
private NotificationCompat.Builder createNotification() {
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID,
R.layout.player_popup_notification);
notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle());
notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName());
notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail());
notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE),
PendingIntent.FLAG_UPDATE_CURRENT));
notRemoteView.setOnClickPendingIntent(R.id.notificationStop,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE),
PendingIntent.FLAG_UPDATE_CURRENT));
notRemoteView.setOnClickPendingIntent(R.id.notificationRepeat,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT),
PendingIntent.FLAG_UPDATE_CURRENT));
// Starts popup player activity -- attempts to unlock lockscreen
final Intent intent = NavigationHelper.getPopupPlayerActivityIntent(this);
notRemoteView.setOnClickPendingIntent(R.id.notificationContent,
PendingIntent.getActivity(this, NOTIFICATION_ID, intent,
PendingIntent.FLAG_UPDATE_CURRENT));
setRepeatModeRemote(notRemoteView, playerImpl.getRepeatMode());
NotificationCompat.Builder builder = new NotificationCompat
.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContent(notRemoteView);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
builder.setPriority(NotificationCompat.PRIORITY_MAX);
}
return builder;
}
/**
* Updates the notification, and the play/pause button in it.
* Used for changes on the remoteView
*
* @param drawableId if != -1, sets the drawable with that id on the play/pause button
*/
private void updateNotification(final int drawableId) {
if (DEBUG) {
Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
}
if (notBuilder == null || notRemoteView == null) {
return;
}
if (drawableId != -1) {
notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
}
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
}
/*//////////////////////////////////////////////////////////////////////////
// Misc
//////////////////////////////////////////////////////////////////////////*/
public void closePopup() {
if (DEBUG) {
Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing);
}
if (isPopupClosing) {
return;
}
isPopupClosing = true;
if (playerImpl != null) {
playerImpl.savePlaybackState();
if (playerImpl.getRootView() != null) {
windowManager.removeView(playerImpl.getRootView());
}
playerImpl.setRootView(null);
playerImpl.stopActivityBinding();
playerImpl.destroy();
playerImpl = null;
}
mBinder = null;
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
}
animateOverlayAndFinishService();
}
private void animateOverlayAndFinishService() {
final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight()
- closeOverlayButton.getY());
closeOverlayButton.animate().setListener(null).cancel();
closeOverlayButton.animate()
.setInterpolator(new AnticipateInterpolator())
.translationY(targetTranslationY)
.setDuration(400)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(final Animator animation) {
end();
}
@Override
public void onAnimationEnd(final Animator animation) {
end();
}
private void end() {
windowManager.removeView(closeOverlayView);
stopForeground(true);
stopSelf();
}
}).start();
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
/**
* @see #checkPopupPositionBounds(float, float)
* @return if the popup was out of bounds and have been moved back to it
*/
@SuppressWarnings("UnusedReturnValue")
private boolean checkPopupPositionBounds() {
return checkPopupPositionBounds(screenWidth, screenHeight);
}
/**
* Check if {@link #popupLayoutParams}' position is within a arbitrary boundary
* that goes from (0, 0) to (boundaryWidth, boundaryHeight).
* <p>
* If it's out of these boundaries, {@link #popupLayoutParams}' position is changed
* and {@code true} is returned to represent this change.
* </p>
*
* @param boundaryWidth width of the boundary
* @param boundaryHeight height of the boundary
* @return if the popup was out of bounds and have been moved back to it
*/
private boolean checkPopupPositionBounds(final float boundaryWidth,
final float boundaryHeight) {
if (DEBUG) {
Log.d(TAG, "checkPopupPositionBounds() called with: "
+ "boundaryWidth = [" + boundaryWidth + "], "
+ "boundaryHeight = [" + boundaryHeight + "]");
}
if (popupLayoutParams.x < 0) {
popupLayoutParams.x = 0;
return true;
} else if (popupLayoutParams.x > boundaryWidth - popupLayoutParams.width) {
popupLayoutParams.x = (int) (boundaryWidth - popupLayoutParams.width);
return true;
}
if (popupLayoutParams.y < 0) {
popupLayoutParams.y = 0;
return true;
} else if (popupLayoutParams.y > boundaryHeight - popupLayoutParams.height) {
popupLayoutParams.y = (int) (boundaryHeight - popupLayoutParams.height);
return true;
}
return false;
}
private void savePositionAndSize() {
SharedPreferences sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(PopupVideoPlayer.this);
sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply();
sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply();
sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply();
}
private float getMinimumVideoHeight(final float width) {
final float height = width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
// if (DEBUG) {
// Log.d(TAG, "getMinimumVideoHeight() called with: width = [" + width + "], "
// + "returned: " + height);
// }
return height;
}
private void updateScreenSize() {
DisplayMetrics metrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(metrics);
screenWidth = metrics.widthPixels;
screenHeight = metrics.heightPixels;
if (DEBUG) {
Log.d(TAG, "updateScreenSize() called > screenWidth = " + screenWidth + ", "
+ "screenHeight = " + screenHeight);
}
popupWidth = getResources().getDimension(R.dimen.popup_default_width);
popupHeight = getMinimumVideoHeight(popupWidth);
minimumWidth = getResources().getDimension(R.dimen.popup_minimum_width);
minimumHeight = getMinimumVideoHeight(minimumWidth);
maximumWidth = screenWidth;
maximumHeight = screenHeight;
}
private void updatePopupSize(final int width, final int height) {
if (playerImpl == null) {
return;
}
if (DEBUG) {
Log.d(TAG, "updatePopupSize() called with: "
+ "width = [" + width + "], height = [" + height + "]");
}
final int actualWidth = (int) (width > maximumWidth ? maximumWidth
: width < minimumWidth ? minimumWidth : width);
final int actualHeight;
if (height == -1) {
actualHeight = (int) getMinimumVideoHeight(width);
} else {
actualHeight = (int) (height > maximumHeight ? maximumHeight
: height < minimumHeight ? minimumHeight : height);
}
popupLayoutParams.width = actualWidth;
popupLayoutParams.height = actualHeight;
popupWidth = actualWidth;
popupHeight = actualHeight;
if (DEBUG) {
Log.d(TAG, "updatePopupSize() updated values: "
+ "width = [" + actualWidth + "], height = [" + actualHeight + "]");
}
windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams);
}
protected void setRepeatModeRemote(final RemoteViews remoteViews, final int repeatMode) {
final String methodName = "setImageResource";
if (remoteViews == null) {
return;
}
switch (repeatMode) {
case Player.REPEAT_MODE_OFF:
remoteViews.setInt(R.id.notificationRepeat, methodName,
R.drawable.exo_controls_repeat_off);
break;
case Player.REPEAT_MODE_ONE:
remoteViews.setInt(R.id.notificationRepeat, methodName,
R.drawable.exo_controls_repeat_one);
break;
case Player.REPEAT_MODE_ALL:
remoteViews.setInt(R.id.notificationRepeat, methodName,
R.drawable.exo_controls_repeat_all);
break;
}
}
private void updateWindowFlags(final int flags) {
if (popupLayoutParams == null || windowManager == null || playerImpl == null) {
return;
}
popupLayoutParams.flags = flags;
windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams);
}
///////////////////////////////////////////////////////////////////////////
protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener {
private TextView resizingIndicator;
private ImageButton fullScreenButton;
private ImageView videoPlayPause;
private View extraOptionsView;
private View closingOverlayView;
VideoPlayerImpl(final Context context) {
super("VideoPlayerImpl" + PopupVideoPlayer.TAG, context);
}
@Override
public void handleIntent(final Intent intent) {
super.handleIntent(intent);
resetNotification();
startForeground(NOTIFICATION_ID, notBuilder.build());
}
@Override
public void initViews(final View view) {
super.initViews(view);
resizingIndicator = view.findViewById(R.id.resizing_indicator);
fullScreenButton = view.findViewById(R.id.fullScreenButton);
fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked());
videoPlayPause = view.findViewById(R.id.videoPlayPause);
extraOptionsView = view.findViewById(R.id.extraOptionsView);
closingOverlayView = view.findViewById(R.id.closingOverlay);
view.addOnLayoutChangeListener(this);
}
@Override
public void initListeners() {
super.initListeners();
videoPlayPause.setOnClickListener(v -> onPlayPause());
}
@Override
protected void setupSubtitleView(@NonNull final SubtitleView view, final float captionScale,
@NonNull final CaptionStyleCompat captionStyle) {
float captionRatio = (captionScale - 1f) / 5f + 1f;
view.setFractionalTextSize(SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio);
view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT));
view.setStyle(captionStyle);
}
@Override
public void onLayoutChange(final View view, final int left, final int top, final int right,
final int bottom, final int oldLeft, final int oldTop,
final int oldRight, final int oldBottom) {
float widthDp = Math.abs(right - left) / getResources().getDisplayMetrics().density;
final int visibility = widthDp > MINIMUM_SHOW_EXTRA_WIDTH_DP ? View.VISIBLE : View.GONE;
extraOptionsView.setVisibility(visibility);
}
@Override
public void destroy() {
if (notRemoteView != null) {
notRemoteView.setImageViewBitmap(R.id.notificationCover, null);
}
super.destroy();
}
@Override
public void onFullScreenButtonClicked() {
super.onFullScreenButtonClicked();
if (DEBUG) {
Log.d(TAG, "onFullScreenButtonClicked() called");
}
setRecovery();
final Intent intent = NavigationHelper.getPlayerIntent(
context,
MainVideoPlayer.class,
this.getPlayQueue(),
this.getRepeatMode(),
this.getPlaybackSpeed(),
this.getPlaybackPitch(),
this.getPlaybackSkipSilence(),
this.getPlaybackQuality(),
false,
!isPlaying(),
isMuted()
);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
closePopup();
}
@Override
public void onDismiss(final PopupMenu menu) {
super.onDismiss(menu);
if (isPlaying()) {
hideControls(500, 0);
}
}
@Override
protected int nextResizeMode(final int resizeMode) {
if (resizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) {
return AspectRatioFrameLayout.RESIZE_MODE_FIT;
} else {
return AspectRatioFrameLayout.RESIZE_MODE_FILL;
}
}
@Override
public void onStopTrackingTouch(final SeekBar seekBar) {
super.onStopTrackingTouch(seekBar);
if (wasPlaying()) {
hideControls(100, 0);
}
}
@Override
public void onShuffleClicked() {
super.onShuffleClicked();
updatePlayback();
}
@Override
public void onMuteUnmuteButtonClicked() {
super.onMuteUnmuteButtonClicked();
updatePlayback();
}
@Override
public void onUpdateProgress(final int currentProgress, final int duration,
final int bufferPercent) {
updateProgress(currentProgress, duration, bufferPercent);
super.onUpdateProgress(currentProgress, duration, bufferPercent);
}
@Override
protected VideoPlaybackResolver.QualityResolver getQualityResolver() {
return new VideoPlaybackResolver.QualityResolver() {
@Override
public int getDefaultResolutionIndex(final List<VideoStream> sortedVideos) {
return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos);
}
@Override
public int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
final String playbackQuality) {
return ListHelper.getPopupResolutionIndex(context, sortedVideos,
playbackQuality);
}
};
}
/*//////////////////////////////////////////////////////////////////////////
// Thumbnail Loading
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onLoadingComplete(final String imageUri, final View view,
final Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
if (playerImpl == null) {
return;
}
// rebuild notification here since remote view does not release bitmaps,
// causing memory leaks
resetNotification();
updateNotification(-1);
}
@Override
public void onLoadingFailed(final String imageUri, final View view,
final FailReason failReason) {
super.onLoadingFailed(imageUri, view, failReason);
resetNotification();
updateNotification(-1);
}
@Override
public void onLoadingCancelled(final String imageUri, final View view) {
super.onLoadingCancelled(imageUri, view);
resetNotification();
updateNotification(-1);
}
/*//////////////////////////////////////////////////////////////////////////
// Activity Event Listener
//////////////////////////////////////////////////////////////////////////*/
/*package-private*/ void setActivityListener(final PlayerEventListener listener) {
activityListener = listener;
updateMetadata();
updatePlayback();
triggerProgressUpdate();
}
/*package-private*/ void removeActivityListener(final PlayerEventListener listener) {
if (activityListener == listener) {
activityListener = null;
}
}
private void updateMetadata() {
if (activityListener != null && getCurrentMetadata() != null) {
activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata());
}
}
private void updatePlayback() {
if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
activityListener.onPlaybackUpdate(currentState, getRepeatMode(),
playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters());
}
}
private void updateProgress(final int currentProgress, final int duration,
final int bufferPercent) {
if (activityListener != null) {
activityListener.onProgressUpdate(currentProgress, duration, bufferPercent);
}
}
private void stopActivityBinding() {
if (activityListener != null) {
activityListener.onServiceStopped();
activityListener = null;
}
}
/*//////////////////////////////////////////////////////////////////////////
// ExoPlayer Video Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onRepeatModeChanged(final int i) {
super.onRepeatModeChanged(i);
setRepeatModeRemote(notRemoteView, i);
updatePlayback();
resetNotification();
updateNotification(-1);
}
@Override
public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) {
super.onPlaybackParametersChanged(playbackParameters);
updatePlayback();
}
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
super.onMetadataChanged(tag);
resetNotification();
updateNotification(-1);
updateMetadata();
}
@Override
public void onPlaybackShutdown() {
super.onPlaybackShutdown();
closePopup();
}
/*//////////////////////////////////////////////////////////////////////////
// Broadcast Receiver
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void setupBroadcastReceiver(final IntentFilter intentFltr) {
super.setupBroadcastReceiver(intentFltr);
if (DEBUG) {
Log.d(TAG, "setupBroadcastReceiver() called with: "
+ "intentFilter = [" + intentFltr + "]");
}
intentFltr.addAction(ACTION_CLOSE);
intentFltr.addAction(ACTION_PLAY_PAUSE);
intentFltr.addAction(ACTION_REPEAT);
intentFltr.addAction(Intent.ACTION_SCREEN_ON);
intentFltr.addAction(Intent.ACTION_SCREEN_OFF);
}
@Override
public void onBroadcastReceived(final Intent intent) {
super.onBroadcastReceived(intent);
if (intent == null || intent.getAction() == null) {
return;
}
if (DEBUG) {
Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]");
}
switch (intent.getAction()) {
case ACTION_CLOSE:
closePopup();
break;
case ACTION_PLAY_PAUSE:
onPlayPause();
break;
case ACTION_REPEAT:
onRepeatClicked();
break;
case Intent.ACTION_SCREEN_ON:
enableVideoRenderer(true);
break;
case Intent.ACTION_SCREEN_OFF:
enableVideoRenderer(false);
break;
}
}
/*//////////////////////////////////////////////////////////////////////////
// States
//////////////////////////////////////////////////////////////////////////*/
@Override
public void changeState(final int state) {
super.changeState(state);
updatePlayback();
}
@Override
public void onBlocked() {
super.onBlocked();
resetNotification();
updateNotification(R.drawable.exo_controls_play);
}
@Override
public void onPlaying() {
super.onPlaying();
updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS);
resetNotification();
updateNotification(R.drawable.exo_controls_pause);
videoPlayPause.setBackgroundResource(R.drawable.exo_controls_pause);
hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
startForeground(NOTIFICATION_ID, notBuilder.build());
}
@Override
public void onBuffering() {
super.onBuffering();
resetNotification();
updateNotification(R.drawable.exo_controls_play);
}
@Override
public void onPaused() {
super.onPaused();
updateWindowFlags(IDLE_WINDOW_FLAGS);
resetNotification();
updateNotification(R.drawable.exo_controls_play);
videoPlayPause.setBackgroundResource(R.drawable.exo_controls_play);
stopForeground(false);
}
@Override
public void onPausedSeek() {
super.onPausedSeek();
resetNotification();
updateNotification(R.drawable.exo_controls_play);
videoPlayPause.setBackgroundResource(R.drawable.exo_controls_play);
}
@Override
public void onCompleted() {
super.onCompleted();
updateWindowFlags(IDLE_WINDOW_FLAGS);
resetNotification();
updateNotification(R.drawable.ic_replay_white_24dp);
videoPlayPause.setBackgroundResource(R.drawable.ic_replay_white_24dp);
stopForeground(false);
}
@Override
public void showControlsThenHide() {
videoPlayPause.setVisibility(View.VISIBLE);
super.showControlsThenHide();
}
public void showControls(final long duration) {
videoPlayPause.setVisibility(View.VISIBLE);
super.showControls(duration);
}
public void hideControls(final long duration, final long delay) {
super.hideControlsAndButton(duration, delay, videoPlayPause);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
/*package-private*/ void enableVideoRenderer(final boolean enable) {
final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO);
if (videoRendererIndex != RENDERER_UNAVAILABLE) {
trackSelector.setParameters(trackSelector.buildUponParameters()
.setRendererDisabled(videoRendererIndex, !enable));
}
}
/*//////////////////////////////////////////////////////////////////////////
// Getters
//////////////////////////////////////////////////////////////////////////*/
@SuppressWarnings("WeakerAccess")
public TextView getResizingIndicator() {
return resizingIndicator;
}
public View getClosingOverlayView() {
return closingOverlayView;
}
}
private class PopupWindowGestureListener extends GestureDetector.SimpleOnGestureListener
implements View.OnTouchListener {
private int initialPopupX;
private int initialPopupY;
private boolean isMoving;
private boolean isResizing;
//initial co-ordinates and distance between fingers
private double initPointerDistance = -1;
private float initFirstPointerX = -1;
private float initFirstPointerY = -1;
private float initSecPointerX = -1;
private float initSecPointerY = -1;
@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 (playerImpl == null || !playerImpl.isPlaying()) {
return false;
}
playerImpl.hideControls(0, 0);
if (e.getX() > popupWidth / 2) {
playerImpl.onFastForward();
} else {
playerImpl.onFastRewind();
}
return true;
}
@Override
public boolean onSingleTapConfirmed(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
}
if (playerImpl == null || playerImpl.getPlayer() == null) {
return false;
}
if (playerImpl.isControlsVisible()) {
playerImpl.hideControls(100, 100);
} else {
playerImpl.showControlsThenHide();
}
return true;
}
@Override
public boolean onDown(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onDown() called with: e = [" + e + "]");
}
// Fix popup position when the user touch it, it may have the wrong one
// because the soft input is visible (the draggable area is currently resized).
checkPopupPositionBounds(closeOverlayView.getWidth(), closeOverlayView.getHeight());
initialPopupX = popupLayoutParams.x;
initialPopupY = popupLayoutParams.y;
popupWidth = popupLayoutParams.width;
popupHeight = popupLayoutParams.height;
return super.onDown(e);
}
@Override
public void onLongPress(final MotionEvent e) {
if (DEBUG) {
Log.d(TAG, "onLongPress() called with: e = [" + e + "]");
}
updateScreenSize();
checkPopupPositionBounds();
updatePopupSize((int) screenWidth, -1);
}
@Override
public boolean onScroll(final MotionEvent initialEvent, final MotionEvent movingEvent,
final float distanceX, final float distanceY) {
if (isResizing || playerImpl == null) {
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY);
}
if (!isMoving) {
animateView(closeOverlayButton, true, 200);
}
isMoving = true;
float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX());
float posX = (int) (initialPopupX + diffX);
float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY());
float posY = (int) (initialPopupY + diffY);
if (posX > (screenWidth - popupWidth)) {
posX = (int) (screenWidth - popupWidth);
} else if (posX < 0) {
posX = 0;
}
if (posY > (screenHeight - popupHeight)) {
posY = (int) (screenHeight - popupHeight);
} else if (posY < 0) {
posY = 0;
}
popupLayoutParams.x = (int) posX;
popupLayoutParams.y = (int) posY;
final View closingOverlayView = playerImpl.getClosingOverlayView();
if (isInsideClosingRadius(movingEvent)) {
if (closingOverlayView.getVisibility() == View.GONE) {
animateView(closingOverlayView, true, 250);
}
} else {
if (closingOverlayView.getVisibility() == View.VISIBLE) {
animateView(closingOverlayView, false, 0);
}
}
// if (DEBUG) {
// Log.d(TAG, "PopupVideoPlayer.onScroll = "
// + "e1.getRaw = [" + initialEvent.getRawX() + ", "
// + initialEvent.getRawY() + "], "
// + "e1.getX,Y = [" + initialEvent.getX() + ", "
// + initialEvent.getY() + "], "
// + "e2.getRaw = [" + movingEvent.getRawX() + ", "
// + movingEvent.getRawY() + "], "
// + "e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "], "
// + "distanceX,Y = [" + distanceX + ", " + distanceY + "], "
// + "posX,Y = [" + posX + ", " + posY + "], "
// + "popupW,H = [" + popupWidth + " x " + popupHeight + "]");
// }
windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams);
return true;
}
private void onScrollEnd(final MotionEvent event) {
if (DEBUG) {
Log.d(TAG, "onScrollEnd() called");
}
if (playerImpl == null) {
return;
}
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
if (isInsideClosingRadius(event)) {
closePopup();
} else {
animateView(playerImpl.getClosingOverlayView(), false, 0);
if (!isPopupClosing) {
animateView(closeOverlayButton, false, 200);
}
}
}
@Override
public boolean onFling(final MotionEvent e1, final MotionEvent e2,
final float velocityX, final float velocityY) {
if (DEBUG) {
Log.d(TAG, "Fling velocity: dX=[" + velocityX + "], dY=[" + velocityY + "]");
}
if (playerImpl == null) {
return false;
}
final float absVelocityX = Math.abs(velocityX);
final float absVelocityY = Math.abs(velocityY);
if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) {
if (absVelocityX > tossFlingVelocity) {
popupLayoutParams.x = (int) velocityX;
}
if (absVelocityY > tossFlingVelocity) {
popupLayoutParams.y = (int) velocityY;
}
checkPopupPositionBounds();
windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams);
return true;
}
return false;
}
@Override
public boolean onTouch(final View v, final MotionEvent event) {
popupGestureDetector.onTouchEvent(event);
if (playerImpl == null) {
return false;
}
if (event.getPointerCount() == 2 && !isMoving && !isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.");
}
playerImpl.showAndAnimateControl(-1, true);
playerImpl.getLoadingPanel().setVisibility(View.GONE);
playerImpl.hideControls(0, 0);
animateView(playerImpl.getCurrentDisplaySeek(), false, 0, 0);
animateView(playerImpl.getResizingIndicator(), true, 200, 0);
//record co-ordinates of fingers
initFirstPointerX = event.getX(0);
initFirstPointerY = event.getY(0);
initSecPointerX = event.getX(1);
initSecPointerY = event.getY(1);
//record distance between fingers
initPointerDistance = Math.hypot(initFirstPointerX - initSecPointerX,
initFirstPointerY - initSecPointerY);
isResizing = true;
}
if (event.getAction() == MotionEvent.ACTION_MOVE && !isMoving && isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() ACTION_MOVE > v = [" + v + "], "
+ "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
}
return handleMultiDrag(event);
}
if (event.getAction() == MotionEvent.ACTION_UP) {
if (DEBUG) {
Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], "
+ "e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
}
if (isMoving) {
isMoving = false;
onScrollEnd(event);
}
if (isResizing) {
isResizing = false;
initPointerDistance = -1;
initFirstPointerX = -1;
initFirstPointerY = -1;
initSecPointerX = -1;
initSecPointerY = -1;
animateView(playerImpl.getResizingIndicator(), false, 100, 0);
playerImpl.changeState(playerImpl.getCurrentState());
}
if (!isPopupClosing) {
savePositionAndSize();
}
}
v.performClick();
return true;
}
private boolean handleMultiDrag(final MotionEvent event) {
if (initPointerDistance != -1 && event.getPointerCount() == 2) {
// get the movements of the fingers
double firstPointerMove = Math.hypot(event.getX(0) - initFirstPointerX,
event.getY(0) - initFirstPointerY);
double secPointerMove = Math.hypot(event.getX(1) - initSecPointerX,
event.getY(1) - initSecPointerY);
// minimum threshold beyond which pinch gesture will work
int minimumMove = ViewConfiguration.get(PopupVideoPlayer.this).getScaledTouchSlop();
if (Math.max(firstPointerMove, secPointerMove) > minimumMove) {
// calculate current distance between the pointers
double currentPointerDistance =
Math.hypot(event.getX(0) - event.getX(1),
event.getY(0) - event.getY(1));
// change co-ordinates of popup so the center stays at the same position
double newWidth = (popupWidth * currentPointerDistance / initPointerDistance);
initPointerDistance = currentPointerDistance;
popupLayoutParams.x += (popupWidth - newWidth) / 2;
checkPopupPositionBounds();
updateScreenSize();
updatePopupSize((int) Math.min(screenWidth, newWidth), -1);
return true;
}
}
return false;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private int distanceFromCloseButton(final MotionEvent popupMotionEvent) {
final int closeOverlayButtonX = closeOverlayButton.getLeft()
+ closeOverlayButton.getWidth() / 2;
final int closeOverlayButtonY = closeOverlayButton.getTop()
+ closeOverlayButton.getHeight() / 2;
float fingerX = popupLayoutParams.x + popupMotionEvent.getX();
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 = closeOverlayButton.getWidth() / 2;
// 20% wider than the button itself
return buttonRadius * 1.2f;
}
private boolean isInsideClosingRadius(final MotionEvent popupMotionEvent) {
return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius();
}
}
}