mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	Merge pull request #12337 from Profpatsch/video-detail-fragment-kotlin-conversion
This commit is contained in:
		| @@ -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 if (playerType == PlayerType.AUDIO) { | ||||||
|  |             binding = null; | ||||||
|         } else { |         } else { | ||||||
|                         return PlayerBinding.inflate(LayoutInflater.from(context)); |             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( | ||||||
|         val extras = Bundle() |                 bundleOf( | ||||||
|         extras.putString( |                     MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE | ||||||
|             MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, |                         to context.resources.getString(R.string.tab_bookmarks) | ||||||
|             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,10 +1,8 @@ | |||||||
| 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 | ||||||
| class PlayerUiList(vararg initialPlayerUis: PlayerUi) { |  | ||||||
|     private val playerUis = GuardedByMutex(mutableListOf<PlayerUi>()) |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Creates a [PlayerUiList] starting with the provided player uis. The provided player uis |  * Creates a [PlayerUiList] starting with the provided player uis. The provided player uis | ||||||
| @@ -15,11 +13,8 @@ class PlayerUiList(vararg initialPlayerUis: PlayerUi) { | |||||||
|  * |  * | ||||||
|  * @param initialPlayerUis the player uis this list should start with; the order will be kept |  * @param initialPlayerUis the player uis this list should start with; the order will be kept | ||||||
|  */ |  */ | ||||||
|     init { | class PlayerUiList(vararg initialPlayerUis: PlayerUi) { | ||||||
|         playerUis.runWithLockSync { |     private val playerUis = GuardedByMutex(mutableListOf(*initialPlayerUis)) | ||||||
|             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 | ||||||
|                         null -> continue |                     playerUiType.safeCast(ui)?.let { return@runWithLockSync it } | ||||||
|                         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); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Stypox
					Stypox