mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-08-22 10:09:39 +00:00
PlayerHolder: convert to kotlin (mechanical)
This commit is contained in:
parent
86063fda6a
commit
9b22773070
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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<Player> getPlayer() {
|
||||
return Optional.ofNullable(playerService)
|
||||
.flatMap(s -> Optional.ofNullable(s.getPlayer()));
|
||||
}
|
||||
|
||||
private Optional<PlayQueue> 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<Player> 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -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<Player?>
|
||||
get() = Optional.ofNullable<PlayerService?>(playerService)
|
||||
.flatMap<Player?>(
|
||||
Function { s: PlayerService? ->
|
||||
Optional.ofNullable<Player?>(
|
||||
s!!.player
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
private val playQueue: Optional<PlayQueue?>
|
||||
get() = // player play queue might be null e.g. while player is starting
|
||||
this.player.flatMap<PlayQueue?>(
|
||||
Function { p: Player? ->
|
||||
Optional.ofNullable<PlayQueue?>(
|
||||
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<PlayerType?>(Function { obj: Player? -> obj!!.getPlayerType() })
|
||||
.orElse(null)
|
||||
|
||||
val isPlaying: Boolean
|
||||
get() = this.player.map<Boolean?>(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<Int?>(Function { obj: PlayQueue? -> obj!!.size() }).orElse(0)
|
||||
|
||||
val queuePosition: Int
|
||||
get() = this.playQueue.map<Int?>(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()
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ fun StreamMenu(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val streamViewModel = viewModel<StreamViewModel>()
|
||||
val playerHolder = PlayerHolder.getInstance()
|
||||
val playerHolder = PlayerHolder.Companion.getInstance()
|
||||
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
|
||||
if (playerHolder.isPlayQueueReady) {
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user