Use kotlinx-coroutines-guava in session demo app

I originally tried switching to `Futures.addCallback` (as a follow-up
to Issue: androidx/media#890), but it seemed like a good chance to go further into
Kotlin-ification.

Before this change, if the connection to the session failed, the app
would hang at the 'waiting' screen with nothing logged (and the music
keeps playing). This behaviour is maintained with the `try/catch` around
the `.await()` call (with additional logging). Without this, the failed
connection causes the `PlayerActivity` to crash and the music in the
background stops. The `try/catch` is used to flag to developers who
might be using this app as an example that connecting to the session
may fail, and they may want to handle that.

This change also switches `this.controller` to be `lateinit` instead of
nullable.

Issue: androidx/media#890
PiperOrigin-RevId: 638948568
This commit is contained in:
ibaker 2024-05-31 01:26:07 -07:00 committed by Copybara-Service
parent a652c5b3f5
commit 1329821a35
3 changed files with 41 additions and 26 deletions

View File

@ -29,6 +29,7 @@ project.ext {
// Use the same Guava version as the Android repo:
// https://cs.android.com/android/platform/superproject/main/+/main:external/guava/METADATA
guavaVersion = '33.0.0-android'
kotlinxCoroutinesVersion = '1.8.1'
leakCanaryVersion = '2.10'
mockitoVersion = '3.12.4'
robolectricVersion = '4.11'
@ -46,6 +47,7 @@ project.ext {
// Updating this to 1.9.0+ will import Kotlin stdlib [internal ref: b/277891049].
androidxCoreVersion = '1.8.0'
androidxExifInterfaceVersion = '1.3.6'
androidxLifecycleVersion = '2.6.0'
androidxMediaVersion = '1.7.0'
androidxMultidexVersion = '2.0.1'
androidxRecyclerViewVersion = '1.3.0'

View File

@ -62,9 +62,12 @@ dependencies {
// For detecting and debugging leaks only. LeakCanary is not needed for demo app to work.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion
implementation 'androidx.core:core-ktx:' + androidxCoreVersion
implementation 'androidx.lifecycle:lifecycle-common:' + androidxLifecycleVersion
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:' + androidxLifecycleVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-guava:' + kotlinxCoroutinesVersion
implementation project(modulePrefix + 'lib-ui')
implementation project(modulePrefix + 'lib-session')
implementation project(modulePrefix + 'demo-session-service')

View File

@ -18,6 +18,7 @@ package androidx.media3.demo.session
import android.content.ComponentName
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -28,6 +29,9 @@ import android.widget.TextView
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.media3.common.C.TRACK_TYPE_TEXT
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
@ -40,13 +44,15 @@ import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import androidx.media3.ui.PlayerView
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.launch
private const val TAG = "PlayerActivity"
class PlayerActivity : AppCompatActivity() {
private lateinit var controllerFuture: ListenableFuture<MediaController>
private val controller: MediaController?
get() =
if (controllerFuture.isDone && !controllerFuture.isCancelled) controllerFuture.get() else null
private lateinit var controller: MediaController
private lateinit var playerView: PlayerView
private lateinit var mediaItemListView: ListView
@ -57,6 +63,18 @@ class PlayerActivity : AppCompatActivity() {
@OptIn(UnstableApi::class) // PlayerView.hideController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
try {
initializeController()
awaitCancellation()
} finally {
playerView.player = null
releaseController()
}
}
}
setContentView(R.layout.activity_player)
playerView = findViewById(R.id.player_view)
@ -65,7 +83,6 @@ class PlayerActivity : AppCompatActivity() {
mediaItemListView.adapter = mediaItemListAdapter
mediaItemListView.setOnItemClickListener { _, _, position, _ ->
run {
val controller = this.controller ?: return@run
if (controller.currentMediaItemIndex == position) {
controller.playWhenReady = !controller.playWhenReady
if (controller.playWhenReady) {
@ -79,18 +96,7 @@ class PlayerActivity : AppCompatActivity() {
}
}
override fun onStart() {
super.onStart()
initializeController()
}
override fun onStop() {
super.onStop()
playerView.player = null
releaseController()
}
private fun initializeController() {
private suspend fun initializeController() {
controllerFuture =
MediaController.Builder(
this,
@ -98,7 +104,7 @@ class PlayerActivity : AppCompatActivity() {
)
.buildAsync()
updateMediaMetadataUI()
controllerFuture.addListener({ setController() }, MoreExecutors.directExecutor())
setController()
}
private fun releaseController() {
@ -106,9 +112,13 @@ class PlayerActivity : AppCompatActivity() {
}
@OptIn(UnstableApi::class) // PlayerView.setShowSubtitleButton
private fun setController() {
val controller = this.controller ?: return
private suspend fun setController() {
try {
controller = controllerFuture.await()
} catch (t: Throwable) {
Log.w(TAG, "Failed to connect to MediaController", t)
return
}
playerView.player = controller
updateCurrentPlaylistUI()
@ -137,8 +147,7 @@ class PlayerActivity : AppCompatActivity() {
}
private fun updateMediaMetadataUI() {
val controller = this.controller
if (controller == null || controller.mediaItemCount == 0) {
if (!::controller.isInitialized || controller.mediaItemCount == 0) {
findViewById<TextView>(R.id.media_title).text = getString(R.string.waiting_for_metadata)
findViewById<TextView>(R.id.media_artist).text = ""
return
@ -152,7 +161,9 @@ class PlayerActivity : AppCompatActivity() {
}
private fun updateCurrentPlaylistUI() {
val controller = this.controller ?: return
if (!::controller.isInitialized) {
return
}
mediaItemList.clear()
for (i in 0 until controller.mediaItemCount) {
mediaItemList.add(controller.getMediaItemAt(i))
@ -173,7 +184,7 @@ class PlayerActivity : AppCompatActivity() {
returnConvertView.findViewById<TextView>(R.id.media_item).text = mediaItem.mediaMetadata.title
val deleteButton = returnConvertView.findViewById<Button>(R.id.delete_button)
if (position == controller?.currentMediaItemIndex) {
if (::controller.isInitialized && position == controller.currentMediaItemIndex) {
// Styles for the current media item list item.
returnConvertView.setBackgroundColor(
ContextCompat.getColor(context, R.color.playlist_item_background)
@ -192,7 +203,6 @@ class PlayerActivity : AppCompatActivity() {
.setTextColor(ContextCompat.getColor(context, R.color.white))
deleteButton.visibility = View.VISIBLE
deleteButton.setOnClickListener {
val controller = this@PlayerActivity.controller ?: return@setOnClickListener
controller.removeMediaItem(position)
updateCurrentPlaylistUI()
}