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: // Use the same Guava version as the Android repo:
// https://cs.android.com/android/platform/superproject/main/+/main:external/guava/METADATA // https://cs.android.com/android/platform/superproject/main/+/main:external/guava/METADATA
guavaVersion = '33.0.0-android' guavaVersion = '33.0.0-android'
kotlinxCoroutinesVersion = '1.8.1'
leakCanaryVersion = '2.10' leakCanaryVersion = '2.10'
mockitoVersion = '3.12.4' mockitoVersion = '3.12.4'
robolectricVersion = '4.11' robolectricVersion = '4.11'
@ -46,6 +47,7 @@ project.ext {
// Updating this to 1.9.0+ will import Kotlin stdlib [internal ref: b/277891049]. // Updating this to 1.9.0+ will import Kotlin stdlib [internal ref: b/277891049].
androidxCoreVersion = '1.8.0' androidxCoreVersion = '1.8.0'
androidxExifInterfaceVersion = '1.3.6' androidxExifInterfaceVersion = '1.3.6'
androidxLifecycleVersion = '2.6.0'
androidxMediaVersion = '1.7.0' androidxMediaVersion = '1.7.0'
androidxMultidexVersion = '2.0.1' androidxMultidexVersion = '2.0.1'
androidxRecyclerViewVersion = '1.3.0' 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. // For detecting and debugging leaks only. LeakCanary is not needed for demo app to work.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion debugImplementation 'com.squareup.leakcanary:leakcanary-android:' + leakCanaryVersion
implementation 'androidx.core:core-ktx:' + androidxCoreVersion 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.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-guava:' + kotlinxCoroutinesVersion
implementation project(modulePrefix + 'lib-ui') implementation project(modulePrefix + 'lib-ui')
implementation project(modulePrefix + 'lib-session') implementation project(modulePrefix + 'lib-session')
implementation project(modulePrefix + 'demo-session-service') implementation project(modulePrefix + 'demo-session-service')

View File

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