diff --git a/kotlin-audio-example/build.gradle.kts b/kotlin-audio-example/build.gradle.kts index a1870e27..32c950c9 100644 --- a/kotlin-audio-example/build.gradle.kts +++ b/kotlin-audio-example/build.gradle.kts @@ -60,14 +60,14 @@ dependencies { } implementation(project(":kotlin-audio")) - implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.core:core-ktx:1.10.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") - implementation("androidx.activity:activity-compose:1.7.0") + implementation("androidx.activity:activity-compose:1.7.2") implementation(platform("androidx.compose:compose-bom:2023.03.00")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") - implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material3:material3:1.1.1") implementation("androidx.compose.material:material-icons-extended") implementation("io.coil-kt:coil-compose:2.4.0") testImplementation("junit:junit:4.13.2") diff --git a/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/MainActivity.kt b/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/MainActivity.kt index 7da8f05b..4a960712 100644 --- a/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/MainActivity.kt +++ b/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/MainActivity.kt @@ -38,6 +38,7 @@ import com.doublesymmetry.kotlinaudio.models.NotificationConfig import com.doublesymmetry.kotlinaudio.models.RepeatMode import com.doublesymmetry.kotlinaudio.models.PlayerConfig import com.doublesymmetry.kotlinaudio.players.QueuedAudioPlayer +import com.example.kotlin_audio_example.ui.component.ActionBottomSheet import com.example.kotlin_audio_example.ui.component.PlayerControls import com.example.kotlin_audio_example.ui.component.TrackDisplay import com.example.kotlin_audio_example.ui.theme.KotlinAudioTheme @@ -51,6 +52,7 @@ import kotlin.time.Duration.Companion.seconds class MainActivity : ComponentActivity() { private lateinit var player: QueuedAudioPlayer + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -76,25 +78,43 @@ class MainActivity : ComponentActivity() { var duration by remember { mutableStateOf(0L) } var isLive by remember { mutableStateOf(false) } - Inner( - title = title, - artist = artist, - artwork = artwork, - position = position, - duration = duration, - isLive = isLive, - onPrevious = { player.previous() }, - onNext = { player.next() }, - isPaused = state.value != AudioPlayerState.PLAYING, - onPlayPause = { - if (player.playerState == AudioPlayerState.PLAYING) { - player.pause() - } else { - player.play() + var showSheet by remember { mutableStateOf(false) } + + if (showSheet) { + ActionBottomSheet( + onDismiss = { showSheet = false }, + onRandomMetadata = { + val currentIndex = player.currentIndex + val track = tracks[currentIndex] + track.title = "Random Title - ${System.currentTimeMillis()}" + track.artwork = "https://picsum.photos/200/300" + player.replaceItem(currentIndex, track) } - }, - onSeek = { player.seek(it, TimeUnit.MILLISECONDS) } - ) + ) + } + + KotlinAudioTheme { + MainScreen( + title = title, + artist = artist, + artwork = artwork, + position = position, + duration = duration, + isLive = isLive, + onPrevious = { player.previous() }, + onNext = { player.next() }, + isPaused = state.value != AudioPlayerState.PLAYING, + onTopBarAction = { showSheet = true }, + onPlayPause = { + if (player.playerState == AudioPlayerState.PLAYING) { + player.pause() + } else { + player.play() + } + }, + onSeek = { player.seek(it, TimeUnit.MILLISECONDS) } + ) + } LaunchedEffect(key1 = player, key2 = player.event.audioItemTransition, key3 = player.event.onPlayerActionTriggeredExternally) { player.event.audioItemTransition @@ -202,7 +222,7 @@ class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun Inner( +fun MainScreen( title: String, artist: String, artwork: String, @@ -212,56 +232,53 @@ fun Inner( onPrevious: () -> Unit = {}, onNext: () -> Unit = {}, isPaused: Boolean, + onTopBarAction: () -> Unit = {}, onPlayPause: () -> Unit = {}, onSeek: (Long) -> Unit = {}, ) { - KotlinAudioTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Column(modifier = Modifier.fillMaxSize()) { - TopAppBar( - title = { - Text( - text = "Kotlin Audio Example", - color = MaterialTheme.colorScheme.onPrimary + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Column(modifier = Modifier.fillMaxSize()) { + TopAppBar( + title = { + Text( + text = "Kotlin Audio Example", + color = MaterialTheme.colorScheme.onPrimary + ) + }, + actions = { + IconButton(onClick = onTopBarAction) { + Icon( + Icons.Default.MoreVert, + contentDescription = "Settings", + tint = MaterialTheme.colorScheme.onPrimary ) - }, - actions = { - IconButton(onClick = { - - }) { - Icon( - Icons.Default.MoreVert, - contentDescription = "Settings", - tint = MaterialTheme.colorScheme.onPrimary - ) - } - }, - colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.primary) - ) - TrackDisplay( - title = title, - artist = artist, - artwork = artwork, - position = position, - duration = duration, - isLive = isLive, - onSeek = onSeek, - modifier = Modifier.padding(top = 46.dp) - ) - Spacer(modifier = Modifier.weight(1f)) - PlayerControls( - onPrevious = onPrevious, - onNext = onNext, - isPaused = isPaused, - onPlayPause = onPlayPause, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 60.dp)) - } + } + }, + colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.primary) + ) + TrackDisplay( + title = title, + artist = artist, + artwork = artwork, + position = position, + duration = duration, + isLive = isLive, + onSeek = onSeek, + modifier = Modifier.padding(top = 46.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + PlayerControls( + onPrevious = onPrevious, + onNext = onNext, + isPaused = isPaused, + onPlayPause = onPlayPause, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 60.dp) + ) } } } @@ -269,7 +286,7 @@ fun Inner( @Composable fun ContentPreview() { KotlinAudioTheme { - Inner( + MainScreen( title = "Title", artist = "Artist", artwork = "", diff --git a/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/ui/component/ActionBottomSheet.kt b/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/ui/component/ActionBottomSheet.kt new file mode 100644 index 00000000..bf65205f --- /dev/null +++ b/kotlin-audio-example/src/main/java/com/example/kotlin_audio_example/ui/component/ActionBottomSheet.kt @@ -0,0 +1,51 @@ +package com.example.kotlin_audio_example.ui.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +@ExperimentalMaterial3Api +fun ActionBottomSheet( + onDismiss: () -> Unit, + onRandomMetadata: () -> Unit, +) { + val modalBottomSheetState = rememberModalBottomSheetState() + + ModalBottomSheet( + onDismissRequest = { onDismiss() }, + sheetState = modalBottomSheetState, + dragHandle = { BottomSheetDefaults.DragHandle() }, + ) { + InnerSheet(onRandomMetadata = onRandomMetadata) + } +} + +@Composable +fun InnerSheet(onRandomMetadata: () -> Unit = {}) { + // Add a button to perform an action when clicked + Button( + onClick = onRandomMetadata, + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + Text("Metadata: Update Title Randomly") + } +} + +@Preview +@ExperimentalMaterial3Api +@Composable +fun ActionBottomSheetPreview() { + InnerSheet() +} \ No newline at end of file diff --git a/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/models/AudioItem.kt b/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/models/AudioItem.kt index 510cf616..e29d861e 100644 --- a/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/models/AudioItem.kt +++ b/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/models/AudioItem.kt @@ -52,7 +52,7 @@ data class DefaultAudioItem( override var artist: String? = null, override var title: String? = null, override var albumTitle: String? = null, - override val artwork: String? = null, + override var artwork: String? = null, override val duration: Long = -1, override val options: AudioItemOptions? = null, ) : AudioItem diff --git a/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/notification/NotificationManager.kt b/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/notification/NotificationManager.kt index 29c3b5a6..c6af0f42 100644 --- a/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/notification/NotificationManager.kt +++ b/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/notification/NotificationManager.kt @@ -94,127 +94,84 @@ class NotificationManager internal constructor( private val scope = MainScope() private val buttons = mutableSetOf() private var invalidateThrottleCount = 0 - private var notificationMetadataBitmap: Bitmap? = null - private var notificationMetadataArtworkDisposable: Disposable? = null private var iconPlaceholder = Bitmap.createBitmap(64, 64, Bitmap.Config.ARGB_8888) - var notificationMetadata: NotificationMetadata? = null - set(value) { - if (value == null) { - val changed = field != null - if (changed) { - field = null - notificationMetadataBitmap = null - invalidate() - } - return - } - val holder = player.currentMediaItem?.getAudioItemHolder() - val artworkChanged = field?.artworkUrl != value.artworkUrl - && holder?.audioItem?.artwork != value.artworkUrl - val titleChanged = holder?.audioItem?.title != value.title - val artistChanged = holder?.audioItem?.artist != value.artist - - if (artworkChanged) { - notificationMetadataBitmap = null - // Cancel loading previous artwork: - notificationMetadataArtworkDisposable?.dispose() - if (value.artworkUrl != null) { - notificationMetadataArtworkDisposable = context.imageLoader.enqueue( - ImageRequest.Builder(context) - .data(value.artworkUrl) - .target { result -> - notificationMetadataBitmap = (result as BitmapDrawable).bitmap - invalidate() - } - .build() - ) - } else { - notificationMetadataArtworkDisposable = null - } - } - if (artworkChanged || titleChanged || artistChanged) { - field = value - invalidate() - } - } + + // This causes the builder to opt for audio item holder data over media item data + // This might be used when a user attempts to manually set the metadata, in which + // case we want to prioritize that data. + internal var ignoreMediaMetadata = false private fun getTitle(index: Int? = null): String? { - val mediaItem = if (index == null) player.getCurrentMediaItem() - else player.getMediaItemAt(index) - val isCurrent = index == null || index == player.currentMediaItemIndex - return ((if (isCurrent) notificationMetadata else null)?.title - ?: mediaItem?.mediaMetadata?.title - ?: mediaItem?.getAudioItemHolder()?.audioItem?.title)?.toString() + val mediaItem = if (index == null) player.currentMediaItem else player.getMediaItemAt(index) + + val audioItem = mediaItem?.getAudioItemHolder()?.audioItem + return if (ignoreMediaMetadata) { + audioItem?.title + } else { + mediaItem?.mediaMetadata?.title?.toString() + ?: audioItem?.title + } } private fun getArtist(index: Int? = null): String? { - val mediaItem = if (index == null) player.getCurrentMediaItem() - else player.getMediaItemAt(index) - val isCurrent = index == null || index == player.currentMediaItemIndex - return ( - (if (isCurrent) notificationMetadata else null)?.artist - ?: mediaItem?.mediaMetadata?.artist - ?: mediaItem?.mediaMetadata?.albumArtist - ?: mediaItem?.getAudioItemHolder()?.audioItem?.artist - )?.toString() + val mediaItem = if (index == null) player.currentMediaItem else player.getMediaItemAt(index) + val audioItem = mediaItem?.getAudioItemHolder()?.audioItem + + return if (ignoreMediaMetadata) { + audioItem?.artist + } else { + (mediaItem?.mediaMetadata?.artist ?: mediaItem?.mediaMetadata?.albumArtist)?.toString() + ?: audioItem?.artist + } } private fun getGenre(index: Int? = null): String? { - val mediaItem = if (index == null) player.getCurrentMediaItem() - else player.getMediaItemAt(index) + val mediaItem = if (index == null) player.currentMediaItem else player.getMediaItemAt(index) return mediaItem?.mediaMetadata?.genre?.toString() } private fun getAlbumTitle(index: Int? = null): String? { - val mediaItem = if (index == null) player.getCurrentMediaItem() + val mediaItem = if (index == null) player.currentMediaItem else player.getMediaItemAt(index) - return (mediaItem?.mediaMetadata?.albumTitle - ?: mediaItem?.getAudioItemHolder()?.audioItem?.albumTitle)?.toString() + return mediaItem?.mediaMetadata?.albumTitle?.toString() + ?: mediaItem?.getAudioItemHolder()?.audioItem?.albumTitle } private fun getArtworkUrl(index: Int? = null): String? { - val isCurrent = index == null || index == player.currentMediaItemIndex - return ( - (if (isCurrent) notificationMetadata else null)?.artworkUrl - ?: getMediaItemArtworkUrl(index) - )?.toString() + return getMediaItemArtworkUrl(index) } private fun getMediaItemArtworkUrl(index: Int? = null): String? { - val mediaItem = if (index == null) player.getCurrentMediaItem() - else player.getMediaItemAt(index) - return ( - mediaItem?.mediaMetadata?.artworkUri - ?: mediaItem?.getAudioItemHolder()?.audioItem?.artwork - )?.toString() + val mediaItem = if (index == null) player.currentMediaItem else player.getMediaItemAt(index) + + return if (ignoreMediaMetadata) { + mediaItem?.getAudioItemHolder()?.audioItem?.artwork + } else { + mediaItem?.mediaMetadata?.artworkUri?.toString() + ?: mediaItem?.getAudioItemHolder()?.audioItem?.artwork + } } private fun getArtworkBitmap(index: Int? = null): Bitmap? { - val mediaItem = if (index == null) player.getCurrentMediaItem() - else player.getMediaItemAt(index) + val mediaItem = if (index == null) player.currentMediaItem else player.getMediaItemAt(index) val isCurrent = index == null || index == player.currentMediaItemIndex val artworkData = player.mediaMetadata.artworkData return ( - if (isCurrent && notificationMetadata?.artworkUrl != null) - notificationMetadataBitmap - else - null - ) ?: ( - if (isCurrent && artworkData != null) - BitmapFactory.decodeByteArray(artworkData, 0, artworkData.size) - else - null - ) ?: mediaItem?.getAudioItemHolder()?.artworkBitmap + if (isCurrent && artworkData != null) + BitmapFactory.decodeByteArray(artworkData, 0, artworkData.size) + else + null + ) ?: mediaItem?.getAudioItemHolder()?.artworkBitmap } private fun getDuration(index: Int? = null): Long? { - val mediaItem = if (index == null) player.getCurrentMediaItem() + val mediaItem = if (index == null) player.currentMediaItem else player.getMediaItemAt(index) return mediaItem?.getAudioItemHolder()?.audioItem?.duration ?: -1 } private fun getUserRating(index: Int? = null): RatingCompat? { - val mediaItem = if (index == null) player.getCurrentMediaItem() + val mediaItem = if (index == null) player.currentMediaItem else player.getMediaItemAt(index) return RatingCompat.fromRating(mediaItem?.mediaMetadata?.userRating) } diff --git a/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt b/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt index d81ccfdc..8665564c 100644 --- a/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt +++ b/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt @@ -321,9 +321,10 @@ abstract class BaseAudioPlayer internal constructor( } } - internal fun resetNotificationMetadataIfAutomatic() { + internal fun updateNotificationIfNecessary(ignoreMediaMetadata: Boolean = false) { if (automaticallyUpdateNotificationMetadata) { - notificationManager.notificationMetadata = null + notificationManager.ignoreMediaMetadata = ignoreMediaMetadata + notificationManager.invalidate() } } @@ -678,7 +679,7 @@ abstract class BaseAudioPlayer internal constructor( ) } - resetNotificationMetadataIfAutomatic() + updateNotificationIfNecessary() } /** diff --git a/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/QueuedAudioPlayer.kt b/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/QueuedAudioPlayer.kt index 5143365d..ddf01121 100644 --- a/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/QueuedAudioPlayer.kt +++ b/kotlin-audio/src/main/java/com/doublesymmetry/kotlinaudio/players/QueuedAudioPlayer.kt @@ -218,7 +218,7 @@ class QueuedAudioPlayer( val mediaSource = getMediaSourceFromAudioItem(item) queue[index] = mediaSource if (index == currentIndex) { - resetNotificationMetadataIfAutomatic() + updateNotificationIfNecessary(ignoreMediaMetadata = true) } }