From 1329821a356c1943319de1529efa379205bb32b6 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 31 May 2024 01:26:07 -0700 Subject: [PATCH] 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 --- constants.gradle | 2 + demos/session/build.gradle | 3 + .../media3/demo/session/PlayerActivity.kt | 62 +++++++++++-------- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/constants.gradle b/constants.gradle index b270a3b067..b4b63909c5 100644 --- a/constants.gradle +++ b/constants.gradle @@ -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' diff --git a/demos/session/build.gradle b/demos/session/build.gradle index 8ec5ed7faf..bd2c1dd7eb 100644 --- a/demos/session/build.gradle +++ b/demos/session/build.gradle @@ -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') diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt index aa5c7bc0e8..134405ed9b 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt @@ -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 - 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(R.id.media_title).text = getString(R.string.waiting_for_metadata) findViewById(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(R.id.media_item).text = mediaItem.mediaMetadata.title val deleteButton = returnConvertView.findViewById