diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 9e17ae817..c994b6146 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -3,11 +3,9 @@ package org.schabi.newpipe.fragments.detail; import android.animation.ValueAnimator; import android.app.Activity; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.database.ContentObserver; @@ -16,7 +14,8 @@ import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.os.IBinder; +import android.os.Looper; +import android.view.ViewTreeObserver; import androidx.core.text.HtmlCompat; import androidx.preference.PreferenceManager; import android.provider.Settings; @@ -60,6 +59,7 @@ import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener; +import org.schabi.newpipe.App; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.download.DownloadDialog; @@ -83,12 +83,11 @@ import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.player.BasePlayer; import org.schabi.newpipe.player.MainPlayer; -import org.schabi.newpipe.player.VideoPlayer; import org.schabi.newpipe.player.VideoPlayerImpl; import org.schabi.newpipe.player.event.OnKeyDownListener; -import org.schabi.newpipe.player.event.PlayerEventListener; -import org.schabi.newpipe.player.event.PlayerServiceEventListener; +import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; @@ -98,12 +97,10 @@ import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ImageDisplayConstants; -import org.schabi.newpipe.util.InfoCache; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.AnimatedProgressBar; @@ -135,8 +132,7 @@ public class VideoDetailFragment SharedPreferences.OnSharedPreferenceChangeListener, View.OnClickListener, View.OnLongClickListener, - PlayerEventListener, - PlayerServiceEventListener, + PlayerServiceExtendedEventListener, OnKeyDownListener { public static final String AUTO_PLAY = "auto_play"; @@ -158,9 +154,6 @@ public class VideoDetailFragment private static final String RELATED_TAB_TAG = "NEXT VIDEO"; private static final String EMPTY_TAB_TAG = "EMPTY TAB"; - private static final String INFO_KEY = "info_key"; - private static final String STACK_KEY = "stack_key"; - private boolean showRelatedStreams; private boolean showComments; private String selectedTabTag; @@ -173,14 +166,13 @@ public class VideoDetailFragment protected String name; @State protected String url; - @State - protected PlayQueue playQueue; + protected static PlayQueue playQueue; @State int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED; @State protected boolean autoPlayEnabled = true; - private StreamInfo currentInfo; + private static StreamInfo currentInfo; private Disposable currentWorker; @NonNull private CompositeDisposable disposables = new CompositeDisposable(); @@ -249,8 +241,6 @@ public class VideoDetailFragment private FrameLayout relatedStreamsLayout; private ContentObserver settingsContentObserver; - private ServiceConnection serviceConnection; - private boolean bound; private MainPlayer playerService; private VideoPlayerImpl player; @@ -258,123 +248,56 @@ public class VideoDetailFragment /*////////////////////////////////////////////////////////////////////////// // Service management //////////////////////////////////////////////////////////////////////////*/ + @Override + public void onServiceConnected(final VideoPlayerImpl connectedPlayer, + final MainPlayer connectedPlayerService, + final boolean playAfterConnect) { + player = connectedPlayer; + playerService = connectedPlayerService; - private ServiceConnection getServiceConnection(final Context context, - final boolean playAfterConnect) { - return new ServiceConnection() { - @Override - public void onServiceDisconnected(final ComponentName compName) { - if (DEBUG) { - Log.d(TAG, "Player service is disconnected"); - } + // It will do nothing if the player is not in fullscreen mode + hideSystemUiIfNeeded(); - unbind(context); - } - - @Override - public void onServiceConnected(final ComponentName compName, final IBinder service) { - if (DEBUG) { - Log.d(TAG, "Player service is connected"); - } - final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service; - - playerService = localBinder.getService(); - player = localBinder.getPlayer(); - - startPlayerListener(); - - // It will do nothing if the player is not in fullscreen mode - hideSystemUiIfNeeded(); - - if (!player.videoPlayerSelected() && !playAfterConnect) { - return; - } - - if (playerIsNotStopped() && player.videoPlayerSelected()) { - addVideoPlayerView(); - } - - if (isLandscape()) { - // If the video is playing but orientation changed - // let's make the video in fullscreen again - checkLandscape(); - } else if (player.isFullscreen()) { - // Device is in portrait orientation after rotation but UI is in fullscreen. - // Return back to non-fullscreen state - player.toggleFullscreen(); - } - - if (playAfterConnect - || (currentInfo != null - && isAutoplayEnabled() - && player.getParentActivity() == null)) { - openVideoPlayer(); - } - } - }; - } - - private void bind(final Context context) { - if (DEBUG) { - Log.d(TAG, "bind() called"); + if (!player.videoPlayerSelected() && !playAfterConnect) { + return; } - final Intent serviceIntent = new Intent(context, MainPlayer.class); - bound = context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); - if (!bound) { - context.unbindService(serviceConnection); + if (isLandscape()) { + // If the video is playing but orientation changed + // let's make the video in fullscreen again + checkLandscape(); + } else if (player.isFullscreen() && !player.isVerticalVideo()) { + // Device is in portrait orientation after rotation but UI is in fullscreen. + // Return back to non-fullscreen state + player.toggleFullscreen(); + } + + if (playerIsNotStopped() && player.videoPlayerSelected()) { + addVideoPlayerView(); + } + + if (playAfterConnect + || (currentInfo != null + && isAutoplayEnabled() + && player.getParentActivity() == null)) { + openVideoPlayer(); } } - private void unbind(final Context context) { - if (DEBUG) { - Log.d(TAG, "unbind() called"); - } - - if (bound) { - context.unbindService(serviceConnection); - bound = false; - stopPlayerListener(); - playerService = null; - player = null; - restoreDefaultBrightness(); - } - } - - private void startPlayerListener() { - if (player != null) { - player.setFragmentListener(this); - } - } - - private void stopPlayerListener() { - if (player != null) { - player.removeFragmentListener(this); - } - } - - private void startService(final Context context, final boolean playAfterConnect) { - // startService() can be called concurrently and it will give a random crashes - // and NullPointerExceptions inside the service because the service will be - // bound twice. Prevent it with unbinding first - unbind(context); - context.startService(new Intent(context, MainPlayer.class)); - serviceConnection = getServiceConnection(context, playAfterConnect); - bind(context); - } - - private void stopService(final Context context) { - unbind(context); - context.stopService(new Intent(context, MainPlayer.class)); + @Override + public void onServiceDisconnected() { + playerService = null; + player = null; + restoreDefaultBrightness(); } /*////////////////////////////////////////////////////////////////////////*/ public static VideoDetailFragment getInstance(final int serviceId, final String videoUrl, - final String name, final PlayQueue playQueue) { + final String name, final PlayQueue queue) { final VideoDetailFragment instance = new VideoDetailFragment(); - instance.setInitialData(serviceId, videoUrl, name, playQueue); + instance.setInitialData(serviceId, videoUrl, name, queue); return instance; } @@ -477,9 +400,9 @@ public class VideoDetailFragment // Stop the service when user leaves the app with double back press // if video player is selected. Otherwise unbind if (activity.isFinishing() && player != null && player.videoPlayerSelected()) { - stopService(requireContext()); + PlayerHolder.stopService(App.getApp()); } else { - unbind(requireContext()); + PlayerHolder.removeListener(); } PreferenceManager.getDefaultSharedPreferences(activity) @@ -497,6 +420,12 @@ public class VideoDetailFragment positionSubscriber = null; currentWorker = null; bottomSheetBehavior.setBottomSheetCallback(null); + + if (activity.isFinishing()) { + playQueue = null; + currentInfo = null; + stack = new LinkedList<>(); + } } @Override @@ -529,62 +458,6 @@ public class VideoDetailFragment } } - /*////////////////////////////////////////////////////////////////////////// - // State Saving - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onSaveInstanceState(final Bundle outState) { - super.onSaveInstanceState(outState); - - if (!isLoading.get() && currentInfo != null && isVisible()) { - final String infoCacheKey = SerializedCache.getInstance() - .put(currentInfo, StreamInfo.class); - if (infoCacheKey != null) { - outState.putString(INFO_KEY, infoCacheKey); - } - } - - if (playQueue != null) { - final String queueCacheKey = SerializedCache.getInstance() - .put(playQueue, PlayQueue.class); - if (queueCacheKey != null) { - outState.putString(VideoPlayer.PLAY_QUEUE_KEY, queueCacheKey); - } - } - final String stackCacheKey = SerializedCache.getInstance().put(stack, LinkedList.class); - if (stackCacheKey != null) { - outState.putString(STACK_KEY, stackCacheKey); - } - } - - @Override - protected void onRestoreInstanceState(@NonNull final Bundle savedState) { - super.onRestoreInstanceState(savedState); - - final String infoCacheKey = savedState.getString(INFO_KEY); - if (infoCacheKey != null) { - currentInfo = SerializedCache.getInstance().take(infoCacheKey, StreamInfo.class); - if (currentInfo != null) { - InfoCache.getInstance() - .putInfo(serviceId, url, currentInfo, InfoItem.InfoType.STREAM); - } - } - - final String stackCacheKey = savedState.getString(STACK_KEY); - if (stackCacheKey != null) { - final LinkedList cachedStack = - SerializedCache.getInstance().take(stackCacheKey, LinkedList.class); - if (cachedStack != null) { - stack.addAll(cachedStack); - } - } - final String queueCacheKey = savedState.getString(VideoPlayer.PLAY_QUEUE_KEY); - if (queueCacheKey != null) { - playQueue = SerializedCache.getInstance().take(queueCacheKey, PlayQueue.class); - } - } - /*////////////////////////////////////////////////////////////////////////// // OnClick //////////////////////////////////////////////////////////////////////////*/ @@ -779,8 +652,6 @@ public class VideoDetailFragment relatedStreamsLayout = rootView.findViewById(R.id.relatedStreamsLayout); - setHeightThumbnail(); - thumbnailBackgroundButton.requestFocus(); if (DeviceUtils.isTv(getContext())) { @@ -826,7 +697,11 @@ public class VideoDetailFragment detailControlsPopup.setOnTouchListener(getOnControlsTouchListener()); setupBottomPlayer(); - startService(requireContext(), false); + if (!PlayerHolder.bound) { + setHeightThumbnail(); + } else { + PlayerHolder.startService(App.getApp(), false, this); + } } private View.OnTouchListener getOnControlsTouchListener() { @@ -881,7 +756,7 @@ public class VideoDetailFragment * Stack that contains the "navigation history".
* The peek is the current video. */ - protected final LinkedList stack = new LinkedList<>(); + private static LinkedList stack = new LinkedList<>(); @Override public boolean onKeyDown(final int keyCode) { @@ -965,7 +840,7 @@ public class VideoDetailFragment if (currentInfo == null) { prepareAndLoadInfo(); } else { - prepareAndHandleInfo(currentInfo, false); + prepareAndHandleInfoIfNeededAfterDelay(currentInfo, false, 50); } } @@ -981,6 +856,21 @@ public class VideoDetailFragment startLoading(false, true); } + private void prepareAndHandleInfoIfNeededAfterDelay(final StreamInfo info, + final boolean scrollToTop, + final long delay) { + new Handler(Looper.getMainLooper()).postDelayed(() -> { + if (activity == null) { + return; + } + // Data can already be drawn, don't spend time twice + if (info.getName().equals(videoTitleTextView.getText().toString())) { + return; + } + prepareAndHandleInfo(info, scrollToTop); + }, delay); + } + private void prepareAndHandleInfo(final StreamInfo info, final boolean scrollToTop) { if (DEBUG) { Log.d(TAG, "prepareAndHandleInfo() called with: " @@ -1140,8 +1030,8 @@ public class VideoDetailFragment } // See UI changes while remote playQueue changes - if (!bound) { - startService(requireContext(), false); + if (player == null) { + PlayerHolder.startService(App.getApp(), false, this); } // If a user watched video inside fullscreen mode and than chose another player @@ -1170,8 +1060,8 @@ public class VideoDetailFragment private void openNormalBackgroundPlayer(final boolean append) { // See UI changes while remote playQueue changes - if (!bound) { - startService(requireContext(), false); + if (player == null) { + PlayerHolder.startService(App.getApp(), false, this); } final PlayQueue queue = setupPlayQueueForIntent(append); @@ -1185,7 +1075,7 @@ public class VideoDetailFragment private void openMainPlayer() { if (playerService == null) { - startService(requireContext(), true); + PlayerHolder.startService(App.getApp(), true, this); return; } if (currentInfo == null) { @@ -1290,7 +1180,7 @@ public class VideoDetailFragment // Check if viewHolder already contains a child if (player.getRootView().getParent() != playerPlaceholder) { - removeVideoPlayerView(); + playerService.removeViewFromParent(); } setHeightThumbnail(); @@ -1346,6 +1236,23 @@ public class VideoDetailFragment } } + private final ViewTreeObserver.OnPreDrawListener preDrawListener = + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + + if (getView() != null) { + final int height = isInMultiWindow() + ? requireView().getHeight() + : activity.getWindow().getDecorView().getHeight(); + setHeightThumbnail(height, metrics); + getView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener); + } + return false; + } + }; + /** * Method which controls the size of thumbnail and the size of main player inside * a layout with thumbnail. It decides what height the player should have in both @@ -1356,24 +1263,35 @@ public class VideoDetailFragment private void setHeightThumbnail() { final DisplayMetrics metrics = getResources().getDisplayMetrics(); final boolean isPortrait = metrics.heightPixels > metrics.widthPixels; + requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener); - final int height; if (player != null && player.isFullscreen()) { - height = isInMultiWindow() + final int height = isInMultiWindow() ? requireView().getHeight() : activity.getWindow().getDecorView().getHeight(); + // Height is zero when the view is not yet displayed like after orientation change + if (height != 0) { + setHeightThumbnail(height, metrics); + } else { + requireView().getViewTreeObserver().addOnPreDrawListener(preDrawListener); + } } else { - height = isPortrait + final int height = isPortrait ? (int) (metrics.widthPixels / (16.0f / 9.0f)) : (int) (metrics.heightPixels / 2.0f); + setHeightThumbnail(height, metrics); } + } + private void setHeightThumbnail(final int newHeight, final DisplayMetrics metrics) { thumbnailImageView.setLayoutParams( - new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height)); - thumbnailImageView.setMinimumHeight(height); + new FrameLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, newHeight)); + thumbnailImageView.setMinimumHeight(newHeight); if (player != null) { final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT); - player.getSurfaceView().setHeights(height, player.isFullscreen() ? height : maxHeight); + player.getSurfaceView() + .setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight); } } @@ -1895,7 +1813,10 @@ public class VideoDetailFragment currentInfo = info; setInitialData(info.getServiceId(), info.getUrl(), info.getName(), queue); setAutoplay(false); - prepareAndHandleInfo(info, true); + // Delay execution just because it freezes the main thread, and while playing + // next/previous video you see visual glitches + // (when non-vertical video goes after vertical video) + prepareAndHandleInfoIfNeededAfterDelay(info, true, 200); } @Override @@ -1912,7 +1833,6 @@ public class VideoDetailFragment @Override public void onServiceStopped() { - unbind(requireContext()); setOverlayPlayPauseImage(); if (currentInfo != null) { updateOverlayData(currentInfo.getName(), @@ -2197,7 +2117,7 @@ public class VideoDetailFragment if (currentWorker != null) { currentWorker.dispose(); } - stopService(requireContext()); + PlayerHolder.stopService(App.getApp()); setInitialData(0, null, "", null); currentInfo = null; updateOverlayData(null, null, null); diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java index 7a258a4b8..3a568089e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayerImpl.java @@ -2107,4 +2107,8 @@ public class VideoPlayerImpl extends VideoPlayer public View getClosingOverlayView() { return closingOverlayView; } + + public boolean isVerticalVideo() { + return isVerticalVideo; + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java new file mode 100644 index 000000000..93952a811 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java @@ -0,0 +1,11 @@ +package org.schabi.newpipe.player.event; + +import org.schabi.newpipe.player.MainPlayer; +import org.schabi.newpipe.player.VideoPlayerImpl; + +public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener { + void onServiceConnected(VideoPlayerImpl player, + MainPlayer playerService, + boolean playAfterConnect); + void onServiceDisconnected(); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java new file mode 100644 index 000000000..a5760eddc --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java @@ -0,0 +1,219 @@ +package org.schabi.newpipe.player.helper; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.util.Log; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlaybackParameters; +import org.schabi.newpipe.App; +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.MainPlayer; +import org.schabi.newpipe.player.VideoPlayerImpl; +import org.schabi.newpipe.player.event.PlayerServiceEventListener; +import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; +import org.schabi.newpipe.player.playqueue.PlayQueue; + +public final class PlayerHolder { + private PlayerHolder() { + } + + private static final boolean DEBUG = MainActivity.DEBUG; + private static final String TAG = "PlayerHolder"; + + private static PlayerServiceExtendedEventListener listener; + + private static ServiceConnection serviceConnection; + public static boolean bound; + private static MainPlayer playerService; + private static VideoPlayerImpl player; + + public static void setListener(final PlayerServiceExtendedEventListener newListener) { + listener = newListener; + // Force reload data from service + if (player != null) { + listener.onServiceConnected(player, playerService, false); + startPlayerListener(); + } + } + + public static void removeListener() { + listener = null; + } + + + public static void startService(final Context context, + final boolean playAfterConnect, + final PlayerServiceExtendedEventListener newListener) { + setListener(newListener); + if (bound) { + return; + } + // startService() can be called concurrently and it will give a random crashes + // and NullPointerExceptions inside the service because the service will be + // bound twice. Prevent it with unbinding first + unbind(context); + context.startService(new Intent(context, MainPlayer.class)); + serviceConnection = getServiceConnection(context, playAfterConnect); + bind(context); + } + + public static void stopService(final Context context) { + unbind(context); + context.stopService(new Intent(context, MainPlayer.class)); + } + + private static ServiceConnection getServiceConnection(final Context context, + final boolean playAfterConnect) { + return new ServiceConnection() { + @Override + public void onServiceDisconnected(final ComponentName compName) { + if (DEBUG) { + Log.d(TAG, "Player service is disconnected"); + } + + unbind(context); + } + + @Override + public void onServiceConnected(final ComponentName compName, final IBinder service) { + if (DEBUG) { + Log.d(TAG, "Player service is connected"); + } + final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service; + + playerService = localBinder.getService(); + player = localBinder.getPlayer(); + if (listener != null) { + listener.onServiceConnected(player, playerService, playAfterConnect); + } + startPlayerListener(); + } + }; + } + + private static void bind(final Context context) { + if (DEBUG) { + Log.d(TAG, "bind() called"); + } + + final Intent serviceIntent = new Intent(context, MainPlayer.class); + bound = context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); + if (!bound) { + context.unbindService(serviceConnection); + } + } + + private static void unbind(final Context context) { + if (DEBUG) { + Log.d(TAG, "unbind() called"); + } + + if (bound) { + context.unbindService(serviceConnection); + bound = false; + stopPlayerListener(); + playerService = null; + player = null; + if (listener != null) { + listener.onServiceDisconnected(); + } + } + } + + + private static void startPlayerListener() { + if (player != null) { + player.setFragmentListener(INNER_LISTENER); + } + } + + private static void stopPlayerListener() { + if (player != null) { + player.removeFragmentListener(INNER_LISTENER); + } + } + + + private static final PlayerServiceEventListener INNER_LISTENER = + new PlayerServiceEventListener() { + @Override + public void onFullscreenStateChanged(final boolean fullscreen) { + if (listener != null) { + listener.onFullscreenStateChanged(fullscreen); + } + } + + @Override + public void onScreenRotationButtonClicked() { + if (listener != null) { + listener.onScreenRotationButtonClicked(); + } + } + + @Override + public void onMoreOptionsLongClicked() { + if (listener != null) { + listener.onMoreOptionsLongClicked(); + } + } + + @Override + public void onPlayerError(final ExoPlaybackException error) { + if (listener != null) { + listener.onPlayerError(error); + } + } + + @Override + public void hideSystemUiIfNeeded() { + if (listener != null) { + listener.hideSystemUiIfNeeded(); + } + } + + @Override + public void onQueueUpdate(final PlayQueue queue) { + if (listener != null) { + listener.onQueueUpdate(queue); + } + } + + @Override + public void onPlaybackUpdate(final int state, + final int repeatMode, + final boolean shuffled, + final PlaybackParameters parameters) { + if (listener != null) { + listener.onPlaybackUpdate(state, repeatMode, shuffled, parameters); + } + } + + @Override + public void onProgressUpdate(final int currentProgress, + final int duration, + final int bufferPercent) { + if (listener != null) { + listener.onProgressUpdate(currentProgress, duration, bufferPercent); + } + } + + @Override + public void onMetadataUpdate(final StreamInfo info, final PlayQueue queue) { + if (listener != null) { + listener.onMetadataUpdate(info, queue); + } + } + + @Override + public void onServiceStopped() { + if (listener != null) { + listener.onServiceStopped(); + } + unbind(App.getApp()); + } + }; +}