Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class PlaybackNotificationBroadcaster(
notificationsConfiguration: NotificationsConfiguration,
private val nowPlayingNotificationContentBuilder: BuildNowPlayingNotificationContent,
private val playbackStartingNotification: BuildPlaybackStartingNotification,
) : PlaybackNotificationRouter(registerApplicationMessages), AutoCloseable {
) : PlaybackNotificationRouter(registerApplicationMessages) {

private val notificationId = notificationsConfiguration.notificationId
private val notificationSync = Any()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import android.media.session.PlaybackState
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.RatingCompat
import android.support.v4.media.session.PlaybackStateCompat
import com.lasthopesoftware.bluewater.android.intents.BuildIntents
import com.lasthopesoftware.bluewater.client.browsing.files.ServiceFile
import com.lasthopesoftware.bluewater.client.browsing.files.properties.FilePropertyHelpers.durationInMs
import com.lasthopesoftware.bluewater.client.browsing.files.properties.ProvideLibraryFileProperties
import com.lasthopesoftware.bluewater.client.browsing.library.access.session.ProvideSelectedLibraryId
import com.lasthopesoftware.bluewater.client.browsing.library.repository.LibraryId
import com.lasthopesoftware.bluewater.client.playback.nowplaying.broadcasters.PlaybackNotificationRouter
import com.lasthopesoftware.bluewater.client.playback.nowplaying.storage.GetNowPlayingState
Expand All @@ -18,9 +20,13 @@ import com.lasthopesoftware.bluewater.shared.images.bytes.GetImageBytes
import com.lasthopesoftware.bluewater.shared.lazyLogger
import com.lasthopesoftware.bluewater.shared.messages.application.RegisterForApplicationMessages
import com.lasthopesoftware.bluewater.shared.messages.registerReceiver
import com.lasthopesoftware.bluewater.shared.updateIfDifferent
import com.lasthopesoftware.promises.extensions.toPromise
import com.lasthopesoftware.resources.bitmaps.ProduceBitmaps
import com.lasthopesoftware.resources.closables.PromisingCloseable
import com.namehillsoftware.handoff.promises.Promise
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference

private val logger by lazyLogger<MediaSessionBroadcaster>()
private const val playbackSpeed = 1.0f
Expand All @@ -38,8 +44,10 @@ class MediaSessionBroadcaster(
private val imageProvider: GetImageBytes,
private val bitmapProducer: ProduceBitmaps,
private val mediaSession: ControlMediaSession,
private val selectedLibraryIdProvider: ProvideSelectedLibraryId,
private val intentBuilder: BuildIntents,
applicationMessages: RegisterForApplicationMessages,
) : PlaybackNotificationRouter(applicationMessages) {
) : PlaybackNotificationRouter(applicationMessages), PromisingCloseable {

private val trackPositionUpdatesSubscription = applicationMessages.registerReceiver { m: TrackPositionUpdate ->
updateTrackPosition(m.filePosition.millis)
Expand All @@ -48,15 +56,15 @@ class MediaSessionBroadcaster(
@Volatile
private var playbackState = PlaybackStateCompat.STATE_STOPPED

@Volatile
private var trackPosition = AtomicLong(0)
private val trackPosition = AtomicLong(0)

@Volatile
private var mediaMetadata = MediaMetadataCompat.Builder().build()

@Volatile
private var capabilities = standardCapabilities
private var remoteClientBitmap: Bitmap? = null

private val remoteClientBitmap = AtomicReference<Bitmap?>(null)

@Volatile
private var isPlaying = false
Expand All @@ -67,7 +75,21 @@ class MediaSessionBroadcaster(
super.close()
}

override fun notifyStarting() = notifyPlaying()
override fun promiseClose(): Promise<Unit> {
close()
return if (playbackState == PlaybackStateCompat.STATE_STOPPED) Unit.toPromise()
else selectedLibraryIdProvider
.promiseSelectedLibraryId()
.then {
if (it != null)
mediaSession.setSessionActivity(intentBuilder.buildPendingNowPlayingIntent(it))
}
}

override fun notifyStarting() {
mediaSession.activate()
notifyPlaying()
}

override fun notifyPlaying() {
isPlaying = true
Expand Down Expand Up @@ -101,6 +123,7 @@ class MediaSessionBroadcaster(
playbackSpeed
)
mediaSession.setPlaybackState(builder.build())
mediaSession.deactivate()
}

override fun notifyInterrupted() {
Expand Down Expand Up @@ -134,13 +157,12 @@ class MediaSessionBroadcaster(

@Synchronized
private fun clearClientBitmap() {
if (remoteClientBitmap == null) return
if (remoteClientBitmap.getAndSet(null) == null) return
val metadataBuilder = MediaMetadataCompat.Builder(mediaMetadata)
metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, null)
metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ART, null)
metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, null)
mediaSession.setMetadata(metadataBuilder.build().also { mediaMetadata = it })
remoteClientBitmap = null
}

private fun updateNowPlaying(libraryId: LibraryId, serviceFile: ServiceFile): Promise<*> {
Expand Down Expand Up @@ -175,11 +197,10 @@ class MediaSessionBroadcaster(
)

promisedBitmap.then { bitmap ->
if (remoteClientBitmap !== bitmap) {
if (remoteClientBitmap.updateIfDifferent(bitmap)) {
metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap)
metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ART, bitmap)
metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, bitmap)
remoteClientBitmap = bitmap
}
mediaSession.setMetadata(metadataBuilder.build().also { mediaMetadata = it })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ import java.util.concurrent.TimeoutException
val timeoutResponse =
promisedTimeout.then(
{ throw PlaybackStartingTimeoutException(playbackStartTimeout) },
{ it ->
{
// avoid logging cancellation exceptions
if (it !is CancellationException)
throw it
Expand Down Expand Up @@ -835,11 +835,11 @@ import java.util.concurrent.TimeoutException
val playbackState = promisedPlaybackServices.value

isMarkedForPlay = true
applicationMessageBus.sendMessage(PlaybackMessage.PlaybackStarting)

if (!areListenersRegistered) registerListeners()

return playbackState.eventually {
applicationMessageBus.sendMessage(PlaybackMessage.PlaybackStarting)
it.playbackState
.startPlaylist(
libraryId,
Expand Down Expand Up @@ -879,10 +879,10 @@ import java.util.concurrent.TimeoutException

private fun resumePlayback(libraryId: LibraryId): Promise<Unit> {
isMarkedForPlay = true
applicationMessageBus.sendMessage(PlaybackMessage.PlaybackStarting)

if (!areListenersRegistered) registerListeners()
return restorePlaybackServices(libraryId).eventually {
applicationMessageBus.sendMessage(PlaybackMessage.PlaybackStarting)
it.playbackState.resume().then(forward()) { e -> it.errorHandler.onError(e) }
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package com.lasthopesoftware.bluewater.shared.android.MediaSession

import android.app.PendingIntent
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.PlaybackStateCompat
import com.namehillsoftware.handoff.promises.Promise

interface ControlMediaSession {
fun activate()
fun deactivate()
fun setPlaybackState(playbackStateCompat: PlaybackStateCompat)
fun setMetadata(metadata: MediaMetadataCompat)
fun setSessionActivity(pendingIntent: PendingIntent)
fun promiseInitialization(): Promise<Unit>
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,51 @@
package com.lasthopesoftware.bluewater.shared.android.MediaSession

import android.app.PendingIntent
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import com.lasthopesoftware.bluewater.client.playback.nowplaying.storage.GetNowPlayingState
import com.namehillsoftware.handoff.promises.Promise
import java.lang.AutoCloseable

class MediaSessionController(
private val mediaSessionCompat: MediaSessionCompat,
private val nowPlayingState: GetNowPlayingState,
) : ControlMediaSession, AutoCloseable {
override fun promiseInitialization(): Promise<Unit> {
return nowPlayingState
.promiseActiveNowPlaying()
.then { np ->
np?.apply {
if (playlistPosition > 0 && filePosition > 0) activate()
}
}
}

override fun activate() {
if (!mediaSessionCompat.isActive)
mediaSessionCompat.isActive = true
}

override fun deactivate() {
if (mediaSessionCompat.isActive)
mediaSessionCompat.isActive = false
}

class MediaSessionController(private val mediaSessionCompat: MediaSessionCompat) : ControlMediaSession {
override fun setPlaybackState(playbackStateCompat: PlaybackStateCompat) {
mediaSessionCompat.setPlaybackState(playbackStateCompat)
}

override fun setMetadata(metadata: MediaMetadataCompat) {
mediaSessionCompat.setMetadata(metadata)
}

override fun setSessionActivity(pendingIntent: PendingIntent) {
mediaSessionCompat.setSessionActivity(pendingIntent)
}

override fun close() {
deactivate()
mediaSessionCompat.release()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,26 @@ import com.lasthopesoftware.bluewater.client.browsing.files.properties.LibraryFi
import com.lasthopesoftware.bluewater.client.connection.libraries.LibraryConnectionRegistry
import com.lasthopesoftware.bluewater.client.playback.nowplaying.broadcasters.remote.MediaSessionBroadcaster
import com.lasthopesoftware.bluewater.client.playback.service.receivers.MediaSessionCallbackReceiver
import com.lasthopesoftware.bluewater.shared.lazyLogger
import com.lasthopesoftware.promises.extensions.getSafely
import com.lasthopesoftware.promises.extensions.toFuture
import java.util.concurrent.TimeUnit
import com.lasthopesoftware.resources.closables.PromisingCloseable
import com.lasthopesoftware.resources.closables.PromisingCloseableManager
import java.util.concurrent.TimeoutException

@UnstableApi class MediaSessionService : Service() {
companion object {
private val logger by lazyLogger<MediaSessionService>()
}

private val binder by lazy { GenericBinder(this) }

private val lazyMediaSession = lazy {
val libraryConnectionDependencies = LibraryConnectionRegistry(applicationDependencies)
val libraryFilePropertiesDependents = LibraryFilePropertiesDependentsRegistry(
applicationDependencies,
libraryConnectionDependencies
)
private val promisingCloseableManager = PromisingCloseableManager()

val newMediaSession = MediaSessionCompat(this, MediaSessionConstants.mediaSessionTag)
private val libraryConnectionDependencies by lazy { LibraryConnectionRegistry(applicationDependencies) }

private val mediaSessionServices by lazy {
val newMediaSession = MediaSessionCompat(this, MediaSessionConstants.mediaSessionTag)
with (applicationDependencies) {
newMediaSession.setCallback(
MediaSessionCallbackReceiver(
Expand All @@ -34,44 +39,60 @@ import java.util.concurrent.TimeUnit
)
)

val broadcaster = MediaSessionBroadcaster(
nowPlayingState,
libraryConnectionDependencies.libraryFilePropertiesProvider,
libraryFilePropertiesDependents.imageBytesProvider,
applicationDependencies.bitmapProducer,
MediaSessionController(newMediaSession),
registerForApplicationMessages,
val libraryFilePropertiesDependents = LibraryFilePropertiesDependentsRegistry(
applicationDependencies,
libraryConnectionDependencies
)

Pair(broadcaster, newMediaSession)
val mediaSessionController = promisingCloseableManager.manage(MediaSessionController(newMediaSession, nowPlayingState))
promisingCloseableManager.manage(
MediaSessionBroadcaster(
nowPlayingState,
libraryConnectionDependencies.libraryFilePropertiesProvider,
libraryFilePropertiesDependents.imageBytesProvider,
bitmapProducer,
mediaSessionController,
selectedLibraryIdProvider,
intentBuilder,
registerForApplicationMessages,
) as PromisingCloseable
)
Pair(newMediaSession, mediaSessionController)
}
}

val mediaSession
get() = lazyMediaSession.value.second
val mediaSession: MediaSessionCompat
get() = mediaSessionServices.first

val mediaSessionController: ControlMediaSession
get() = mediaSessionServices.second

override fun onBind(intent: Intent) = binder

override fun onCreate() {
lazyMediaSession.value.second.isActive = true
super.onCreate()

try {
mediaSessionController
.promiseInitialization()
.toFuture()
.getSafely()
} catch (e: TimeoutException) {
logger.warn("Timed out initializing the media session controller.", e)
} catch (e: Exception) {
logger.error("An unexpected error occurred initializing the media session controller.", e)
}
}

override fun onDestroy() {
if (lazyMediaSession.isInitialized()) {
val (broadcaster, mediaSession) = lazyMediaSession.value

val futureLibraryId = applicationDependencies.selectedLibraryIdProvider.promiseSelectedLibraryId().toFuture()

broadcaster.close()
with (mediaSession) {
isActive = false
futureLibraryId
.get(30, TimeUnit.SECONDS)
?.also {
setSessionActivity(applicationDependencies.intentBuilder.buildPendingNowPlayingIntent(it))
}
release()
}
try {
promisingCloseableManager
.promiseClose()
.toFuture()
.getSafely()
} catch (e: TimeoutException) {
logger.warn("Timed out closing the resources.", e)
} catch (e: Exception) {
logger.error("An unexpected error occurred closing resources.", e)
}
super.onDestroy()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ fun <T> Promise<T>.toState(initialValue: T): State<T> = toState(initialValue, Un
@Composable
fun <T> Promise<T>.toState(initialValue: T, key1: Any?): State<T> {
val result = remember { mutableStateOf(initialValue) }
DisposableEffect(key1) {
DisposableEffect(this, key1) {
val promisedSet = then { it, cs ->
if (!cs.isCancelled)
result.value = it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ package com.lasthopesoftware.resources.closables
import com.lasthopesoftware.bluewater.shared.lazyLogger
import com.lasthopesoftware.compilation.DebugFlag
import com.lasthopesoftware.promises.ForwardedResponse.Companion.forward
import com.lasthopesoftware.promises.extensions.keepPromise
import com.lasthopesoftware.promises.extensions.regardless
import com.lasthopesoftware.promises.PromiseMachines
import com.lasthopesoftware.promises.extensions.toPromise
import com.namehillsoftware.handoff.promises.Promise
import com.namehillsoftware.handoff.promises.response.ImmediateResponse
Expand Down Expand Up @@ -33,7 +32,7 @@ open class PromisingCloseableManager : ManagePromisingCloseables, ImmediateRespo

override fun createNestedManager(): ManagePromisingCloseables = NestedPromisingCloseableManager(this)

override fun promiseClose(): Promise<Unit> =
override fun promiseClose(): Promise<Unit> = PromiseMachines.loop { _, cancellable ->
(nestedContainerStack
.pop()
?.promiseClose()
Expand All @@ -49,8 +48,8 @@ open class PromisingCloseableManager : ManagePromisingCloseables, ImmediateRespo
logger.debug(closedMessage, resource)
}, this)
})
?.regardless { promiseClose() }
.keepPromise(Unit)
?: cancellable.cancel().toPromise()
}

override fun respond(resolution: Throwable?) {
logger.warn("There was an error closing a resource", resolution)
Expand Down
Loading