Minor session demo app improvements

Basically this change removes a bug that makes video playback stuck when
a video is playing and the user taps the UMO notification to get to
the player activity.

- Use `launchMode="singleTop"` for `PlayerActivity`
- Change session activity to a back stacked activity on service `onDestroy`.

Using a back stacked activity `onDestroy()` will be useful once this demo
app implements playback resumption.

The rest of the changes are aesthetic:

- clean up and optimize screen space usage in UI of `PlayerActivity`
- changed some colors, paddings and spacings
- adds a default artwork for the `PlayerView`

PiperOrigin-RevId: 534152052
This commit is contained in:
bachinger 2023-05-22 20:44:05 +01:00 committed by tonihei
parent b9a4e614f7
commit 96a4ae7e40
9 changed files with 113 additions and 164 deletions

View File

@ -40,7 +40,9 @@
<activity <activity
android:name=".PlayerActivity" android:name=".PlayerActivity"
android:exported="true"/> android:exported="true"
android:launchMode="singleTop"
android:theme="@style/Theme.AppCompat.NoActionBar"/>
<activity <activity
android:name=".PlayableFolderActivity" android:name=".PlayableFolderActivity"

View File

@ -17,7 +17,6 @@ package androidx.media3.demo.session
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
@ -70,9 +69,9 @@ class MainActivity : AppCompatActivity() {
findViewById<ExtendedFloatingActionButton>(R.id.open_player_floating_button) findViewById<ExtendedFloatingActionButton>(R.id.open_player_floating_button)
.setOnClickListener { .setOnClickListener {
// display the playing media items // Start the session activity that shows the playback activity. The System UI uses the same
val intent = Intent(this, PlayerActivity::class.java) // intent in the same way to start the activity from the notification.
startActivity(intent) browser?.sessionActivity?.send()
} }
onBackPressedDispatcher.addCallback( onBackPressedDispatcher.addCallback(

View File

@ -51,6 +51,7 @@ class PlayableFolderActivity : AppCompatActivity() {
companion object { companion object {
private const val MEDIA_ITEM_ID_KEY = "MEDIA_ITEM_ID_KEY" private const val MEDIA_ITEM_ID_KEY = "MEDIA_ITEM_ID_KEY"
fun createIntent(context: Context, mediaItemID: String): Intent { fun createIntent(context: Context, mediaItemID: String): Intent {
val intent = Intent(context, PlayableFolderActivity::class.java) val intent = Intent(context, PlayableFolderActivity::class.java)
intent.putExtra(MEDIA_ITEM_ID_KEY, mediaItemID) intent.putExtra(MEDIA_ITEM_ID_KEY, mediaItemID)
@ -77,8 +78,7 @@ class PlayableFolderActivity : AppCompatActivity() {
browser.shuffleModeEnabled = false browser.shuffleModeEnabled = false
browser.prepare() browser.prepare()
browser.play() browser.play()
val intent = Intent(this, PlayerActivity::class.java) browser.sessionActivity?.send()
startActivity(intent)
} }
} }
@ -104,9 +104,9 @@ class PlayableFolderActivity : AppCompatActivity() {
findViewById<ExtendedFloatingActionButton>(R.id.open_player_floating_button) findViewById<ExtendedFloatingActionButton>(R.id.open_player_floating_button)
.setOnClickListener { .setOnClickListener {
// display the playing media items // Start the session activity that shows the playback activity. The System UI uses the same
val intent = Intent(this, PlayerActivity::class.java) // intent in the same way to start the activity from the notification.
startActivity(intent) browser?.sessionActivity?.send()
} }
} }

View File

@ -18,6 +18,7 @@ package androidx.media3.demo.session
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent
import android.app.PendingIntent.* import android.app.PendingIntent.*
import android.app.TaskStackBuilder import android.app.TaskStackBuilder
import android.content.Intent import android.content.Intent
@ -55,6 +56,7 @@ class PlaybackService : MediaLibraryService() {
"android.media3.session.demo.SHUFFLE_OFF" "android.media3.session.demo.SHUFFLE_OFF"
private const val NOTIFICATION_ID = 123 private const val NOTIFICATION_ID = 123
private const val CHANNEL_ID = "demo_session_notification_channel_id" private const val CHANNEL_ID = "demo_session_notification_channel_id"
private val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0
} }
override fun onCreate() { override fun onCreate() {
@ -78,14 +80,15 @@ class PlaybackService : MediaLibraryService() {
} }
override fun onTaskRemoved(rootIntent: Intent?) { override fun onTaskRemoved(rootIntent: Intent?) {
if (!player.playWhenReady) { if (!player.playWhenReady || player.mediaItemCount == 0) {
stopSelf() stopSelf()
} }
} }
override fun onDestroy() { override fun onDestroy() {
player.release() mediaLibrarySession.setSessionActivity(getBackStackedActivity())
mediaLibrarySession.release() mediaLibrarySession.release()
player.release()
clearListener() clearListener()
super.onDestroy() super.onDestroy()
} }
@ -235,18 +238,9 @@ class PlaybackService : MediaLibraryService() {
.build() .build()
MediaItemTree.initialize(assets) MediaItemTree.initialize(assets)
val sessionActivityPendingIntent =
TaskStackBuilder.create(this).run {
addNextIntent(Intent(this@PlaybackService, MainActivity::class.java))
addNextIntent(Intent(this@PlaybackService, PlayerActivity::class.java))
val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0
getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT)
}
mediaLibrarySession = mediaLibrarySession =
MediaLibrarySession.Builder(this, player, librarySessionCallback) MediaLibrarySession.Builder(this, player, librarySessionCallback)
.setSessionActivity(sessionActivityPendingIntent) .setSessionActivity(getSingleTopActivity())
.setBitmapLoader(CacheBitmapLoader(DataSourceBitmapLoader(/* context= */ this))) .setBitmapLoader(CacheBitmapLoader(DataSourceBitmapLoader(/* context= */ this)))
.build() .build()
if (!customLayout.isEmpty()) { if (!customLayout.isEmpty()) {
@ -255,6 +249,23 @@ class PlaybackService : MediaLibraryService() {
} }
} }
private fun getSingleTopActivity(): PendingIntent {
return getActivity(
this,
0,
Intent(this, PlayerActivity::class.java),
immutableFlag or FLAG_UPDATE_CURRENT
)
}
private fun getBackStackedActivity(): PendingIntent {
return TaskStackBuilder.create(this).run {
addNextIntent(Intent(this@PlaybackService, MainActivity::class.java))
addNextIntent(Intent(this@PlaybackService, PlayerActivity::class.java))
getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT)
}
}
private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton { private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton {
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
return CommandButton.Builder() return CommandButton.Builder()

View File

@ -19,7 +19,6 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
@ -46,41 +45,33 @@ class PlayerActivity : AppCompatActivity() {
get() = if (controllerFuture.isDone) controllerFuture.get() else null get() = if (controllerFuture.isDone) controllerFuture.get() else null
private lateinit var playerView: PlayerView private lateinit var playerView: PlayerView
private lateinit var mediaList: ListView private lateinit var mediaItemListView: ListView
private lateinit var mediaListAdapter: PlayingMediaItemArrayAdapter private lateinit var mediaItemListAdapter: MediaItemListAdapter
private val subItemMediaList: MutableList<MediaItem> = mutableListOf() private val mediaItemList: MutableList<MediaItem> = mutableListOf()
private var lastMediaItemId: String? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player) setContentView(R.layout.activity_player)
playerView = findViewById(R.id.player_view) playerView = findViewById(R.id.player_view)
mediaList = findViewById(R.id.current_playing_list) mediaItemListView = findViewById(R.id.current_playing_list)
mediaListAdapter = PlayingMediaItemArrayAdapter(this, R.layout.folder_items, subItemMediaList) mediaItemListAdapter = MediaItemListAdapter(this, R.layout.folder_items, mediaItemList)
mediaList.adapter = mediaListAdapter mediaItemListView.adapter = mediaItemListAdapter
mediaList.setOnItemClickListener { _, _, position, _ -> mediaItemListView.setOnItemClickListener { _, _, position, _ ->
run { run {
val controller = this.controller ?: return@run val controller = this.controller ?: return@run
controller.seekToDefaultPosition(/* windowIndex= */ position) if (controller.currentMediaItemIndex == position) {
mediaListAdapter.notifyDataSetChanged() controller.playWhenReady = !controller.playWhenReady
if (controller.playWhenReady) {
playerView.hideController()
}
} else {
controller.seekToDefaultPosition(/* mediaItemIndex= */ position)
mediaItemListAdapter.notifyDataSetChanged()
} }
} }
findViewById<ImageView>(R.id.shuffle_switch).setOnClickListener {
val controller = this.controller ?: return@setOnClickListener
controller.shuffleModeEnabled = !controller.shuffleModeEnabled
} }
findViewById<ImageView>(R.id.repeat_switch).setOnClickListener {
val controller = this.controller ?: return@setOnClickListener
when (controller.repeatMode) {
Player.REPEAT_MODE_ALL -> controller.repeatMode = Player.REPEAT_MODE_OFF
Player.REPEAT_MODE_OFF -> controller.repeatMode = Player.REPEAT_MODE_ONE
Player.REPEAT_MODE_ONE -> controller.repeatMode = Player.REPEAT_MODE_ALL
}
}
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
} }
override fun onStart() { override fun onStart() {
@ -94,14 +85,6 @@ class PlayerActivity : AppCompatActivity() {
releaseController() releaseController()
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
private fun initializeController() { private fun initializeController() {
controllerFuture = controllerFuture =
MediaController.Builder( MediaController.Builder(
@ -123,8 +106,6 @@ class PlayerActivity : AppCompatActivity() {
updateCurrentPlaylistUI() updateCurrentPlaylistUI()
updateMediaMetadataUI(controller.mediaMetadata) updateMediaMetadataUI(controller.mediaMetadata)
updateShuffleSwitchUI(controller.shuffleModeEnabled)
updateRepeatSwitchUI(controller.repeatMode)
playerView.setShowSubtitleButton(controller.currentTracks.isTypeSupported(TRACK_TYPE_TEXT)) playerView.setShowSubtitleButton(controller.currentTracks.isTypeSupported(TRACK_TYPE_TEXT))
controller.addListener( controller.addListener(
@ -133,14 +114,6 @@ class PlayerActivity : AppCompatActivity() {
updateMediaMetadataUI(mediaItem?.mediaMetadata ?: MediaMetadata.EMPTY) updateMediaMetadataUI(mediaItem?.mediaMetadata ?: MediaMetadata.EMPTY)
} }
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
updateShuffleSwitchUI(shuffleModeEnabled)
}
override fun onRepeatModeChanged(repeatMode: Int) {
updateRepeatSwitchUI(repeatMode)
}
override fun onTracksChanged(tracks: Tracks) { override fun onTracksChanged(tracks: Tracks) {
playerView.setShowSubtitleButton(tracks.isTypeSupported(TRACK_TYPE_TEXT)) playerView.setShowSubtitleButton(tracks.isTypeSupported(TRACK_TYPE_TEXT))
} }
@ -148,48 +121,26 @@ class PlayerActivity : AppCompatActivity() {
) )
} }
private fun updateShuffleSwitchUI(shuffleModeEnabled: Boolean) {
val resId =
if (shuffleModeEnabled) R.drawable.exo_styled_controls_shuffle_on
else R.drawable.exo_styled_controls_shuffle_off
findViewById<ImageView>(R.id.shuffle_switch)
.setImageDrawable(ContextCompat.getDrawable(this, resId))
}
private fun updateRepeatSwitchUI(repeatMode: Int) {
val resId: Int =
when (repeatMode) {
Player.REPEAT_MODE_OFF -> R.drawable.exo_styled_controls_repeat_off
Player.REPEAT_MODE_ONE -> R.drawable.exo_styled_controls_repeat_one
Player.REPEAT_MODE_ALL -> R.drawable.exo_styled_controls_repeat_all
else -> R.drawable.exo_styled_controls_repeat_off
}
findViewById<ImageView>(R.id.repeat_switch)
.setImageDrawable(ContextCompat.getDrawable(this, resId))
}
private fun updateMediaMetadataUI(mediaMetadata: MediaMetadata) { private fun updateMediaMetadataUI(mediaMetadata: MediaMetadata) {
val title: CharSequence = mediaMetadata.title ?: getString(R.string.no_item_prompt) val title: CharSequence = mediaMetadata.title ?: "getString(R.string.no_item_prompt)"
findViewById<TextView>(R.id.video_title).text = title findViewById<TextView>(R.id.media_title).text = title
findViewById<TextView>(R.id.video_album).text = mediaMetadata.albumTitle findViewById<TextView>(R.id.media_artist).text = mediaMetadata.artist
findViewById<TextView>(R.id.video_artist).text = mediaMetadata.artist
findViewById<TextView>(R.id.video_genre).text = mediaMetadata.genre
// Trick to update playlist UI // Trick to update playlist UI
mediaListAdapter.notifyDataSetChanged() mediaItemListAdapter.notifyDataSetChanged()
} }
private fun updateCurrentPlaylistUI() { private fun updateCurrentPlaylistUI() {
val controller = this.controller ?: return val controller = this.controller ?: return
subItemMediaList.clear() mediaItemList.clear()
for (i in 0 until controller.mediaItemCount) { for (i in 0 until controller.mediaItemCount) {
subItemMediaList.add(controller.getMediaItemAt(i)) mediaItemList.add(controller.getMediaItemAt(i))
} }
mediaListAdapter.notifyDataSetChanged() mediaItemListAdapter.notifyDataSetChanged()
} }
private inner class PlayingMediaItemArrayAdapter( private inner class MediaItemListAdapter(
context: Context, context: Context,
viewID: Int, viewID: Int,
mediaItemList: List<MediaItem> mediaItemList: List<MediaItem>
@ -201,23 +152,31 @@ 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)
if (position == controller?.currentMediaItemIndex) { if (position == controller?.currentMediaItemIndex) {
returnConvertView.setBackgroundColor(ContextCompat.getColor(context, R.color.white)) // Styles for the current media item list item.
returnConvertView returnConvertView.setBackgroundColor(
.findViewById<TextView>(R.id.media_item) ContextCompat.getColor(context, R.color.playlist_item_background)
.setTextColor(ContextCompat.getColor(context, R.color.black)) )
} else {
returnConvertView.setBackgroundColor(ContextCompat.getColor(context, R.color.black))
returnConvertView returnConvertView
.findViewById<TextView>(R.id.media_item) .findViewById<TextView>(R.id.media_item)
.setTextColor(ContextCompat.getColor(context, R.color.white)) .setTextColor(ContextCompat.getColor(context, R.color.white))
} deleteButton.visibility = View.GONE
} else {
returnConvertView.findViewById<Button>(R.id.delete_button).setOnClickListener { // Styles for any other media item list item.
returnConvertView.setBackgroundColor(
ContextCompat.getColor(context, R.color.player_background)
)
returnConvertView
.findViewById<TextView>(R.id.media_item)
.setTextColor(ContextCompat.getColor(context, R.color.white))
deleteButton.visibility = View.VISIBLE
deleteButton.setOnClickListener {
val controller = this@PlayerActivity.controller ?: return@setOnClickListener val controller = this@PlayerActivity.controller ?: return@setOnClickListener
controller.removeMediaItem(position) controller.removeMediaItem(position)
updateCurrentPlaylistUI() updateCurrentPlaylistUI()
} }
}
return returnConvertView return returnConvertView
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -19,7 +19,7 @@
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/black" android:background="@color/player_background"
tools:context=".PlayerActivity"> tools:context=".PlayerActivity">
<androidx.media3.ui.AspectRatioFrameLayout <androidx.media3.ui.AspectRatioFrameLayout
@ -28,77 +28,47 @@
> >
<androidx.media3.ui.PlayerView <androidx.media3.ui.PlayerView
android:id="@+id/player_view" android:id="@+id/player_view"
android:background="@color/player_background"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:use_artwork="true" /> app:default_artwork="@drawable/artwork_placeholder"
app:repeat_toggle_modes="one|all"
app:show_shuffle_button="true"
app:shutter_background_color="@color/player_background" />
</androidx.media3.ui.AspectRatioFrameLayout> </androidx.media3.ui.AspectRatioFrameLayout>
<TextView <TextView
android:id="@+id/video_title" android:id="@+id/media_artist"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="10dp" android:paddingLeft="10dp"
android:paddingStart="10dp"
android:paddingTop="10dp"
android:textColor="@color/white"
android:textSize="14sp"/>
<TextView
android:id="@+id/media_title"
android:ellipsize="end"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:paddingLeft="10dp"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:paddingStart="10dp"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="20sp" /> android:textSize="20sp" />
<TextView <View
android:id="@+id/video_album" android:background="@color/divider"
android:layout_width="match_parent" android:layout_height="1dp"
android:layout_height="wrap_content" android:layout_width="match_parent" />
android:paddingLeft="10dp"
android:paddingBottom="10dp"
android:textColor="@color/white"
android:textSize="20sp" />
<TextView
android:id="@+id/video_artist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:paddingLeft="10dp"
android:paddingBottom="10dp"/>
<TextView
android:id="@+id/video_genre"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:paddingLeft="10dp"
android:paddingBottom="10dp"
android:textSize="12sp" />
<LinearLayout
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/shuffle_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/shuffle"
android:src="@drawable/exo_styled_controls_shuffle_off"
android:textColor="@color/white" />
<ImageView
android:layout_margin="@dimen/exo_icon_horizontal_margin"
android:id="@+id/repeat_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/exo_styled_controls_repeat_off"
android:textColor="@color/white"
android:contentDescription="@string/repeat"
/>
</LinearLayout>
<ListView <ListView
android:id="@+id/current_playing_list" android:id="@+id/current_playing_list"
android:layout_width="match_parent"
android:divider="@drawable/divider" android:divider="@drawable/divider"
android:dividerHeight="1px" android:dividerHeight="1px"
android:layout_height="wrap_content"/> android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>

View File

@ -25,6 +25,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingStart="10dp" android:paddingStart="10dp"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/white" android:textColor="@color/white"
android:paddingEnd="10dp" android:paddingEnd="10dp"
android:minHeight="50dp" /> android:minHeight="50dp" />
@ -35,6 +37,7 @@
android:layout_width="50dp" android:layout_width="50dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:id="@+id/delete_button" android:id="@+id/delete_button"
android:backgroundTint="@color/playlist_item_foreground"
android:background="@drawable/baseline_playlist_remove_white_48" android:background="@drawable/baseline_playlist_remove_white_48"
/> />

View File

@ -22,4 +22,9 @@
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="grey">#FF999999</color> <color name="grey">#FF999999</color>
<color name="background">#292929</color>
<color name="player_background">#1c1c1c</color>
<color name="playlist_item_background">#363434</color>
<color name="playlist_item_foreground">#635E5E</color>
<color name="divider">#646464</color>
</resources> </resources>