mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-08-22 10:09:39 +00:00
Merge pull request #12337 from Profpatsch/video-detail-fragment-kotlin-conversion
This commit is contained in:
commit
da36b8a140
@ -849,7 +849,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PlayerHolder.getInstance().isPlayerOpen()) {
|
if (PlayerHolder.INSTANCE.isPlayerOpen()) {
|
||||||
// if the player is already open, no need for a broadcast receiver
|
// if the player is already open, no need for a broadcast receiver
|
||||||
openMiniPlayerIfMissing();
|
openMiniPlayerIfMissing();
|
||||||
} else {
|
} else {
|
||||||
@ -859,7 +859,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
public void onReceive(final Context context, final Intent intent) {
|
public void onReceive(final Context context, final Intent intent) {
|
||||||
if (Objects.equals(intent.getAction(),
|
if (Objects.equals(intent.getAction(),
|
||||||
VideoDetailFragment.ACTION_PLAYER_STARTED)
|
VideoDetailFragment.ACTION_PLAYER_STARTED)
|
||||||
&& PlayerHolder.getInstance().isPlayerOpen()) {
|
&& PlayerHolder.INSTANCE.isPlayerOpen()) {
|
||||||
openMiniPlayerIfMissing();
|
openMiniPlayerIfMissing();
|
||||||
// At this point the player is added 100%, we can unregister. Other actions
|
// At this point the player is added 100%, we can unregister. Other actions
|
||||||
// are useless since the fragment will not be removed after that.
|
// 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.
|
// 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.
|
// Once the connection is established, the ACTION_PLAYER_STARTED will be sent.
|
||||||
PlayerHolder.getInstance().tryBindIfNeeded(this);
|
PlayerHolder.INSTANCE.tryBindIfNeeded(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -701,7 +701,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ...the player is not running or in normal Video-mode/type
|
// ...the player is not running or in normal Video-mode/type
|
||||||
final PlayerType playerType = PlayerHolder.getInstance().getType();
|
final PlayerType playerType = PlayerHolder.INSTANCE.getType();
|
||||||
return playerType == null || playerType == PlayerType.MAIN;
|
return playerType == null || playerType == PlayerType.MAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -252,7 +252,7 @@ public final class InfoItemDialog {
|
|||||||
* @return the current {@link Builder} instance
|
* @return the current {@link Builder} instance
|
||||||
*/
|
*/
|
||||||
public Builder addEnqueueEntriesIfNeeded() {
|
public Builder addEnqueueEntriesIfNeeded() {
|
||||||
final PlayerHolder holder = PlayerHolder.getInstance();
|
final PlayerHolder holder = PlayerHolder.INSTANCE;
|
||||||
if (holder.isPlayQueueReady()) {
|
if (holder.isPlayQueueReady()) {
|
||||||
addEntry(StreamDialogDefaultEntry.ENQUEUE);
|
addEntry(StreamDialogDefaultEntry.ENQUEUE);
|
||||||
|
|
||||||
|
@ -97,8 +97,48 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
getSupportActionBar().setTitle(R.string.title_activity_play_queue);
|
getSupportActionBar().setTitle(R.string.title_activity_play_queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceConnection = getServiceConnection();
|
serviceConnection = new ServiceConnection() {
|
||||||
bind();
|
@Override
|
||||||
|
public void onServiceDisconnected(final ComponentName name) {
|
||||||
|
Log.d(TAG, "Player service is disconnected");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(final ComponentName name, final IBinder binder) {
|
||||||
|
Log.d(TAG, "Player service is connected");
|
||||||
|
|
||||||
|
if (binder instanceof PlayerService.LocalBinder) {
|
||||||
|
@Nullable final PlayerService s =
|
||||||
|
((PlayerService.LocalBinder) binder).getService();
|
||||||
|
if (s == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"PlayerService.LocalBinder.getService() must never be"
|
||||||
|
+ "null after the service connects");
|
||||||
|
}
|
||||||
|
player = s.getPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
|
||||||
|
unbind();
|
||||||
|
} else {
|
||||||
|
onQueueUpdate(player.getPlayQueue());
|
||||||
|
buildComponents();
|
||||||
|
if (player != null) {
|
||||||
|
player.setActivityListener(PlayQueueActivity.this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Note: this code should not really exist, and PlayerHolder should be used instead, but
|
||||||
|
// it will be rewritten when NewPlayer will replace the current player.
|
||||||
|
final Intent bindIntent = new Intent(this, PlayerService.class);
|
||||||
|
bindIntent.setAction(PlayerService.BIND_PLAYER_HOLDER_ACTION);
|
||||||
|
final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
|
||||||
|
if (!success) {
|
||||||
|
unbindService(serviceConnection);
|
||||||
|
}
|
||||||
|
serviceBound = success;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -180,19 +220,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Service Connection
|
// Service Connection
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
private void bind() {
|
|
||||||
// Note: this code should not really exist, and PlayerHolder should be used instead, but
|
|
||||||
// it will be rewritten when NewPlayer will replace the current player.
|
|
||||||
final Intent bindIntent = new Intent(this, PlayerService.class);
|
|
||||||
bindIntent.setAction(PlayerService.BIND_PLAYER_HOLDER_ACTION);
|
|
||||||
final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
|
|
||||||
if (!success) {
|
|
||||||
unbindService(serviceConnection);
|
|
||||||
}
|
|
||||||
serviceBound = success;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void unbind() {
|
private void unbind() {
|
||||||
if (serviceBound) {
|
if (serviceBound) {
|
||||||
@ -212,41 +239,6 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ServiceConnection getServiceConnection() {
|
|
||||||
return new ServiceConnection() {
|
|
||||||
@Override
|
|
||||||
public void onServiceDisconnected(final ComponentName name) {
|
|
||||||
Log.d(TAG, "Player service is disconnected");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onServiceConnected(final ComponentName name, final IBinder binder) {
|
|
||||||
Log.d(TAG, "Player service is connected");
|
|
||||||
|
|
||||||
if (binder instanceof PlayerService.LocalBinder) {
|
|
||||||
@Nullable final PlayerService s =
|
|
||||||
((PlayerService.LocalBinder) binder).getService();
|
|
||||||
if (s == null) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"PlayerService.LocalBinder.getService() must never be"
|
|
||||||
+ "null after the service connects");
|
|
||||||
}
|
|
||||||
player = s.getPlayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
|
|
||||||
unbind();
|
|
||||||
} else {
|
|
||||||
onQueueUpdate(player.getPlayQueue());
|
|
||||||
buildComponents();
|
|
||||||
if (player != null) {
|
|
||||||
player.setActivityListener(PlayQueueActivity.this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Component Building
|
// Component Building
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -133,6 +133,10 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
import io.reactivex.rxjava3.disposables.SerialDisposable;
|
import io.reactivex.rxjava3.disposables.SerialDisposable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ExoPlayer wrapper & Player business logic.
|
||||||
|
* Only instantiated once, from {@link PlayerService}.
|
||||||
|
*/
|
||||||
public final class Player implements PlaybackListener, Listener {
|
public final class Player implements PlaybackListener, Listener {
|
||||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
public static final String TAG = Player.class.getSimpleName();
|
public static final String TAG = Player.class.getSimpleName();
|
||||||
@ -473,22 +477,23 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initUIsForCurrentPlayerType() {
|
private void initUIsForCurrentPlayerType() {
|
||||||
if ((UIs.getOpt(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
|
if ((UIs.get(MainPlayerUi.class) != null && playerType == PlayerType.MAIN)
|
||||||
|| (UIs.getOpt(PopupPlayerUi.class).isPresent()
|
|| (UIs.get(PopupPlayerUi.class) != null
|
||||||
&& playerType == PlayerType.POPUP)) {
|
&& playerType == PlayerType.POPUP)) {
|
||||||
// correct UI already in place
|
// correct UI already in place
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to reuse binding if possible
|
// try to reuse binding if possible
|
||||||
final PlayerBinding binding = UIs.getOpt(VideoPlayerUi.class).map(VideoPlayerUi::getBinding)
|
@Nullable final VideoPlayerUi ui = UIs.get(VideoPlayerUi.class);
|
||||||
.orElseGet(() -> {
|
final PlayerBinding binding;
|
||||||
if (playerType == PlayerType.AUDIO) {
|
if (ui != null) {
|
||||||
return null;
|
binding = ui.getBinding();
|
||||||
} else {
|
} else if (playerType == PlayerType.AUDIO) {
|
||||||
return PlayerBinding.inflate(LayoutInflater.from(context));
|
binding = null;
|
||||||
}
|
} else {
|
||||||
});
|
binding = PlayerBinding.inflate(LayoutInflater.from(context));
|
||||||
|
}
|
||||||
|
|
||||||
switch (playerType) {
|
switch (playerType) {
|
||||||
case MAIN:
|
case MAIN:
|
||||||
|
@ -37,7 +37,6 @@ import org.schabi.newpipe.player.notification.NotificationPlayerUi
|
|||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.ThemeHelper
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.function.BiConsumer
|
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,13 +46,13 @@ class PlayerService : MediaBrowserServiceCompat() {
|
|||||||
// These objects are used to cleanly separate the Service implementation (in this file) and the
|
// These objects are used to cleanly separate the Service implementation (in this file) and the
|
||||||
// media browser and playback preparer implementations. At the moment the playback preparer is
|
// media browser and playback preparer implementations. At the moment the playback preparer is
|
||||||
// only used in conjunction with the media browser.
|
// only used in conjunction with the media browser.
|
||||||
private var mediaBrowserImpl: MediaBrowserImpl? = null
|
private lateinit var mediaBrowserImpl: MediaBrowserImpl
|
||||||
private var mediaBrowserPlaybackPreparer: MediaBrowserPlaybackPreparer? = null
|
private lateinit var mediaBrowserPlaybackPreparer: MediaBrowserPlaybackPreparer
|
||||||
|
|
||||||
// these are instantiated in onCreate() as per
|
// these are instantiated in onCreate() as per
|
||||||
// https://developer.android.com/training/cars/media#browser_workflow
|
// https://developer.android.com/training/cars/media#browser_workflow
|
||||||
private var mediaSession: MediaSessionCompat? = null
|
private lateinit var mediaSession: MediaSessionCompat
|
||||||
private var sessionConnector: MediaSessionConnector? = null
|
private lateinit var sessionConnector: MediaSessionConnector
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the current active player instance. May be null, since the player service can outlive
|
* @return the current active player instance. May be null, since the player service can outlive
|
||||||
@ -68,7 +67,7 @@ class PlayerService : MediaBrowserServiceCompat() {
|
|||||||
* The parameter taken by this [Consumer] can be null to indicate the player is being
|
* The parameter taken by this [Consumer] can be null to indicate the player is being
|
||||||
* stopped.
|
* stopped.
|
||||||
*/
|
*/
|
||||||
private var onPlayerStartedOrStopped: Consumer<Player?>? = null
|
private var onPlayerStartedOrStopped: ((player: Player?) -> Unit)? = null
|
||||||
|
|
||||||
//region Service lifecycle
|
//region Service lifecycle
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@ -80,14 +79,7 @@ class PlayerService : MediaBrowserServiceCompat() {
|
|||||||
Localization.assureCorrectAppLanguage(this)
|
Localization.assureCorrectAppLanguage(this)
|
||||||
ThemeHelper.setTheme(this)
|
ThemeHelper.setTheme(this)
|
||||||
|
|
||||||
mediaBrowserImpl = MediaBrowserImpl(
|
mediaBrowserImpl = MediaBrowserImpl(this, this::notifyChildrenChanged)
|
||||||
this,
|
|
||||||
Consumer { parentId: String ->
|
|
||||||
this.notifyChildrenChanged(
|
|
||||||
parentId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// see https://developer.android.com/training/cars/media#browser_workflow
|
// see https://developer.android.com/training/cars/media#browser_workflow
|
||||||
val session = MediaSessionCompat(this, "MediaSessionPlayerServ")
|
val session = MediaSessionCompat(this, "MediaSessionPlayerServ")
|
||||||
@ -98,17 +90,10 @@ class PlayerService : MediaBrowserServiceCompat() {
|
|||||||
connector.setMetadataDeduplicationEnabled(true)
|
connector.setMetadataDeduplicationEnabled(true)
|
||||||
|
|
||||||
mediaBrowserPlaybackPreparer = MediaBrowserPlaybackPreparer(
|
mediaBrowserPlaybackPreparer = MediaBrowserPlaybackPreparer(
|
||||||
this,
|
context = this,
|
||||||
BiConsumer { message: String, code: Int ->
|
setMediaSessionError = connector::setCustomErrorMessage,
|
||||||
connector.setCustomErrorMessage(
|
clearMediaSessionError = { connector.setCustomErrorMessage(null) },
|
||||||
message,
|
onPrepare = { player?.onPrepare() }
|
||||||
code
|
|
||||||
)
|
|
||||||
},
|
|
||||||
Runnable { connector.setCustomErrorMessage(null) },
|
|
||||||
Consumer { playWhenReady: Boolean? ->
|
|
||||||
player?.onPrepare()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
connector.setPlaybackPreparer(mediaBrowserPlaybackPreparer)
|
connector.setPlaybackPreparer(mediaBrowserPlaybackPreparer)
|
||||||
|
|
||||||
@ -125,11 +110,8 @@ class PlayerService : MediaBrowserServiceCompat() {
|
|||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
(
|
"onStartCommand() called with: intent = [$intent], extras = [${
|
||||||
"onStartCommand() called with: intent = [" + intent +
|
intent.extras.toDebugString()}], flags = [$flags], startId = [$startId]"
|
||||||
"], extras = [" + intent.extras.toDebugString() +
|
|
||||||
"], flags = [" + flags + "], startId = [" + startId + "]"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,7 +122,7 @@ class PlayerService : MediaBrowserServiceCompat() {
|
|||||||
val playerWasNull = (player == null)
|
val playerWasNull = (player == null)
|
||||||
if (playerWasNull) {
|
if (playerWasNull) {
|
||||||
// make sure the player exists, in case the service was resumed
|
// make sure the player exists, in case the service was resumed
|
||||||
player = Player(this, mediaSession!!, sessionConnector!!)
|
player = Player(this, mediaSession, sessionConnector)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Be sure that the player notification is set and the service is started in foreground,
|
// Be sure that the player notification is set and the service is started in foreground,
|
||||||
@ -150,35 +132,29 @@ class PlayerService : MediaBrowserServiceCompat() {
|
|||||||
// no one already and starting the service in foreground should not create any issues.
|
// no one already and starting the service in foreground should not create any issues.
|
||||||
// If the service is already started in foreground, requesting it to be started
|
// If the service is already started in foreground, requesting it to be started
|
||||||
// shouldn't do anything.
|
// shouldn't do anything.
|
||||||
player!!.UIs().get(NotificationPlayerUi::class.java)
|
player?.UIs()?.get(NotificationPlayerUi::class)?.createNotificationAndStartForeground()
|
||||||
?.createNotificationAndStartForeground()
|
|
||||||
|
|
||||||
val startedOrStopped = onPlayerStartedOrStopped
|
if (playerWasNull) {
|
||||||
if (playerWasNull && startedOrStopped != null) {
|
|
||||||
// notify that a new player was created (but do it after creating the foreground
|
// notify that a new player was created (but do it after creating the foreground
|
||||||
// notification just to make sure we don't incur, due to slowness, in
|
// notification just to make sure we don't incur, due to slowness, in
|
||||||
// "Context.startForegroundService() did not then call Service.startForeground()")
|
// "Context.startForegroundService() did not then call Service.startForeground()")
|
||||||
startedOrStopped.accept(player)
|
onPlayerStartedOrStopped?.invoke(player)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val p = player
|
val p = player
|
||||||
if (Intent.ACTION_MEDIA_BUTTON == intent.action &&
|
if (Intent.ACTION_MEDIA_BUTTON == intent.action && p?.playQueue == null) {
|
||||||
(p == null || p.playQueue == null)
|
// No need to process media button's actions if the player is not working, otherwise
|
||||||
) {
|
// the player service would strangely start with nothing to play
|
||||||
/*
|
// Stop the service in this case, which will be removed from the foreground and its
|
||||||
No need to process media button's actions if the player is not working, otherwise
|
// notification cancelled in its destruction
|
||||||
the player service would strangely start with nothing to play
|
|
||||||
Stop the service in this case, which will be removed from the foreground and its
|
|
||||||
notification cancelled in its destruction
|
|
||||||
*/
|
|
||||||
destroyPlayerAndStopService()
|
destroyPlayerAndStopService()
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p != null) {
|
if (p != null) {
|
||||||
p.handleIntent(intent)
|
p.handleIntent(intent)
|
||||||
p.UIs().get(MediaSessionPlayerUi::class.java)
|
p.UIs().get(MediaSessionPlayerUi::class)
|
||||||
?.handleMediaButtonIntent(intent)
|
?.handleMediaButtonIntent(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,22 +194,22 @@ class PlayerService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
cleanup()
|
cleanup()
|
||||||
|
|
||||||
mediaBrowserPlaybackPreparer?.dispose()
|
mediaBrowserPlaybackPreparer.dispose()
|
||||||
mediaSession?.release()
|
mediaSession.release()
|
||||||
mediaBrowserImpl?.dispose()
|
mediaBrowserImpl.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanup() {
|
private fun cleanup() {
|
||||||
val p = player
|
val p = player
|
||||||
if (p != null) {
|
if (p != null) {
|
||||||
// notify that the player is being destroyed
|
// notify that the player is being destroyed
|
||||||
onPlayerStartedOrStopped?.accept(null)
|
onPlayerStartedOrStopped?.invoke(null)
|
||||||
p.saveAndShutdown()
|
p.saveAndShutdown()
|
||||||
player = null
|
player = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should already be handled by MediaSessionPlayerUi, but just to be sure.
|
// Should already be handled by MediaSessionPlayerUi, but just to be sure.
|
||||||
mediaSession?.setActive(false)
|
mediaSession.setActive(false)
|
||||||
|
|
||||||
// Should already be handled by NotificationUtil.cancelNotificationAndStopForeground() in
|
// Should already be handled by NotificationUtil.cancelNotificationAndStopForeground() in
|
||||||
// NotificationPlayerUi, but let's make sure that the foreground service is stopped.
|
// NotificationPlayerUi, but let's make sure that the foreground service is stopped.
|
||||||
@ -273,29 +249,27 @@ class PlayerService : MediaBrowserServiceCompat() {
|
|||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
(
|
"onBind() called with: intent = [$intent], extras = [${
|
||||||
"onBind() called with: intent = [" + intent +
|
intent.extras.toDebugString()}]"
|
||||||
"], extras = [" + intent.extras.toDebugString() + "]"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BIND_PLAYER_HOLDER_ACTION == intent.action) {
|
return if (BIND_PLAYER_HOLDER_ACTION == intent.action) {
|
||||||
// Note that this binder might be reused multiple times while the service is alive, even
|
// Note that this binder might be reused multiple times while the service is alive, even
|
||||||
// after unbind() has been called: https://stackoverflow.com/a/8794930 .
|
// after unbind() has been called: https://stackoverflow.com/a/8794930 .
|
||||||
return mBinder
|
mBinder
|
||||||
} else if (SERVICE_INTERFACE == intent.action) {
|
} else if (SERVICE_INTERFACE == intent.action) {
|
||||||
// MediaBrowserService also uses its own binder, so for actions related to the media
|
// MediaBrowserService also uses its own binder, so for actions related to the media
|
||||||
// browser service, pass the onBind to the superclass.
|
// browser service, pass the onBind to the superclass.
|
||||||
return super.onBind(intent)
|
super.onBind(intent)
|
||||||
} else {
|
} else {
|
||||||
// This is an unknown request, avoid returning any binder to not leak objects.
|
// This is an unknown request, avoid returning any binder to not leak objects.
|
||||||
return null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalBinder internal constructor(playerService: PlayerService) : Binder() {
|
class LocalBinder internal constructor(playerService: PlayerService) : Binder() {
|
||||||
private val playerService = WeakReference<PlayerService?>(playerService)
|
private val playerService = WeakReference(playerService)
|
||||||
|
|
||||||
val service: PlayerService?
|
val service: PlayerService?
|
||||||
get() = playerService.get()
|
get() = playerService.get()
|
||||||
@ -307,9 +281,9 @@ class PlayerService : MediaBrowserServiceCompat() {
|
|||||||
* by the [Consumer] can be null to indicate that the player is stopping.
|
* by the [Consumer] can be null to indicate that the player is stopping.
|
||||||
* @param listener the listener to set or unset
|
* @param listener the listener to set or unset
|
||||||
*/
|
*/
|
||||||
fun setPlayerListener(listener: Consumer<Player?>?) {
|
fun setPlayerListener(listener: ((player: Player?) -> Unit)?) {
|
||||||
this.onPlayerStartedOrStopped = listener
|
this.onPlayerStartedOrStopped = listener
|
||||||
listener?.accept(player)
|
listener?.invoke(player)
|
||||||
}
|
}
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
@ -320,14 +294,14 @@ class PlayerService : MediaBrowserServiceCompat() {
|
|||||||
rootHints: Bundle?
|
rootHints: Bundle?
|
||||||
): BrowserRoot? {
|
): BrowserRoot? {
|
||||||
// TODO check if the accessing package has permission to view data
|
// TODO check if the accessing package has permission to view data
|
||||||
return mediaBrowserImpl?.onGetRoot(clientPackageName, clientUid, rootHints)
|
return mediaBrowserImpl.onGetRoot(clientPackageName, clientUid, rootHints)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadChildren(
|
override fun onLoadChildren(
|
||||||
parentId: String,
|
parentId: String,
|
||||||
result: Result<List<MediaBrowserCompat.MediaItem>>
|
result: Result<List<MediaBrowserCompat.MediaItem>>
|
||||||
) {
|
) {
|
||||||
mediaBrowserImpl?.onLoadChildren(parentId, result)
|
mediaBrowserImpl.onLoadChildren(parentId, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSearch(
|
override fun onSearch(
|
||||||
@ -335,7 +309,7 @@ class PlayerService : MediaBrowserServiceCompat() {
|
|||||||
extras: Bundle?,
|
extras: Bundle?,
|
||||||
result: Result<List<MediaBrowserCompat.MediaItem>>
|
result: Result<List<MediaBrowserCompat.MediaItem>>
|
||||||
) {
|
) {
|
||||||
mediaBrowserImpl?.onSearch(query, result)
|
mediaBrowserImpl.onSearch(query, result)
|
||||||
} //endregion
|
} //endregion
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -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,316 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
private val DEBUG = MainActivity.DEBUG
|
||||||
|
private val TAG: String = PlayerHolder::class.java.getSimpleName()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton that manages a `PlayerService`
|
||||||
|
* and can be used to control the player instance through the service.
|
||||||
|
*/
|
||||||
|
object PlayerHolder {
|
||||||
|
private var listener: PlayerServiceExtendedEventListener? = null
|
||||||
|
|
||||||
|
var isBound: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var playerService: PlayerService? = null
|
||||||
|
|
||||||
|
private val player: Player?
|
||||||
|
get() = playerService?.player
|
||||||
|
|
||||||
|
// player play queue might be null e.g. while player is starting
|
||||||
|
private val playQueue: PlayQueue?
|
||||||
|
get() = this.player?.playQueue
|
||||||
|
|
||||||
|
val type: PlayerType?
|
||||||
|
/**
|
||||||
|
* Returns the current [PlayerType] of the [PlayerService] service,
|
||||||
|
* otherwise `null` if no service is running.
|
||||||
|
*
|
||||||
|
* @return Current PlayerType
|
||||||
|
*/
|
||||||
|
get() = this.player?.playerType
|
||||||
|
|
||||||
|
val isPlaying: Boolean
|
||||||
|
get() = this.player?.isPlaying == true
|
||||||
|
|
||||||
|
val isPlayerOpen: Boolean
|
||||||
|
get() = this.player != null
|
||||||
|
|
||||||
|
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 != null
|
||||||
|
|
||||||
|
val queueSize: Int
|
||||||
|
get() = this.playQueue?.size() ?: 0
|
||||||
|
|
||||||
|
val queuePosition: Int
|
||||||
|
get() = this.playQueue?.index ?: 0
|
||||||
|
|
||||||
|
fun setListener(newListener: PlayerServiceExtendedEventListener?) {
|
||||||
|
listener = newListener
|
||||||
|
|
||||||
|
// Force reload data from service
|
||||||
|
newListener?.let { listener ->
|
||||||
|
playerService?.let { service ->
|
||||||
|
listener.onServiceConnected(service)
|
||||||
|
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)
|
||||||
|
PlayerServiceConnection.doPlayAfterConnect(playAfterConnect)
|
||||||
|
bind(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopService() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "stopService() called")
|
||||||
|
}
|
||||||
|
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 object 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
|
||||||
|
listener?.let { l ->
|
||||||
|
l.onServiceConnected(s)
|
||||||
|
player?.let { l.onPlayerConnected(it, 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(PlayerServiceConnection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, PlayerServiceConnection, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unbind(context: Context) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "unbind() called")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isBound) {
|
||||||
|
context.unbindService(PlayerServiceConnection)
|
||||||
|
this.isBound = false
|
||||||
|
stopPlayerListener()
|
||||||
|
playerService = null
|
||||||
|
listener?.onPlayerDisconnected()
|
||||||
|
listener?.onServiceDisconnected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startPlayerListener() {
|
||||||
|
// 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?.setFragmentListener(HolderPlayerServiceEventListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopPlayerListener() {
|
||||||
|
playerService?.setPlayerListener(null)
|
||||||
|
this.player?.removeFragmentListener(HolderPlayerServiceEventListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This listener will be held by the players created by [PlayerService].
|
||||||
|
*/
|
||||||
|
private object HolderPlayerServiceEventListener : PlayerServiceEventListener {
|
||||||
|
override fun onViewCreated() {
|
||||||
|
listener?.onViewCreated()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFullscreenStateChanged(fullscreen: Boolean) {
|
||||||
|
listener?.onFullscreenStateChanged(fullscreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScreenRotationButtonClicked() {
|
||||||
|
listener?.onScreenRotationButtonClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMoreOptionsLongClicked() {
|
||||||
|
listener?.onMoreOptionsLongClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayerError(
|
||||||
|
error: PlaybackException?,
|
||||||
|
isCatchableException: Boolean
|
||||||
|
) {
|
||||||
|
listener?.onPlayerError(error, isCatchableException)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hideSystemUiIfNeeded() {
|
||||||
|
listener?.hideSystemUiIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueueUpdate(queue: PlayQueue?) {
|
||||||
|
listener?.onQueueUpdate(queue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlaybackUpdate(
|
||||||
|
state: Int,
|
||||||
|
repeatMode: Int,
|
||||||
|
shuffled: Boolean,
|
||||||
|
parameters: PlaybackParameters?
|
||||||
|
) {
|
||||||
|
listener?.onPlaybackUpdate(state, repeatMode, shuffled, parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProgressUpdate(
|
||||||
|
currentProgress: Int,
|
||||||
|
duration: Int,
|
||||||
|
bufferPercent: Int
|
||||||
|
) {
|
||||||
|
listener?.onProgressUpdate(currentProgress, duration, bufferPercent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMetadataUpdate(info: StreamInfo?, queue: PlayQueue?) {
|
||||||
|
listener?.onMetadataUpdate(info, queue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceStopped() {
|
||||||
|
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: (Player?) -> Unit = { player: Player? ->
|
||||||
|
listener?.let { l ->
|
||||||
|
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
|
||||||
|
l.onPlayerDisconnected()
|
||||||
|
} else {
|
||||||
|
l.onPlayerConnected(player, PlayerServiceConnection.playAfterConnect)
|
||||||
|
// reset the value of playAfterConnect: if it was true before, it is now "consumed"
|
||||||
|
PlayerServiceConnection.playAfterConnect = false
|
||||||
|
player.setFragmentListener(HolderPlayerServiceEventListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ import android.support.v4.media.MediaDescriptionCompat
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
import androidx.media.MediaBrowserServiceCompat
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
import androidx.media.MediaBrowserServiceCompat.Result
|
import androidx.media.MediaBrowserServiceCompat.Result
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
@ -36,7 +37,6 @@ import org.schabi.newpipe.local.playlist.RemotePlaylistManager
|
|||||||
import org.schabi.newpipe.util.ExtractorHelper
|
import org.schabi.newpipe.util.ExtractorHelper
|
||||||
import org.schabi.newpipe.util.ServiceHelper
|
import org.schabi.newpipe.util.ServiceHelper
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
import java.util.function.Consumer
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is used to cleanly separate the Service implementation (in
|
* This class is used to cleanly separate the Service implementation (in
|
||||||
@ -46,16 +46,14 @@ import java.util.function.Consumer
|
|||||||
*/
|
*/
|
||||||
class MediaBrowserImpl(
|
class MediaBrowserImpl(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
notifyChildrenChanged: Consumer<String>, // parentId
|
notifyChildrenChanged: (parentId: String) -> Unit,
|
||||||
) {
|
) {
|
||||||
private val database = NewPipeDatabase.getInstance(context)
|
private val database = NewPipeDatabase.getInstance(context)
|
||||||
private var disposables = CompositeDisposable()
|
private var disposables = CompositeDisposable()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// this will listen to changes in the bookmarks until this MediaBrowserImpl is dispose()d
|
// this will listen to changes in the bookmarks until this MediaBrowserImpl is dispose()d
|
||||||
disposables.add(
|
disposables.add(getMergedPlaylists().subscribe { notifyChildrenChanged(ID_BOOKMARKS) })
|
||||||
getMergedPlaylists().subscribe { notifyChildrenChanged.accept(ID_BOOKMARKS) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//region Cleanup
|
//region Cleanup
|
||||||
@ -183,17 +181,16 @@ class MediaBrowserImpl(
|
|||||||
|
|
||||||
private fun createPlaylistMediaItem(playlist: PlaylistLocalItem): MediaBrowserCompat.MediaItem {
|
private fun createPlaylistMediaItem(playlist: PlaylistLocalItem): MediaBrowserCompat.MediaItem {
|
||||||
val builder = MediaDescriptionCompat.Builder()
|
val builder = MediaDescriptionCompat.Builder()
|
||||||
builder
|
|
||||||
.setMediaId(createMediaIdForInfoItem(playlist is PlaylistRemoteEntity, playlist.uid))
|
.setMediaId(createMediaIdForInfoItem(playlist is PlaylistRemoteEntity, playlist.uid))
|
||||||
.setTitle(playlist.orderingName)
|
.setTitle(playlist.orderingName)
|
||||||
.setIconUri(imageUriOrNullIfDisabled(playlist.thumbnailUrl))
|
.setIconUri(imageUriOrNullIfDisabled(playlist.thumbnailUrl))
|
||||||
|
.setExtras(
|
||||||
|
bundleOf(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE
|
||||||
|
to context.resources.getString(R.string.tab_bookmarks)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
val extras = Bundle()
|
|
||||||
extras.putString(
|
|
||||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
|
||||||
context.resources.getString(R.string.tab_bookmarks),
|
|
||||||
)
|
|
||||||
builder.setExtras(extras)
|
|
||||||
return MediaBrowserCompat.MediaItem(
|
return MediaBrowserCompat.MediaItem(
|
||||||
builder.build(),
|
builder.build(),
|
||||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE,
|
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE,
|
||||||
@ -202,8 +199,9 @@ class MediaBrowserImpl(
|
|||||||
|
|
||||||
private fun createInfoItemMediaItem(item: InfoItem): MediaBrowserCompat.MediaItem? {
|
private fun createInfoItemMediaItem(item: InfoItem): MediaBrowserCompat.MediaItem? {
|
||||||
val builder = MediaDescriptionCompat.Builder()
|
val builder = MediaDescriptionCompat.Builder()
|
||||||
builder.setMediaId(createMediaIdForInfoItem(item))
|
.setMediaId(createMediaIdForInfoItem(item))
|
||||||
.setTitle(item.name)
|
.setTitle(item.name)
|
||||||
|
.setIconUri(ImageStrategy.choosePreferredImage(item.thumbnails)?.toUri())
|
||||||
|
|
||||||
when (item.infoType) {
|
when (item.infoType) {
|
||||||
InfoType.STREAM -> builder.setSubtitle((item as StreamInfoItem).uploaderName)
|
InfoType.STREAM -> builder.setSubtitle((item as StreamInfoItem).uploaderName)
|
||||||
@ -212,10 +210,6 @@ class MediaBrowserImpl(
|
|||||||
else -> return null
|
else -> return null
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageStrategy.choosePreferredImage(item.thumbnails)?.let {
|
|
||||||
builder.setIconUri(imageUriOrNullIfDisabled(it))
|
|
||||||
}
|
|
||||||
|
|
||||||
return MediaBrowserCompat.MediaItem(
|
return MediaBrowserCompat.MediaItem(
|
||||||
builder.build(),
|
builder.build(),
|
||||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||||
@ -256,7 +250,7 @@ class MediaBrowserImpl(
|
|||||||
index: Int,
|
index: Int,
|
||||||
): MediaBrowserCompat.MediaItem {
|
): MediaBrowserCompat.MediaItem {
|
||||||
val builder = MediaDescriptionCompat.Builder()
|
val builder = MediaDescriptionCompat.Builder()
|
||||||
builder.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index))
|
.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index))
|
||||||
.setTitle(item.streamEntity.title)
|
.setTitle(item.streamEntity.title)
|
||||||
.setSubtitle(item.streamEntity.uploader)
|
.setSubtitle(item.streamEntity.uploader)
|
||||||
.setIconUri(imageUriOrNullIfDisabled(item.streamEntity.thumbnailUrl))
|
.setIconUri(imageUriOrNullIfDisabled(item.streamEntity.thumbnailUrl))
|
||||||
@ -276,10 +270,7 @@ class MediaBrowserImpl(
|
|||||||
builder.setMediaId(createMediaIdForPlaylistIndex(true, playlistId, index))
|
builder.setMediaId(createMediaIdForPlaylistIndex(true, playlistId, index))
|
||||||
.setTitle(item.name)
|
.setTitle(item.name)
|
||||||
.setSubtitle(item.uploaderName)
|
.setSubtitle(item.uploaderName)
|
||||||
|
.setIconUri(ImageStrategy.choosePreferredImage(item.thumbnails)?.toUri())
|
||||||
ImageStrategy.choosePreferredImage(item.thumbnails)?.let {
|
|
||||||
builder.setIconUri(imageUriOrNullIfDisabled(it))
|
|
||||||
}
|
|
||||||
|
|
||||||
return MediaBrowserCompat.MediaItem(
|
return MediaBrowserCompat.MediaItem(
|
||||||
builder.build(),
|
builder.build(),
|
||||||
|
@ -124,8 +124,10 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||||||
MediaButtonReceiver.handleIntent(mediaSession, intent);
|
MediaButtonReceiver.handleIntent(mediaSession, intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<MediaSessionCompat.Token> getSessionToken() {
|
|
||||||
return Optional.ofNullable(mediaSession).map(MediaSessionCompat::getSessionToken);
|
@NonNull
|
||||||
|
public MediaSessionCompat.Token getSessionToken() {
|
||||||
|
return mediaSession.getSessionToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -138,7 +140,10 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||||||
public void play() {
|
public void play() {
|
||||||
player.play();
|
player.play();
|
||||||
// hide the player controls even if the play command came from the media session
|
// hide the player controls even if the play command came from the media session
|
||||||
player.UIs().getOpt(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
|
final VideoPlayerUi ui = player.UIs().get(VideoPlayerUi.class);
|
||||||
|
if (ui != null) {
|
||||||
|
ui.hideControls(0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -101,10 +101,10 @@ public final class NotificationUtil {
|
|||||||
final int[] compactSlots = initializeNotificationSlots();
|
final int[] compactSlots = initializeNotificationSlots();
|
||||||
mediaStyle.setShowActionsInCompactView(compactSlots);
|
mediaStyle.setShowActionsInCompactView(compactSlots);
|
||||||
}
|
}
|
||||||
player.UIs()
|
@Nullable final MediaSessionPlayerUi ui = player.UIs().get(MediaSessionPlayerUi.class);
|
||||||
.getOpt(MediaSessionPlayerUi.class)
|
if (ui != null) {
|
||||||
.flatMap(MediaSessionPlayerUi::getSessionToken)
|
mediaStyle.setMediaSession(ui.getSessionToken());
|
||||||
.ifPresent(mediaStyle::setMediaSession);
|
}
|
||||||
|
|
||||||
// setup notification builder
|
// setup notification builder
|
||||||
builder.setStyle(mediaStyle)
|
builder.setStyle(mediaStyle)
|
||||||
|
@ -1,25 +1,20 @@
|
|||||||
package org.schabi.newpipe.player.ui
|
package org.schabi.newpipe.player.ui
|
||||||
|
|
||||||
import org.schabi.newpipe.util.GuardedByMutex
|
import org.schabi.newpipe.util.GuardedByMutex
|
||||||
import java.util.Optional
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.safeCast
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a [PlayerUiList] starting with the provided player uis. The provided player uis
|
||||||
|
* will not be prepared like those passed to [.addAndPrepare], because when
|
||||||
|
* the [PlayerUiList] constructor is called, the player is still not running and it
|
||||||
|
* wouldn't make sense to initialize uis then. Instead the player will initialize them by doing
|
||||||
|
* proper calls to [.call].
|
||||||
|
*
|
||||||
|
* @param initialPlayerUis the player uis this list should start with; the order will be kept
|
||||||
|
*/
|
||||||
class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
|
class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
|
||||||
private val playerUis = GuardedByMutex(mutableListOf<PlayerUi>())
|
private val playerUis = GuardedByMutex(mutableListOf(*initialPlayerUis))
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a [PlayerUiList] starting with the provided player uis. The provided player uis
|
|
||||||
* will not be prepared like those passed to [.addAndPrepare], because when
|
|
||||||
* the [PlayerUiList] constructor is called, the player is still not running and it
|
|
||||||
* wouldn't make sense to initialize uis then. Instead the player will initialize them by doing
|
|
||||||
* proper calls to [.call].
|
|
||||||
*
|
|
||||||
* @param initialPlayerUis the player uis this list should start with; the order will be kept
|
|
||||||
*/
|
|
||||||
init {
|
|
||||||
playerUis.runWithLockSync {
|
|
||||||
lockData.addAll(listOf(*initialPlayerUis))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the provided player ui to the list and calls on it the initialization functions that
|
* Adds the provided player ui to the list and calls on it the initialization functions that
|
||||||
@ -83,30 +78,22 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) {
|
|||||||
* @param T the class type parameter
|
* @param T the class type parameter
|
||||||
* @return the first player UI of the required type found in the list, or null
|
* @return the first player UI of the required type found in the list, or null
|
||||||
</T> */
|
</T> */
|
||||||
fun <T : PlayerUi> get(playerUiType: Class<T>): T? =
|
fun <T : PlayerUi> get(playerUiType: KClass<T>): T? =
|
||||||
playerUis.runWithLockSync {
|
playerUis.runWithLockSync {
|
||||||
for (ui in lockData) {
|
for (ui in lockData) {
|
||||||
if (playerUiType.isInstance(ui)) {
|
if (playerUiType.isInstance(ui)) {
|
||||||
when (val r = playerUiType.cast(ui)) {
|
// try all UIs before returning null
|
||||||
// try all UIs before returning null
|
playerUiType.safeCast(ui)?.let { return@runWithLockSync it }
|
||||||
null -> continue
|
|
||||||
else -> return@runWithLockSync r
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@runWithLockSync null
|
return@runWithLockSync null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param playerUiType the class of the player UI to return;
|
* See [get] above
|
||||||
* the [Class.isInstance] method will be used, so even subclasses could be returned
|
*/
|
||||||
* @param T the class type parameter
|
fun <T : PlayerUi> get(playerUiType: Class<T>): T? =
|
||||||
* @return the first player UI of the required type found in the list, or an empty
|
get(playerUiType.kotlin)
|
||||||
* [Optional] otherwise
|
|
||||||
</T> */
|
|
||||||
@Deprecated("use get", ReplaceWith("get(playerUiType)"))
|
|
||||||
fun <T : PlayerUi> getOpt(playerUiType: Class<T>): Optional<T> =
|
|
||||||
Optional.ofNullable(get(playerUiType))
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls the provided consumer on all player UIs in the list, in order of addition.
|
* Calls the provided consumer on all player UIs in the list, in order of addition.
|
||||||
|
@ -28,10 +28,9 @@ fun StreamMenu(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val streamViewModel = viewModel<StreamViewModel>()
|
val streamViewModel = viewModel<StreamViewModel>()
|
||||||
val playerHolder = PlayerHolder.getInstance()
|
|
||||||
|
|
||||||
DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
|
DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
|
||||||
if (playerHolder.isPlayQueueReady) {
|
if (PlayerHolder.isPlayQueueReady) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(text = stringResource(R.string.enqueue_stream)) },
|
text = { Text(text = stringResource(R.string.enqueue_stream)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
@ -42,7 +41,7 @@ fun StreamMenu(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (playerHolder.queuePosition < playerHolder.queueSize - 1) {
|
if (PlayerHolder.queuePosition < PlayerHolder.queueSize - 1) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(text = stringResource(R.string.enqueue_next_stream)) },
|
text = { Text(text = stringResource(R.string.enqueue_next_stream)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
|
@ -200,7 +200,7 @@ public final class NavigationHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void enqueueOnPlayer(final Context context, final PlayQueue queue) {
|
public static void enqueueOnPlayer(final Context context, final PlayQueue queue) {
|
||||||
PlayerType playerType = PlayerHolder.getInstance().getType();
|
PlayerType playerType = PlayerHolder.INSTANCE.getType();
|
||||||
if (playerType == null) {
|
if (playerType == null) {
|
||||||
Log.e(TAG, "Enqueueing but no player is open; defaulting to background player");
|
Log.e(TAG, "Enqueueing but no player is open; defaulting to background player");
|
||||||
playerType = PlayerType.AUDIO;
|
playerType = PlayerType.AUDIO;
|
||||||
@ -211,7 +211,7 @@ public final class NavigationHelper {
|
|||||||
|
|
||||||
/* ENQUEUE NEXT */
|
/* ENQUEUE NEXT */
|
||||||
public static void enqueueNextOnPlayer(final Context context, final PlayQueue queue) {
|
public static void enqueueNextOnPlayer(final Context context, final PlayQueue queue) {
|
||||||
PlayerType playerType = PlayerHolder.getInstance().getType();
|
PlayerType playerType = PlayerHolder.INSTANCE.getType();
|
||||||
if (playerType == null) {
|
if (playerType == null) {
|
||||||
Log.e(TAG, "Enqueueing next but no player is open; defaulting to background player");
|
Log.e(TAG, "Enqueueing next but no player is open; defaulting to background player");
|
||||||
playerType = PlayerType.AUDIO;
|
playerType = PlayerType.AUDIO;
|
||||||
@ -421,13 +421,13 @@ public final class NavigationHelper {
|
|||||||
final boolean switchingPlayers) {
|
final boolean switchingPlayers) {
|
||||||
|
|
||||||
final boolean autoPlay;
|
final boolean autoPlay;
|
||||||
@Nullable final PlayerType playerType = PlayerHolder.getInstance().getType();
|
@Nullable final PlayerType playerType = PlayerHolder.INSTANCE.getType();
|
||||||
if (playerType == null) {
|
if (playerType == null) {
|
||||||
// no player open
|
// no player open
|
||||||
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
|
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
|
||||||
} else if (switchingPlayers) {
|
} else if (switchingPlayers) {
|
||||||
// switching player to main player
|
// switching player to main player
|
||||||
autoPlay = PlayerHolder.getInstance().isPlaying(); // keep play/pause state
|
autoPlay = PlayerHolder.INSTANCE.isPlaying(); // keep play/pause state
|
||||||
} else if (playerType == PlayerType.MAIN) {
|
} else if (playerType == PlayerType.MAIN) {
|
||||||
// opening new stream while already playing in main player
|
// opening new stream while already playing in main player
|
||||||
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
|
autoPlay = PlayerHelper.isAutoplayAllowedByUser(context);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user