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 63077e92d44..976d5bdbeb9 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 @@ -95,7 +95,8 @@ import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.event.OnKeyDownListener; -import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; +import org.schabi.newpipe.player.event.PlayerHolderLifecycleEventListener; +import org.schabi.newpipe.player.event.PlayerServiceEventListener; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -136,7 +137,8 @@ public final class VideoDetailFragment extends BaseStateFragment implements BackPressable, - PlayerServiceExtendedEventListener, + PlayerServiceEventListener, + PlayerHolderLifecycleEventListener, OnKeyDownListener { public static final String KEY_SWITCHING_PLAYERS = "switching_players"; @@ -234,10 +236,9 @@ public final class VideoDetailFragment // Service management //////////////////////////////////////////////////////////////////////////*/ @Override - public void onServiceConnected(final Player connectedPlayer, - final PlayerService connectedPlayerService, + public void onServiceConnected(final PlayerService connectedPlayerService, final boolean playAfterConnect) { - player = connectedPlayer; + player = connectedPlayerService.getPlayer(); playerService = connectedPlayerService; // It will do nothing if the player is not in fullscreen mode @@ -393,7 +394,7 @@ public void onDestroy() { if (activity.isFinishing() && isPlayerAvailable() && player.videoPlayerSelected()) { playerHolder.stopService(); } else { - playerHolder.setListener(null); + playerHolder.unsetListeners(); } PreferenceManager.getDefaultSharedPreferences(activity) @@ -658,10 +659,10 @@ protected void initListeners() { }); setupBottomPlayer(); - if (!playerHolder.isBound()) { + if (playerHolder.isNotBoundYet()) { setHeightThumbnail(); } else { - playerHolder.startService(false, this); + playerHolder.startService(false, this, this); } } @@ -1052,7 +1053,7 @@ private void openPopupPlayer(final boolean append) { // See UI changes while remote playQueue changes if (!isPlayerAvailable()) { - playerHolder.startService(false, this); + playerHolder.startService(false, this, this); } else { // FIXME Workaround #7427 player.setRecovery(); @@ -1115,7 +1116,7 @@ public void openVideoPlayerAutoFullscreen() { private void openNormalBackgroundPlayer(final boolean append) { // See UI changes while remote playQueue changes if (!isPlayerAvailable()) { - playerHolder.startService(false, this); + playerHolder.startService(false, this, this); } final PlayQueue queue = setupPlayQueueForIntent(append); @@ -1129,7 +1130,7 @@ private void openNormalBackgroundPlayer(final boolean append) { private void openMainPlayer() { if (!isPlayerServiceAvailable()) { - playerHolder.startService(autoPlayEnabled, this); + playerHolder.startService(autoPlayEnabled, this, this); return; } if (currentInfo == null) { @@ -1384,9 +1385,11 @@ public void onReceive(final Context context, final Intent intent) { bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); } // Rebound to the service if it was closed via notification or mini player - if (!playerHolder.isBound()) { + if (playerHolder.isNotBoundYet()) { playerHolder.startService( - false, VideoDetailFragment.this); + false, + VideoDetailFragment.this, + VideoDetailFragment.this); } break; } diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java index 195baecbda8..e936b9f4556 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java @@ -217,11 +217,16 @@ public void onServiceDisconnected(final ComponentName name) { } @Override - public void onServiceConnected(final ComponentName name, final IBinder service) { + public void onServiceConnected(final ComponentName name, final IBinder binder) { Log.d(TAG, "Player service is connected"); - if (service instanceof PlayerService.LocalBinder) { - player = ((PlayerService.LocalBinder) service).getPlayer(); + if (binder instanceof PlayerService.LocalBinder localBinder) { + final @Nullable PlayerService s = localBinder.getService(); + if (s == null) { + player = null; + } else { + player = s.getPlayer(); + } } if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) { diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java index e7abf4320d5..924a0e25151 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java @@ -28,6 +28,8 @@ import android.os.IBinder; import android.util.Log; +import androidx.annotation.Nullable; + import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi; import org.schabi.newpipe.player.notification.NotificationPlayerUi; import org.schabi.newpipe.util.ThemeHelper; @@ -36,7 +38,9 @@ /** - * One service for all players. + * One background service for our player. Even though the player has multiple UIs + * (e.g. the audio-only UI, the main UI, the pulldown-menu UI), + * this allows us to keep playing even when switching between the different UIs. */ public final class PlayerService extends Service { private static final String TAG = PlayerService.class.getSimpleName(); @@ -46,6 +50,9 @@ public final class PlayerService extends Service { private final IBinder mBinder = new PlayerService.LocalBinder(this); + public Player getPlayer() { + return player; + } /*////////////////////////////////////////////////////////////////////////// // Service's LifeCycle @@ -167,6 +174,9 @@ public IBinder onBind(final Intent intent) { return mBinder; } + /** Allows us this {@link org.schabi.newpipe.player.PlayerService} over the Service boundary + * back to our {@link org.schabi.newpipe.player.helper.PlayerHolder}. + */ public static class LocalBinder extends Binder { private final WeakReference playerService; @@ -174,12 +184,11 @@ public static class LocalBinder extends Binder { this.playerService = new WeakReference<>(playerService); } - public PlayerService getService() { + /** Get the PlayerService object itself. + * @return this + * */ + public @Nullable PlayerService getService() { return playerService.get(); } - - public Player getPlayer() { - return playerService.get().player; - } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java index 2cca259c2f3..80c795e8c9a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java @@ -5,6 +5,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.playqueue.PlayQueue; +/** Player-specific events like queue or progress updates. */ public interface PlayerEventListener { void onQueueUpdate(PlayQueue queue); void onPlaybackUpdate(int state, int repeatMode, boolean shuffled, diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerHolderLifecycleEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerHolderLifecycleEventListener.java new file mode 100644 index 00000000000..e5eaa09c734 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerHolderLifecycleEventListener.java @@ -0,0 +1,10 @@ +package org.schabi.newpipe.player.event; + +import org.schabi.newpipe.player.PlayerService; + +/** Gets signalled if our PlayerHolder (dis)connects from the PlayerService. */ +public interface PlayerHolderLifecycleEventListener { + void onServiceConnected(PlayerService playerService, + boolean playAfterConnect); + void onServiceDisconnected(); +} diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java index 8c18fd2ad1c..5feaebc0073 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java @@ -2,6 +2,9 @@ import com.google.android.exoplayer2.PlaybackException; +/** {@link org.schabi.newpipe.player.event.PlayerEventListener} that also gets called for + * application-specific events like screen rotation or UI changes. + */ public interface PlayerServiceEventListener extends PlayerEventListener { void onViewCreated(); 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 deleted file mode 100644 index 8effe2f0e93..00000000000 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.schabi.newpipe.player.event; - -import org.schabi.newpipe.player.PlayerService; -import org.schabi.newpipe.player.Player; - -public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener { - void onServiceConnected(Player player, - PlayerService 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 index 24939c1d8ad..478436d93e0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java @@ -7,6 +7,7 @@ import android.os.IBinder; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; @@ -20,9 +21,11 @@ import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.event.PlayerServiceEventListener; -import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; +import org.schabi.newpipe.player.event.PlayerHolderLifecycleEventListener; import org.schabi.newpipe.player.playqueue.PlayQueue; +/** Singleton that manages a `PlayerService` + * and can be used to control the player instance through the service. */ public final class PlayerHolder { private PlayerHolder() { @@ -39,10 +42,12 @@ public static synchronized PlayerHolder getInstance() { private static final boolean DEBUG = MainActivity.DEBUG; private static final String TAG = PlayerHolder.class.getSimpleName(); - @Nullable private PlayerServiceExtendedEventListener listener; + @Nullable private PlayerServiceEventListener listener; + @Nullable private PlayerHolderLifecycleEventListener holderListener; private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection(); private boolean bound; + @Nullable private PlayerService playerService; @Nullable private Player player; @@ -80,8 +85,8 @@ public boolean isPlayQueueReady() { return player != null && player.getPlayQueue() != null; } - public boolean isBound() { - return bound; + public boolean isNotBoundYet() { + return !bound; } public int getQueueSize() { @@ -99,30 +104,46 @@ public int getQueuePosition() { return player.getPlayQueue().getIndex(); } - public void setListener(@Nullable final PlayerServiceExtendedEventListener newListener) { - listener = newListener; + public void unsetListeners() { + listener = null; + holderListener = null; + } - if (listener == null) { - return; - } + public void setListener(@NonNull final PlayerServiceEventListener newListener, + @NonNull final PlayerHolderLifecycleEventListener newHolderListener) { + listener = newListener; + holderListener = newHolderListener; // Force reload data from service if (player != null) { - listener.onServiceConnected(player, playerService, false); - startPlayerListener(); + holderListener.onServiceConnected(playerService, false); + player.setFragmentListener(internalListener); } } - // helper to handle context in common place as using the same - // context to bind/unbind a service is crucial + /** Helper to handle context in common place as using the same + * context to bind/unbind a service is crucial. + * + * @return the common context + * */ private Context getCommonContext() { return App.getInstance(); } + + /** Connect to (and if needed start) the {@link PlayerService} + * and bind {@link PlayerServiceConnection} to it. + * If the service is already started, only set the listener. + * @param playAfterConnect If the service is started, start playing immediately + * @param newListener set this listener + * @param newHolderListener set this listener + * */ public void startService(final boolean playAfterConnect, - final PlayerServiceExtendedEventListener newListener) { + final PlayerServiceEventListener newListener, + final PlayerHolderLifecycleEventListener newHolderListener + ) { final Context context = getCommonContext(); - setListener(newListener); + setListener(newListener, newHolderListener); if (bound) { return; } @@ -131,8 +152,18 @@ public void startService(final boolean playAfterConnect, // bound twice. Prevent it with unbinding first unbind(context); ContextCompat.startForegroundService(context, new Intent(context, PlayerService.class)); - serviceConnection.doPlayAfterConnect(playAfterConnect); - bind(context); + serviceConnection.playAfterConnect = playAfterConnect; + + if (DEBUG) { + Log.d(TAG, "bind() called"); + } + + final Intent serviceIntent = new Intent(context, PlayerService.class); + bound = context.bindService(serviceIntent, serviceConnection, + Context.BIND_AUTO_CREATE); + if (!bound) { + context.unbindService(serviceConnection); + } } public void stopService() { @@ -141,14 +172,34 @@ public void stopService() { context.stopService(new Intent(context, PlayerService.class)); } + /** Call {@link Context#unbindService(ServiceConnection)} on our service + * (does not necesarily stop the service right away). + * Remove all our listeners and deinitialize them. + * @param context shared context + * */ + private void unbind(final Context context) { + if (DEBUG) { + Log.d(TAG, "unbind() called"); + } + + if (bound) { + context.unbindService(serviceConnection); + bound = false; + if (player != null) { + player.removeFragmentListener(internalListener); + } + playerService = null; + player = null; + if (holderListener != null) { + holderListener.onServiceDisconnected(); + } + } + } + class PlayerServiceConnection implements ServiceConnection { private boolean playAfterConnect = false; - public void doPlayAfterConnect(final boolean playAfterConnection) { - this.playAfterConnect = playAfterConnection; - } - @Override public void onServiceDisconnected(final ComponentName compName) { if (DEBUG) { @@ -167,56 +218,21 @@ public void onServiceConnected(final ComponentName compName, final IBinder servi final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service; playerService = localBinder.getService(); - player = localBinder.getPlayer(); - if (listener != null) { - listener.onServiceConnected(player, playerService, playAfterConnect); - } - startPlayerListener(); - } - } - - private void bind(final Context context) { - if (DEBUG) { - Log.d(TAG, "bind() called"); - } - - final Intent serviceIntent = new Intent(context, PlayerService.class); - bound = context.bindService(serviceIntent, serviceConnection, - Context.BIND_AUTO_CREATE); - if (!bound) { - context.unbindService(serviceConnection); - } - } - - private void unbind(final Context context) { - if (DEBUG) { - Log.d(TAG, "unbind() called"); - } + player = playerService != null ? playerService.getPlayer() : null; - if (bound) { - context.unbindService(serviceConnection); - bound = false; - stopPlayerListener(); - playerService = null; - player = null; - if (listener != null) { - listener.onServiceDisconnected(); + if (holderListener != null) { + holderListener.onServiceConnected(playerService, playAfterConnect); + } + if (player != null) { + player.setFragmentListener(internalListener); } } } - private void startPlayerListener() { - if (player != null) { - player.setFragmentListener(internalListener); - } - } - - private void stopPlayerListener() { - if (player != null) { - player.removeFragmentListener(internalListener); - } - } - + /** Delegate all {@link PlayerServiceEventListener} events to our current `listener` object. + * Only difference is that if {@link PlayerServiceEventListener#onServiceStopped()} is called, + * it also calls {@link PlayerHolder#unbind(Context)}. + * */ private final PlayerServiceEventListener internalListener = new PlayerServiceEventListener() { @Override