Add media controls to notification

Add media controls to the Playback Notification [Loop, Last, Play/Pause, Next, Shuffle]
This commit is contained in:
OxygenCobalt 2020-11-01 11:06:26 -07:00
parent 09971afb42
commit 08bd0ece3a
20 changed files with 265 additions and 79 deletions

View file

@ -15,6 +15,20 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
// Get a bitmap for a song, onDone will be called when the bitmap is loaded.
// Don't use this on UI elements, thats what the BindingAdapters are for.
fun getBitmap(song: Song, context: Context, onDone: (Bitmap) -> Unit) {
Coil.enqueue(
ImageRequest.Builder(context)
.data(song.album.coverUri)
.error(R.drawable.ic_song)
.target { onDone(it.toBitmap()) }
.build()
)
}
// --- BINDING ADAPTERS ---
// Get the cover art for a song // Get the cover art for a song
@BindingAdapter("coverArt") @BindingAdapter("coverArt")
fun ImageView.bindCoverArt(song: Song) { fun ImageView.bindCoverArt(song: Song) {
@ -131,17 +145,6 @@ fun ImageView.bindGenreImage(genre: Genre) {
Coil.imageLoader(context).enqueue(request) Coil.imageLoader(context).enqueue(request)
} }
// Get a bitmap for a song, onDone will be called when the bitmap is loaded.
fun getBitmap(song: Song, context: Context, onDone: (Bitmap) -> Unit) {
Coil.enqueue(
ImageRequest.Builder(context)
.data(song.album.coverUri)
.error(R.drawable.ic_song)
.target { onDone(it.toBitmap()) }
.build()
)
}
// Get the base request used across the other functions. // Get the base request used across the other functions.
private fun getDefaultRequest(context: Context, imageView: ImageView): ImageRequest.Builder { private fun getDefaultRequest(context: Context, imageView: ImageView): ImageRequest.Builder {
return ImageRequest.Builder(context) return ImageRequest.Builder(context)

View file

@ -25,8 +25,6 @@ class CompactPlaybackFragment : Fragment() {
): View? { ): View? {
val binding = FragmentCompactPlaybackBinding.inflate(inflater) val binding = FragmentCompactPlaybackBinding.inflate(inflater)
// FIXME: Prevent the play/pause icon from animating on startup
// [requires new callback from PlaybackStateManager]
val iconPauseToPlay = ContextCompat.getDrawable( val iconPauseToPlay = ContextCompat.getDrawable(
requireContext(), R.drawable.ic_pause_to_play requireContext(), R.drawable.ic_pause_to_play
) as AnimatedVectorDrawable ) as AnimatedVectorDrawable
@ -62,21 +60,13 @@ class CompactPlaybackFragment : Fragment() {
} }
playbackModel.isPlaying.observe(viewLifecycleOwner) { playbackModel.isPlaying.observe(viewLifecycleOwner) {
if (true) { if (it) {
if (it) { // Animate the icon transition when the playing status switches
// Animate the icon transition when the playing status switches binding.playbackControls.setImageDrawable(iconPauseToPlay)
binding.playbackControls.setImageDrawable(iconPauseToPlay) iconPauseToPlay.start()
iconPauseToPlay.start()
} else {
binding.playbackControls.setImageDrawable(iconPlayToPause)
iconPlayToPause.start()
}
} else { } else {
if (it) { binding.playbackControls.setImageDrawable(iconPlayToPause)
binding.playbackControls.setImageResource(R.drawable.ic_pause) iconPlayToPause.start()
} else {
binding.playbackControls.setImageResource(R.drawable.ic_play)
}
} }
} }
@ -88,10 +78,4 @@ class CompactPlaybackFragment : Fragment() {
return binding.root return binding.root
} }
override fun onPause() {
super.onPause()
// playbackModel.resetAnimStatus()
}
} }

View file

@ -128,15 +128,15 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
when (it) { when (it) {
LoopMode.NONE -> { LoopMode.NONE -> {
binding.playbackLoop.imageTintList = controlColor binding.playbackLoop.imageTintList = controlColor
binding.playbackLoop.setImageResource(R.drawable.ic_loop) binding.playbackLoop.setImageResource(R.drawable.ic_loop_large)
} }
LoopMode.ONCE -> { LoopMode.ONCE -> {
binding.playbackLoop.imageTintList = accentColor binding.playbackLoop.imageTintList = accentColor
binding.playbackLoop.setImageResource(R.drawable.ic_loop_one) binding.playbackLoop.setImageResource(R.drawable.ic_loop_one_large)
} }
LoopMode.INFINITE -> { LoopMode.INFINITE -> {
binding.playbackLoop.imageTintList = accentColor binding.playbackLoop.imageTintList = accentColor
binding.playbackLoop.setImageResource(R.drawable.ic_loop) binding.playbackLoop.setImageResource(R.drawable.ic_loop_large)
} }
else -> return@observe else -> return@observe

View file

@ -1,29 +1,39 @@
package org.oxycblt.auxio.playback package org.oxycblt.auxio.playback
import android.annotation.SuppressLint
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.os.Build import android.os.Build
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.media.app.NotificationCompat.MediaStyle import androidx.media.app.NotificationCompat.MediaStyle
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.coil.getBitmap import org.oxycblt.auxio.music.coil.getBitmap
import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.state.PlaybackStateManager
// Manager for the playback notification // Holder for the playback notification, should only be used by PlaybackService.
// TODO: Implement some ability // TODO: You really need to rewrite this class. Christ.
internal class PlaybackNotificationHolder { // TODO: Disable skip prev/next buttons when you cant do those actions
// TODO: Implement a way to exit the notification
class PlaybackNotificationHolder {
private lateinit var mNotification: Notification private lateinit var mNotification: Notification
private lateinit var notificationManager: NotificationManager private lateinit var notificationManager: NotificationManager
private lateinit var baseNotification: NotificationCompat.Builder private lateinit var baseNotification: NotificationCompat.Builder
private val playbackManager = PlaybackStateManager.getInstance()
private var isForeground = false private var isForeground = false
fun init(context: Context, session: MediaSessionCompat) { fun init(context: Context, session: MediaSessionCompat, playbackService: PlaybackService) {
// Never run if the notification has already been created // Never run if the notification has already been created
if (!::mNotification.isInitialized) { if (!::mNotification.isInitialized) {
notificationManager = notificationManager =
@ -41,10 +51,20 @@ internal class PlaybackNotificationHolder {
baseNotification = NotificationCompat.Builder(context, CHANNEL_ID) baseNotification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_song) .setSmallIcon(R.drawable.ic_song)
.setStyle(MediaStyle().setMediaSession(session.sessionToken)) .setStyle(
MediaStyle()
.setMediaSession(session.sessionToken)
.setShowActionsInCompactView(1, 2, 3)
)
.setCategory(NotificationCompat.CATEGORY_SERVICE) .setCategory(NotificationCompat.CATEGORY_SERVICE)
.setChannelId(CHANNEL_ID) .setChannelId(CHANNEL_ID)
.setShowWhen(false) .setShowWhen(false)
.setTicker(playbackService.getString(R.string.title_playback))
.addAction(createAction(ACTION_LOOP, playbackService))
.addAction(createAction(ACTION_SKIP_PREV, playbackService))
.addAction(createAction(ACTION_PLAY_PAUSE, playbackService))
.addAction(createAction(ACTION_SKIP_NEXT, playbackService))
.addAction(createAction(ACTION_SHUFFLE, playbackService))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
mNotification = baseNotification.build() mNotification = baseNotification.build()
@ -67,23 +87,34 @@ internal class PlaybackNotificationHolder {
getBitmap(song, playbackService) { getBitmap(song, playbackService) {
baseNotification.setLargeIcon(it) baseNotification.setLargeIcon(it)
mNotification = baseNotification.build()
if (!isForeground) { startForegroundOrNotify(playbackService)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
playbackService.startForeground(
NOTIFICATION_ID, mNotification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
)
} else {
playbackService.startForeground(NOTIFICATION_ID, mNotification)
}
} else {
notificationManager.notify(NOTIFICATION_ID, mNotification)
}
} }
} }
@SuppressLint("RestrictedApi")
fun updatePlaying(playbackService: PlaybackService) {
baseNotification.mActions[2] = createAction(ACTION_PLAY_PAUSE, playbackService)
Log.d(this::class.simpleName, baseNotification.mActions[1].iconCompat?.resId.toString())
startForegroundOrNotify(playbackService)
}
@SuppressLint("RestrictedApi")
fun updateLoop(playbackService: PlaybackService) {
baseNotification.mActions[0] = createAction(ACTION_LOOP, playbackService)
startForegroundOrNotify(playbackService)
}
@SuppressLint("RestrictedApi")
fun updateShuffle(playbackService: PlaybackService) {
baseNotification.mActions[4] = createAction(ACTION_SHUFFLE, playbackService)
startForegroundOrNotify(playbackService)
}
fun stop(playbackService: PlaybackService) { fun stop(playbackService: PlaybackService) {
playbackService.stopForeground(true) playbackService.stopForeground(true)
notificationManager.cancel(NOTIFICATION_ID) notificationManager.cancel(NOTIFICATION_ID)
@ -91,8 +122,87 @@ internal class PlaybackNotificationHolder {
isForeground = false isForeground = false
} }
private fun createAction(action: String, playbackService: PlaybackService): NotificationCompat.Action {
val drawable = when (action) {
ACTION_LOOP -> {
when (playbackManager.loopMode) {
LoopMode.NONE -> R.drawable.ic_loop_disabled
LoopMode.ONCE -> R.drawable.ic_loop_one
LoopMode.INFINITE -> R.drawable.ic_loop
}
}
ACTION_SKIP_PREV -> {
R.drawable.ic_skip_prev
}
ACTION_PLAY_PAUSE -> {
if (playbackManager.isPlaying) {
R.drawable.ic_pause
} else {
R.drawable.ic_play
}
}
ACTION_SKIP_NEXT -> {
R.drawable.ic_skip_next
}
ACTION_SHUFFLE -> {
if (playbackManager.isShuffling) {
R.drawable.ic_shuffle
} else {
R.drawable.ic_shuffle_disabled
}
}
else -> R.drawable.ic_play
}
return NotificationCompat.Action.Builder(
drawable, action, createPlaybackAction(action, playbackService)
).build()
}
private fun createPlaybackAction(action: String, playbackService: PlaybackService): PendingIntent {
val intent = Intent()
intent.action = action
return PendingIntent.getBroadcast(
playbackService,
REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
private fun startForegroundOrNotify(playbackService: PlaybackService) {
mNotification = baseNotification.build()
// Start the service in the foreground if haven't already.
if (!isForeground) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
playbackService.startForeground(
NOTIFICATION_ID, mNotification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
)
} else {
playbackService.startForeground(NOTIFICATION_ID, mNotification)
}
} else {
notificationManager.notify(NOTIFICATION_ID, mNotification)
}
}
companion object { companion object {
const val CHANNEL_ID = "CHANNEL_AUXIO_PLAYBACK" const val CHANNEL_ID = "CHANNEL_AUXIO_PLAYBACK"
const val NOTIFICATION_ID = 0xA0A0 const val NOTIFICATION_ID = 0xA0A0
const val REQUEST_CODE = 0xA0C0
const val ACTION_LOOP = "ACTION_AUXIO_LOOP"
const val ACTION_SKIP_PREV = "ACTION_AUXIO_SKIP_PREV"
const val ACTION_PLAY_PAUSE = "ACTION_AUXIO_PLAY_PAUSE"
const val ACTION_SKIP_NEXT = "ACTION_AUXIO_SKIP_NEXT"
const val ACTION_SHUFFLE = "ACTION_AUXIO_SHUFFLE"
} }
} }

View file

@ -103,6 +103,11 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
// Set up callback for system events // Set up callback for system events
systemReceiver = SystemEventReceiver() systemReceiver = SystemEventReceiver()
IntentFilter().apply { IntentFilter().apply {
addAction(PlaybackNotificationHolder.ACTION_LOOP)
addAction(PlaybackNotificationHolder.ACTION_SKIP_PREV)
addAction(PlaybackNotificationHolder.ACTION_PLAY_PAUSE)
addAction(PlaybackNotificationHolder.ACTION_SKIP_NEXT)
addAction(PlaybackNotificationHolder.ACTION_SHUFFLE)
addAction(BluetoothDevice.ACTION_ACL_CONNECTED) addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED) addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY) addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
@ -115,7 +120,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
notificationHolder = PlaybackNotificationHolder() notificationHolder = PlaybackNotificationHolder()
notificationHolder.init(applicationContext, mediaSession) notificationHolder.init(applicationContext, mediaSession, this)
// --- PLAYBACKSTATEMANAGER SETUP --- // --- PLAYBACKSTATEMANAGER SETUP ---
@ -186,7 +191,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
// --- PLAYBACK STATE CALLBACK OVERRIDES --- // --- PLAYBACK STATE CALLBACK OVERRIDES ---
override fun onSongUpdate(song: Song?) { override fun onSongUpdate(song: Song?) {
song?.let { song?.let {
val item = MediaItem.fromUri(it.id.toURI()) val item = MediaItem.fromUri(it.id.toURI())
@ -210,13 +214,20 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
if (isPlaying && !player.isPlaying) { if (isPlaying && !player.isPlaying) {
player.play() player.play()
notificationHolder.updatePlaying(this)
startPollingPosition() startPollingPosition()
} else { } else {
player.pause() player.pause()
notificationHolder.updatePlaying(this)
} }
} }
override fun onShuffleUpdate(isShuffling: Boolean) {
changeIsFromAudioFocus = false
notificationHolder.updateShuffle(this)
}
override fun onLoopUpdate(mode: LoopMode) { override fun onLoopUpdate(mode: LoopMode) {
changeIsFromAudioFocus = false changeIsFromAudioFocus = false
@ -228,6 +239,8 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
player.repeatMode = Player.REPEAT_MODE_ONE player.repeatMode = Player.REPEAT_MODE_ONE
} }
} }
notificationHolder.updateLoop(this)
} }
override fun onSeekConfirm(position: Long) { override fun onSeekConfirm(position: Long) {
@ -331,6 +344,15 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
action?.let { action?.let {
when (it) { when (it) {
PlaybackNotificationHolder.ACTION_LOOP ->
playbackManager.setLoopMode(playbackManager.loopMode.increment())
PlaybackNotificationHolder.ACTION_SKIP_PREV -> playbackManager.prev()
PlaybackNotificationHolder.ACTION_PLAY_PAUSE ->
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
PlaybackNotificationHolder.ACTION_SKIP_NEXT -> playbackManager.next()
PlaybackNotificationHolder.ACTION_SHUFFLE ->
playbackManager.setShuffleStatus(!playbackManager.isShuffling)
BluetoothDevice.ACTION_ACL_CONNECTED -> resume() BluetoothDevice.ACTION_ACL_CONNECTED -> resume()
BluetoothDevice.ACTION_ACL_DISCONNECTED -> pause() BluetoothDevice.ACTION_ACL_DISCONNECTED -> pause()

View file

@ -16,9 +16,8 @@ import org.oxycblt.auxio.playback.state.PlaybackStateCallback
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
// A ViewModel that acts as an intermediary between the UI and PlaybackStateManager // A ViewModel that acts as an intermediary between the UI and PlaybackStateManager
// TODO: Implement Looping Modes
// TODO: Implement User Queue // TODO: Implement User Queue
// TODO: Implement Persistence through Bundles/Databases/Idk // TODO: Implement Persistence through a Database
class PlaybackViewModel : ViewModel(), PlaybackStateCallback { class PlaybackViewModel : ViewModel(), PlaybackStateCallback {
// Playback // Playback
private val mSong = MutableLiveData<Song?>() private val mSong = MutableLiveData<Song?>()

View file

@ -16,7 +16,7 @@ import kotlin.random.Random
// If you want to add ui-side things, add to PlaybackViewModel. // If you want to add ui-side things, add to PlaybackViewModel.
// [Yes, I know MediaSessionCompat exists, but I like having full control over the // [Yes, I know MediaSessionCompat exists, but I like having full control over the
// playback state instead of dealing with android's likely buggy code.] // playback state instead of dealing with android's likely buggy code.]
internal class PlaybackStateManager { class PlaybackStateManager private constructor() {
// Playback // Playback
private var mSong: Song? = null private var mSong: Song? = null
set(value) { set(value) {

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp" android:width="24dp"
android:height="32dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal"> android:tint="?android:attr/colorControlNormal">

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#80ffffff"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/>
</vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/>
</vector>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp" android:width="24dp"
android:height="32dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal"> android:tint="?android:attr/colorControlNormal">

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4zM13,15L13,9h-1l-2,1v1h1.5v4L13,15z"/>
</vector>

View file

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp" android:width="24dp"
android:height="32dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal"> android:tint="?android:attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="m 7.5,6 h 3 v 12 h -3 z m 9,0 h -3 v 12 h 3 z" /> android:pathData="M 5.571429,19.5 H 9.857143 V 4.5000003 H 5.571429 Z M 14.142857,4.5000003 V 19.5 h 4.285714 V 4.5000003 Z"/>
</vector> </vector>

View file

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp" android:width="24dp"
android:height="32dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal"> android:tint="?android:attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M8.25,6L8.25,12L18.75,12L18.75,12ZM8.25,18L8.25,12L18.75,12L18.75,12Z" /> android:pathData="M 5.0778755,4.0890012 V 12 H 18.922123 v 0 z m 0,15.8219978 V 12 H 18.922123 v 0 z" />
</vector> </vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#80ffffff"
android:pathData="M10.59,9.17L5.41,4 4,5.41l5.17,5.17 1.42,-1.41zM14.5,4l2.04,2.04L4,18.59 5.41,20 17.96,7.46 20,9.5L20,4h-5.5zM14.83,13.41l-1.41,1.41 3.13,3.13L14.5,20L20,20v-5.5l-2.04,2.04 -3.13,-3.13z" />
</vector>

View file

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp" android:width="24dp"
android:height="32dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:autoMirrored="true"
android:tint="?android:attr/colorControlNormal"> android:tint="?android:attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:autoMirrored="true"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z" />
</vector>

View file

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp" android:width="24dp"
android:height="32dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24"
android:autoMirrored="true"
android:tint="?android:attr/colorControlNormal"> android:tint="?android:attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:autoMirrored="true"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z" />
</vector>

View file

@ -167,7 +167,7 @@
android:layout_marginStart="@dimen/margin_mid_large" android:layout_marginStart="@dimen/margin_mid_large"
android:contentDescription="@string/description_skip_next" android:contentDescription="@string/description_skip_next"
android:background="@drawable/ui_unbounded_ripple" android:background="@drawable/ui_unbounded_ripple"
android:src="@drawable/ic_skip_next" android:src="@drawable/ic_skip_next_large"
android:onClick="@{() -> playbackModel.skipNext()}" android:onClick="@{() -> playbackModel.skipNext()}"
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause" app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
app:layout_constraintStart_toEndOf="@+id/playback_play_pause" app:layout_constraintStart_toEndOf="@+id/playback_play_pause"
@ -178,7 +178,7 @@
style="@style/Widget.AppCompat.Button.Borderless" style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="@dimen/size_play_pause_compact" android:layout_width="@dimen/size_play_pause_compact"
android:layout_height="@dimen/size_play_pause_compact" android:layout_height="@dimen/size_play_pause_compact"
android:src="@drawable/ic_skip_prev" android:src="@drawable/ic_skip_prev_large"
android:contentDescription="@string/description_skip_prev" android:contentDescription="@string/description_skip_prev"
android:background="@drawable/ui_unbounded_ripple" android:background="@drawable/ui_unbounded_ripple"
android:layout_marginEnd="@dimen/margin_mid_large" android:layout_marginEnd="@dimen/margin_mid_large"
@ -207,7 +207,7 @@
android:layout_width="@dimen/size_play_pause_compact" android:layout_width="@dimen/size_play_pause_compact"
android:layout_height="@dimen/size_play_pause_compact" android:layout_height="@dimen/size_play_pause_compact"
android:background="@drawable/ui_unbounded_ripple" android:background="@drawable/ui_unbounded_ripple"
android:src="@drawable/ic_loop" android:src="@drawable/ic_loop_large"
android:layout_marginStart="@dimen/margin_mid_large" android:layout_marginStart="@dimen/margin_mid_large"
android:onClick="@{() -> playbackModel.incrementLoopStatus()}" android:onClick="@{() -> playbackModel.incrementLoopStatus()}"
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause" app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"