diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index bf23d3d70..157511c9f 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -849,7 +849,7 @@ public class MainActivity extends AppCompatActivity { return; } - if (PlayerHolder.getInstance().isPlayerOpen()) { + if (PlayerHolder.Companion.getInstance().isPlayerOpen()) { // if the player is already open, no need for a broadcast receiver openMiniPlayerIfMissing(); } else { @@ -859,7 +859,7 @@ public class MainActivity extends AppCompatActivity { public void onReceive(final Context context, final Intent intent) { if (Objects.equals(intent.getAction(), VideoDetailFragment.ACTION_PLAYER_STARTED) - && PlayerHolder.getInstance().isPlayerOpen()) { + && PlayerHolder.Companion.getInstance().isPlayerOpen()) { openMiniPlayerIfMissing(); // At this point the player is added 100%, we can unregister. Other actions // are useless since the fragment will not be removed after that. @@ -874,7 +874,7 @@ public class MainActivity extends AppCompatActivity { // If the PlayerHolder is not bound yet, but the service is running, try to bind to it. // Once the connection is established, the ACTION_PLAYER_STARTED will be sent. - PlayerHolder.getInstance().tryBindIfNeeded(this); + PlayerHolder.Companion.getInstance().tryBindIfNeeded(this); } } diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 197c965ba..27ae603c7 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -701,7 +701,7 @@ public class RouterActivity extends AppCompatActivity { } // ...the player is not running or in normal Video-mode/type - final PlayerType playerType = PlayerHolder.getInstance().getType(); + final PlayerType playerType = PlayerHolder.Companion.getInstance().getType(); return playerType == null || playerType == PlayerType.MAIN; } 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 5e0373122..ce1a50ad1 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 @@ -228,7 +228,7 @@ public final class VideoDetailFragment @Nullable private PlayerService playerService; private Player player; - private final PlayerHolder playerHolder = PlayerHolder.getInstance(); + private final PlayerHolder playerHolder = PlayerHolder.Companion.getInstance(); /*////////////////////////////////////////////////////////////////////////// // Service management diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java index dcf01e190..55d49b145 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java @@ -252,7 +252,7 @@ public final class InfoItemDialog { * @return the current {@link Builder} instance */ public Builder addEnqueueEntriesIfNeeded() { - final PlayerHolder holder = PlayerHolder.getInstance(); + final PlayerHolder holder = PlayerHolder.Companion.getInstance(); if (holder.isPlayQueueReady()) { addEntry(StreamDialogDefaultEntry.ENQUEUE); 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 deleted file mode 100644 index ba8a5e0ff..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java +++ /dev/null @@ -1,385 +0,0 @@ -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 androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; - -import com.google.android.exoplayer2.PlaybackException; -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.PlayerService; -import org.schabi.newpipe.player.Player; -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.playqueue.PlayQueue; -import org.schabi.newpipe.util.NavigationHelper; - -import java.util.Optional; -import java.util.function.Consumer; - -public final class PlayerHolder { - - private PlayerHolder() { - } - - private static PlayerHolder instance; - public static synchronized PlayerHolder getInstance() { - if (PlayerHolder.instance == null) { - PlayerHolder.instance = new PlayerHolder(); - } - return PlayerHolder.instance; - } - - private static final boolean DEBUG = MainActivity.DEBUG; - private static final String TAG = PlayerHolder.class.getSimpleName(); - - @Nullable private PlayerServiceExtendedEventListener listener; - - private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection(); - private boolean bound; - @Nullable private PlayerService playerService; - - private Optional getPlayer() { - return Optional.ofNullable(playerService) - .flatMap(s -> Optional.ofNullable(s.getPlayer())); - } - - private Optional getPlayQueue() { - // player play queue might be null e.g. while player is starting - return getPlayer().flatMap(p -> Optional.ofNullable(p.getPlayQueue())); - } - - /** - * Returns the current {@link PlayerType} of the {@link PlayerService} service, - * otherwise `null` if no service is running. - * - * @return Current PlayerType - */ - @Nullable - public PlayerType getType() { - return getPlayer().map(Player::getPlayerType).orElse(null); - } - - public boolean isPlaying() { - return getPlayer().map(Player::isPlaying).orElse(false); - } - - public boolean isPlayerOpen() { - return getPlayer().isPresent(); - } - - /** - * Use this method to only allow the user to manipulate the play queue (e.g. by enqueueing via - * the stream long press menu) when there actually is a play queue to manipulate. - * @return true only if the player is open and its play queue is ready (i.e. it is not null) - */ - public boolean isPlayQueueReady() { - return getPlayQueue().isPresent(); - } - - public boolean isBound() { - return bound; - } - - public int getQueueSize() { - return getPlayQueue().map(PlayQueue::size).orElse(0); - } - - public int getQueuePosition() { - return getPlayQueue().map(PlayQueue::getIndex).orElse(0); - } - - public void setListener(@Nullable final PlayerServiceExtendedEventListener newListener) { - listener = newListener; - - if (listener == null) { - return; - } - - // Force reload data from service - if (playerService != null) { - listener.onServiceConnected(playerService); - startPlayerListener(); - // ^ will call listener.onPlayerConnected() down the line if there is an active player - } - } - - // helper to handle context in common place as using the same - // context to bind/unbind a service is crucial - 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 this holder’s service was already started, - * start playing immediately - * @param newListener set this listener - * */ - public void startService(final boolean playAfterConnect, - final PlayerServiceExtendedEventListener newListener) { - if (DEBUG) { - Log.d(TAG, "startService() called with playAfterConnect=" + playAfterConnect); - } - final Context context = getCommonContext(); - 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); - final Intent intent = new Intent(context, PlayerService.class); - intent.putExtra(PlayerService.SHOULD_START_FOREGROUND_EXTRA, true); - ContextCompat.startForegroundService(context, intent); - serviceConnection.doPlayAfterConnect(playAfterConnect); - bind(context); - } - - public void stopService() { - if (DEBUG) { - Log.d(TAG, "stopService() called"); - } - if (playerService != null) { - playerService.destroyPlayerAndStopService(); - } - final Context context = getCommonContext(); - unbind(context); - // destroyPlayerAndStopService() already runs the next line of code, but run it again just - // to make sure to stop the service even if playerService is null by any chance. - context.stopService(new Intent(context, PlayerService.class)); - } - - class PlayerServiceConnection implements ServiceConnection { - - private boolean playAfterConnect = false; - - /** - * @param playAfterConnection Sets the value of `playAfterConnect` to pass to the {@link - * PlayerServiceExtendedEventListener#onPlayerConnected(Player, boolean)} the next time it - * is called. The value of `playAfterConnect` will be reset to false after that. - */ - public void doPlayAfterConnect(final boolean playAfterConnection) { - this.playAfterConnect = playAfterConnection; - } - - @Override - public void onServiceDisconnected(final ComponentName compName) { - if (DEBUG) { - Log.d(TAG, "Player service is disconnected"); - } - - final Context context = getCommonContext(); - unbind(context); - } - - @Override - public void onServiceConnected(final ComponentName compName, final IBinder service) { - if (DEBUG) { - Log.d(TAG, "Player service is connected"); - } - final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service; - - @Nullable final PlayerService s = localBinder.getService(); - if (s == null) { - throw new IllegalArgumentException( - "PlayerService.LocalBinder.getService() must never be" - + "null after the service connects"); - } - playerService = s; - if (listener != null) { - listener.onServiceConnected(s); - getPlayer().ifPresent(p -> listener.onPlayerConnected(p, playAfterConnect)); - } - startPlayerListener(); - // ^ will call listener.onPlayerConnected() down the line if there is an active player - - // notify the main activity that binding the service has completed, so that it can - // open the bottom mini-player - NavigationHelper.sendPlayerStartedEvent(s); - } - } - - private void bind(final Context context) { - if (DEBUG) { - Log.d(TAG, "bind() called"); - } - // BIND_AUTO_CREATE starts the service if it's not already running - bound = bind(context, Context.BIND_AUTO_CREATE); - if (!bound) { - context.unbindService(serviceConnection); - } - } - - public void tryBindIfNeeded(final Context context) { - if (!bound) { - // flags=0 means the service will not be started if it does not already exist. In this - // case the return value is not useful, as a value of "true" does not really indicate - // that the service is going to be bound. - bind(context, 0); - } - } - - private boolean bind(final Context context, final int flags) { - final Intent serviceIntent = new Intent(context, PlayerService.class); - serviceIntent.setAction(PlayerService.BIND_PLAYER_HOLDER_ACTION); - return context.bindService(serviceIntent, serviceConnection, flags); - } - - private void unbind(final Context context) { - if (DEBUG) { - Log.d(TAG, "unbind() called"); - } - - if (bound) { - context.unbindService(serviceConnection); - bound = false; - stopPlayerListener(); - playerService = null; - if (listener != null) { - listener.onPlayerDisconnected(); - listener.onServiceDisconnected(); - } - } - } - - private void startPlayerListener() { - if (playerService != null) { - // setting the player listener will take care of calling relevant callbacks if the - // player in the service is (not) already active, also see playerStateListener below - playerService.setPlayerListener(playerStateListener); - } - getPlayer().ifPresent(p -> p.setFragmentListener(internalListener)); - } - - private void stopPlayerListener() { - if (playerService != null) { - playerService.setPlayerListener(null); - } - getPlayer().ifPresent(p -> p.removeFragmentListener(internalListener)); - } - - /** - * This listener will be held by the players created by {@link PlayerService}. - */ - private final PlayerServiceEventListener internalListener = - new PlayerServiceEventListener() { - @Override - public void onViewCreated() { - if (listener != null) { - listener.onViewCreated(); - } - } - - @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 PlaybackException error, - final boolean isCatchableException) { - if (listener != null) { - listener.onPlayerError(error, isCatchableException); - } - } - - @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(getCommonContext()); - } - }; - - /** - * This listener will be held by bound {@link PlayerService}s to notify of the player starting - * or stopping. This is necessary since the service outlives the player e.g. to answer Android - * Auto media browser queries. - */ - private final Consumer playerStateListener = (@Nullable final Player player) -> { - if (listener != null) { - if (player == null) { - // player.fragmentListener=null is already done by player.stopActivityBinding(), - // which is called by player.destroy(), which is in turn called by PlayerService - // before setting its player to null - listener.onPlayerDisconnected(); - } else { - listener.onPlayerConnected(player, serviceConnection.playAfterConnect); - // reset the value of playAfterConnect: if it was true before, it is now "consumed" - serviceConnection.playAfterConnect = false; - player.setFragmentListener(internalListener); - } - } - }; -} diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.kt b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.kt new file mode 100644 index 000000000..8cef16bec --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.kt @@ -0,0 +1,384 @@ +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 androidx.core.content.ContextCompat +import com.google.android.exoplayer2.PlaybackException +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.Player +import org.schabi.newpipe.player.PlayerService +import org.schabi.newpipe.player.PlayerService.LocalBinder +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.playqueue.PlayQueue +import org.schabi.newpipe.util.NavigationHelper +import java.util.Optional +import java.util.function.Consumer +import java.util.function.Function + +class PlayerHolder private constructor() { + private var listener: PlayerServiceExtendedEventListener? = null + + private val serviceConnection = PlayerServiceConnection() + var isBound: Boolean = false + private set + private var playerService: PlayerService? = null + + private val player: Optional + get() = Optional.ofNullable(playerService) + .flatMap( + Function { s: PlayerService? -> + Optional.ofNullable( + s!!.player + ) + } + ) + + private val playQueue: Optional + get() = // player play queue might be null e.g. while player is starting + this.player.flatMap( + Function { p: Player? -> + Optional.ofNullable( + p!!.getPlayQueue() + ) + } + ) + + val type: PlayerType? + /** + * Returns the current [PlayerType] of the [PlayerService] service, + * otherwise `null` if no service is running. + * + * @return Current PlayerType + */ + get() = this.player.map(Function { obj: Player? -> obj!!.getPlayerType() }) + .orElse(null) + + val isPlaying: Boolean + get() = this.player.map(Function { obj: Player? -> obj!!.isPlaying() }) + .orElse(false) + + val isPlayerOpen: Boolean + get() = this.player.isPresent() + + val isPlayQueueReady: Boolean + /** + * Use this method to only allow the user to manipulate the play queue (e.g. by enqueueing via + * the stream long press menu) when there actually is a play queue to manipulate. + * @return true only if the player is open and its play queue is ready (i.e. it is not null) + */ + get() = this.playQueue.isPresent() + + val queueSize: Int + get() = this.playQueue.map(Function { obj: PlayQueue? -> obj!!.size() }).orElse(0) + + val queuePosition: Int + get() = this.playQueue.map(Function { obj: PlayQueue? -> obj!!.getIndex() }).orElse(0) + + fun setListener(newListener: PlayerServiceExtendedEventListener?) { + listener = newListener + + if (listener == null) { + return + } + + // Force reload data from service + if (playerService != null) { + listener!!.onServiceConnected(playerService!!) + startPlayerListener() + // ^ will call listener.onPlayerConnected() down the line if there is an active player + } + } + + private val commonContext: Context + // helper to handle context in common place as using the same + get() = App.instance + + /** + * Connect to (and if needed start) the [PlayerService] + * and bind [PlayerServiceConnection] to it. + * If the service is already started, only set the listener. + * @param playAfterConnect If this holder’s service was already started, + * start playing immediately + * @param newListener set this listener + */ + fun startService( + playAfterConnect: Boolean, + newListener: PlayerServiceExtendedEventListener? + ) { + if (DEBUG) { + Log.d(TAG, "startService() called with playAfterConnect=" + playAfterConnect) + } + val context = this.commonContext + setListener(newListener) + if (this.isBound) { + 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) + val intent = Intent(context, PlayerService::class.java) + intent.putExtra(PlayerService.SHOULD_START_FOREGROUND_EXTRA, true) + ContextCompat.startForegroundService(context, intent) + serviceConnection.doPlayAfterConnect(playAfterConnect) + bind(context) + } + + fun stopService() { + if (DEBUG) { + Log.d(TAG, "stopService() called") + } + if (playerService != null) { + playerService!!.destroyPlayerAndStopService() + } + val context = this.commonContext + unbind(context) + // destroyPlayerAndStopService() already runs the next line of code, but run it again just + // to make sure to stop the service even if playerService is null by any chance. + context.stopService(Intent(context, PlayerService::class.java)) + } + + internal inner class PlayerServiceConnection : ServiceConnection { + internal var playAfterConnect = false + + /** + * @param playAfterConnection Sets the value of [playAfterConnect] to pass to the + * [PlayerServiceExtendedEventListener.onPlayerConnected] the next time it + * is called. The value of [playAfterConnect] will be reset to false after that. + */ + fun doPlayAfterConnect(playAfterConnection: Boolean) { + this.playAfterConnect = playAfterConnection + } + + override fun onServiceDisconnected(compName: ComponentName?) { + if (DEBUG) { + Log.d(TAG, "Player service is disconnected") + } + + val context: Context = this@PlayerHolder.commonContext + unbind(context) + } + + override fun onServiceConnected(compName: ComponentName?, service: IBinder?) { + if (DEBUG) { + Log.d(TAG, "Player service is connected") + } + val localBinder = service as LocalBinder + + val s = localBinder.service + requireNotNull(s) { + ( + "PlayerService.LocalBinder.getService() must never be" + + "null after the service connects" + ) + } + playerService = s + if (listener != null) { + listener!!.onServiceConnected(s) + this@PlayerHolder.player.ifPresent( + Consumer { p: Player? -> + listener!!.onPlayerConnected( + p!!, + playAfterConnect + ) + } + ) + } + startPlayerListener() + + // ^ will call listener.onPlayerConnected() down the line if there is an active player + + // notify the main activity that binding the service has completed, so that it can + // open the bottom mini-player + NavigationHelper.sendPlayerStartedEvent(s) + } + } + + private fun bind(context: Context) { + if (DEBUG) { + Log.d(TAG, "bind() called") + } + // BIND_AUTO_CREATE starts the service if it's not already running + this.isBound = bind(context, Context.BIND_AUTO_CREATE) + if (!this.isBound) { + context.unbindService(serviceConnection) + } + } + + fun tryBindIfNeeded(context: Context) { + if (!this.isBound) { + // flags=0 means the service will not be started if it does not already exist. In this + // case the return value is not useful, as a value of "true" does not really indicate + // that the service is going to be bound. + bind(context, 0) + } + } + + private fun bind(context: Context, flags: Int): Boolean { + val serviceIntent = Intent(context, PlayerService::class.java) + serviceIntent.setAction(PlayerService.BIND_PLAYER_HOLDER_ACTION) + return context.bindService(serviceIntent, serviceConnection, flags) + } + + private fun unbind(context: Context) { + if (DEBUG) { + Log.d(TAG, "unbind() called") + } + + if (this.isBound) { + context.unbindService(serviceConnection) + this.isBound = false + stopPlayerListener() + playerService = null + if (listener != null) { + listener!!.onPlayerDisconnected() + listener!!.onServiceDisconnected() + } + } + } + + private fun startPlayerListener() { + if (playerService != null) { + // setting the player listener will take care of calling relevant callbacks if the + // player in the service is (not) already active, also see playerStateListener below + playerService!!.setPlayerListener(playerStateListener) + } + this.player.ifPresent(Consumer { p: Player? -> p!!.setFragmentListener(internalListener) }) + } + + private fun stopPlayerListener() { + if (playerService != null) { + playerService!!.setPlayerListener(null) + } + this.player.ifPresent(Consumer { p: Player? -> p!!.removeFragmentListener(internalListener) }) + } + + /** + * This listener will be held by the players created by [PlayerService]. + */ + private val internalListener: PlayerServiceEventListener = object : PlayerServiceEventListener { + override fun onViewCreated() { + if (listener != null) { + listener!!.onViewCreated() + } + } + + override fun onFullscreenStateChanged(fullscreen: Boolean) { + if (listener != null) { + listener!!.onFullscreenStateChanged(fullscreen) + } + } + + override fun onScreenRotationButtonClicked() { + if (listener != null) { + listener!!.onScreenRotationButtonClicked() + } + } + + override fun onMoreOptionsLongClicked() { + if (listener != null) { + listener!!.onMoreOptionsLongClicked() + } + } + + override fun onPlayerError( + error: PlaybackException?, + isCatchableException: Boolean + ) { + if (listener != null) { + listener!!.onPlayerError(error, isCatchableException) + } + } + + override fun hideSystemUiIfNeeded() { + if (listener != null) { + listener!!.hideSystemUiIfNeeded() + } + } + + override fun onQueueUpdate(queue: PlayQueue?) { + if (listener != null) { + listener!!.onQueueUpdate(queue) + } + } + + override fun onPlaybackUpdate( + state: Int, + repeatMode: Int, + shuffled: Boolean, + parameters: PlaybackParameters? + ) { + if (listener != null) { + listener!!.onPlaybackUpdate(state, repeatMode, shuffled, parameters) + } + } + + override fun onProgressUpdate( + currentProgress: Int, + duration: Int, + bufferPercent: Int + ) { + if (listener != null) { + listener!!.onProgressUpdate(currentProgress, duration, bufferPercent) + } + } + + override fun onMetadataUpdate(info: StreamInfo?, queue: PlayQueue?) { + if (listener != null) { + listener!!.onMetadataUpdate(info, queue) + } + } + + override fun onServiceStopped() { + if (listener != null) { + listener!!.onServiceStopped() + } + unbind(this@PlayerHolder.commonContext) + } + } + + /** + * This listener will be held by bound [PlayerService]s to notify of the player starting + * or stopping. This is necessary since the service outlives the player e.g. to answer Android + * Auto media browser queries. + */ + private val playerStateListener = Consumer { player: Player? -> + if (listener != null) { + if (player == null) { + // player.fragmentListener=null is already done by player.stopActivityBinding(), + // which is called by player.destroy(), which is in turn called by PlayerService + // before setting its player to null + listener!!.onPlayerDisconnected() + } else { + listener!!.onPlayerConnected(player, serviceConnection.playAfterConnect) + // reset the value of playAfterConnect: if it was true before, it is now "consumed" + serviceConnection.playAfterConnect = false; + player.setFragmentListener(internalListener) + } + } + } + + companion object { + private var instance: PlayerHolder? = null + + @Synchronized + fun getInstance(): PlayerHolder { + if (instance == null) { + instance = PlayerHolder() + } + return instance!! + } + + private val DEBUG = MainActivity.DEBUG + private val TAG: String = PlayerHolder::class.java.getSimpleName() + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt index 935bda85f..26d385518 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt @@ -28,7 +28,7 @@ fun StreamMenu( ) { val context = LocalContext.current val streamViewModel = viewModel() - val playerHolder = PlayerHolder.getInstance() + val playerHolder = PlayerHolder.Companion.getInstance() DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) { if (playerHolder.isPlayQueueReady) { diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index aba27c259..9d8d3c3b2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -200,7 +200,7 @@ public final class NavigationHelper { } public static void enqueueOnPlayer(final Context context, final PlayQueue queue) { - PlayerType playerType = PlayerHolder.getInstance().getType(); + PlayerType playerType = PlayerHolder.Companion.getInstance().getType(); if (playerType == null) { Log.e(TAG, "Enqueueing but no player is open; defaulting to background player"); playerType = PlayerType.AUDIO; @@ -211,7 +211,7 @@ public final class NavigationHelper { /* ENQUEUE NEXT */ public static void enqueueNextOnPlayer(final Context context, final PlayQueue queue) { - PlayerType playerType = PlayerHolder.getInstance().getType(); + PlayerType playerType = PlayerHolder.Companion.getInstance().getType(); if (playerType == null) { Log.e(TAG, "Enqueueing next but no player is open; defaulting to background player"); playerType = PlayerType.AUDIO; @@ -421,13 +421,13 @@ public final class NavigationHelper { final boolean switchingPlayers) { final boolean autoPlay; - @Nullable final PlayerType playerType = PlayerHolder.getInstance().getType(); + @Nullable final PlayerType playerType = PlayerHolder.Companion.getInstance().getType(); if (playerType == null) { // no player open autoPlay = PlayerHelper.isAutoplayAllowedByUser(context); } else if (switchingPlayers) { // switching player to main player - autoPlay = PlayerHolder.getInstance().isPlaying(); // keep play/pause state + autoPlay = PlayerHolder.Companion.getInstance().isPlaying(); // keep play/pause state } else if (playerType == PlayerType.MAIN) { // opening new stream while already playing in main player autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);