From ae82900ae0562c9bc103939e393072b7298e88c0 Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:05:34 +0100 Subject: [PATCH] 4.9.2 commit --- app/build.gradle | 4 +- .../ac/mdiq/podcini/playback/PlayableUtils.kt | 8 +- .../podcini/playback/PlaybackController.kt | 57 +- .../base/PlaybackServiceMediaPlayer.kt | 8 +- .../playback/base/RewindAfterPauseUtils.kt | 13 +- .../CustomMediaNotificationProvider.kt | 28 +- .../playback/service/ExoPlayerWrapper.kt | 34 +- ...dButton.kt => NotificationCustomButton.kt} | 17 +- .../playback/service/PlaybackService.kt | 553 ++--- .../playback/service/PlaybackService.kt1 | 1865 ----------------- .../service/PlaybackServiceInterface.kt | 6 +- .../PlaybackServiceNotificationBuilder.kt | 1 + .../service/PlaybackServiceStateManager.kt | 1 + .../service/PlaybackServiceTaskManager.kt | 45 +- .../playback/service/PlaybackVolumeUpdater.kt | 8 +- .../service/QuickSettingsTileService.kt | 10 +- .../podcini/playback/service/ShakeListener.kt | 5 +- .../mdiq/podcini/ui/activity/MainActivity.kt | 16 + .../ui/fragment/AudioPlayerFragment.kt | 66 +- changelog.md | 7 +- .../android/en-US/changelogs/3020133.txt | 5 + 21 files changed, 416 insertions(+), 2341 deletions(-) rename app/src/main/java/ac/mdiq/podcini/playback/service/{NotificationPlayerCustomCommandButton.kt => NotificationCustomButton.kt} (58%) delete mode 100644 app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt1 create mode 100644 fastlane/metadata/android/en-US/changelogs/3020133.txt diff --git a/app/build.gradle b/app/build.gradle index 51edfed2..623d5a2a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -158,8 +158,8 @@ android { // Version code schema (not used): // "1.2.3-beta4" -> 1020304 // "1.2.3" -> 1020395 - versionCode 3020132 - versionName "4.9.1" + versionCode 3020133 + versionName "4.9.2" def commit = "" try { diff --git a/app/src/main/java/ac/mdiq/podcini/playback/PlayableUtils.kt b/app/src/main/java/ac/mdiq/podcini/playback/PlayableUtils.kt index 09153fe6..659d943d 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/PlayableUtils.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/PlayableUtils.kt @@ -23,12 +23,10 @@ object PlayableUtils { if (playable is FeedMedia) { val item = playable.item - if (item != null && item.isNew) { - DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.id) - } + if (item != null && item.isNew) DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.id) + if (playable.startPosition >= 0 && playable.getPosition() > playable.startPosition) { - playable.playedDuration = (playable.playedDurationWhenStarted - + playable.getPosition() - playable.startPosition) + playable.playedDuration = (playable.playedDurationWhenStarted + playable.getPosition() - playable.startPosition) } DBWriter.setFeedMediaPlaybackInformation(playable) } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt b/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt index f11d14e8..ecf6ef6e 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt @@ -52,18 +52,13 @@ abstract class PlaybackController(private val activity: FragmentActivity) { EventBus.getDefault().register(this) eventsRegistered = true } - if (PlaybackService.isRunning) { - initServiceRunning() - } else { - updatePlayButtonShowsPlay(true) - } + if (PlaybackService.isRunning) initServiceRunning() + else updatePlayButtonShowsPlay(true) } @Subscribe(threadMode = ThreadMode.MAIN) fun onEventMainThread(event: PlaybackServiceEvent) { - if (event.action == PlaybackServiceEvent.Action.SERVICE_STARTED) { - init() - } + if (event.action == PlaybackServiceEvent.Action.SERVICE_STARTED) init() } @Synchronized @@ -72,20 +67,16 @@ abstract class PlaybackController(private val activity: FragmentActivity) { initialized = true if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - activity.registerReceiver(statusUpdate, IntentFilter( - PlaybackService.ACTION_PLAYER_STATUS_CHANGED), Context.RECEIVER_NOT_EXPORTED) - activity.registerReceiver(notificationReceiver, IntentFilter( - PlaybackServiceInterface.ACTION_PLAYER_NOTIFICATION), Context.RECEIVER_NOT_EXPORTED) + activity.registerReceiver(statusUpdate, IntentFilter(PlaybackService.ACTION_PLAYER_STATUS_CHANGED), Context.RECEIVER_NOT_EXPORTED) + activity.registerReceiver(notificationReceiver, IntentFilter(PlaybackServiceInterface.ACTION_PLAYER_NOTIFICATION), Context.RECEIVER_NOT_EXPORTED) } else { activity.registerReceiver(statusUpdate, IntentFilter(PlaybackService.ACTION_PLAYER_STATUS_CHANGED)) activity.registerReceiver(notificationReceiver, IntentFilter(PlaybackServiceInterface.ACTION_PLAYER_NOTIFICATION)) } - if (!released) { - bindToService() - } else { - throw IllegalStateException("Can't call init() after release() has been called") - } + if (!released) bindToService() + else throw IllegalStateException("Can't call init() after release() has been called") + checkMediaInfoLoaded() } @@ -157,10 +148,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) { if (!released) { queryService() Log.d(TAG, "Connection to Service established") - } else { - Log.i(TAG, "Connection to playback service has been established, " + - "but controller has already been released") - } + } else Log.i(TAG, "Connection to playback service has been established, but controller has already been released") } } @@ -224,9 +212,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) { checkMediaInfoLoaded() when (status) { PlayerStatus.PLAYING -> updatePlayButtonShowsPlay(false) - PlayerStatus.PREPARING -> if (playbackService != null) { - updatePlayButtonShowsPlay(!playbackService!!.isStartWhenPrepared) - } + PlayerStatus.PREPARING -> if (playbackService != null) updatePlayButtonShowsPlay(!playbackService!!.isStartWhenPrepared) PlayerStatus.FALLBACK, PlayerStatus.PAUSED, PlayerStatus.PREPARED, PlayerStatus.STOPPED, PlayerStatus.INITIALIZED -> updatePlayButtonShowsPlay(true) else -> {} @@ -262,8 +248,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) { mediaInfoLoaded = false handleStatus() } else { - Log.e(TAG, - "queryService() was called without an existing connection to playbackservice") + Log.e(TAG, "queryService() was called without an existing connection to playbackservice") } } @@ -362,9 +347,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) { } fun speedForward(speed: Float) { - if (playbackService != null) { - playbackService!!.speedForward(speed) - } + playbackService?.speedForward(speed) } fun fallbackSpeed(speed: Float) { @@ -392,9 +375,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) { val audioTracks: List get() { - if (playbackService?.audioTracks.isNullOrEmpty()) { - return emptyList() - } + if (playbackService?.audioTracks.isNullOrEmpty()) return emptyList() return playbackService!!.audioTracks.filterNotNull().map { it } } @@ -409,15 +390,9 @@ abstract class PlaybackController(private val activity: FragmentActivity) { val isPlayingVideoLocally: Boolean get() = when { - PlaybackService.isCasting -> { - false - } - playbackService != null -> { - PlaybackService.currentMediaType == MediaType.VIDEO - } - else -> { - getMedia()?.getMediaType() == MediaType.VIDEO - } + PlaybackService.isCasting -> false + playbackService != null -> PlaybackService.currentMediaType == MediaType.VIDEO + else -> getMedia()?.getMediaType() == MediaType.VIDEO } val videoSize: Pair? diff --git a/app/src/main/java/ac/mdiq/podcini/playback/base/PlaybackServiceMediaPlayer.kt b/app/src/main/java/ac/mdiq/podcini/playback/base/PlaybackServiceMediaPlayer.kt index 2199a1d2..5649d9de 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/base/PlaybackServiceMediaPlayer.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/base/PlaybackServiceMediaPlayer.kt @@ -333,12 +333,8 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co if (newMedia != null && newStatus != PlayerStatus.INDETERMINATE) { when { - oldPlayerStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING -> { - callback.onPlaybackPause(newMedia, position) - } - oldPlayerStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING -> { - callback.onPlaybackStart(newMedia, position) - } + oldPlayerStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING -> callback.onPlaybackPause(newMedia, position) + oldPlayerStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING -> callback.onPlaybackStart(newMedia, position) } } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/base/RewindAfterPauseUtils.kt b/app/src/main/java/ac/mdiq/podcini/playback/base/RewindAfterPauseUtils.kt index 40768780..a98b0466 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/base/RewindAfterPauseUtils.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/base/RewindAfterPauseUtils.kt @@ -37,17 +37,10 @@ object RewindAfterPauseUtils { var rewindTime: Long = 0 when { - elapsedTime > ELAPSED_TIME_FOR_LONG_REWIND -> { - rewindTime = LONG_REWIND - } - elapsedTime > ELAPSED_TIME_FOR_MEDIUM_REWIND -> { - rewindTime = MEDIUM_REWIND - } - elapsedTime > ELAPSED_TIME_FOR_SHORT_REWIND -> { - rewindTime = SHORT_REWIND - } + elapsedTime > ELAPSED_TIME_FOR_LONG_REWIND -> rewindTime = LONG_REWIND + elapsedTime > ELAPSED_TIME_FOR_MEDIUM_REWIND -> rewindTime = MEDIUM_REWIND + elapsedTime > ELAPSED_TIME_FOR_SHORT_REWIND -> rewindTime = SHORT_REWIND } - val newPosition = currentPosition - rewindTime.toInt() return max(newPosition.toDouble(), 0.0).toInt() diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/CustomMediaNotificationProvider.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/CustomMediaNotificationProvider.kt index ab15bbdd..936258ea 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/CustomMediaNotificationProvider.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/CustomMediaNotificationProvider.kt @@ -1,12 +1,11 @@ package ac.mdiq.podcini.playback.service +import android.app.Notification import android.content.Context import androidx.core.app.NotificationCompat +import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi -import androidx.media3.session.CommandButton -import androidx.media3.session.DefaultMediaNotificationProvider -import androidx.media3.session.MediaNotification -import androidx.media3.session.MediaSession +import androidx.media3.session.* import com.google.common.collect.ImmutableList @UnstableApi @@ -15,23 +14,22 @@ class CustomMediaNotificationProvider(context: Context) : DefaultMediaNotificati override fun addNotificationActions(mediaSession: MediaSession, mediaButtons: ImmutableList, builder: NotificationCompat.Builder, actionFactory: MediaNotification.ActionFactory): IntArray { /* Retrieving notification default play/pause button from mediaButtons list. */ - val defaultPlayPauseCommandButton = mediaButtons.getOrNull(0) - val notificationMediaButtons = if (defaultPlayPauseCommandButton != null) { + val defaultPlayPauseButton = mediaButtons.getOrNull(1) + val defaultRestartButton = mediaButtons.getOrNull(0) + val notificationMediaButtons = if (defaultPlayPauseButton != null) { /* Overriding received mediaButtons list to ensure required buttons order: [rewind15, play/pause, forward15]. */ ImmutableList.builder().apply { - add(NotificationPlayerCustomCommandButton.REWIND.commandButton) - add(defaultPlayPauseCommandButton) - add(NotificationPlayerCustomCommandButton.FORWARD.commandButton) + if (defaultRestartButton != null) add(defaultRestartButton) + add(NotificationCustomButton.REWIND.commandButton) + add(defaultPlayPauseButton) + add(NotificationCustomButton.FORWARD.commandButton) + add(NotificationCustomButton.SKIP.commandButton) }.build() } else { /* Fallback option to handle nullability, in case retrieving default play/pause button fails for some reason (should never happen). */ mediaButtons } - return super.addNotificationActions( - mediaSession, - notificationMediaButtons, - builder, - actionFactory - ) + return super.addNotificationActions(mediaSession, notificationMediaButtons, builder, actionFactory) } + } \ No newline at end of file diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt index d84be978..903d0fe3 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt @@ -69,15 +69,9 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { exoPlayer?.addListener(object : Player.Listener { override fun onPlaybackStateChanged(playbackState: @Player.State Int) { when { - audioCompletionListener != null && playbackState == Player.STATE_ENDED -> { - audioCompletionListener?.run() - } - playbackState == Player.STATE_BUFFERING -> { - bufferingUpdateListener?.accept(BUFFERING_STARTED) - } - else -> { - bufferingUpdateListener?.accept(BUFFERING_ENDED) - } + audioCompletionListener != null && playbackState == Player.STATE_ENDED -> audioCompletionListener?.run() + playbackState == Player.STATE_BUFFERING -> bufferingUpdateListener?.accept(BUFFERING_STARTED) + else -> bufferingUpdateListener?.accept(BUFFERING_ENDED) } } @@ -87,21 +81,15 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { } else { var cause = error.cause if (cause is HttpDataSourceException) { - if (cause.cause != null) { - cause = cause.cause - } - } - if (cause != null && "Source error" == cause.message) { - cause = cause.cause + if (cause.cause != null) cause = cause.cause } + if (cause != null && "Source error" == cause.message) cause = cause.cause audioErrorListener?.accept(if (cause != null) cause.message else error.message) } } override fun onPositionDiscontinuity(oldPosition: PositionInfo, newPosition: PositionInfo, reason: @DiscontinuityReason Int) { - if (reason == Player.DISCONTINUITY_REASON_SEEK) { - audioSeekCompleteListener?.run() - } + if (reason == Player.DISCONTINUITY_REASON_SEEK) audioSeekCompleteListener?.run() } override fun onAudioSessionIdChanged(audioSessionId: Int) { @@ -218,6 +206,8 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { } fun start() { + if (exoPlayer?.playbackState == Player.STATE_IDLE) prepare() + exoPlayer?.play() // Can't set params when paused - so always set it on start in case they changed exoPlayer!!.playbackParameters = playbackParameters @@ -271,9 +261,7 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { val availableFormats = formats for (i in 0 until trackSelections.length) { val track = trackSelections[i] as ExoTrackSelection? ?: continue - if (availableFormats.contains(track.selectedFormat)) { - return availableFormats.indexOf(track.selectedFormat) - } + if (availableFormats.contains(track.selectedFormat)) return availableFormats.indexOf(track.selectedFormat) } return -1 } @@ -309,9 +297,7 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { val oldEnhancer = this.loudnessEnhancer if (oldEnhancer != null) { newEnhancer.setEnabled(oldEnhancer.enabled) - if (oldEnhancer.enabled) { - newEnhancer.setTargetGain(oldEnhancer.targetGain.toInt()) - } + if (oldEnhancer.enabled) newEnhancer.setTargetGain(oldEnhancer.targetGain.toInt()) oldEnhancer.release() } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/NotificationPlayerCustomCommandButton.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/NotificationCustomButton.kt similarity index 58% rename from app/src/main/java/ac/mdiq/podcini/playback/service/NotificationPlayerCustomCommandButton.kt rename to app/src/main/java/ac/mdiq/podcini/playback/service/NotificationCustomButton.kt index 7f6fd0a1..5c4e8e41 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/NotificationPlayerCustomCommandButton.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/NotificationCustomButton.kt @@ -5,10 +5,19 @@ import android.os.Bundle import androidx.media3.session.CommandButton import androidx.media3.session.SessionCommand -private const val CUSTOM_COMMAND_REWIND_ACTION_ID = "REWIND_15" -private const val CUSTOM_COMMAND_FORWARD_ACTION_ID = "FAST_FWD_15" +private const val CUSTOM_COMMAND_REWIND_ACTION_ID = "1_REWIND" +private const val CUSTOM_COMMAND_FORWARD_ACTION_ID = "2_FAST_FWD" +private const val CUSTOM_COMMAND_SKIP_ACTION_ID = "3_SKIP" -enum class NotificationPlayerCustomCommandButton(val customAction: String, val commandButton: CommandButton) { +enum class NotificationCustomButton(val customAction: String, val commandButton: CommandButton) { + SKIP( + customAction = CUSTOM_COMMAND_SKIP_ACTION_ID, + commandButton = CommandButton.Builder() + .setDisplayName("Skip") + .setSessionCommand(SessionCommand(CUSTOM_COMMAND_SKIP_ACTION_ID, Bundle())) + .setIconResId(R.drawable.ic_notification_skip) + .build(), + ), REWIND( customAction = CUSTOM_COMMAND_REWIND_ACTION_ID, commandButton = CommandButton.Builder() @@ -24,5 +33,5 @@ enum class NotificationPlayerCustomCommandButton(val customAction: String, val c .setSessionCommand(SessionCommand(CUSTOM_COMMAND_FORWARD_ACTION_ID, Bundle())) .setIconResId(R.drawable.ic_notification_fast_forward) .build(), - ); + ), } \ No newline at end of file diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt index d53a15ce..c4fe091c 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -69,17 +69,15 @@ import ac.mdiq.podcini.util.event.settings.SpeedPresetChangedEvent import ac.mdiq.podcini.util.event.settings.VolumeAdaptionChangedEvent import android.Manifest import android.annotation.SuppressLint -import android.app.NotificationManager import android.app.PendingIntent +import android.app.PendingIntent.FLAG_IMMUTABLE +import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.bluetooth.BluetoothA2dp import android.content.* import android.content.pm.PackageManager import android.media.AudioManager -import android.os.Binder -import android.os.Build +import android.os.* import android.os.Build.VERSION_CODES -import android.os.IBinder -import android.os.Vibrator import android.service.quicksettings.TileService import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.session.MediaSessionCompat @@ -93,10 +91,13 @@ import android.webkit.URLUtil import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat +import androidx.media3.common.Player +import androidx.media3.common.Player.* import androidx.media3.common.util.UnstableApi -import androidx.media3.session.MediaSession -import androidx.media3.session.MediaSessionService +import androidx.media3.session.* +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture import io.reactivex.Observable import io.reactivex.Single import io.reactivex.SingleEmitter @@ -119,9 +120,12 @@ class PlaybackService : MediaSessionService() { private var mediaPlayer: PlaybackServiceMediaPlayer? = null private var positionEventTimer: Disposable? = null + private lateinit var customMediaNotificationProvider: CustomMediaNotificationProvider + private val notificationCustomButtons = NotificationCustomButton.entries.map { command -> command.commandButton } + private lateinit var taskManager: PlaybackServiceTaskManager - private lateinit var stateManager: PlaybackServiceStateManager - private lateinit var notificationBuilder: PlaybackServiceNotificationBuilder +// private lateinit var stateManager: PlaybackServiceStateManager +// private lateinit var notificationBuilder: PlaybackServiceNotificationBuilder private lateinit var castStateListener: CastStateListener private var autoSkippedFeedMediaId: String? = null @@ -156,8 +160,10 @@ class PlaybackService : MediaSessionService() { Log.d(TAG, "Service created.") isRunning = true - stateManager = PlaybackServiceStateManager(this) - notificationBuilder = PlaybackServiceNotificationBuilder(this) +// this.startForeground() + +// stateManager = PlaybackServiceStateManager(this) +// notificationBuilder = PlaybackServiceNotificationBuilder(this) if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS"), RECEIVER_NOT_EXPORTED) @@ -185,9 +191,15 @@ class PlaybackService : MediaSessionService() { fun recreateMediaSessionIfNeeded() { if (mediaSession != null) return + Log.d(TAG, "recreateMediaSessionIfNeeded") + customMediaNotificationProvider = CustomMediaNotificationProvider(applicationContext) + setMediaNotificationProvider(customMediaNotificationProvider) + if (ExoPlayerWrapper.exoPlayer == null) ExoPlayerWrapper.createStaticPlayer(applicationContext) mediaSession = MediaSession.Builder(applicationContext, ExoPlayerWrapper.exoPlayer!!) - .setCallback(sessionCallback) + .setCallback(MyCallback()) +// .setCustomLayout(customMediaNotificationProvider.notificationMediaButtons) + .setCustomLayout(notificationCustomButtons) .build() recreateMediaPlayer() @@ -203,39 +215,47 @@ class PlaybackService : MediaSessionService() { mediaPlayer!!.shutdown() } mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback) - if (mediaPlayer == null) { - mediaPlayer = LocalPSMP(applicationContext, mediaPlayerCallback) // Cast not supported or not connected - } - if (media != null) { - mediaPlayer!!.playMediaObject(media, !media.localFileAvailable(), wasPlaying, true) - } + if (mediaPlayer == null) mediaPlayer = LocalPSMP(applicationContext, mediaPlayerCallback) // Cast not supported or not connected + if (media != null) mediaPlayer!!.playMediaObject(media, !media.localFileAvailable(), wasPlaying, true) isCasting = mediaPlayer!!.isCasting() } + override fun onTaskRemoved(rootIntent: Intent?) { + Log.d(TAG, "onTaskRemoved") + val player = mediaSession?.player + if (player != null) { + if (!player.playWhenReady || player.mediaItemCount == 0 || player.playbackState == Player.STATE_ENDED) { + // Stop the service if not playing, continue playing in the background + // otherwise. + stopSelf() + } + } + } + override fun onDestroy() { super.onDestroy() Log.d(TAG, "Service is about to be destroyed") - if (notificationBuilder.playerStatus == PlayerStatus.PLAYING || notificationBuilder.playerStatus == PlayerStatus.FALLBACK) { - notificationBuilder.playerStatus = PlayerStatus.STOPPED - val notificationManager = NotificationManagerCompat.from(this) - if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this, - Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - // TODO: Consider calling - // ActivityCompat#requestPermissions -// requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) - // here to request the missing permissions, and then overriding - // public void onRequestPermissionsResult(int requestCode, String[] permissions, - // int[] grantResults) - // to handle the case where the user grants the permission. See the documentation - // for ActivityCompat#requestPermissions for more details. - Log.e(TAG, "onDestroy: require POST_NOTIFICATIONS permission") - Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show() - return - } - notificationManager.notify(R.id.notification_playing, notificationBuilder.build()) - } - stateManager.stopForeground(!isPersistNotify) +// if (notificationBuilder.playerStatus == PlayerStatus.PLAYING || notificationBuilder.playerStatus == PlayerStatus.FALLBACK) { +// notificationBuilder.playerStatus = PlayerStatus.STOPPED +// val notificationManager = NotificationManagerCompat.from(this) +// if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this, +// Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { +// // TODO: Consider calling +// // ActivityCompat#requestPermissions +//// requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) +// // here to request the missing permissions, and then overriding +// // public void onRequestPermissionsResult(int requestCode, String[] permissions, +// // int[] grantResults) +// // to handle the case where the user grants the permission. See the documentation +// // for ActivityCompat#requestPermissions for more details. +// Log.e(TAG, "onDestroy: require POST_NOTIFICATIONS permission") +// Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show() +// return +// } +// notificationManager.notify(R.id.notification_playing, notificationBuilder.build()) +// } +// stateManager.stopForeground(!isPersistNotify) isRunning = false currentMediaType = MediaType.UNKNOWN castStateListener.destroy() @@ -259,6 +279,81 @@ class PlaybackService : MediaSessionService() { EventBus.getDefault().unregister(this) } + private inner class MyCallback : MediaSession.Callback { + override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult { + Log.d(TAG, "in onConnect") + val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() +// .add(NotificationCustomButton.REWIND) +// .add(NotificationCustomButton.FORWARD) + if (session.isMediaNotificationController(controller)) { + val playerCommands = MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() +// .remove(COMMAND_SEEK_TO_PREVIOUS) +// .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) +// .remove(COMMAND_SEEK_TO_NEXT) +// .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) +// .removeAll() + +// +// // Custom layout and available commands to configure the legacy/framework session. +// return MediaSession.ConnectionResult.AcceptedResultBuilder(session) +//// .setCustomLayout( +//// ImmutableList.of( +//// createSeekBackwardButton(NotificationCustomButton.REWIND), +//// createSeekForwardButton(customCommandSeekForward)) +//// ) +// .setAvailablePlayerCommands(playerCommands.build()) +// .setAvailableSessionCommands(sessionCommands.build()) +// .build() + + val connectionResult = super.onConnect(session, controller) + val defaultPlayerCommands = connectionResult.availablePlayerCommands + Log.d(TAG, defaultPlayerCommands.toString()) +// for (command in defaultPlayerCommands.toString()) { +// Log.d(TAG, command.toString()) +// } + +// val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon() + + /* Registering custom player command buttons for player notification. */ + notificationCustomButtons.forEach { commandButton -> + Log.d(TAG, "onConnect commandButton ${commandButton.displayName}") + commandButton.sessionCommand?.let(sessionCommands::add) + } + + return MediaSession.ConnectionResult.accept( + sessionCommands.build(), + playerCommands.build() + ) + } else if (session.isAutoCompanionController(controller)) { + // Available session commands to accept incoming custom commands from Auto. + return MediaSession.ConnectionResult.AcceptedResultBuilder(session) + .setAvailableSessionCommands(sessionCommands.build()) + .build() + } + // Default commands with default custom layout for all other controllers. + return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build() + } + + override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { + super.onPostConnect(session, controller) + if (notificationCustomButtons.isNotEmpty()) { + /* Setting custom player command buttons to mediaLibrarySession for player notification. */ + mediaSession?.setCustomLayout(notificationCustomButtons) +// mediaSession?.setCustomLayout(customMediaNotificationProvider.notificationMediaButtons) + } + } + + override fun onCustomCommand(session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle): ListenableFuture { + /* Handling custom command buttons from player notification. */ + when (customCommand.customAction) { + NotificationCustomButton.REWIND.customAction -> mediaPlayer?.seekDelta(-rewindSecs * 1000) + NotificationCustomButton.FORWARD.customAction -> mediaPlayer?.seekDelta(fastForwardSecs * 1000) + NotificationCustomButton.SKIP.customAction -> mediaPlayer?.skip() + } + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + } + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { return mediaSession } @@ -414,9 +509,9 @@ class PlaybackService : MediaSessionService() { super.onStartCommand(intent, flags, startId) Log.d(TAG, "OnStartCommand called") - stateManager.startForeground(R.id.notification_playing, notificationBuilder.build()) - val notificationManager = NotificationManagerCompat.from(this) - notificationManager.cancel(R.id.notification_streaming_confirmation) +// stateManager.startForeground(R.id.notification_playing, notificationBuilder.build()) +// val notificationManager = NotificationManagerCompat.from(this) +// notificationManager.cancel(R.id.notification_streaming_confirmation) val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1 val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION) @@ -424,13 +519,13 @@ class PlaybackService : MediaSessionService() { val playable = intent?.getParcelableExtra(PlaybackServiceInterface.EXTRA_PLAYABLE) if (keycode == -1 && playable == null && customAction == null) { Log.e(TAG, "PlaybackService was started with no arguments") - stateManager.stopService() +// stateManager.stopService() return START_NOT_STICKY } if ((flags and START_FLAG_REDELIVERY) != 0) { Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now.") - stateManager.stopForeground(true) +// stateManager.stopForeground(true) } else { when { keycode != -1 -> { @@ -443,25 +538,21 @@ class PlaybackService : MediaSessionService() { notificationButton = true } val handled = handleKeycode(keycode, notificationButton) - if (!handled && !stateManager.hasReceivedValidStartCommand()) { - stateManager.stopService() - return START_NOT_STICKY - } +// if (!handled && !stateManager.hasReceivedValidStartCommand()) { +// stateManager.stopService() +// return START_NOT_STICKY +// } } playable != null -> { - stateManager.validStartCommandWasReceived() +// stateManager.validStartCommandWasReceived() val allowStreamThisTime = intent.getBooleanExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, false) val allowStreamAlways = intent.getBooleanExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, false) sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0) - if (allowStreamAlways) { - isAllowMobileStreaming = true - } + if (allowStreamAlways) isAllowMobileStreaming = true Observable.fromCallable { - if (playable is FeedMedia) { - return@fromCallable DBReader.getFeedMedia(playable.id) - } else { - return@fromCallable playable - } + if (playable is FeedMedia) return@fromCallable DBReader.getFeedMedia(playable.id) + else return@fromCallable playable + } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -470,7 +561,7 @@ class PlaybackService : MediaSessionService() { { error: Throwable -> Log.d(TAG, "Playable was not found. Stopping service.") error.printStackTrace() - stateManager.stopService() +// stateManager.stopService() }) return START_NOT_STICKY } @@ -544,7 +635,7 @@ class PlaybackService : MediaSessionService() { .addAction(R.drawable.ic_notification_stream, getString(R.string.confirm_mobile_streaming_button_once), pendingIntentAllowThisTime) .addAction(R.drawable.ic_notification_stream, getString(R.string.confirm_mobile_streaming_button_always), pendingIntentAlwaysAllow) .setAutoCancel(true) - val notificationManager = NotificationManagerCompat.from(this) +// val notificationManager = NotificationManagerCompat.from(this) if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { // TODO: Consider calling @@ -559,7 +650,7 @@ class PlaybackService : MediaSessionService() { Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show() return } - notificationManager.notify(R.id.notification_streaming_confirmation, builder.build()) +// notificationManager.notify(R.id.notification_streaming_confirmation, builder.build()) } /** @@ -573,44 +664,28 @@ class PlaybackService : MediaSessionService() { when (keycode) { KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { when { - status == PlayerStatus.PLAYING -> { - mediaPlayer?.pause(!isPersistNotify, false) - } - status == PlayerStatus.FALLBACK || status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED -> { - mediaPlayer?.resume() - } - status == PlayerStatus.PREPARING -> { - mediaPlayer?.setStartWhenPrepared(!mediaPlayer!!.isStartWhenPrepared()) - } + status == PlayerStatus.PLAYING -> mediaPlayer?.pause(!isPersistNotify, false) + status == PlayerStatus.FALLBACK || status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED -> mediaPlayer?.resume() + status == PlayerStatus.PREPARING -> mediaPlayer?.setStartWhenPrepared(!mediaPlayer!!.isStartWhenPrepared()) status == PlayerStatus.INITIALIZED -> { mediaPlayer?.setStartWhenPrepared(true) mediaPlayer?.prepare() } - mediaPlayer?.getPlayable() == null -> { - startPlayingFromPreferences() - } - else -> { - return false - } + mediaPlayer?.getPlayable() == null -> startPlayingFromPreferences() + else -> return false } taskManager.restartSleepTimer() return true } KeyEvent.KEYCODE_MEDIA_PLAY -> { when { - status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED -> { - mediaPlayer?.resume() - } + status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED -> mediaPlayer?.resume() status == PlayerStatus.INITIALIZED -> { mediaPlayer?.setStartWhenPrepared(true) mediaPlayer?.prepare() } - mediaPlayer?.getPlayable() == null -> { - startPlayingFromPreferences() - } - else -> { - return false - } + mediaPlayer?.getPlayable() == null -> startPlayingFromPreferences() + else -> return false } taskManager.restartSleepTimer() return true @@ -624,10 +699,8 @@ class PlaybackService : MediaSessionService() { } KeyEvent.KEYCODE_MEDIA_NEXT -> { when { - !notificationButton -> { - // Handle remapped button as notification button which is not remapped again. - return handleKeycode(hardwareForwardButton, true) - } + // Handle remapped button as notification button which is not remapped again. + !notificationButton -> return handleKeycode(hardwareForwardButton, true) this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED -> { mediaPlayer?.skip() return true @@ -644,10 +717,8 @@ class PlaybackService : MediaSessionService() { } KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { when { - !notificationButton -> { - // Handle remapped button as notification button which is not remapped again. - return handleKeycode(hardwarePreviousButton, true) - } + // Handle remapped button as notification button which is not remapped again. + !notificationButton -> return handleKeycode(hardwarePreviousButton, true) this.status == PlayerStatus.FALLBACK || this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED -> { mediaPlayer?.seekTo(0) return true @@ -666,7 +737,7 @@ class PlaybackService : MediaSessionService() { if (this.status == PlayerStatus.FALLBACK || status == PlayerStatus.PLAYING) { mediaPlayer?.pause(true, true) } - stateManager.stopForeground(true) // gets rid of persistent notification +// stateManager.stopForeground(true) // gets rid of persistent notification return true } else -> { @@ -692,7 +763,7 @@ class PlaybackService : MediaSessionService() { { error: Throwable -> Log.d(TAG, "Playable was not loaded from preferences. Stopping service.") error.printStackTrace() - stateManager.stopService() +// stateManager.stopService() }) } @@ -704,7 +775,7 @@ class PlaybackService : MediaSessionService() { if (stream && !localFeed && !isStreamingAllowed && !allowStreamThisTime) { displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, playable).intent) writeNoMediaPlaying() - stateManager.stopService() +// stateManager.stopService() return } @@ -713,8 +784,8 @@ class PlaybackService : MediaSessionService() { } mediaPlayer?.playMediaObject(playable, stream, true, true) - stateManager.validStartCommandWasReceived() - stateManager.startForeground(R.id.notification_playing, notificationBuilder.build()) +// stateManager.validStartCommandWasReceived() +// stateManager.startForeground(R.id.notification_playing, notificationBuilder.build()) recreateMediaSessionIfNeeded() updateNotificationAndMediaSession(playable) addPlayableToQueue(playable) @@ -733,7 +804,7 @@ class PlaybackService : MediaSessionService() { mediaPlayer?.pause(true, false) mediaPlayer?.resetVideoSurface() updateNotificationAndMediaSession(playable) - stateManager.stopForeground(!isPersistNotify) +// stateManager.stopForeground(!isPersistNotify) } private val taskManagerCallback: PSTMCallback = object : PSTMCallback { @@ -760,21 +831,17 @@ class PlaybackService : MediaSessionService() { if (newInfo != null) { when (newInfo.playerStatus) { PlayerStatus.INITIALIZED -> { - if (mediaPlayer != null) { - writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus, currentitem) - } + if (mediaPlayer != null) writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus, currentitem) updateNotificationAndMediaSession(newInfo.playable) } PlayerStatus.PREPARED -> { - if (mediaPlayer != null) { - writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus, currentitem) - } + if (mediaPlayer != null) writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus, currentitem) taskManager.startChapterLoader(newInfo.playable!!) } PlayerStatus.PAUSED -> { updateNotificationAndMediaSession(newInfo.playable) if (!isCasting) { - stateManager.stopForeground(!isPersistNotify) +// stateManager.stopForeground(!isPersistNotify) } cancelPositionObserver() if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus) @@ -786,8 +853,8 @@ class PlaybackService : MediaSessionService() { recreateMediaSessionIfNeeded() updateNotificationAndMediaSession(newInfo.playable) setupPositionObserver() - stateManager.validStartCommandWasReceived() - stateManager.startForeground(R.id.notification_playing, notificationBuilder.build()) +// stateManager.validStartCommandWasReceived() +// stateManager.startForeground(R.id.notification_playing, notificationBuilder.build()) // set sleep timer if auto-enabled var autoEnableByTime = true val fromSetting = autoEnableFrom() @@ -807,7 +874,7 @@ class PlaybackService : MediaSessionService() { } PlayerStatus.ERROR -> { writeNoMediaPlaying() - stateManager.stopService() +// stateManager.stopService() } else -> {} } @@ -822,15 +889,14 @@ class PlaybackService : MediaSessionService() { taskManager.requestWidgetUpdate() } + override fun shouldStop() { - stateManager.stopForeground(!isPersistNotify) +// stateManager.stopForeground(!isPersistNotify) } override fun onMediaChanged(reloadUI: Boolean) { Log.d(TAG, "reloadUI callback reached") - if (reloadUI) { - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0) - } + if (reloadUI) sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0) updateNotificationAndMediaSession(this@PlaybackService.playable) } @@ -840,11 +906,8 @@ class PlaybackService : MediaSessionService() { override fun onPlaybackStart(playable: Playable, position: Int) { taskManager.startWidgetUpdater() - if (position != Playable.INVALID_TIME) { - playable.setPosition(position) - } else { - skipIntro(playable) - } + if (position != Playable.INVALID_TIME) playable.setPosition(position) + else skipIntro(playable) playable.onPlaybackStart() taskManager.startPositionSaver() } @@ -855,9 +918,8 @@ class PlaybackService : MediaSessionService() { saveCurrentPosition(position == Playable.INVALID_TIME || playable == null, playable, position) taskManager.cancelWidgetUpdater() if (playable != null) { - if (playable is FeedMedia) { + if (playable is FeedMedia) SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(applicationContext, playable, false) - } playable.onPlaybackPause(applicationContext) } } @@ -876,19 +938,16 @@ class PlaybackService : MediaSessionService() { } override fun ensureMediaInfoLoaded(media: Playable) { - if (media is FeedMedia && media.item == null) { - media.setItem(DBReader.getFeedItem(media.itemId)) - } + if (media is FeedMedia && media.item == null) media.setItem(DBReader.getFeedItem(media.itemId)) } } @Subscribe(threadMode = ThreadMode.MAIN) @Suppress("unused") fun playerError(event: PlayerErrorEvent?) { - if (mediaPlayer?.playerStatus == PlayerStatus.PLAYING || mediaPlayer?.playerStatus == PlayerStatus.FALLBACK) { + if (mediaPlayer?.playerStatus == PlayerStatus.PLAYING || mediaPlayer?.playerStatus == PlayerStatus.FALLBACK) mediaPlayer!!.pause(true, false) - } - stateManager.stopService() +// stateManager.stopService() } @Subscribe(threadMode = ThreadMode.MAIN) @@ -920,9 +979,7 @@ class PlaybackService : MediaSessionService() { Log.d(TAG, "onSleepTimerAlmostExpired: $multiplicator") mediaPlayer?.setVolume(multiplicator, multiplicator) } - event.isCancelled -> { - mediaPlayer?.setVolume(1.0f, 1.0f) - } + event.isCancelled -> mediaPlayer?.setVolume(1.0f, 1.0f) } } @@ -933,9 +990,7 @@ class PlaybackService : MediaSessionService() { return null } Log.d(TAG, "getNextInQueue()") - if (currentMedia.item == null) { - currentMedia.setItem(DBReader.getFeedItem(currentMedia.itemId)) - } + if (currentMedia.item == null) currentMedia.setItem(DBReader.getFeedItem(currentMedia.itemId)) val item = currentMedia.item if (item == null) { Log.w(TAG, "getNextInQueue() with FeedMedia object whose FeedItem is null") @@ -959,7 +1014,7 @@ class PlaybackService : MediaSessionService() { if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed != null && !nextItem.feed!!.isLocalFeed) { displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, nextItem.media!!).intent) writeNoMediaPlaying() - stateManager.stopService() +// stateManager.stopService() return null } return nextItem.media @@ -974,10 +1029,10 @@ class PlaybackService : MediaSessionService() { if (stopPlaying) { taskManager.cancelPositionSaver() cancelPositionObserver() - if (!isCasting) { - stateManager.stopForeground(true) - stateManager.stopService() - } +// if (!isCasting) { +// stateManager.stopForeground(true) +// stateManager.stopService() +// } } if (mediaType == null) { sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_PLAYBACK_END, 0) @@ -1020,19 +1075,14 @@ class PlaybackService : MediaSessionService() { if (playable !is FeedMedia) { Log.d(TAG, "Not doing post-playback processing: media not of type FeedMedia") - if (ended) { - playable.onPlaybackCompleted(applicationContext) - } else { - playable.onPlaybackPause(applicationContext) - } + if (ended) playable.onPlaybackCompleted(applicationContext) + else playable.onPlaybackPause(applicationContext) // return } val media = playable val item = (media as? FeedMedia)?.item ?: currentitem val smartMarkAsPlayed = hasAlmostEnded(media) - if (!ended && smartMarkAsPlayed) { - Log.d(TAG, "smart mark as played") - } + if (!ended && smartMarkAsPlayed) Log.d(TAG, "smart mark as played") var autoSkipped = false if (autoSkippedFeedMediaId != null && autoSkippedFeedMediaId == item?.identifyingValue) { @@ -1068,9 +1118,7 @@ class PlaybackService : MediaSessionService() { } } - if (media is FeedMedia && (ended || skipped || playingNext)) { - DBWriter.addItemToPlaybackHistory(media) - } + if (media is FeedMedia && (ended || skipped || playingNext)) DBWriter.addItemToPlaybackHistory(media) } fun setSleepTimer(waitingTime: Long) { @@ -1158,23 +1206,20 @@ class PlaybackService : MediaSessionService() { WearMediaSession.addWearExtrasToAction(fastForwardBuilder) sessionState.addCustomAction(fastForwardBuilder.build()) - if (showPlaybackSpeedOnFullNotification()) { + if (showPlaybackSpeedOnFullNotification()) sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED, getString(R.string.playback_speed), R.drawable.ic_notification_playback_speed).build()) - } if (showNextChapterOnFullNotification()) { - if (!playable?.getChapters().isNullOrEmpty()) { + if (!playable?.getChapters().isNullOrEmpty()) sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_NEXT_CHAPTER, getString(R.string.next_chapter), R.drawable.ic_notification_next_chapter).build()) - } } - if (showSkipOnFullNotification()) { + if (showSkipOnFullNotification()) sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_SKIP_TO_NEXT, - getString(R.string.skip_episode_label), R.drawable.ic_notification_skip).build() - ) - } + getString(R.string.skip_episode_label), R.drawable.ic_notification_skip).build()) + if (mediaSession != null) { WearMediaSession.mediaSessionSetExtraForWear(mediaSession!!) @@ -1199,34 +1244,36 @@ class PlaybackService : MediaSessionService() { builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, p.getFeedTitle()) - if (notificationBuilder.isIconCached) { - builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, notificationBuilder.cachedIcon) - } else { - var iconUri = p.getImageLocation() - // Don't use embedded cover etc, which Android can't load - if (p is FeedMedia) { - val m = p - if (m.item != null) { - val item = m.item!! - when { - item.imageUrl != null -> { - iconUri = item.imageUrl - } - item.feed != null -> { - iconUri = item.feed!!.imageUrl - } - } - } - } - if (!iconUri.isNullOrEmpty()) { - builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, iconUri) - } - } +// if (notificationBuilder.isIconCached) { +// builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, notificationBuilder.cachedIcon) +// } else { +// var iconUri = p.getImageLocation() +// // Don't use embedded cover etc, which Android can't load +// if (p is FeedMedia) { +// val m = p +// if (m.item != null) { +// val item = m.item!! +// when { +// item.imageUrl != null -> { +// iconUri = item.imageUrl +// } +// item.feed != null -> { +// iconUri = item.feed!!.imageUrl +// } +// } +// } +// } +// if (!iconUri.isNullOrEmpty()) { +// builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, iconUri) +// } +// } - if (stateManager.hasReceivedValidStartCommand()) { - mediaSession!!.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity, - getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT - or (if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0))) +// if (stateManager.hasReceivedValidStartCommand()) { +// mediaSession!!.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity, +// getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT +// or (if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0))) + mediaSession!!.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity, + getPlayerActivityIntent(this), FLAG_IMMUTABLE)) // try { // mediaSession!!.setMetadata(builder.build()) // } catch (e: OutOfMemoryError) { @@ -1234,7 +1281,7 @@ class PlaybackService : MediaSessionService() { // builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, null) // mediaSession!!.setMetadata(builder.build()) // } - } +// } } /** @@ -1252,19 +1299,19 @@ class PlaybackService : MediaSessionService() { if (playable == null || mediaPlayer == null) { Log.d(TAG, "setupNotification: playable=$playable mediaPlayer=$mediaPlayer") - if (!stateManager.hasReceivedValidStartCommand()) { - stateManager.stopService() - } +// if (!stateManager.hasReceivedValidStartCommand()) { +// stateManager.stopService() +// } return } val playerStatus = mediaPlayer!!.playerStatus - notificationBuilder.setPlayable(playable) - if (mediaSession != null) notificationBuilder.setMediaSessionToken(mediaSession!!.getSessionCompatToken()) - notificationBuilder.playerStatus = playerStatus - notificationBuilder.updatePosition(currentPosition, currentPlaybackSpeed) +// notificationBuilder.setPlayable(playable) +// if (mediaSession != null) notificationBuilder.setMediaSessionToken(mediaSession!!.getSessionCompatToken()) +// notificationBuilder.playerStatus = playerStatus +// notificationBuilder.updatePosition(currentPosition, currentPlaybackSpeed) - val notificationManager = NotificationManagerCompat.from(this) +// val notificationManager = NotificationManagerCompat.from(this) if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { // TODO: Consider calling @@ -1279,19 +1326,19 @@ class PlaybackService : MediaSessionService() { Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show() return } - notificationManager.notify(R.id.notification_playing, notificationBuilder.build()) +// notificationManager.notify(R.id.notification_playing, notificationBuilder.build()) - if (!notificationBuilder.isIconCached) { - playableIconLoaderThread = Thread { - Log.d(TAG, "Loading notification icon") - notificationBuilder.loadIcon() - if (!Thread.currentThread().isInterrupted) { - notificationManager.notify(R.id.notification_playing, notificationBuilder.build()) - updateMediaSessionMetadata(playable) - } - } - playableIconLoaderThread?.start() - } +// if (!notificationBuilder.isIconCached) { +// playableIconLoaderThread = Thread { +// Log.d(TAG, "Loading notification icon") +// notificationBuilder.loadIcon() +// if (!Thread.currentThread().isInterrupted) { +// notificationManager.notify(R.id.notification_playing, notificationBuilder.build()) +// updateMediaSessionMetadata(playable) +// } +// } +// playableIconLoaderThread?.start() +// } } /** @@ -1312,9 +1359,8 @@ class PlaybackService : MediaSessionService() { position = currentPosition duration = this.duration playable = mediaPlayer?.getPlayable() - } else { - duration = playable?.getDuration() ?: Playable.INVALID_TIME - } + } else duration = playable?.getDuration() ?: Playable.INVALID_TIME + if (position != Playable.INVALID_TIME && duration != Playable.INVALID_TIME && playable != null) { // Log.d(TAG, "Saving current position to $position $duration") saveCurrentPosition(playable, position, System.currentTimeMillis()) @@ -1331,9 +1377,7 @@ class PlaybackService : MediaSessionService() { private fun bluetoothNotifyChange(info: PSMPInfo?, whatChanged: String) { var isPlaying = false - if (info?.playerStatus == PlayerStatus.PLAYING || info?.playerStatus == PlayerStatus.FALLBACK) { - isPlaying = true - } + if (info?.playerStatus == PlayerStatus.PLAYING || info?.playerStatus == PlayerStatus.FALLBACK) isPlaying = true if (info?.playable != null) { val i = Intent(whatChanged) @@ -1358,12 +1402,8 @@ class PlaybackService : MediaSessionService() { } else { val playerStatus = mediaPlayer?.playerStatus when (playerStatus) { - PlayerStatus.PAUSED, PlayerStatus.PREPARED -> { - mediaPlayer?.resume() - } - PlayerStatus.PREPARING -> { - mediaPlayer?.setStartWhenPrepared(!mediaPlayer!!.isStartWhenPrepared()) - } + PlayerStatus.PAUSED, PlayerStatus.PREPARED -> mediaPlayer?.resume() + PlayerStatus.PREPARING -> mediaPlayer?.setStartWhenPrepared(!mediaPlayer!!.isStartWhenPrepared()) PlayerStatus.INITIALIZED -> { mediaPlayer?.setStartWhenPrepared(true) mediaPlayer?.prepare() @@ -1384,20 +1424,16 @@ class PlaybackService : MediaSessionService() { private val PLUGGED = 1 override fun onReceive(context: Context, intent: Intent) { - if (isInitialStickyBroadcast) { - // Don't pause playback after we just started, just because the receiver - // delivers the current headset state (instead of a change) - return - } + // Don't pause playback after we just started, just because the receiver + // delivers the current headset state (instead of a change) + if (isInitialStickyBroadcast) return if (TextUtils.equals(intent.action, Intent.ACTION_HEADSET_PLUG)) { val state = intent.getIntExtra("state", -1) Log.d(TAG, "Headset plug event. State is $state") if (state != -1) { when (state) { - UNPLUGGED -> { - Log.d(TAG, "Headset was unplugged during playback.") - } + UNPLUGGED -> Log.d(TAG, "Headset was unplugged during playback.") PLUGGED -> { Log.d(TAG, "Headset was plugged in during playback.") unpauseIfPauseOnDisconnect(false) @@ -1436,9 +1472,7 @@ class PlaybackService : MediaSessionService() { private fun pauseIfPauseOnDisconnect() { Log.d(TAG, "pauseIfPauseOnDisconnect()") transientPause = (mediaPlayer?.playerStatus == PlayerStatus.PLAYING || mediaPlayer?.playerStatus == PlayerStatus.FALLBACK) - if (isPauseOnHeadsetDisconnect && !isCasting) { - mediaPlayer?.pause(!isPersistNotify, false) - } + if (isPauseOnHeadsetDisconnect && !isCasting) mediaPlayer?.pause(!isPersistNotify, false) } /** @@ -1452,13 +1486,11 @@ class PlaybackService : MediaSessionService() { if (transientPause) { transientPause = false if (Build.VERSION.SDK_INT >= 31) { - stateManager.stopService() +// stateManager.stopService() return } when { - !bluetooth && isUnpauseOnHeadsetReconnect -> { - mediaPlayer?.resume() - } + !bluetooth && isUnpauseOnHeadsetReconnect -> mediaPlayer?.resume() bluetooth && isUnpauseOnBluetoothReconnect -> { // let the user know we've started playback again... val v = applicationContext.getSystemService(VIBRATOR_SERVICE) as? Vibrator @@ -1473,7 +1505,7 @@ class PlaybackService : MediaSessionService() { override fun onReceive(context: Context, intent: Intent) { if (TextUtils.equals(intent.action, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { EventBus.getDefault().post(PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN)) - stateManager.stopService() +// stateManager.stopService() } } } @@ -1492,11 +1524,8 @@ class PlaybackService : MediaSessionService() { val item = (playable as? FeedMedia)?.item ?: currentitem // if (playable is FeedMedia) { if (item?.feed?.id == event.feedId) { - if (event.speed == FeedPreferences.SPEED_USE_GLOBAL) { - setSpeed(getPlaybackSpeed(playable!!.getMediaType())) - } else { - setSpeed(event.speed) - } + if (event.speed == FeedPreferences.SPEED_USE_GLOBAL) setSpeed(getPlaybackSpeed(playable!!.getMediaType())) + else setSpeed(event.speed) } // } } @@ -1570,9 +1599,7 @@ class PlaybackService : MediaSessionService() { } if (item != null) { var feed = item.feed - if (feed == null) { - feed = DBReader.getFeed(item.feedId) - } + if (feed == null) feed = DBReader.getFeed(item.feedId) if (feed != null) { val feedPreferences = feed.preferences if (feedPreferences != null) { @@ -1601,9 +1628,8 @@ class PlaybackService : MediaSessionService() { if (!isSpeedForward) { normalSpeed = mediaPlayer!!.getPlaybackSpeed() mediaPlayer!!.setPlaybackParams(speed, isSkipSilence) - } else { - mediaPlayer!!.setPlaybackParams(normalSpeed, isSkipSilence) - } + } else mediaPlayer!!.setPlaybackParams(normalSpeed, isSkipSilence) + isSpeedForward = !isSpeedForward } @@ -1613,9 +1639,8 @@ class PlaybackService : MediaSessionService() { if (!isFallbackSpeed) { normalSpeed = mediaPlayer!!.getPlaybackSpeed() mediaPlayer!!.setPlaybackParams(speed, isSkipSilence) - } else { - mediaPlayer!!.setPlaybackParams(normalSpeed, isSkipSilence) - } + } else mediaPlayer!!.setPlaybackParams(normalSpeed, isSkipSilence) + isFallbackSpeed = !isFallbackSpeed } @@ -1688,13 +1713,13 @@ class PlaybackService : MediaSessionService() { positionEventTimer = Observable.interval(POSITION_EVENT_INTERVAL, TimeUnit.SECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { - Log.d(TAG, "notificationBuilder.updatePosition currentPosition: $currentPosition, currentPlaybackSpeed: $currentPlaybackSpeed") + Log.d(TAG, "setupPositionObserver currentPosition: $currentPosition, currentPlaybackSpeed: $currentPlaybackSpeed") EventBus.getDefault().post(PlaybackPositionEvent(currentPosition, duration)) // TODO: why set SDK_INT < 29 if (Build.VERSION.SDK_INT < 29) { - notificationBuilder.updatePosition(currentPosition, currentPlaybackSpeed) - val notificationManager = getSystemService(NOTIFICATION_SERVICE) as? NotificationManager - notificationManager?.notify(R.id.notification_playing, notificationBuilder.build()) +// notificationBuilder.updatePosition(currentPosition, currentPlaybackSpeed) +// val notificationManager = getSystemService(NOTIFICATION_SERVICE) as? NotificationManager +// notificationManager?.notify(R.id.notification_playing, notificationBuilder.build()) } skipEndingIfNecessary() } @@ -1712,10 +1737,10 @@ class PlaybackService : MediaSessionService() { } } - private val sessionCallback: MediaSession.Callback = object : MediaSession.Callback { - private val TAG = "MediaSessionCompat" -// TODO: not used now with media3 - } +// private val sessionCallback: MediaSession.Callback = object : MediaSession.Callback { +// private val TAG = "MediaSessionCompat" +//// TODO: not used now with media3 +// } companion object { private const val TAG = "PlaybackService" @@ -1771,17 +1796,11 @@ class PlaybackService : MediaSessionService() { */ @JvmStatic fun getPlayerActivityIntent(context: Context): Intent { - val showVideoPlayer = if (isRunning) { - currentMediaType == MediaType.VIDEO && !isCasting - } else { - currentEpisodeIsVideo - } + val showVideoPlayer = if (isRunning) currentMediaType == MediaType.VIDEO && !isCasting + else currentEpisodeIsVideo - return if (showVideoPlayer) { - VideoPlayerActivityStarter(context).intent - } else { - MainActivityStarter(context).withOpenPlayer().getIntent() - } + return if (showVideoPlayer) VideoPlayerActivityStarter(context).intent + else MainActivityStarter(context).withOpenPlayer().getIntent() } /** @@ -1790,12 +1809,8 @@ class PlaybackService : MediaSessionService() { */ @JvmStatic fun getPlayerActivityIntent(context: Context, mediaType: MediaType?): Intent { - return if (mediaType == MediaType.VIDEO && !isCasting) { - VideoPlayerActivityStarter(context).intent - } else { - MainActivityStarter(context).withOpenPlayer().getIntent() - } + return if (mediaType == MediaType.VIDEO && !isCasting) VideoPlayerActivityStarter(context).intent + else MainActivityStarter(context).withOpenPlayer().getIntent() } - } } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt1 b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt1 deleted file mode 100644 index 64d4d8b1..00000000 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt1 +++ /dev/null @@ -1,1865 +0,0 @@ -package ac.mdiq.podcini.playback.service - -import ac.mdiq.podcini.R -import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink -import ac.mdiq.podcini.playback.PlayableUtils.saveCurrentPosition -import ac.mdiq.podcini.playback.PlaybackServiceStarter -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPCallback -import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPInfo -import ac.mdiq.podcini.playback.base.PlayerStatus -import ac.mdiq.podcini.playback.cast.CastPsmp -import ac.mdiq.podcini.playback.cast.CastStateListener -import ac.mdiq.podcini.playback.service.PlaybackServiceTaskManager.PSTMCallback -import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.clearCurrentlyPlayingTemporaryPlaybackSpeed -import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.createInstanceFromPreferences -import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentEpisodeIsVideo -import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId -import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingTemporaryPlaybackSpeed -import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.writeMediaPlaying -import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.writeNoMediaPlaying -import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.writePlayerStatus -import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnable -import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom -import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo -import ac.mdiq.podcini.preferences.SleepTimerPreferences.isInTimeRange -import ac.mdiq.podcini.preferences.SleepTimerPreferences.timerMillis -import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs -import ac.mdiq.podcini.preferences.UserPreferences.getPlaybackSpeed -import ac.mdiq.podcini.preferences.UserPreferences.hardwareForwardButton -import ac.mdiq.podcini.preferences.UserPreferences.hardwarePreviousButton -import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileStreaming -import ac.mdiq.podcini.preferences.UserPreferences.isFollowQueue -import ac.mdiq.podcini.preferences.UserPreferences.isPauseOnHeadsetDisconnect -import ac.mdiq.podcini.preferences.UserPreferences.isPersistNotify -import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence -import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnBluetoothReconnect -import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnHeadsetReconnect -import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs -import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed -import ac.mdiq.podcini.preferences.UserPreferences.shouldFavoriteKeepEpisode -import ac.mdiq.podcini.preferences.UserPreferences.shouldSkipKeepEpisode -import ac.mdiq.podcini.preferences.UserPreferences.showNextChapterOnFullNotification -import ac.mdiq.podcini.preferences.UserPreferences.showPlaybackSpeedOnFullNotification -import ac.mdiq.podcini.preferences.UserPreferences.showSkipOnFullNotification -import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed -import ac.mdiq.podcini.receiver.MediaButtonReceiver -import ac.mdiq.podcini.service.playback.WearMediaSession -import ac.mdiq.podcini.storage.DBReader -import ac.mdiq.podcini.storage.DBWriter -import ac.mdiq.podcini.storage.model.feed.FeedItem -import ac.mdiq.podcini.storage.model.feed.FeedMedia -import ac.mdiq.podcini.storage.model.feed.FeedPreferences -import ac.mdiq.podcini.storage.model.feed.FeedPreferences.AutoDeleteAction -import ac.mdiq.podcini.storage.model.playback.MediaType -import ac.mdiq.podcini.storage.model.playback.Playable -import ac.mdiq.podcini.ui.activity.appstartintent.MainActivityStarter -import ac.mdiq.podcini.ui.activity.appstartintent.VideoPlayerActivityStarter -import ac.mdiq.podcini.ui.utils.NotificationUtils -import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState -import ac.mdiq.podcini.util.FeedItemUtil.hasAlmostEnded -import ac.mdiq.podcini.util.FeedUtil.shouldAutoDeleteItemsOnThatFeed -import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast -import ac.mdiq.podcini.util.NetworkUtils.isStreamingAllowed -import ac.mdiq.podcini.util.event.MessageEvent -import ac.mdiq.podcini.util.event.PlayerErrorEvent -import ac.mdiq.podcini.util.event.playback.* -import ac.mdiq.podcini.util.event.settings.SkipIntroEndingChangedEvent -import ac.mdiq.podcini.util.event.settings.SpeedPresetChangedEvent -import ac.mdiq.podcini.util.event.settings.VolumeAdaptionChangedEvent -import android.Manifest -import android.annotation.SuppressLint -import android.app.Notification -import android.app.NotificationManager -import android.app.PendingIntent -import android.bluetooth.BluetoothA2dp -import android.content.* -import android.content.pm.PackageManager -import android.media.AudioManager -import android.os.* -import android.os.Build.VERSION_CODES -import android.service.quicksettings.TileService -import android.support.v4.media.MediaMetadataCompat -import android.support.v4.media.session.MediaSessionCompat -import android.support.v4.media.session.PlaybackStateCompat -import android.text.TextUtils -import android.util.Log -import android.util.Pair -import android.view.KeyEvent -import android.view.SurfaceHolder -import android.webkit.URLUtil -import android.widget.Toast -import androidx.core.app.ActivityCompat -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.media3.common.util.UnstableApi -import androidx.media3.session.* -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture -import io.reactivex.Observable -import io.reactivex.Single -import io.reactivex.SingleEmitter -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import java.util.* -import java.util.concurrent.TimeUnit -import kotlin.concurrent.Volatile -import kotlin.math.max - -/** - * Controls the MediaPlayer that plays a FeedMedia-file - */ -@UnstableApi -class PlaybackService : MediaLibraryService() { - private var mediaPlayer: PlaybackServiceMediaPlayer? = null - private var positionEventTimer: Disposable? = null - - private lateinit var customMediaNotificationProvider: CustomMediaNotificationProvider - private val notificationPlayerCustomCommandButtons = NotificationPlayerCustomCommandButton.values().map { command -> command.commandButton } - - private lateinit var mediaLibrarySession: MediaLibrarySession - - private lateinit var taskManager: PlaybackServiceTaskManager - private lateinit var stateManager: PlaybackServiceStateManager -// private lateinit var notificationBuilder: PlaybackServiceNotificationBuilder - private lateinit var castStateListener: CastStateListener - - private var autoSkippedFeedMediaId: String? = null -// private var clickCount = 0 -// private val clickHandler = Handler(Looper.getMainLooper()) - - private var isSpeedForward = false - private var normalSpeed = 1.0f - private var isFallbackSpeed = false - - private var currentitem: FeedItem? = null - - /** - * Used for Lollipop notifications, Android Wear, and Android Auto. - */ - private var mediaSession: MediaSession? = null - - private val mBinder: IBinder = LocalBinder() - - inner class LocalBinder : Binder() { - val service: PlaybackService - get() = this@PlaybackService - } - - override fun onUnbind(intent: Intent): Boolean { - Log.d(TAG, "Received onUnbind event") - return super.onUnbind(intent) - } - - override fun onCreate() { - super.onCreate() - Log.d(TAG, "Service created.") - isRunning = true - - this.startForeground() - - stateManager = PlaybackServiceStateManager(this) -// notificationBuilder = PlaybackServiceNotificationBuilder(this) - - if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { - registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS"), RECEIVER_NOT_EXPORTED) - registerReceiver(shutdownReceiver, IntentFilter(PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE), RECEIVER_NOT_EXPORTED) - } else { - registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS")) - registerReceiver(shutdownReceiver, IntentFilter(PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) - } - - registerReceiver(headsetDisconnected, IntentFilter(Intent.ACTION_HEADSET_PLUG)) - registerReceiver(bluetoothStateUpdated, IntentFilter(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) - registerReceiver(audioBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) - EventBus.getDefault().register(this) - taskManager = PlaybackServiceTaskManager(this, taskManagerCallback) - - recreateMediaSessionIfNeeded() - castStateListener = object : CastStateListener(this) { - override fun onSessionStartedOrEnded() { - recreateMediaPlayer() - } - } - EventBus.getDefault().post(PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_STARTED)) - } - - fun recreateMediaSessionIfNeeded() { - if (mediaSession != null) return - - customMediaNotificationProvider = CustomMediaNotificationProvider(applicationContext) - setMediaNotificationProvider(customMediaNotificationProvider) - - if (ExoPlayerWrapper.exoPlayer == null) ExoPlayerWrapper.createStaticPlayer(applicationContext) - mediaSession = MediaSession.Builder(applicationContext, ExoPlayerWrapper.exoPlayer!!) - .setCallback(MyCallback()) - .setCustomLayout(notificationPlayerCustomCommandButtons) - .build() - - recreateMediaPlayer() - } - - fun recreateMediaPlayer() { - var media: Playable? = null - var wasPlaying = false - if (mediaPlayer != null) { - media = mediaPlayer!!.getPlayable() - wasPlaying = mediaPlayer!!.playerStatus == PlayerStatus.PLAYING || mediaPlayer!!.playerStatus == PlayerStatus.FALLBACK - mediaPlayer!!.pause(true, false) - mediaPlayer!!.shutdown() - } - mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback) - if (mediaPlayer == null) { - mediaPlayer = LocalPSMP(applicationContext, mediaPlayerCallback) // Cast not supported or not connected - } - if (media != null) { - mediaPlayer!!.playMediaObject(media, !media.localFileAvailable(), wasPlaying, true) - } - isCasting = mediaPlayer!!.isCasting() - } - - override fun onDestroy() { - super.onDestroy() - Log.d(TAG, "Service is about to be destroyed") - -// if (notificationBuilder.playerStatus == PlayerStatus.PLAYING || notificationBuilder.playerStatus == PlayerStatus.FALLBACK) { -// notificationBuilder.playerStatus = PlayerStatus.STOPPED -// val notificationManager = NotificationManagerCompat.from(this) -// if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this, -// Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { -// // TODO: Consider calling -// // ActivityCompat#requestPermissions -//// requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) -// // here to request the missing permissions, and then overriding -// // public void onRequestPermissionsResult(int requestCode, String[] permissions, -// // int[] grantResults) -// // to handle the case where the user grants the permission. See the documentation -// // for ActivityCompat#requestPermissions for more details. -// Log.e(TAG, "onDestroy: require POST_NOTIFICATIONS permission") -// Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show() -// return -// } -// notificationManager.notify(R.id.notification_playing, notificationBuilder.build()) -// } - stateManager.stopForeground(!isPersistNotify) - isRunning = false - currentMediaType = MediaType.UNKNOWN - castStateListener.destroy() - - cancelPositionObserver() - mediaSession?.run { - player.release() - release() - mediaSession = null - } - ExoPlayerWrapper.exoPlayer?.release() - ExoPlayerWrapper.exoPlayer = null - mediaPlayer?.shutdown() - - unregisterReceiver(autoStateUpdated) - unregisterReceiver(headsetDisconnected) - unregisterReceiver(shutdownReceiver) - unregisterReceiver(bluetoothStateUpdated) - unregisterReceiver(audioBecomingNoisy) - taskManager.shutdown() - EventBus.getDefault().unregister(this) - } - - private inner class MyCallback : MediaSession.Callback { - override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult { - Log.d(TAG, "in onConnect") - return MediaSession.ConnectionResult.AcceptedResultBuilder(session) - .setAvailablePlayerCommands( - MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() -// .remove(COMMAND_SEEK_TO_NEXT) -// .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) -// .remove(COMMAND_SEEK_TO_PREVIOUS) -// .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) - .build() - ) - .setAvailableSessionCommands( - MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() -// .add(customCommandFavorites) - .build() - ) - .build() - -// val connectionResult = super.onConnect(session, controller) -// val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon() -// -// /* Registering custom player command buttons for player notification. */ -// notificationPlayerCustomCommandButtons.forEach { commandButton -> -// Log.d(TAG, "onConnect commandButton ${commandButton.displayName}") -// commandButton.sessionCommand?.let(availableSessionCommands::add) -// } -// -// return MediaSession.ConnectionResult.accept( -// availableSessionCommands.build(), -// connectionResult.availablePlayerCommands -// ) - } - - override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { - super.onPostConnect(session, controller) - if (notificationPlayerCustomCommandButtons.isNotEmpty()) { - /* Setting custom player command buttons to mediaLibrarySession for player notification. */ - mediaLibrarySession.setCustomLayout(notificationPlayerCustomCommandButtons) - } - } - - override fun onCustomCommand(session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle): ListenableFuture { - /* Handling custom command buttons from player notification. */ - if (customCommand.customAction == NotificationPlayerCustomCommandButton.REWIND.customAction) { - session.player.seekBack() - } - if (customCommand.customAction == NotificationPlayerCustomCommandButton.FORWARD.customAction) { - session.player.seekForward() - } - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } - } - - override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? { - return null - } - - private fun loadQueueForMediaSession() { - Single.create { emitter: SingleEmitter?> -> - val queueItems: MutableList = ArrayList() - for (feedItem in DBReader.getQueue()) { - if (feedItem.media != null) { - val mediaDescription = feedItem.media!!.mediaItem.description - queueItems.add(MediaSessionCompat.QueueItem(mediaDescription, feedItem.id)) - } - } - emitter.onSuccess(queueItems) - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ queueItems: List? -> -// mediaSession?.setQueue(queueItems) - }, - { obj: Throwable -> obj.printStackTrace() }) - } - -// private fun createBrowsableMediaItem( -// @StringRes title: Int, @DrawableRes icon: Int, numEpisodes: Int -// ): MediaBrowserCompat.MediaItem { -// val uri = Uri.Builder() -// .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) -// .authority(resources.getResourcePackageName(icon)) -// .appendPath(resources.getResourceTypeName(icon)) -// .appendPath(resources.getResourceEntryName(icon)) -// .build() -// -// val description = MediaDescriptionCompat.Builder() -// .setIconUri(uri) -// .setMediaId(resources.getString(title)) -// .setTitle(resources.getString(title)) -// .setSubtitle(resources.getQuantityString(R.plurals.num_episodes, numEpisodes, numEpisodes)) -// .build() -// return MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) -// } - -// private fun createBrowsableMediaItemForFeed(feed: Feed): MediaBrowserCompat.MediaItem { -// val builder = MediaDescriptionCompat.Builder() -// .setMediaId("FeedId:" + feed.id) -// .setTitle(feed.title) -// .setDescription(feed.description) -// .setSubtitle(feed.getCustomTitle()) -// if (feed.imageUrl != null) { -// builder.setIconUri(Uri.parse(feed.imageUrl)) -// } -// if (feed.link != null) { -// builder.setMediaUri(Uri.parse(feed.link)) -// } -// val description = builder.build() -// return MediaBrowserCompat.MediaItem(description, -// MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) -// } - -// override fun onLoadChildren(parentId: String, -// result: Result> -// ) { -// Log.d(TAG, "OnLoadChildren: parentMediaId=$parentId") -// result.detach() -// -// Completable.create { emitter: CompletableEmitter -> -// result.sendResult(loadChildrenSynchronous(parentId)) -// emitter.onComplete() -// } -// .subscribeOn(Schedulers.io()) -// .observeOn(AndroidSchedulers.mainThread()) -// .subscribe( -// {}, { e: Throwable -> -// e.printStackTrace() -// result.sendResult(null) -// }) -// } - -// private fun loadChildrenSynchronous(parentId: String): List? { -// val mediaItems: MutableList = ArrayList() -// if (parentId == resources.getString(R.string.app_name)) { -// val currentlyPlaying = currentPlayerStatus.toLong() -// if (currentlyPlaying == PlaybackPreferences.PLAYER_STATUS_PLAYING.toLong() -// || currentlyPlaying == PlaybackPreferences.PLAYER_STATUS_PAUSED.toLong()) { -// mediaItems.add(createBrowsableMediaItem(R.string.current_playing_episode, R.drawable.ic_play_48dp, 1)) -// } -// mediaItems.add(createBrowsableMediaItem(R.string.queue_label, R.drawable.ic_playlist_play_black, -// DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.QUEUED)))) -// mediaItems.add(createBrowsableMediaItem(R.string.downloads_label, R.drawable.ic_download_black, -// DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.DOWNLOADED)))) -// mediaItems.add(createBrowsableMediaItem(R.string.episodes_label, R.drawable.ic_feed_black, -// DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.UNPLAYED)))) -// val feeds = DBReader.getFeedList() -// for (feed in feeds) { -// mediaItems.add(createBrowsableMediaItemForFeed(feed)) -// } -// return mediaItems -// } -// -// val feedItems: List -// when { -// parentId == resources.getString(R.string.queue_label) -> { -// feedItems = DBReader.getQueue() -// } -// parentId == resources.getString(R.string.downloads_label) -> { -// feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED, -// FeedItemFilter(FeedItemFilter.DOWNLOADED), downloadsSortedOrder) -// } -// parentId == resources.getString(R.string.episodes_label) -> { -// feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED, -// FeedItemFilter(FeedItemFilter.UNPLAYED), allEpisodesSortOrder) -// } -// parentId.startsWith("FeedId:") -> { -// val feedId = parentId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1].toLong() -// val feed = DBReader.getFeed(feedId) -// feedItems = if (feed != null) DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(), feed.sortOrder) else listOf() -// } -// parentId == getString(R.string.current_playing_episode) -> { -// val playable = createInstanceFromPreferences(this) -// if (playable is FeedMedia) { -// feedItems = listOf(playable.item) -// } else { -// return null -// } -// } -// else -> { -// Log.e(TAG, "Parent ID not found: $parentId") -// return null -// } -// } -// var count = 0 -// for (feedItem in feedItems) { -// if (feedItem?.media != null) { -// mediaItems.add(feedItem.media!!.mediaItem) -// if (++count >= MAX_ANDROID_AUTO_EPISODES_PER_FEED) { -// break -// } -// } -// } -// return mediaItems -// } - - override fun onBind(intent: Intent?): IBinder? { - Log.d(TAG, "Received onBind event") - return if (intent?.action != null && TextUtils.equals(intent.action, SERVICE_INTERFACE)) { - super.onBind(intent) - } else { - mBinder - } - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - Log.d(TAG, "OnStartCommand called") - - stateManager.startForeground(R.id.notification_playing, notificationBuilder.build()) -// val notificationManager = NotificationManagerCompat.from(this) -// notificationManager.cancel(R.id.notification_streaming_confirmation) - - val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1 - val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION) - val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) ?: false - val playable = intent?.getParcelableExtra(PlaybackServiceInterface.EXTRA_PLAYABLE) - if (keycode == -1 && playable == null && customAction == null) { - Log.e(TAG, "PlaybackService was started with no arguments") - stateManager.stopService() - return START_NOT_STICKY - } - - if ((flags and START_FLAG_REDELIVERY) != 0) { - Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now.") - stateManager.stopForeground(true) - } else { - when { - keycode != -1 -> { - val notificationButton: Boolean - if (hardwareButton) { - Log.d(TAG, "Received hardware button event") - notificationButton = false - } else { - Log.d(TAG, "Received media button event") - notificationButton = true - } - val handled = handleKeycode(keycode, notificationButton) - if (!handled && !stateManager.hasReceivedValidStartCommand()) { - stateManager.stopService() - return START_NOT_STICKY - } - } - playable != null -> { - stateManager.validStartCommandWasReceived() - val allowStreamThisTime = intent.getBooleanExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, false) - val allowStreamAlways = intent.getBooleanExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, false) - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0) - if (allowStreamAlways) { - isAllowMobileStreaming = true - } - Observable.fromCallable { - if (playable is FeedMedia) { - return@fromCallable DBReader.getFeedMedia(playable.id) - } else { - return@fromCallable playable - } - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { loadedPlayable: Playable? -> startPlaying(loadedPlayable, allowStreamThisTime) }, - { error: Throwable -> - Log.d(TAG, "Playable was not found. Stopping service.") - error.printStackTrace() - stateManager.stopService() - }) - return START_NOT_STICKY - } - else -> { -// mediaSession?.controller?.transportControls?.sendCustomAction(customAction, null) - } - } - } - - return START_NOT_STICKY - } - - private fun skipIntro(playable: Playable) { - val item = (playable as? FeedMedia)?.item ?: currentitem ?: return -// val item = currentitem ?: (playable as? FeedMedia)?.item ?: return - val feed = item.feed ?: DBReader.getFeed(item.feedId) - val preferences = feed?.preferences - - val skipIntro = preferences?.feedSkipIntro ?: 0 - val skipIntroMS = skipIntro * 1000 - if (skipIntro > 0 && playable.getPosition() < skipIntroMS) { - val duration = duration - if (skipIntroMS < duration || duration <= 0) { - Log.d(TAG, "skipIntro " + playable.getEpisodeTitle()) - mediaPlayer?.seekTo(skipIntroMS) - val skipIntroMesg = applicationContext.getString(R.string.pref_feed_skip_intro_toast, skipIntro) - val toast = Toast.makeText(applicationContext, skipIntroMesg, Toast.LENGTH_LONG) - toast.show() - } - } - } - - @SuppressLint("LaunchActivityFromNotification") - private fun displayStreamingNotAllowedNotification(originalIntent: Intent) { - if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent::class.java)) { - EventBus.getDefault().post(MessageEvent(getString(R.string.confirm_mobile_streaming_notification_message))) - return - } - - val intentAllowThisTime = Intent(originalIntent) - intentAllowThisTime.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME) - intentAllowThisTime.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, true) - val pendingIntentAllowThisTime = if (Build.VERSION.SDK_INT >= VERSION_CODES.O) { - PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - } else { - PendingIntent.getService(this, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - } - - val intentAlwaysAllow = Intent(intentAllowThisTime) - intentAlwaysAllow.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS) - intentAlwaysAllow.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, true) - val pendingIntentAlwaysAllow = if (Build.VERSION.SDK_INT >= VERSION_CODES.O) { - PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - } else { - PendingIntent.getService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - } - - val builder = NotificationCompat.Builder(this, - NotificationUtils.CHANNEL_ID_USER_ACTION) - .setSmallIcon(R.drawable.ic_notification_stream) - .setContentTitle(getString(R.string.confirm_mobile_streaming_notification_title)) - .setContentText(getString(R.string.confirm_mobile_streaming_notification_message)) - .setStyle(NotificationCompat.BigTextStyle() - .bigText(getString(R.string.confirm_mobile_streaming_notification_message))) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setContentIntent(pendingIntentAllowThisTime) - .addAction(R.drawable.ic_notification_stream, getString(R.string.confirm_mobile_streaming_button_once), pendingIntentAllowThisTime) - .addAction(R.drawable.ic_notification_stream, getString(R.string.confirm_mobile_streaming_button_always), pendingIntentAlwaysAllow) - .setAutoCancel(true) -// val notificationManager = NotificationManagerCompat.from(this) - if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this, - Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - // TODO: Consider calling - // ActivityCompat#requestPermissions -// requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) - // here to request the missing permissions, and then overriding - // public void onRequestPermissionsResult(int requestCode, String[] permissions, - // int[] grantResults) - // to handle the case where the user grants the permission. See the documentation - // for ActivityCompat#requestPermissions for more details. - Log.e(TAG, "displayStreamingNotAllowedNotification: require POST_NOTIFICATIONS permission") - Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show() - return - } -// notificationManager.notify(R.id.notification_streaming_confirmation, builder.build()) - } - - /** - * Handles media button events - * return: keycode was handled - */ - private fun handleKeycode(keycode: Int, notificationButton: Boolean): Boolean { - Log.d(TAG, "Handling keycode: $keycode") - val info = mediaPlayer?.pSMPInfo - val status = info?.playerStatus - when (keycode) { - KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { - when { - status == PlayerStatus.PLAYING -> { - mediaPlayer?.pause(!isPersistNotify, false) - } - status == PlayerStatus.FALLBACK || status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED -> { - mediaPlayer?.resume() - } - status == PlayerStatus.PREPARING -> { - mediaPlayer?.setStartWhenPrepared(!mediaPlayer!!.isStartWhenPrepared()) - } - status == PlayerStatus.INITIALIZED -> { - mediaPlayer?.setStartWhenPrepared(true) - mediaPlayer?.prepare() - } - mediaPlayer?.getPlayable() == null -> { - startPlayingFromPreferences() - } - else -> { - return false - } - } - taskManager.restartSleepTimer() - return true - } - KeyEvent.KEYCODE_MEDIA_PLAY -> { - when { - status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED -> { - mediaPlayer?.resume() - } - status == PlayerStatus.INITIALIZED -> { - mediaPlayer?.setStartWhenPrepared(true) - mediaPlayer?.prepare() - } - mediaPlayer?.getPlayable() == null -> { - startPlayingFromPreferences() - } - else -> { - return false - } - } - taskManager.restartSleepTimer() - return true - } - KeyEvent.KEYCODE_MEDIA_PAUSE -> { - if (status == PlayerStatus.PLAYING) { - mediaPlayer?.pause(!isPersistNotify, false) - return true - } - return false - } - KeyEvent.KEYCODE_MEDIA_NEXT -> { - when { - !notificationButton -> { - // Handle remapped button as notification button which is not remapped again. - return handleKeycode(hardwareForwardButton, true) - } - this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED -> { - mediaPlayer?.skip() - return true - } - else -> return false - } - } - KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> { - if (this.status == PlayerStatus.FALLBACK || this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED) { - mediaPlayer?.seekDelta(fastForwardSecs * 1000) - return true - } - return false - } - KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { - when { - !notificationButton -> { - // Handle remapped button as notification button which is not remapped again. - return handleKeycode(hardwarePreviousButton, true) - } - this.status == PlayerStatus.FALLBACK || this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED -> { - mediaPlayer?.seekTo(0) - return true - } - else -> return false - } - } - KeyEvent.KEYCODE_MEDIA_REWIND -> { - if (this.status == PlayerStatus.FALLBACK || this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED) { - mediaPlayer?.seekDelta(-rewindSecs * 1000) - return true - } - return false - } - KeyEvent.KEYCODE_MEDIA_STOP -> { - if (this.status == PlayerStatus.FALLBACK || status == PlayerStatus.PLAYING) { - mediaPlayer?.pause(true, true) - } - stateManager.stopForeground(true) // gets rid of persistent notification - return true - } - else -> { - Log.d(TAG, "Unhandled key code: $keycode") - if (info?.playable != null && info.playerStatus == PlayerStatus.PLAYING) { - // only notify the user about an unknown key event if it is actually doing something - val message = String.format(resources.getString(R.string.unknown_media_key), keycode) - Toast.makeText(this, message, Toast.LENGTH_SHORT).show() - } - } - } - return false - } - - private fun startPlayingFromPreferences() { - Observable.fromCallable { - createInstanceFromPreferences(applicationContext) - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { playable: Playable? -> startPlaying(playable, false) }, - { error: Throwable -> - Log.d(TAG, "Playable was not loaded from preferences. Stopping service.") - error.printStackTrace() - stateManager.stopService() - }) - } - - private fun startPlaying(playable: Playable?, allowStreamThisTime: Boolean) { - if (playable == null) return - - val localFeed = URLUtil.isContentUrl(playable.getStreamUrl()) - val stream = !playable.localFileAvailable() || localFeed - if (stream && !localFeed && !isStreamingAllowed && !allowStreamThisTime) { - displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, playable).intent) - writeNoMediaPlaying() - stateManager.stopService() - return - } - - if (playable.getIdentifier() != currentlyPlayingFeedMediaId) { - clearCurrentlyPlayingTemporaryPlaybackSpeed() - } - - mediaPlayer?.playMediaObject(playable, stream, true, true) - stateManager.validStartCommandWasReceived() -// stateManager.startForeground(R.id.notification_playing, notificationBuilder.build()) - recreateMediaSessionIfNeeded() - updateNotificationAndMediaSession(playable) - addPlayableToQueue(playable) - } - - /** - * Called by a mediaplayer Activity as soon as it has prepared its - * mediaplayer. - */ - fun setVideoSurface(sh: SurfaceHolder?) { - Log.d(TAG, "Setting display") - mediaPlayer?.setVideoSurface(sh) - } - - fun notifyVideoSurfaceAbandoned() { - mediaPlayer?.pause(true, false) - mediaPlayer?.resetVideoSurface() - updateNotificationAndMediaSession(playable) - stateManager.stopForeground(!isPersistNotify) - } - - private val taskManagerCallback: PSTMCallback = object : PSTMCallback { - override fun positionSaverTick() { - saveCurrentPosition(true, null, Playable.INVALID_TIME) - } - - override fun requestWidgetState(): WidgetState { - return WidgetState(this@PlaybackService.playable, this@PlaybackService.status, - this@PlaybackService.currentPosition, this@PlaybackService.duration, this@PlaybackService.currentPlaybackSpeed) - } - - override fun onChapterLoaded(media: Playable?) { - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0) - updateMediaSession(mediaPlayer?.playerStatus) - } - } - - private val mediaPlayerCallback: PSMPCallback = object : PSMPCallback { - override fun statusChanged(newInfo: PSMPInfo?) { - currentMediaType = mediaPlayer?.getCurrentMediaType() ?: MediaType.UNKNOWN - Log.d(TAG, "statusChanged called") - updateMediaSession(newInfo?.playerStatus) - if (newInfo != null) { - when (newInfo.playerStatus) { - PlayerStatus.INITIALIZED -> { - if (mediaPlayer != null) { - writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus, currentitem) - } - updateNotificationAndMediaSession(newInfo.playable) - } - PlayerStatus.PREPARED -> { - if (mediaPlayer != null) { - writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus, currentitem) - } - taskManager.startChapterLoader(newInfo.playable!!) - } - PlayerStatus.PAUSED -> { - updateNotificationAndMediaSession(newInfo.playable) - if (!isCasting) { - stateManager.stopForeground(!isPersistNotify) - } - cancelPositionObserver() - if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus) - } - PlayerStatus.STOPPED -> {} - PlayerStatus.PLAYING -> { - if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus) - saveCurrentPosition(true, null, Playable.INVALID_TIME) - recreateMediaSessionIfNeeded() - updateNotificationAndMediaSession(newInfo.playable) - setupPositionObserver() - stateManager.validStartCommandWasReceived() -// stateManager.startForeground(R.id.notification_playing, notificationBuilder.build()) - // set sleep timer if auto-enabled - var autoEnableByTime = true - val fromSetting = autoEnableFrom() - val toSetting = autoEnableTo() - if (fromSetting != toSetting) { - val now: Calendar = GregorianCalendar() - now.timeInMillis = System.currentTimeMillis() - val currentHour = now[Calendar.HOUR_OF_DAY] - autoEnableByTime = isInTimeRange(fromSetting, toSetting, currentHour) - } - - if (newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING && autoEnable() && autoEnableByTime && !sleepTimerActive()) { - setSleepTimer(timerMillis()) - EventBus.getDefault().post(MessageEvent(getString(R.string.sleep_timer_enabled_label), { disableSleepTimer() }, getString(R.string.undo))) - } - loadQueueForMediaSession() - } - PlayerStatus.ERROR -> { - writeNoMediaPlaying() - stateManager.stopService() - } - else -> {} - } - } - if (Build.VERSION.SDK_INT >= VERSION_CODES.N) { - TileService.requestListeningState(applicationContext, ComponentName(applicationContext, QuickSettingsTileService::class.java)) - } - - sendLocalBroadcast(applicationContext, ACTION_PLAYER_STATUS_CHANGED) - bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED) - bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED) - taskManager.requestWidgetUpdate() - } - - override fun shouldStop() { - stateManager.stopForeground(!isPersistNotify) - } - - override fun onMediaChanged(reloadUI: Boolean) { - Log.d(TAG, "reloadUI callback reached") - if (reloadUI) { - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0) - } - updateNotificationAndMediaSession(this@PlaybackService.playable) - } - - override fun onPostPlayback(media: Playable, ended: Boolean, skipped: Boolean, playingNext: Boolean) { - this@PlaybackService.onPostPlayback(media, ended, skipped, playingNext) - } - - override fun onPlaybackStart(playable: Playable, position: Int) { - taskManager.startWidgetUpdater() - if (position != Playable.INVALID_TIME) { - playable.setPosition(position) - } else { - skipIntro(playable) - } - playable.onPlaybackStart() - taskManager.startPositionSaver() - } - - override fun onPlaybackPause(playable: Playable?, position: Int) { - taskManager.cancelPositionSaver() - cancelPositionObserver() - saveCurrentPosition(position == Playable.INVALID_TIME || playable == null, playable, position) - taskManager.cancelWidgetUpdater() - if (playable != null) { - if (playable is FeedMedia) { - SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(applicationContext, playable, false) - } - playable.onPlaybackPause(applicationContext) - } - } - - override fun getNextInQueue(currentMedia: Playable?): Playable? { - return this@PlaybackService.getNextInQueue(currentMedia) - } - - override fun findMedia(url: String): Playable? { - val item = DBReader.getFeedItemByGuidOrEpisodeUrl(null, url) - return item?.media - } - - override fun onPlaybackEnded(mediaType: MediaType?, stopPlaying: Boolean) { - this@PlaybackService.onPlaybackEnded(mediaType, stopPlaying) - } - - override fun ensureMediaInfoLoaded(media: Playable) { - if (media is FeedMedia && media.item == null) { - media.setItem(DBReader.getFeedItem(media.itemId)) - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun playerError(event: PlayerErrorEvent?) { - if (mediaPlayer?.playerStatus == PlayerStatus.PLAYING || mediaPlayer?.playerStatus == PlayerStatus.FALLBACK) { - mediaPlayer!!.pause(true, false) - } - stateManager.stopService() - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun bufferUpdate(event: BufferUpdateEvent) { - if (event.hasEnded()) { - val playable = playable - if (this.playable is FeedMedia && playable!!.getDuration() <= 0 && (mediaPlayer?.getDuration()?:0) > 0) { - // Playable is being streamed and does not have a duration specified in the feed - playable.setDuration(mediaPlayer!!.getDuration()) - DBWriter.setFeedMedia(playable as FeedMedia?) - updateNotificationAndMediaSession(playable) - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun sleepTimerUpdate(event: SleepTimerUpdatedEvent) { - when { - event.isOver -> { - mediaPlayer?.pause(true, true) - mediaPlayer?.setVolume(1.0f, 1.0f) - } - event.getTimeLeft() < PlaybackServiceTaskManager.NOTIFICATION_THRESHOLD -> { - val multiplicators = floatArrayOf(0.1f, 0.2f, 0.3f, 0.3f, 0.3f, 0.4f, 0.4f, 0.4f, 0.6f, 0.8f) - val multiplicator = multiplicators[max(0.0, (event.getTimeLeft().toInt() / 1000).toDouble()) - .toInt()] - Log.d(TAG, "onSleepTimerAlmostExpired: $multiplicator") - mediaPlayer?.setVolume(multiplicator, multiplicator) - } - event.isCancelled -> { - mediaPlayer?.setVolume(1.0f, 1.0f) - } - } - } - - private fun getNextInQueue(currentMedia: Playable?): Playable? { - if (currentMedia !is FeedMedia) { - Log.d(TAG, "getNextInQueue(), but playable not an instance of FeedMedia, so not proceeding") - writeNoMediaPlaying() - return null - } - Log.d(TAG, "getNextInQueue()") - if (currentMedia.item == null) { - currentMedia.setItem(DBReader.getFeedItem(currentMedia.itemId)) - } - val item = currentMedia.item - if (item == null) { - Log.w(TAG, "getNextInQueue() with FeedMedia object whose FeedItem is null") - writeNoMediaPlaying() - return null - } - val nextItem = DBReader.getNextInQueue(item) - - if (nextItem?.media == null) { - writeNoMediaPlaying() - return null - } - - if (!isFollowQueue) { - Log.d(TAG, "getNextInQueue(), but follow queue is not enabled.") - writeMediaPlaying(nextItem.media, PlayerStatus.STOPPED, currentitem) - updateNotificationAndMediaSession(nextItem.media) - return null - } - - if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed != null && !nextItem.feed!!.isLocalFeed) { - displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, nextItem.media!!).intent) - writeNoMediaPlaying() - stateManager.stopService() - return null - } - return nextItem.media - } - - /** - * Set of instructions to be performed when playback ends. - */ - private fun onPlaybackEnded(mediaType: MediaType?, stopPlaying: Boolean) { - Log.d(TAG, "Playback ended") - clearCurrentlyPlayingTemporaryPlaybackSpeed() - if (stopPlaying) { - taskManager.cancelPositionSaver() - cancelPositionObserver() - if (!isCasting) { - stateManager.stopForeground(true) - stateManager.stopService() - } - } - if (mediaType == null) { - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_PLAYBACK_END, 0) - } else { - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, - when { - isCasting -> PlaybackServiceInterface.EXTRA_CODE_CAST - mediaType == MediaType.VIDEO -> PlaybackServiceInterface.EXTRA_CODE_VIDEO - else -> PlaybackServiceInterface.EXTRA_CODE_AUDIO - }) - } - } - - /** - * This method processes the media object after its playback ended, either because it completed - * or because a different media object was selected for playback. - * - * - * Even though these tasks aren't supposed to be resource intensive, a good practice is to - * usually call this method on a background thread. - * - * @param playable the media object that was playing. It is assumed that its position - * property was updated before this method was called. - * @param ended if true, it signals that {@param playable} was played until its end. - * In such case, the position property of the media becomes irrelevant for - * most of the tasks (although it's still a good practice to keep it - * accurate). - * @param skipped if the user pressed a skip >| button. - * @param playingNext if true, it means another media object is being loaded in place of this - * one. - * Instances when we'd set it to false would be when we're not following the - * queue or when the queue has ended. - */ - private fun onPostPlayback(playable: Playable?, ended: Boolean, skipped: Boolean, playingNext: Boolean) { - if (playable == null) { - Log.e(TAG, "Cannot do post-playback processing: media was null") - return - } - Log.d(TAG, "onPostPlayback(): media=" + playable.getEpisodeTitle()) - - if (playable !is FeedMedia) { - Log.d(TAG, "Not doing post-playback processing: media not of type FeedMedia") - if (ended) { - playable.onPlaybackCompleted(applicationContext) - } else { - playable.onPlaybackPause(applicationContext) - } -// return - } - val media = playable - val item = (media as? FeedMedia)?.item ?: currentitem - val smartMarkAsPlayed = hasAlmostEnded(media) - if (!ended && smartMarkAsPlayed) { - Log.d(TAG, "smart mark as played") - } - - var autoSkipped = false - if (autoSkippedFeedMediaId != null && autoSkippedFeedMediaId == item?.identifyingValue) { - autoSkippedFeedMediaId = null - autoSkipped = true - } - - if (media is FeedMedia) { - if (ended || smartMarkAsPlayed) { - SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(applicationContext, media, true) - media.onPlaybackCompleted(applicationContext) - } else { - SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(applicationContext, media, false) - media.onPlaybackPause(applicationContext) - } - } - - if (item != null) { - if (ended || smartMarkAsPlayed || autoSkipped || (skipped && !shouldSkipKeepEpisode())) { - // only mark the item as played if we're not keeping it anyways - DBWriter.markItemPlayed(item, FeedItem.PLAYED, ended || (skipped && smartMarkAsPlayed)) - // don't know if it actually matters to not autodownload when smart mark as played is triggered - DBWriter.removeQueueItem(this@PlaybackService, ended, item) - // Delete episode if enabled - val action = item.feed?.preferences?.currentAutoDelete - val shouldAutoDelete = (action == AutoDeleteAction.ALWAYS - || (action == AutoDeleteAction.GLOBAL && item.feed != null && shouldAutoDeleteItemsOnThatFeed(item.feed!!))) - if (media is FeedMedia && shouldAutoDelete && (!item.isTagged(FeedItem.TAG_FAVORITE) || !shouldFavoriteKeepEpisode())) { - DBWriter.deleteFeedMediaOfItem(this@PlaybackService, media.id) - Log.d(TAG, "Episode Deleted") - } -// notifyChildrenChanged(getString(R.string.queue_label)) - } - } - - if (media is FeedMedia && (ended || skipped || playingNext)) { - DBWriter.addItemToPlaybackHistory(media) - } - } - - fun setSleepTimer(waitingTime: Long) { - Log.d(TAG, "Setting sleep timer to $waitingTime milliseconds") - taskManager.setSleepTimer(waitingTime) - } - - fun disableSleepTimer() { - taskManager.disableSleepTimer() - } - - private fun sendNotificationBroadcast(type: Int, code: Int) { - val intent = Intent(PlaybackServiceInterface.ACTION_PLAYER_NOTIFICATION) - intent.putExtra(PlaybackServiceInterface.EXTRA_NOTIFICATION_TYPE, type) - intent.putExtra(PlaybackServiceInterface.EXTRA_NOTIFICATION_CODE, code) - intent.setPackage(packageName) - sendBroadcast(intent) - } - - private fun skipEndingIfNecessary() { - val playable = mediaPlayer?.getPlayable() as? FeedMedia - - val duration = duration - val remainingTime = duration - currentPosition - - val item = playable?.item ?: currentitem ?: return - val feed = item.feed ?: DBReader.getFeed(item.feedId) - val preferences = feed?.preferences - - val skipEnd = preferences?.feedSkipEnding?:0 - val skipEndMS = skipEnd * 1000 -// Log.d(TAG, "skipEndingIfNecessary: checking " + remainingTime + " " + skipEndMS + " speed " + currentPlaybackSpeed) - if (skipEnd > 0 && skipEndMS < this.duration && (remainingTime - skipEndMS < 0)) { - Log.d(TAG, "skipEndingIfNecessary: Skipping the remaining $remainingTime $skipEndMS speed $currentPlaybackSpeed") - val context = applicationContext - val skipMesg = context.getString(R.string.pref_feed_skip_ending_toast, skipEnd) - val toast = Toast.makeText(context, skipMesg, Toast.LENGTH_LONG) - toast.show() - - this.autoSkippedFeedMediaId = item.identifyingValue - mediaPlayer?.skip() - } - } - - /** - * Updates the Media Session for the corresponding status. - * - * @param playerStatus the current [PlayerStatus] - */ - private fun updateMediaSession(playerStatus: PlayerStatus?) { - val sessionState = PlaybackStateCompat.Builder() - val state = if (playerStatus != null) { - when (playerStatus) { - PlayerStatus.PLAYING -> PlaybackStateCompat.STATE_PLAYING - PlayerStatus.FALLBACK -> PlaybackStateCompat.STATE_PLAYING - PlayerStatus.PREPARED, PlayerStatus.PAUSED -> PlaybackStateCompat.STATE_PAUSED - PlayerStatus.STOPPED -> PlaybackStateCompat.STATE_STOPPED - PlayerStatus.SEEKING -> PlaybackStateCompat.STATE_FAST_FORWARDING - PlayerStatus.PREPARING, PlayerStatus.INITIALIZING -> PlaybackStateCompat.STATE_CONNECTING - PlayerStatus.ERROR -> PlaybackStateCompat.STATE_ERROR - PlayerStatus.INITIALIZED, PlayerStatus.INDETERMINATE -> PlaybackStateCompat.STATE_NONE - } - } else { - PlaybackStateCompat.STATE_NONE - } - - sessionState.setState(state, currentPosition.toLong(), currentPlaybackSpeed) - val capabilities = (PlaybackStateCompat.ACTION_PLAY - or PlaybackStateCompat.ACTION_PLAY_PAUSE - or PlaybackStateCompat.ACTION_REWIND - or PlaybackStateCompat.ACTION_PAUSE - or PlaybackStateCompat.ACTION_FAST_FORWARD - or PlaybackStateCompat.ACTION_SEEK_TO - or PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED) - - sessionState.setActions(capabilities) - - // On Android Auto, custom actions are added in the following order around the play button, if no default - // actions are present: Near left, near right, far left, far right, additional actions panel - val rewindBuilder = PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_REWIND, getString(R.string.rewind_label), R.drawable.ic_notification_fast_rewind) - WearMediaSession.addWearExtrasToAction(rewindBuilder) - sessionState.addCustomAction(rewindBuilder.build()) - - val fastForwardBuilder = PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_FAST_FORWARD, getString(R.string.fast_forward_label), R.drawable.ic_notification_fast_forward) - WearMediaSession.addWearExtrasToAction(fastForwardBuilder) - sessionState.addCustomAction(fastForwardBuilder.build()) - - if (showPlaybackSpeedOnFullNotification()) { - sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED, - getString(R.string.playback_speed), R.drawable.ic_notification_playback_speed).build()) - } - - if (showNextChapterOnFullNotification()) { - if (!playable?.getChapters().isNullOrEmpty()) { - sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_NEXT_CHAPTER, - getString(R.string.next_chapter), R.drawable.ic_notification_next_chapter).build()) - } - } - - if (showSkipOnFullNotification()) { - sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_SKIP_TO_NEXT, - getString(R.string.skip_episode_label), R.drawable.ic_notification_skip).build() - ) - } - - if (mediaSession != null) { - WearMediaSession.mediaSessionSetExtraForWear(mediaSession!!) -// mediaSession!!.setPlaybackState(sessionState.build()) - } - } - - private fun updateNotificationAndMediaSession(p: Playable?) { - setupNotification(p) - updateMediaSessionMetadata(p) - } - - private fun updateMediaSessionMetadata(p: Playable?) { - if (p == null || mediaSession == null) return - - val builder = MediaMetadataCompat.Builder() - builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle()) - builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle()) - builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle()) - builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration().toLong()) - builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle()) - builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, p.getFeedTitle()) - - -// if (notificationBuilder.isIconCached) { -// builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, notificationBuilder.cachedIcon) -// } else { -// var iconUri = p.getImageLocation() -// // Don't use embedded cover etc, which Android can't load -// if (p is FeedMedia) { -// val m = p -// if (m.item != null) { -// val item = m.item!! -// when { -// item.imageUrl != null -> { -// iconUri = item.imageUrl -// } -// item.feed != null -> { -// iconUri = item.feed!!.imageUrl -// } -// } -// } -// } -// if (!iconUri.isNullOrEmpty()) { -// builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, iconUri) -// } -// } - - if (stateManager.hasReceivedValidStartCommand()) { - mediaSession!!.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity, - getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT - or (if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0))) -// try { -// mediaSession!!.setMetadata(builder.build()) -// } catch (e: OutOfMemoryError) { -// Log.e(TAG, "Setting media session metadata", e) -// builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, null) -// mediaSession!!.setMetadata(builder.build()) -// } - } - } - - /** - * Used by setupNotification to load notification data in another thread. - */ - private var playableIconLoaderThread: Thread? = null - - /** - * Prepares notification and starts the service in the foreground. - */ - @Synchronized - private fun setupNotification(playable: Playable?) { - Log.d(TAG, "setupNotification") - playableIconLoaderThread?.interrupt() - - if (playable == null || mediaPlayer == null) { - Log.d(TAG, "setupNotification: playable=$playable mediaPlayer=$mediaPlayer") - if (!stateManager.hasReceivedValidStartCommand()) { - stateManager.stopService() - } - return - } - - val playerStatus = mediaPlayer!!.playerStatus -// notificationBuilder.setPlayable(playable) -// if (mediaSession != null) notificationBuilder.setMediaSessionToken(mediaSession!!.getSessionCompatToken()) -// notificationBuilder.playerStatus = playerStatus -// notificationBuilder.updatePosition(currentPosition, currentPlaybackSpeed) - -// val notificationManager = NotificationManagerCompat.from(this) - if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this, - Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - // TODO: Consider calling - // ActivityCompat#requestPermissions -// requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) - // here to request the missing permissions, and then overriding - // public void onRequestPermissionsResult(int requestCode, String[] permissions, - // int[] grantResults) - // to handle the case where the user grants the permission. See the documentation - // for ActivityCompat#requestPermissions for more details. - Log.e(TAG, "setupNotification: require POST_NOTIFICATIONS permission") - Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show() - return - } -// notificationManager.notify(R.id.notification_playing, notificationBuilder.build()) - -// if (!notificationBuilder.isIconCached) { -// playableIconLoaderThread = Thread { -// Log.d(TAG, "Loading notification icon") -// notificationBuilder.loadIcon() -// if (!Thread.currentThread().isInterrupted) { -// notificationManager.notify(R.id.notification_playing, notificationBuilder.build()) -// updateMediaSessionMetadata(playable) -// } -// } -// playableIconLoaderThread?.start() -// } - } - - /** - * Persists the current position and last played time of the media file. - * - * @param fromMediaPlayer if true, the information is gathered from the current Media Player - * and {@param playable} and {@param position} become irrelevant. - * @param playable the playable for which the current position should be saved, unless - * {@param fromMediaPlayer} is true. - * @param position the position that should be saved, unless {@param fromMediaPlayer} is true. - */ - @Synchronized - private fun saveCurrentPosition(fromMediaPlayer: Boolean, playable: Playable?, position: Int) { - var playable = playable - var position = position - val duration: Int - if (fromMediaPlayer) { - position = currentPosition - duration = this.duration - playable = mediaPlayer?.getPlayable() - } else { - duration = playable?.getDuration() ?: Playable.INVALID_TIME - } - if (position != Playable.INVALID_TIME && duration != Playable.INVALID_TIME && playable != null) { -// Log.d(TAG, "Saving current position to $position $duration") - saveCurrentPosition(playable, position, System.currentTimeMillis()) - } - } - - fun sleepTimerActive(): Boolean { - return taskManager.isSleepTimerActive - } - - val sleepTimerTimeLeft: Long - get() = taskManager.sleepTimerTimeLeft - - private fun bluetoothNotifyChange(info: PSMPInfo?, whatChanged: String) { - var isPlaying = false - - if (info?.playerStatus == PlayerStatus.PLAYING || info?.playerStatus == PlayerStatus.FALLBACK) { - isPlaying = true - } - - if (info?.playable != null) { - val i = Intent(whatChanged) - i.putExtra("id", 1L) - i.putExtra("artist", "") - i.putExtra("album", info.playable!!.getFeedTitle()) - i.putExtra("track", info.playable!!.getEpisodeTitle()) - i.putExtra("playing", isPlaying) - i.putExtra("duration", info.playable!!.getDuration().toLong()) - i.putExtra("position", info.playable!!.getPosition().toLong()) - sendBroadcast(i) - } - } - - private val autoStateUpdated: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val status = intent.getStringExtra("media_connection_status") - val isConnectedToCar = "media_connected" == status - Log.d(TAG, "Received Auto Connection update: $status") - if (!isConnectedToCar) { - Log.d(TAG, "Car was unplugged during playback.") - } else { - val playerStatus = mediaPlayer?.playerStatus - when (playerStatus) { - PlayerStatus.PAUSED, PlayerStatus.PREPARED -> { - mediaPlayer?.resume() - } - PlayerStatus.PREPARING -> { - mediaPlayer?.setStartWhenPrepared(!mediaPlayer!!.isStartWhenPrepared()) - } - PlayerStatus.INITIALIZED -> { - mediaPlayer?.setStartWhenPrepared(true) - mediaPlayer?.prepare() - } - else -> {} - } - } - } - } - - /** - * Pauses playback when the headset is disconnected and the preference is - * set - */ - private val headsetDisconnected: BroadcastReceiver = object : BroadcastReceiver() { - private val TAG = "headsetDisconnected" - private val UNPLUGGED = 0 - private val PLUGGED = 1 - - override fun onReceive(context: Context, intent: Intent) { - if (isInitialStickyBroadcast) { - // Don't pause playback after we just started, just because the receiver - // delivers the current headset state (instead of a change) - return - } - - if (TextUtils.equals(intent.action, Intent.ACTION_HEADSET_PLUG)) { - val state = intent.getIntExtra("state", -1) - Log.d(TAG, "Headset plug event. State is $state") - if (state != -1) { - when (state) { - UNPLUGGED -> { - Log.d(TAG, "Headset was unplugged during playback.") - } - PLUGGED -> { - Log.d(TAG, "Headset was plugged in during playback.") - unpauseIfPauseOnDisconnect(false) - } - } - } else { - Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent") - } - } - } - } - - private val bluetoothStateUpdated: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (TextUtils.equals(intent.action, BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) { - val state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1) - if (state == BluetoothA2dp.STATE_CONNECTED) { - Log.d(TAG, "Received bluetooth connection intent") - unpauseIfPauseOnDisconnect(true) - } - } - } - } - - private val audioBecomingNoisy: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - // sound is about to change, eg. bluetooth -> speaker - Log.d(TAG, "Pausing playback because audio is becoming noisy") - pauseIfPauseOnDisconnect() - } - } - - /** - * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. - */ - private fun pauseIfPauseOnDisconnect() { - Log.d(TAG, "pauseIfPauseOnDisconnect()") - transientPause = (mediaPlayer?.playerStatus == PlayerStatus.PLAYING || mediaPlayer?.playerStatus == PlayerStatus.FALLBACK) - if (isPauseOnHeadsetDisconnect && !isCasting) { - mediaPlayer?.pause(!isPersistNotify, false) - } - } - - /** - * @param bluetooth true if the event for unpausing came from bluetooth - */ - private fun unpauseIfPauseOnDisconnect(bluetooth: Boolean) { - if (mediaPlayer != null && mediaPlayer!!.isAudioChannelInUse) { - Log.d(TAG, "unpauseIfPauseOnDisconnect() audio is in use") - return - } - if (transientPause) { - transientPause = false - if (Build.VERSION.SDK_INT >= 31) { - stateManager.stopService() - return - } - when { - !bluetooth && isUnpauseOnHeadsetReconnect -> { - mediaPlayer?.resume() - } - bluetooth && isUnpauseOnBluetoothReconnect -> { - // let the user know we've started playback again... - val v = applicationContext.getSystemService(VIBRATOR_SERVICE) as? Vibrator - v?.vibrate(500) - mediaPlayer?.resume() - } - } - } - } - - private val shutdownReceiver: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (TextUtils.equals(intent.action, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { - EventBus.getDefault().post(PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN)) - stateManager.stopService() - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun volumeAdaptionChanged(event: VolumeAdaptionChangedEvent) { - val playbackVolumeUpdater = PlaybackVolumeUpdater() - if (mediaPlayer != null) playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer!!, event.feedId, event.volumeAdaptionSetting) - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun speedPresetChanged(event: SpeedPresetChangedEvent) { -// TODO: speed - val item = (playable as? FeedMedia)?.item ?: currentitem -// if (playable is FeedMedia) { - if (item?.feed?.id == event.feedId) { - if (event.speed == FeedPreferences.SPEED_USE_GLOBAL) { - setSpeed(getPlaybackSpeed(playable!!.getMediaType())) - } else { - setSpeed(event.speed) - } - } -// } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun skipIntroEndingPresetChanged(event: SkipIntroEndingChangedEvent) { - val item = (playable as? FeedMedia)?.item ?: currentitem -// if (playable is FeedMedia) { - if (item?.feed?.id == event.feedId) { - val feedPreferences = item.feed?.preferences - if (feedPreferences != null) { - Log.d(TAG, "skipIntroEndingPresetChanged ${event.skipIntro} ${event.skipEnding}") - feedPreferences.feedSkipIntro = event.skipIntro - feedPreferences.feedSkipEnding = event.skipEnding - } - } -// } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEvenStartPlay(event: StartPlayEvent) { - Log.d(TAG, "onEvenStartPlay ${event.item.title}") - currentitem = event.item - } - - - fun resume() { - mediaPlayer?.resume() - taskManager.restartSleepTimer() - } - - fun prepare() { - mediaPlayer?.prepare() - taskManager.restartSleepTimer() - } - - fun pause(abandonAudioFocus: Boolean, reinit: Boolean) { - mediaPlayer?.pause(abandonAudioFocus, reinit) - isSpeedForward = false - isFallbackSpeed = false - } - - val pSMPInfo: PSMPInfo - get() = mediaPlayer!!.pSMPInfo - - val status: PlayerStatus - get() = mediaPlayer!!.playerStatus - - val playable: Playable? - get() = mediaPlayer?.getPlayable() - - fun setSpeed(speed: Float, codeArray: BooleanArray? = null) { - isSpeedForward = false - isFallbackSpeed = false - - if (currentMediaType == MediaType.VIDEO) { - currentlyPlayingTemporaryPlaybackSpeed = speed - videoPlaybackSpeed = speed - mediaPlayer?.setPlaybackParams(speed, isSkipSilence) - } else { - if (codeArray != null && codeArray.size == 3) { - Log.d(TAG, "setSpeed codeArray: ${codeArray[0]} ${codeArray[1]} ${codeArray[2]}") - if (codeArray[2]) setPlaybackSpeed(speed) - if (codeArray[1]) { - var item = (playable as? FeedMedia)?.item ?: currentitem -// var item = (playable as FeedMedia).item - if (item == null) { - val itemId = (playable as? FeedMedia)?.itemId - if (itemId != null) item = DBReader.getFeedItem(itemId) - } - if (item != null) { - var feed = item.feed - if (feed == null) { - feed = DBReader.getFeed(item.feedId) - } - if (feed != null) { - val feedPreferences = feed.preferences - if (feedPreferences != null) { - feedPreferences.feedPlaybackSpeed = speed - Log.d(TAG, "setSpeed ${feed.title} $speed") - DBWriter.setFeedPreferences(feedPreferences) - EventBus.getDefault().post(SpeedPresetChangedEvent(feedPreferences.feedPlaybackSpeed, feed.id)) - } - } - } - } - if (codeArray[0]) { - currentlyPlayingTemporaryPlaybackSpeed = speed - mediaPlayer?.setPlaybackParams(speed, isSkipSilence) - } - } else { - currentlyPlayingTemporaryPlaybackSpeed = speed - mediaPlayer?.setPlaybackParams(speed, isSkipSilence) - } - } - } - - fun speedForward(speed: Float) { - if (mediaPlayer == null || isFallbackSpeed) return - - if (!isSpeedForward) { - normalSpeed = mediaPlayer!!.getPlaybackSpeed() - mediaPlayer!!.setPlaybackParams(speed, isSkipSilence) - } else { - mediaPlayer!!.setPlaybackParams(normalSpeed, isSkipSilence) - } - isSpeedForward = !isSpeedForward - } - - fun fallbackSpeed(speed: Float) { - if (mediaPlayer == null || isSpeedForward) return - - if (!isFallbackSpeed) { - normalSpeed = mediaPlayer!!.getPlaybackSpeed() - mediaPlayer!!.setPlaybackParams(speed, isSkipSilence) - } else { - mediaPlayer!!.setPlaybackParams(normalSpeed, isSkipSilence) - } - isFallbackSpeed = !isFallbackSpeed - } - - fun skipSilence(skipSilence: Boolean) { - mediaPlayer?.setPlaybackParams(currentPlaybackSpeed, skipSilence) - } - - val currentPlaybackSpeed: Float - get() { - return mediaPlayer?.getPlaybackSpeed() ?: 1.0f - } - - var isStartWhenPrepared: Boolean - get() = mediaPlayer?.isStartWhenPrepared() ?: false - set(s) { - mediaPlayer?.setStartWhenPrepared(s) - } - - fun seekTo(t: Int) { - mediaPlayer?.seekTo(t) - EventBus.getDefault().post(PlaybackPositionEvent(t, duration)) - } - - private fun seekDelta(d: Int) { - mediaPlayer?.seekDelta(d) - } - - val duration: Int - /** - * call getDuration() on mediaplayer or return INVALID_TIME if player is in - * an invalid state. - */ - get() { - return mediaPlayer?.getDuration() ?: Playable.INVALID_TIME - } - - val currentPosition: Int - /** - * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player - * is in an invalid state. - */ - get() { - return mediaPlayer?.getPosition() ?: Playable.INVALID_TIME - } - - val audioTracks: List - get() { - return mediaPlayer?.getAudioTracks() ?: listOf() - } - - val selectedAudioTrack: Int - get() { - return mediaPlayer?.getSelectedAudioTrack() ?: -1 - } - - fun setAudioTrack(track: Int) { - mediaPlayer?.setAudioTrack(track) - } - - val isStreaming: Boolean - get() = mediaPlayer?.isStreaming() ?: false - - val videoSize: Pair? - get() = mediaPlayer?.getVideoSize() - - private fun setupPositionObserver() { - positionEventTimer?.dispose() - - Log.d(TAG, "Setting up position observer") - positionEventTimer = Observable.interval(POSITION_EVENT_INTERVAL, TimeUnit.SECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - Log.d(TAG, "notificationBuilder.updatePosition currentPosition: $currentPosition, currentPlaybackSpeed: $currentPlaybackSpeed") - EventBus.getDefault().post(PlaybackPositionEvent(currentPosition, duration)) -// TODO: why set SDK_INT < 29 - if (Build.VERSION.SDK_INT < 29) { -// notificationBuilder.updatePosition(currentPosition, currentPlaybackSpeed) -// val notificationManager = getSystemService(NOTIFICATION_SERVICE) as? NotificationManager -// notificationManager?.notify(R.id.notification_playing, notificationBuilder.build()) - } - skipEndingIfNecessary() - } - } - - private fun cancelPositionObserver() { - positionEventTimer?.dispose() - } - - private fun addPlayableToQueue(playable: Playable?) { - if (playable is FeedMedia) { - val itemId = playable.item?.id ?: return - DBWriter.addQueueItem(this, false, true, itemId) -// notifyChildrenChanged(getString(R.string.queue_label)) - } - } - -// private val sessionCallback: MediaSession.Callback = object : MediaSession.Callback { -// private val TAG = "MediaSessionCompat" -//// TODO: not used now with media3 -// } - - companion object { - private const val TAG = "PlaybackService" - -// TODO: need to experiment this value - private const val POSITION_EVENT_INTERVAL = 5L - - const val ACTION_PLAYER_STATUS_CHANGED: String = "action.ac.mdiq.podcini.service.playerStatusChanged" - private const val AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged" - private const val AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged" - - /** - * Custom actions used by Android Wear, Android Auto, and Android (API 33+ only) - */ - private const val CUSTOM_ACTION_SKIP_TO_NEXT = "action.ac.mdiq.podcini.service.skipToNext" - private const val CUSTOM_ACTION_FAST_FORWARD = "action.ac.mdiq.podcini.service.fastForward" - private const val CUSTOM_ACTION_REWIND = "action.ac.mdiq.podcini.service.rewind" - private const val CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED = "action.ac.mdiq.podcini.service.changePlaybackSpeed" - const val CUSTOM_ACTION_NEXT_CHAPTER: String = "action.ac.mdiq.podcini.service.next_chapter" - - /** - * Set a max number of episodes to load for Android Auto, otherwise there could be performance issues - */ - const val MAX_ANDROID_AUTO_EPISODES_PER_FEED: Int = 100 - - /** - * Is true if service is running. - */ - @JvmField - var isRunning: Boolean = false - - /** - * Is true if the service was running, but paused due to headphone disconnect - */ - private var transientPause = false - - /** - * Is true if a Cast Device is connected to the service. - */ - @JvmStatic - @Volatile - var isCasting: Boolean = false - private set - - @Volatile - var currentMediaType: MediaType? = MediaType.UNKNOWN - private set - - /** - * Returns an intent which starts an audio- or videoplayer, depending on the - * type of media that is being played. If the playbackservice is not - * running, the type of the last played media will be looked up. - */ - @JvmStatic - fun getPlayerActivityIntent(context: Context): Intent { - val showVideoPlayer = if (isRunning) { - currentMediaType == MediaType.VIDEO && !isCasting - } else { - currentEpisodeIsVideo - } - - return if (showVideoPlayer) { - VideoPlayerActivityStarter(context).intent - } else { - MainActivityStarter(context).withOpenPlayer().getIntent() - } - } - - /** - * Same as [.getPlayerActivityIntent], but here the type of activity - * depends on the medaitype that is provided as an argument. - */ - @JvmStatic - fun getPlayerActivityIntent(context: Context, mediaType: MediaType?): Intent { - return if (mediaType == MediaType.VIDEO && !isCasting) { - VideoPlayerActivityStarter(context).intent - } else { - MainActivityStarter(context).withOpenPlayer().getIntent() - } - } - - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceInterface.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceInterface.kt index e5779d2c..e3111fd1 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceInterface.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceInterface.kt @@ -5,8 +5,7 @@ object PlaybackServiceInterface { const val EXTRA_ALLOW_STREAM_THIS_TIME: String = "extra.ac.mdiq.podcini.service.allowStream" const val EXTRA_ALLOW_STREAM_ALWAYS: String = "extra.ac.mdiq.podcini.service.allowStreamAlways" - const val ACTION_PLAYER_NOTIFICATION - : String = "action.ac.mdiq.podcini.service.playerNotification" + const val ACTION_PLAYER_NOTIFICATION: String = "action.ac.mdiq.podcini.service.playerNotification" const val EXTRA_NOTIFICATION_CODE: String = "extra.ac.mdiq.podcini.service.notificationCode" const val EXTRA_NOTIFICATION_TYPE: String = "extra.ac.mdiq.podcini.service.notificationType" const val NOTIFICATION_TYPE_PLAYBACK_END: Int = 7 @@ -15,6 +14,5 @@ object PlaybackServiceInterface { const val EXTRA_CODE_VIDEO: Int = 2 const val EXTRA_CODE_CAST: Int = 3 - const val ACTION_SHUTDOWN_PLAYBACK_SERVICE - : String = "action.ac.mdiq.podcini.service.actionShutdownPlaybackService" + const val ACTION_SHUTDOWN_PLAYBACK_SERVICE: String = "action.ac.mdiq.podcini.service.actionShutdownPlaybackService" } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceNotificationBuilder.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceNotificationBuilder.kt index 81441bab..d52bc1cd 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceNotificationBuilder.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceNotificationBuilder.kt @@ -28,6 +28,7 @@ import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.util.Converter import org.apache.commons.lang3.ArrayUtils +// TODO: not needed with media3 @UnstableApi class PlaybackServiceNotificationBuilder(private val context: Context) { private var playable: Playable? = null diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceStateManager.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceStateManager.kt index 4035a35d..af4a0f40 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceStateManager.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceStateManager.kt @@ -7,6 +7,7 @@ import android.util.Log import androidx.core.app.ServiceCompat import kotlin.concurrent.Volatile +// TODO: not needed with media3 internal class PlaybackServiceStateManager(private val playbackService: PlaybackService) { @Volatile private var isInForeground = false diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceTaskManager.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceTaskManager.kt index 977adc11..c4b11eb6 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceTaskManager.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceTaskManager.kt @@ -31,9 +31,7 @@ import kotlin.concurrent.Volatile * The PlaybackServiceTaskManager(PSTM) uses a callback object (PSTMCallback) * to notify the PlaybackService about updates from the running tasks. */ -class PlaybackServiceTaskManager(private val context: Context, - private val callback: PSTMCallback -) { +class PlaybackServiceTaskManager(private val context: Context, private val callback: PSTMCallback) { private val schedExecutor: ScheduledThreadPoolExecutor private var positionSaverFuture: ScheduledFuture<*>? = null @@ -67,10 +65,8 @@ class PlaybackServiceTaskManager(private val context: Context, if (!isPositionSaverActive) { var positionSaver = Runnable { callback.positionSaverTick() } positionSaver = useMainThreadIfNecessary(positionSaver) - positionSaverFuture = - schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL.toLong(), - POSITION_SAVER_WAITING_INTERVAL.toLong(), TimeUnit.MILLISECONDS) - + positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL.toLong(), + POSITION_SAVER_WAITING_INTERVAL.toLong(), TimeUnit.MILLISECONDS) Log.d(TAG, "Started PositionSaver") } else { Log.d(TAG, "Call to startPositionSaver was ignored.") @@ -103,10 +99,8 @@ class PlaybackServiceTaskManager(private val context: Context, if (!isWidgetUpdaterActive && !schedExecutor.isShutdown) { var widgetUpdater = Runnable { this.requestWidgetUpdate() } widgetUpdater = useMainThreadIfNecessary(widgetUpdater) - widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, - WIDGET_UPDATER_NOTIFICATION_INTERVAL.toLong(), - WIDGET_UPDATER_NOTIFICATION_INTERVAL.toLong(), - TimeUnit.MILLISECONDS) + widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL.toLong(), + WIDGET_UPDATER_NOTIFICATION_INTERVAL.toLong(), TimeUnit.MILLISECONDS) Log.d(TAG, "Started WidgetUpdater") } else { Log.d(TAG, "Call to startWidgetUpdater was ignored.") @@ -119,11 +113,8 @@ class PlaybackServiceTaskManager(private val context: Context, @Synchronized fun requestWidgetUpdate() { val state = callback.requestWidgetState() - if (!schedExecutor.isShutdown) { - schedExecutor.execute { WidgetUpdater.updateWidget(context, state) } - } else { - Log.d(TAG, "Call to requestWidgetUpdate was ignored.") - } + if (!schedExecutor.isShutdown) schedExecutor.execute { WidgetUpdater.updateWidget(context, state) } + else Log.d(TAG, "Call to requestWidgetUpdate was ignored.") } /** @@ -138,9 +129,7 @@ class PlaybackServiceTaskManager(private val context: Context, require(waitingTime > 0) { "Waiting time <= 0" } Log.d(TAG, "Setting sleep timer to $waitingTime milliseconds") - if (isSleepTimerActive) { - sleepTimerFuture!!.cancel(true) - } + if (isSleepTimerActive) sleepTimerFuture!!.cancel(true) sleepTimer = SleepTimer(waitingTime) sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS) EventBus.getDefault().post(SleepTimerUpdatedEvent.justEnabled(waitingTime)) @@ -181,11 +170,7 @@ class PlaybackServiceTaskManager(private val context: Context, /** * Returns the current sleep timer time or 0 if the sleep timer is not active. */ - get() = if (isSleepTimerActive) { - sleepTimer!!.getWaitingTime() - } else { - 0 - } + get() = if (isSleepTimerActive) sleepTimer!!.getWaitingTime() else 0 @get:Synchronized val isWidgetUpdaterActive: Boolean @@ -224,8 +209,7 @@ class PlaybackServiceTaskManager(private val context: Context, .observeOn(AndroidSchedulers.mainThread()) .subscribe({ callback.onChapterLoaded(media) }, { throwable: Throwable? -> - Log.d(TAG, - "Error loading chapters: " + Log.getStackTraceString(throwable)) + Log.d(TAG, "Error loading chapters: " + Log.getStackTraceString(throwable)) }) } } @@ -299,9 +283,7 @@ class PlaybackServiceTaskManager(private val context: Context, hasVibrated = true } } - if (shakeListener == null && SleepTimerPreferences.shakeToReset()) { - shakeListener = ShakeListener(context, this) - } + if (shakeListener == null && SleepTimerPreferences.shakeToReset()) shakeListener = ShakeListener(context, this) } if (timeLeft <= 0) { Log.d(TAG, "Sleep timer expired") @@ -329,9 +311,8 @@ class PlaybackServiceTaskManager(private val context: Context, fun cancel() { sleepTimerFuture!!.cancel(true) - if (shakeListener != null) { - shakeListener!!.pause() - } + shakeListener?.pause() + EventBus.getDefault().post(SleepTimerUpdatedEvent.cancelled()) } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackVolumeUpdater.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackVolumeUpdater.kt index 03c826d3..d5c2c24c 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackVolumeUpdater.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackVolumeUpdater.kt @@ -10,9 +10,7 @@ internal class PlaybackVolumeUpdater { volumeAdaptionSetting: VolumeAdaptionSetting) { val playable = mediaPlayer.getPlayable() - if (playable is FeedMedia) { - updateFeedMediaVolumeIfNecessary(mediaPlayer, feedId, volumeAdaptionSetting, playable) - } + if (playable is FeedMedia) updateFeedMediaVolumeIfNecessary(mediaPlayer, feedId, volumeAdaptionSetting, playable) } private fun updateFeedMediaVolumeIfNecessary(mediaPlayer: PlaybackServiceMediaPlayer, feedId: Long, @@ -21,9 +19,7 @@ internal class PlaybackVolumeUpdater { val preferences = feedMedia.item!!.feed!!.preferences if (preferences != null) preferences.volumeAdaptionSetting = volumeAdaptionSetting - if (mediaPlayer.playerStatus == PlayerStatus.PLAYING) { - forceUpdateVolume(mediaPlayer) - } + if (mediaPlayer.playerStatus == PlayerStatus.PLAYING) forceUpdateVolume(mediaPlayer) } } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/QuickSettingsTileService.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/QuickSettingsTileService.kt index 6b4df506..55805172 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/QuickSettingsTileService.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/QuickSettingsTileService.kt @@ -25,8 +25,7 @@ class QuickSettingsTileService : TileService() { super.onClick() val intent = Intent(this, MediaButtonReceiver::class.java) intent.setAction(MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER) - intent.putExtra(Intent.EXTRA_KEY_EVENT, - KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)) + intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)) sendBroadcast(intent) } @@ -47,11 +46,8 @@ class QuickSettingsTileService : TileService() { if (qsTile == null) { Log.d(TAG, "Ignored call to update QS tile: getQsTile() returned null.") } else { - val isPlaying = (PlaybackService.isRunning - && PlaybackPreferences.currentPlayerStatus - == PlaybackPreferences.PLAYER_STATUS_PLAYING) - qsTile.state = - if (isPlaying) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE + val isPlaying = (PlaybackService.isRunning && PlaybackPreferences.currentPlayerStatus == PlaybackPreferences.PLAYER_STATUS_PLAYING) + qsTile.state = if (isPlaying) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE qsTile.updateTile() } } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/ShakeListener.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/ShakeListener.kt index 403ae12c..8f2c10d1 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/ShakeListener.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/ShakeListener.kt @@ -21,9 +21,8 @@ internal class ShakeListener(private val mContext: Context, private val mSleepTi // only a precaution, the user should actually not be able to activate shake to reset // when the accelerometer is not available mSensorMgr = mContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager - if (mSensorMgr == null) { - throw UnsupportedOperationException("Sensors not supported") - } + if (mSensorMgr == null) throw UnsupportedOperationException("Sensors not supported") + mAccelerometer = mSensorMgr!!.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) if (!mSensorMgr!!.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_UI)) { // if not supported mSensorMgr!!.unregisterListener(this) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt index fcf9275a..2713c7d6 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt @@ -8,6 +8,7 @@ import ac.mdiq.podcini.net.download.FeedUpdateManager.runOnceOrAsk import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink import ac.mdiq.podcini.playback.cast.CastEnabledActivity +import ac.mdiq.podcini.playback.service.PlaybackService import ac.mdiq.podcini.preferences.ThemeSwitcher.getNoTitleTheme import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.backButtonOpensDrawer @@ -27,6 +28,7 @@ import ac.mdiq.podcini.util.event.FeedUpdateRunningEvent import ac.mdiq.podcini.util.event.MessageEvent import android.Manifest import android.annotation.SuppressLint +import android.content.ComponentName import android.content.Context import android.content.DialogInterface import android.content.Intent @@ -53,6 +55,8 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentContainerView import androidx.media3.common.util.UnstableApi +import androidx.media3.session.MediaController +import androidx.media3.session.SessionToken import androidx.recyclerview.widget.RecyclerView import androidx.work.WorkInfo import androidx.work.WorkManager @@ -62,6 +66,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import com.google.common.util.concurrent.MoreExecutors import org.apache.commons.lang3.ArrayUtils import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -463,6 +468,17 @@ class MainActivity : CastEnabledActivity() { super.onStart() EventBus.getDefault().register(this) RatingDialog.init(this) + + val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java)) + val controllerFuture = MediaController.Builder(this, sessionToken).buildAsync() + controllerFuture.addListener({ + // Call controllerFuture.get() to retrieve the MediaController. + // MediaController implements the Player interface, so it can be + // attached to the PlayerView UI component. +// playerView.setPlayer(controllerFuture.get()) + }, + MoreExecutors.directExecutor() + ) } override fun onResume() { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index 82fb23dc..6e13bac4 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -104,10 +104,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar private var currentMedia: Playable? = null private var currentitem: FeedItem? = null - override fun onCreateView(inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) _binding = AudioplayerFragmentBinding.inflate(inflater) @@ -143,8 +140,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar .replace(R.id.playerFragment2, playerFragment2!!, InternalPlayerFragment.TAG) .commit() playerView2 = binding.root.findViewById(R.id.playerFragment2) - playerView2.setBackgroundColor( - SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density)) + playerView2.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density)) cardViewSeek = binding.cardViewSeek txtvSeek = binding.txtvSeek @@ -188,9 +184,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar @Subscribe(threadMode = ThreadMode.MAIN) fun onPlaybackServiceChanged(event: PlaybackServiceEvent) { - if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) { + if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) (activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED - } } // private fun setupLengthTextView() { @@ -223,9 +218,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar disposable = Maybe.create { emitter: MaybeEmitter -> val media: Playable? = theMedia if (media != null) { - if (includingChapters) { - ChapterUtils.loadChapters(media, requireContext(), false) - } + if (includingChapters) ChapterUtils.loadChapters(media, requireContext(), false) emitter.onSuccess(media) } else { emitter.onComplete() @@ -238,9 +231,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar updateUi(media) playerFragment1?.updateUi(media) playerFragment2?.updateUi(media) - if (!includingChapters) { - loadMediaInfo(true) - } + if (!includingChapters) loadMediaInfo(true) }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }, { updateUi(null) @@ -276,9 +267,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar @Subscribe(threadMode = ThreadMode.MAIN) @Suppress("unused") fun sleepTimerUpdate(event: SleepTimerUpdatedEvent) { - if (event.isCancelled || event.wasJustEnabled()) { - this@AudioPlayerFragment.loadMediaInfo(false) - } + if (event.isCancelled || event.wasJustEnabled()) this@AudioPlayerFragment.loadMediaInfo(false) } override fun onCreate(savedInstanceState: Bundle?) { @@ -354,15 +343,12 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar // updateUi(controller!!.getMedia()) // sbPosition.highlightCurrentChapter() // } - txtvSeek.text = controller!!.getMedia()?.getChapters()?.get(newChapterIndex)?.title ?: ("" - + "\n" + Converter.getDurationStringLong(position)) + txtvSeek.text = controller!!.getMedia()?.getChapters()?.get(newChapterIndex)?.title ?: ("\n${Converter.getDurationStringLong(position)}") } else { txtvSeek.text = Converter.getDurationStringLong(position) } } - duration != controller!!.duration -> { - updateUi(controller!!.getMedia()) - } + duration != controller!!.duration -> updateUi(controller!!.getMedia()) } } @@ -396,9 +382,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar } fun setupOptionsMenu(media: Playable?) { - if (toolbar.menu.size() == 0) { - toolbar.inflateMenu(R.menu.mediaplayer) - } + if (toolbar.menu.size() == 0) toolbar.inflateMenu(R.menu.mediaplayer) val isFeedMedia = media is FeedMedia toolbar.menu?.findItem(R.id.open_feed_item)?.setVisible(isFeedMedia) @@ -422,9 +406,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar var feedItem = currentitem if (feedItem == null && media is FeedMedia) feedItem = media.item // feedItem: FeedItem? = if (media is FeedMedia) media.item else null - if (feedItem != null && FeedItemMenuHandler.onMenuItemClicked(this, menuItem.itemId, feedItem)) { - return true - } + if (feedItem != null && FeedItemMenuHandler.onMenuItemClicked(this, menuItem.itemId, feedItem)) return true val itemId = menuItem.itemId when (itemId) { @@ -510,8 +492,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar private var disposable: Disposable? = null @UnstableApi - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = InternalPlayerFragmentBinding.inflate(inflater) Log.d(TAG, "fragment onCreateView") @@ -610,8 +591,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar } } butFF.setOnLongClickListener { - SkipPreferenceDialog.showSkipPreference(requireContext(), - SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF) + SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF) true } butSkip.setOnClickListener { @@ -630,9 +610,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar @OptIn(UnstableApi::class) private fun setupLengthTextView() { showTimeLeft = UserPreferences.shouldShowRemainingTime() txtvLength.setOnClickListener(View.OnClickListener { - if (controller == null) { - return@OnClickListener - } + if (controller == null) return@OnClickListener showTimeLeft = !showTimeLeft UserPreferences.setShowRemainTimeSetting(showTimeLeft) onPositionObserverUpdate(PlaybackPositionEvent(controller!!.position, controller!!.duration)) @@ -649,9 +627,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) fun onPositionObserverUpdate(event: PlaybackPositionEvent) { - if (controller == null || controller!!.position == Playable.INVALID_TIME || controller!!.duration == Playable.INVALID_TIME) { - return - } + if (controller == null || controller!!.position == Playable.INVALID_TIME || controller!!.duration == Playable.INVALID_TIME) return + val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier) val currentPosition: Int = converter.convert(event.position) val duration: Int = converter.convert(event.duration) @@ -684,12 +661,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) fun onPlaybackServiceChanged(event: PlaybackServiceEvent) { when (event.action) { - PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN -> { - (activity as MainActivity).setPlayerVisible(false) - } - PlaybackServiceEvent.Action.SERVICE_STARTED -> { - (activity as MainActivity).setPlayerVisible(true) - } + PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN -> (activity as MainActivity).setPlayerVisible(false) + PlaybackServiceEvent.Action.SERVICE_STARTED -> (activity as MainActivity).setPlayerVisible(true) } } @@ -703,9 +676,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar super.onStart() txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()) txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong()) - if (UserPreferences.speedforwardSpeed > 0.1f) { - txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed) - } else txtvSkip.visibility = View.GONE + if (UserPreferences.speedforwardSpeed > 0.1f) txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed) + else txtvSkip.visibility = View.GONE val media = controller?.getMedia() ?: return updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media))) } diff --git a/changelog.md b/changelog.md index c5da6bf2..b43db30a 100644 --- a/changelog.md +++ b/changelog.md @@ -288,4 +288,9 @@ * reader mode of episode home view observes the theme of the app * reader mode content of episode home view is cached so that subsequent loading is quicker -* episode home reader content can be switched on in player detailed view from the action bar \ No newline at end of file +* episode home reader content can be switched on in player detailed view from the action bar + +## 4.9.2 + +* fixed the action buttons on notification widget. bit strange with the order though as they appear different on my Android 9 and Android 14 devices +* media3 requires quite some logic change, so be mindful with any issues \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/3020133.txt b/fastlane/metadata/android/en-US/changelogs/3020133.txt new file mode 100644 index 00000000..f711495a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020133.txt @@ -0,0 +1,5 @@ + +Version 4.9.2 brings several changes: + +* fixed the action buttons on notification widget. bit strange with the order though as they appear different on my Android 9 and Android 14 devices +* media3 requires quite some logic change, so be mindful with any issues