mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-06-27 15:43:07 +00:00
PlayerService: Convert to kotlin (mechanical)
This commit is contained in:
parent
731efc2124
commit
0a885492b6
@ -16,103 +16,101 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.schabi.newpipe.player
|
||||
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.support.v4.media.MediaBrowserCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ServiceCompat;
|
||||
import androidx.media.MediaBrowserServiceCompat;
|
||||
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||
|
||||
import org.schabi.newpipe.ktx.BundleKt;
|
||||
import org.schabi.newpipe.player.mediabrowser.MediaBrowserImpl;
|
||||
import org.schabi.newpipe.player.mediabrowser.MediaBrowserPlaybackPreparer;
|
||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
||||
import org.schabi.newpipe.player.notification.NotificationPlayerUi;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.support.v4.media.MediaBrowserCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.util.Log
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
import org.schabi.newpipe.ktx.toDebugString
|
||||
import org.schabi.newpipe.player.mediabrowser.MediaBrowserImpl
|
||||
import org.schabi.newpipe.player.mediabrowser.MediaBrowserPlaybackPreparer
|
||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi
|
||||
import org.schabi.newpipe.player.notification.NotificationPlayerUi
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.ThemeHelper
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.function.BiConsumer
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* One service for all players.
|
||||
*/
|
||||
public final class PlayerService extends MediaBrowserServiceCompat {
|
||||
private static final String TAG = PlayerService.class.getSimpleName();
|
||||
private static final boolean DEBUG = Player.DEBUG;
|
||||
|
||||
public static final String SHOULD_START_FOREGROUND_EXTRA = "should_start_foreground_extra";
|
||||
public static final String BIND_PLAYER_HOLDER_ACTION = "bind_player_holder_action";
|
||||
|
||||
class PlayerService : MediaBrowserServiceCompat() {
|
||||
// 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
|
||||
// only used in conjunction with the media browser.
|
||||
private MediaBrowserImpl mediaBrowserImpl;
|
||||
private MediaBrowserPlaybackPreparer mediaBrowserPlaybackPreparer;
|
||||
private var mediaBrowserImpl: MediaBrowserImpl? = null
|
||||
private var mediaBrowserPlaybackPreparer: MediaBrowserPlaybackPreparer? = null
|
||||
|
||||
// these are instantiated in onCreate() as per
|
||||
// https://developer.android.com/training/cars/media#browser_workflow
|
||||
private MediaSessionCompat mediaSession;
|
||||
private MediaSessionConnector sessionConnector;
|
||||
|
||||
@Nullable
|
||||
private Player player;
|
||||
|
||||
private final IBinder mBinder = new PlayerService.LocalBinder(this);
|
||||
private var mediaSession: MediaSessionCompat? = null
|
||||
private var sessionConnector: MediaSessionConnector? = null
|
||||
|
||||
/**
|
||||
* The parameter taken by this {@link Consumer} can be null to indicate the player is being
|
||||
* @return the current active player instance. May be null, since the player service can outlive
|
||||
* the player e.g. to respond to Android Auto media browser queries.
|
||||
*/
|
||||
var player: Player? = null
|
||||
private set
|
||||
|
||||
private val mBinder: IBinder = LocalBinder(this)
|
||||
|
||||
/**
|
||||
* The parameter taken by this [Consumer] can be null to indicate the player is being
|
||||
* stopped.
|
||||
*/
|
||||
@Nullable
|
||||
private Consumer<Player> onPlayerStartedOrStopped = null;
|
||||
|
||||
private var onPlayerStartedOrStopped: Consumer<Player?>? = null
|
||||
|
||||
//region Service lifecycle
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreate() called");
|
||||
Log.d(TAG, "onCreate() called")
|
||||
}
|
||||
assureCorrectAppLanguage(this);
|
||||
ThemeHelper.setTheme(this);
|
||||
Localization.assureCorrectAppLanguage(this)
|
||||
ThemeHelper.setTheme(this)
|
||||
|
||||
mediaBrowserImpl = new MediaBrowserImpl(this, this::notifyChildrenChanged);
|
||||
mediaBrowserImpl = MediaBrowserImpl(
|
||||
this,
|
||||
Consumer { parentId: String? ->
|
||||
this.notifyChildrenChanged(
|
||||
parentId!!
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// see https://developer.android.com/training/cars/media#browser_workflow
|
||||
mediaSession = new MediaSessionCompat(this, "MediaSessionPlayerServ");
|
||||
setSessionToken(mediaSession.getSessionToken());
|
||||
sessionConnector = new MediaSessionConnector(mediaSession);
|
||||
sessionConnector.setMetadataDeduplicationEnabled(true);
|
||||
mediaSession = MediaSessionCompat(this, "MediaSessionPlayerServ")
|
||||
setSessionToken(mediaSession!!.getSessionToken())
|
||||
sessionConnector = MediaSessionConnector(mediaSession!!)
|
||||
sessionConnector!!.setMetadataDeduplicationEnabled(true)
|
||||
|
||||
mediaBrowserPlaybackPreparer = new MediaBrowserPlaybackPreparer(
|
||||
mediaBrowserPlaybackPreparer = MediaBrowserPlaybackPreparer(
|
||||
this,
|
||||
sessionConnector::setCustomErrorMessage,
|
||||
() -> sessionConnector.setCustomErrorMessage(null),
|
||||
(playWhenReady) -> {
|
||||
BiConsumer { message: String?, code: Int? ->
|
||||
sessionConnector!!.setCustomErrorMessage(
|
||||
message,
|
||||
code!!
|
||||
)
|
||||
},
|
||||
Runnable { sessionConnector!!.setCustomErrorMessage(null) },
|
||||
Consumer { playWhenReady: Boolean? ->
|
||||
if (player != null) {
|
||||
player.onPrepare();
|
||||
player!!.onPrepare()
|
||||
}
|
||||
}
|
||||
);
|
||||
sessionConnector.setPlaybackPreparer(mediaBrowserPlaybackPreparer);
|
||||
)
|
||||
sessionConnector!!.setPlaybackPreparer(mediaBrowserPlaybackPreparer)
|
||||
|
||||
// Note: you might be tempted to create the player instance and call startForeground here,
|
||||
// but be aware that the Android system might start the service just to perform media
|
||||
@ -123,22 +121,26 @@ public final class PlayerService extends MediaBrowserServiceCompat {
|
||||
// useless notification.
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
|
||||
+ "], extras = [" + BundleKt.toDebugString(intent.getExtras())
|
||||
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
Log.d(
|
||||
TAG,
|
||||
(
|
||||
"onStartCommand() called with: intent = [" + intent +
|
||||
"], extras = [" + intent.getExtras().toDebugString() +
|
||||
"], flags = [" + flags + "], startId = [" + startId + "]"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// All internal NewPipe intents used to interact with the player, that are sent to the
|
||||
// PlayerService using startForegroundService(), will have SHOULD_START_FOREGROUND_EXTRA,
|
||||
// to ensure startForeground() is called (otherwise Android will force-crash the app).
|
||||
if (intent.getBooleanExtra(SHOULD_START_FOREGROUND_EXTRA, false)) {
|
||||
final boolean playerWasNull = (player == null);
|
||||
val playerWasNull = (player == null)
|
||||
if (playerWasNull) {
|
||||
// make sure the player exists, in case the service was resumed
|
||||
player = new Player(this, mediaSession, sessionConnector);
|
||||
player = Player(this, mediaSession!!, sessionConnector!!)
|
||||
}
|
||||
|
||||
// Be sure that the player notification is set and the service is started in foreground,
|
||||
@ -148,107 +150,112 @@ public final class PlayerService extends MediaBrowserServiceCompat {
|
||||
// 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
|
||||
// shouldn't do anything.
|
||||
player.UIs().getOpt(NotificationPlayerUi.class)
|
||||
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
|
||||
player!!.UIs().getOpt<NotificationPlayerUi>(NotificationPlayerUi::class.java)
|
||||
.ifPresent(Consumer { obj: NotificationPlayerUi? -> obj!!.createNotificationAndStartForeground() })
|
||||
|
||||
if (playerWasNull && onPlayerStartedOrStopped != null) {
|
||||
// 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
|
||||
// "Context.startForegroundService() did not then call Service.startForeground()")
|
||||
onPlayerStartedOrStopped.accept(player);
|
||||
onPlayerStartedOrStopped!!.accept(player)
|
||||
}
|
||||
}
|
||||
|
||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
||||
&& (player == null || player.getPlayQueue() == null)) {
|
||||
if (Intent.ACTION_MEDIA_BUTTON == intent.getAction() &&
|
||||
(player == null || player!!.getPlayQueue() == 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
|
||||
notification cancelled in its destruction
|
||||
*/
|
||||
destroyPlayerAndStopService();
|
||||
return START_NOT_STICKY;
|
||||
destroyPlayerAndStopService()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
if (player != null) {
|
||||
player.handleIntent(intent);
|
||||
player.UIs().getOpt(MediaSessionPlayerUi.class)
|
||||
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
||||
player!!.handleIntent(intent)
|
||||
player!!.UIs().getOpt<MediaSessionPlayerUi>(MediaSessionPlayerUi::class.java)
|
||||
.ifPresent(
|
||||
Consumer { ui: MediaSessionPlayerUi? ->
|
||||
ui!!.handleMediaButtonIntent(
|
||||
intent
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return START_NOT_STICKY;
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
public void stopForImmediateReusing() {
|
||||
fun stopForImmediateReusing() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "stopForImmediateReusing() called");
|
||||
Log.d(TAG, "stopForImmediateReusing() called")
|
||||
}
|
||||
|
||||
if (player != null && !player.exoPlayerIsNull()) {
|
||||
if (player != null && !player!!.exoPlayerIsNull()) {
|
||||
// Releases wifi & cpu, disables keepScreenOn, etc.
|
||||
// We can't just pause the player here because it will make transition
|
||||
// from one stream to a new stream not smooth
|
||||
player.smoothStopForImmediateReusing();
|
||||
player!!.smoothStopForImmediateReusing()
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskRemoved(final Intent rootIntent) {
|
||||
super.onTaskRemoved(rootIntent);
|
||||
if (player != null && !player.videoPlayerSelected()) {
|
||||
return;
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
if (player != null && !player!!.videoPlayerSelected()) {
|
||||
return
|
||||
}
|
||||
onDestroy();
|
||||
onDestroy()
|
||||
// Unload from memory completely
|
||||
Runtime.getRuntime().halt(0);
|
||||
Runtime.getRuntime().halt(0)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
override fun onDestroy() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "destroy() called");
|
||||
Log.d(TAG, "destroy() called")
|
||||
}
|
||||
super.onDestroy();
|
||||
super.onDestroy()
|
||||
|
||||
cleanup();
|
||||
cleanup()
|
||||
|
||||
mediaBrowserPlaybackPreparer.dispose();
|
||||
mediaSession.release();
|
||||
mediaBrowserImpl.dispose();
|
||||
mediaBrowserPlaybackPreparer!!.dispose()
|
||||
mediaSession!!.release()
|
||||
mediaBrowserImpl!!.dispose()
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
private fun cleanup() {
|
||||
if (player != null) {
|
||||
if (onPlayerStartedOrStopped != null) {
|
||||
// notify that the player is being destroyed
|
||||
onPlayerStartedOrStopped.accept(null);
|
||||
onPlayerStartedOrStopped!!.accept(null)
|
||||
}
|
||||
player.destroy();
|
||||
player = null;
|
||||
player!!.destroy()
|
||||
player = null
|
||||
}
|
||||
|
||||
// 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
|
||||
// NotificationPlayerUi, but let's make sure that the foreground service is stopped.
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the player and allows the player instance to be garbage collected. Sets the media
|
||||
* session to inactive. Stops the foreground service and removes the player notification
|
||||
* associated with it. Tries to stop the {@link PlayerService} completely, but this step will
|
||||
* associated with it. Tries to stop the [PlayerService] completely, but this step will
|
||||
* have no effect in case some service connection still uses the service (e.g. the Android Auto
|
||||
* system accesses the media browser even when no player is running).
|
||||
*/
|
||||
public void destroyPlayerAndStopService() {
|
||||
fun destroyPlayerAndStopService() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "destroyPlayerAndStopService() called");
|
||||
Log.d(TAG, "destroyPlayerAndStopService() called")
|
||||
}
|
||||
|
||||
cleanup();
|
||||
cleanup()
|
||||
|
||||
// This only really stops the service if there are no other service connections (see docs):
|
||||
// for example the (Android Auto) media browser binder will block stopService().
|
||||
@ -256,95 +263,96 @@ public final class PlayerService extends MediaBrowserServiceCompat {
|
||||
// If we were to call stopSelf(), then the service would be surely stopped (regardless of
|
||||
// other service connections), but this would be a waste of resources since the service
|
||||
// would be immediately restarted by those same connections to perform the queries.
|
||||
stopService(new Intent(this, PlayerService.class));
|
||||
stopService(Intent(this, PlayerService::class.java))
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(final Context base) {
|
||||
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base))
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region Bind
|
||||
@Override
|
||||
public IBinder onBind(final Intent intent) {
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBind() called with: intent = [" + intent
|
||||
+ "], extras = [" + BundleKt.toDebugString(intent.getExtras()) + "]");
|
||||
Log.d(
|
||||
TAG,
|
||||
(
|
||||
"onBind() called with: intent = [" + intent +
|
||||
"], extras = [" + intent.getExtras().toDebugString() + "]"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (BIND_PLAYER_HOLDER_ACTION.equals(intent.getAction())) {
|
||||
if (BIND_PLAYER_HOLDER_ACTION == intent.getAction()) {
|
||||
// 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 .
|
||||
return mBinder;
|
||||
|
||||
} else if (MediaBrowserServiceCompat.SERVICE_INTERFACE.equals(intent.getAction())) {
|
||||
return mBinder
|
||||
} else if (SERVICE_INTERFACE == intent.getAction()) {
|
||||
// MediaBrowserService also uses its own binder, so for actions related to the media
|
||||
// browser service, pass the onBind to the superclass.
|
||||
return super.onBind(intent);
|
||||
|
||||
return super.onBind(intent)
|
||||
} else {
|
||||
// This is an unknown request, avoid returning any binder to not leak objects.
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
public static class LocalBinder extends Binder {
|
||||
private final WeakReference<PlayerService> playerService;
|
||||
class LocalBinder internal constructor(playerService: PlayerService?) : Binder() {
|
||||
private val playerService: WeakReference<PlayerService?>
|
||||
|
||||
LocalBinder(final PlayerService playerService) {
|
||||
this.playerService = new WeakReference<>(playerService);
|
||||
init {
|
||||
this.playerService = WeakReference<PlayerService?>(playerService)
|
||||
}
|
||||
|
||||
public PlayerService getService() {
|
||||
return playerService.get();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the current active player instance. May be null, since the player service can outlive
|
||||
* the player e.g. to respond to Android Auto media browser queries.
|
||||
*/
|
||||
@Nullable
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
val service: PlayerService?
|
||||
get() = playerService.get()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener that will be called when the player is started or stopped. If a
|
||||
* {@code null} listener is passed, then the current listener will be unset. The parameter taken
|
||||
* by the {@link Consumer} can be null to indicate that the player is stopping.
|
||||
* `null` listener is passed, then the current listener will be unset. The parameter taken
|
||||
* by the [Consumer] can be null to indicate that the player is stopping.
|
||||
* @param listener the listener to set or unset
|
||||
*/
|
||||
public void setPlayerListener(@Nullable final Consumer<Player> listener) {
|
||||
this.onPlayerStartedOrStopped = listener;
|
||||
fun setPlayerListener(listener: Consumer<Player?>?) {
|
||||
this.onPlayerStartedOrStopped = listener
|
||||
if (listener != null) {
|
||||
// if there is no player, then `null` will be sent here, to ensure the state is synced
|
||||
listener.accept(player);
|
||||
listener.accept(player)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
//endregion
|
||||
//region Media browser
|
||||
@Override
|
||||
public BrowserRoot onGetRoot(@NonNull final String clientPackageName,
|
||||
final int clientUid,
|
||||
@Nullable final Bundle rootHints) {
|
||||
override fun onGetRoot(
|
||||
clientPackageName: String,
|
||||
clientUid: Int,
|
||||
rootHints: Bundle?
|
||||
): BrowserRoot {
|
||||
// TODO check if the accessing package has permission to view data
|
||||
return mediaBrowserImpl.onGetRoot(clientPackageName, clientUid, rootHints);
|
||||
return mediaBrowserImpl!!.onGetRoot(clientPackageName, clientUid, rootHints)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadChildren(@NonNull final String parentId,
|
||||
@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
|
||||
mediaBrowserImpl.onLoadChildren(parentId, result);
|
||||
override fun onLoadChildren(
|
||||
parentId: String,
|
||||
result: Result<List<MediaBrowserCompat.MediaItem>>
|
||||
) {
|
||||
mediaBrowserImpl!!.onLoadChildren(parentId, result)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearch(@NonNull final String query,
|
||||
final Bundle extras,
|
||||
@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
|
||||
mediaBrowserImpl.onSearch(query, result);
|
||||
override fun onSearch(
|
||||
query: String,
|
||||
extras: Bundle?,
|
||||
result: Result<List<MediaBrowserCompat.MediaItem>>
|
||||
) {
|
||||
mediaBrowserImpl!!.onSearch(query, result)
|
||||
} //endregion
|
||||
|
||||
companion object {
|
||||
private val TAG: String = PlayerService::class.java.getSimpleName()
|
||||
private val DEBUG = Player.DEBUG
|
||||
|
||||
const val SHOULD_START_FOREGROUND_EXTRA: String = "should_start_foreground_extra"
|
||||
const val BIND_PLAYER_HOLDER_ACTION: String = "bind_player_holder_action"
|
||||
}
|
||||
//endregion
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user