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:
parent
a652c5b3f5
commit
1329821a35
@ -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'
|
||||||
|
@ -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')
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user