From 0a885492b6bb1b2b962c75ea30d46a5312226d4d Mon Sep 17 00:00:00 2001 From: Profpatsch Date: Tue, 13 May 2025 15:15:33 +0200 Subject: [PATCH] PlayerService: Convert to kotlin (mechanical) --- .../{PlayerService.java => PlayerService.kt} | 352 +++++++++--------- 1 file changed, 180 insertions(+), 172 deletions(-) rename app/src/main/java/org/schabi/newpipe/player/{PlayerService.java => PlayerService.kt} (50%) diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt similarity index 50% rename from app/src/main/java/org/schabi/newpipe/player/PlayerService.java rename to app/src/main/java/org/schabi/newpipe/player/PlayerService.kt index f465bbe79..cebdf339c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt @@ -16,103 +16,101 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +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 onPlayerStartedOrStopped = null; - + private var onPlayerStartedOrStopped: Consumer? = 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( - this, - sessionConnector::setCustomErrorMessage, - () -> sessionConnector.setCustomErrorMessage(null), - (playWhenReady) -> { - if (player != null) { - player.onPrepare(); - } + mediaBrowserPlaybackPreparer = MediaBrowserPlaybackPreparer( + this, + BiConsumer { message: String?, code: Int? -> + sessionConnector!!.setCustomErrorMessage( + message, + code!! + ) + }, + Runnable { sessionConnector!!.setCustomErrorMessage(null) }, + Consumer { playWhenReady: Boolean? -> + if (player != null) { + 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::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::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; + class LocalBinder internal constructor(playerService: PlayerService?) : Binder() { + private val playerService: WeakReference - LocalBinder(final PlayerService playerService) { - this.playerService = new WeakReference<>(playerService); + init { + this.playerService = WeakReference(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 listener) { - this.onPlayerStartedOrStopped = listener; + fun setPlayerListener(listener: Consumer?) { + 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> result) { - mediaBrowserImpl.onLoadChildren(parentId, result); + override fun onLoadChildren( + parentId: String, + result: Result> + ) { + mediaBrowserImpl!!.onLoadChildren(parentId, result) } - @Override - public void onSearch(@NonNull final String query, - final Bundle extras, - @NonNull final Result> result) { - mediaBrowserImpl.onSearch(query, result); + override fun onSearch( + query: String, + extras: Bundle?, + result: Result> + ) { + 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 }