music: improve indexer callback impl

Improve the indexer callback system to be more coherent and efficient.

This delegates the old Callback role to a new singular Callback and
Controller roles. IndexerService also handles the loading process
more gracefully, reducing the amount of time music loads take.
This commit is contained in:
OxygenCobalt 2022-07-02 15:31:01 -06:00
parent 49a964705b
commit bbf3b1778b
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 177 additions and 132 deletions

View file

@ -25,7 +25,6 @@ import coil.request.Disposable
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Size import coil.size.Size
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
/** /**
* A utility to provide bitmaps in a manner less prone to race conditions. * A utility to provide bitmaps in a manner less prone to race conditions.
@ -54,7 +53,6 @@ class BitmapProvider(private val context: Context) {
// Increment the generation value so that previous requests are invalidated. // Increment the generation value so that previous requests are invalidated.
// This is a second safeguard to mitigate instruction-by-instruction race conditions. // This is a second safeguard to mitigate instruction-by-instruction race conditions.
val generation = synchronized(this) { ++currentGeneration } val generation = synchronized(this) { ++currentGeneration }
logD("new generation for ${song.rawName}: $generation $currentGeneration")
currentRequest?.run { disposable.dispose() } currentRequest?.run { disposable.dispose() }
currentRequest = null currentRequest = null

View file

@ -23,14 +23,14 @@ import android.content.pm.PackageManager
import android.database.Cursor import android.database.Cursor
import android.os.Build import android.os.Build
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.coroutines.Dispatchers import org.oxycblt.auxio.BuildConfig
import kotlinx.coroutines.withContext
import org.oxycblt.auxio.music.backend.Api21MediaStoreBackend import org.oxycblt.auxio.music.backend.Api21MediaStoreBackend
import org.oxycblt.auxio.music.backend.Api29MediaStoreBackend import org.oxycblt.auxio.music.backend.Api29MediaStoreBackend
import org.oxycblt.auxio.music.backend.Api30MediaStoreBackend import org.oxycblt.auxio.music.backend.Api30MediaStoreBackend
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW
/** /**
* Auxio's media indexer. * Auxio's media indexer.
@ -58,7 +58,8 @@ class Indexer {
private var indexingState: Indexing? = null private var indexingState: Indexing? = null
private var currentGeneration = 0L private var currentGeneration = 0L
private val callbacks = mutableListOf<Callback>() private var controller: Controller? = null
private var callback: Callback? = null
/** /**
* Whether this instance is in an indeterminate state or not, where nothing has been previously * Whether this instance is in an indeterminate state or not, where nothing has been previously
@ -67,19 +68,50 @@ class Indexer {
val isIndeterminate: Boolean val isIndeterminate: Boolean
get() = lastResponse == null && indexingState == null get() = lastResponse == null && indexingState == null
fun addCallback(callback: Callback) { /** Register a [Controller] with this instance. */
fun registerController(controller: Controller) {
if (BuildConfig.DEBUG && this.controller != null) {
logW("Controller is already registered")
return
}
synchronized(this) { this.controller = controller }
}
/** Unregister a [Controller] with this instance. */
fun unregisterController(controller: Controller) {
if (BuildConfig.DEBUG && this.controller !== controller) {
logW("Given controller did not match current controller")
return
}
synchronized(this) { this.controller = null }
}
fun registerCallback(callback: Callback) {
if (BuildConfig.DEBUG && this.callback != null) {
logW("Callback is already registered")
return
}
val currentState = val currentState =
indexingState?.let { State.Indexing(it) } ?: lastResponse?.let { State.Complete(it) } indexingState?.let { State.Indexing(it) } ?: lastResponse?.let { State.Complete(it) }
callback.onIndexerStateChanged(currentState) callback.onIndexerStateChanged(currentState)
callbacks.add(callback)
this.callback = callback
} }
fun removeCallback(callback: Callback) { fun unregisterCallback(callback: Callback) {
callbacks.remove(callback) if (BuildConfig.DEBUG && this.callback !== callback) {
logW("Given controller did not match current controller")
return
}
this.callback = null
} }
suspend fun index(context: Context) { fun index(context: Context) {
val generation = synchronized(this) { ++currentGeneration } val generation = synchronized(this) { ++currentGeneration }
val notGranted = val notGranted =
@ -94,7 +126,7 @@ class Indexer {
val response = val response =
try { try {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val library = withContext(Dispatchers.IO) { indexImpl(context, generation) } val library = indexImpl(context, generation)
if (library != null) { if (library != null) {
logD( logD(
"Music indexing completed successfully in " + "Music indexing completed successfully in " +
@ -119,9 +151,7 @@ class Indexer {
*/ */
fun requestReindex() { fun requestReindex() {
logD("Requesting reindex") logD("Requesting reindex")
for (callback in callbacks) { controller?.onStartIndexing()
callback.onRequestReindex()
}
} }
/** /**
@ -152,9 +182,8 @@ class Indexer {
indexingState?.let { State.Indexing(it) } indexingState?.let { State.Indexing(it) }
?: lastResponse?.let { State.Complete(it) } ?: lastResponse?.let { State.Complete(it) }
for (callback in callbacks) { controller?.onIndexerStateChanged(state)
callback.onIndexerStateChanged(state) callback?.onIndexerStateChanged(state)
}
} }
} }
@ -168,9 +197,8 @@ class Indexer {
indexingState = null indexingState = null
val state = State.Complete(response) val state = State.Complete(response)
for (callback in callbacks) { controller?.onIndexerStateChanged(state)
callback.onIndexerStateChanged(state) callback?.onIndexerStateChanged(state)
}
} }
} }
@ -377,12 +405,10 @@ class Indexer {
* canceled for one reason or another. * canceled for one reason or another.
*/ */
fun onIndexerStateChanged(state: State?) fun onIndexerStateChanged(state: State?)
}
/** interface Controller : Callback {
* Called when some piece of code that cannot index music requests a reindex. Callbacks that fun onStartIndexing()
* can index music should begin reindexing at this call.
*/
fun onRequestReindex() {}
} }
/** Represents a backend that metadata can be extracted from. */ /** Represents a backend that metadata can be extracted from. */

View file

@ -0,0 +1,88 @@
/*
* Copyright (c) 2022 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newMainPendingIntent
class IndexerNotification(private val context: Context) :
NotificationCompat.Builder(context, CHANNEL_ID) {
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(
CHANNEL_ID,
context.getString(R.string.info_indexer_channel_name),
NotificationManager.IMPORTANCE_LOW)
notificationManager.createNotificationChannel(channel)
}
setSmallIcon(R.drawable.ic_indexer_24)
setCategory(NotificationCompat.CATEGORY_PROGRESS)
setShowWhen(false)
setSilent(true)
setContentIntent(context.newMainPendingIntent())
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
setContentTitle(context.getString(R.string.info_indexer_channel_name))
setContentText(context.getString(R.string.lbl_indexing))
setProgress(0, 0, true)
}
fun renotify() {
notificationManager.notify(IntegerTable.INDEXER_NOTIFICATION_CODE, build())
}
fun updateIndexingState(indexing: Indexer.Indexing): Boolean {
when (indexing) {
is Indexer.Indexing.Indeterminate -> {
logD("Updating state to $indexing")
setContentText(context.getString(R.string.lbl_indexing))
setProgress(0, 0, true)
return true
}
is Indexer.Indexing.Songs -> {
// Only update the notification every 50 songs to prevent excessive updates.
if (indexing.current % 50 == 0) {
logD("Updating state to $indexing")
setContentText(
context.getString(R.string.fmt_indexing, indexing.current, indexing.total))
setProgress(indexing.total, indexing.current, false)
return true
}
}
}
return false
}
companion object {
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER"
}
}

View file

@ -17,25 +17,17 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service import android.app.Service
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig import kotlinx.coroutines.withContext
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newMainPendingIntent
/** /**
* A [Service] that handles the music loading process. * A [Service] that handles the music loading process.
@ -48,13 +40,13 @@ import org.oxycblt.auxio.util.newMainPendingIntent
* *
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class IndexerService : Service(), Indexer.Callback { class IndexerService : Service(), Indexer.Controller {
private val indexer = Indexer.getInstance() private val indexer = Indexer.getInstance()
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
private val serviceJob = Job() private val serviceJob = Job()
private val indexScope = CoroutineScope(serviceJob + Dispatchers.Main) private val indexScope = CoroutineScope(serviceJob + Dispatchers.IO)
private val updateScope = CoroutineScope(serviceJob + Dispatchers.Main) private val updateScope = CoroutineScope(serviceJob + Dispatchers.IO)
private var isForeground = false private var isForeground = false
private lateinit var notification: IndexerNotification private lateinit var notification: IndexerNotification
@ -64,10 +56,10 @@ class IndexerService : Service(), Indexer.Callback {
notification = IndexerNotification(this) notification = IndexerNotification(this)
indexer.addCallback(this) indexer.registerController(this)
if (musicStore.library == null && indexer.isIndeterminate) { if (musicStore.library == null && indexer.isIndeterminate) {
logD("No library present and no previous response, indexing music now") logD("No library present and no previous response, indexing music now")
onRequestReindex() onStartIndexing()
} }
logD("Service created.") logD("Service created.")
@ -83,10 +75,14 @@ class IndexerService : Service(), Indexer.Callback {
// cancelLast actually stops foreground for us as it updates the loading state to // cancelLast actually stops foreground for us as it updates the loading state to
// null or completed. // null or completed.
indexer.cancelLast() indexer.cancelLast()
indexer.removeCallback(this) indexer.unregisterController(this)
serviceJob.cancel() serviceJob.cancel()
} }
override fun onStartIndexing() {
indexScope.launch { indexer.index(this@IndexerService) }
}
override fun onIndexerStateChanged(state: Indexer.State?) { override fun onIndexerStateChanged(state: Indexer.State?) {
when (state) { when (state) {
is Indexer.State.Complete -> { is Indexer.State.Complete -> {
@ -98,7 +94,12 @@ class IndexerService : Service(), Indexer.Callback {
// have not already. Only when we are done updating the library will // have not already. Only when we are done updating the library will
// the service stop it's foreground state. // the service stop it's foreground state.
updateScope.launch { updateScope.launch {
musicStore.updateLibrary(state.response.library) // TODO: Update PlaybackStateManager here
withContext(Dispatchers.Main) {
musicStore.updateLibrary(state.response.library)
}
stopForegroundSession() stopForegroundSession()
} }
} else { } else {
@ -133,10 +134,6 @@ class IndexerService : Service(), Indexer.Callback {
} }
} }
override fun onRequestReindex() {
indexScope.launch { indexer.index(this@IndexerService) }
}
private fun stopForegroundSession() { private fun stopForegroundSession() {
if (isForeground) { if (isForeground) {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
@ -144,61 +141,3 @@ class IndexerService : Service(), Indexer.Callback {
} }
} }
} }
private class IndexerNotification(private val context: Context) :
NotificationCompat.Builder(context, CHANNEL_ID) {
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(
CHANNEL_ID,
context.getString(R.string.info_indexer_channel_name),
NotificationManager.IMPORTANCE_LOW)
notificationManager.createNotificationChannel(channel)
}
setSmallIcon(R.drawable.ic_indexer_24)
setCategory(NotificationCompat.CATEGORY_PROGRESS)
setShowWhen(false)
setSilent(true)
setContentIntent(context.newMainPendingIntent())
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
setContentTitle(context.getString(R.string.info_indexer_channel_name))
setContentText(context.getString(R.string.lbl_indexing))
setProgress(0, 0, true)
}
fun renotify() {
notificationManager.notify(IntegerTable.INDEXER_NOTIFICATION_CODE, build())
}
fun updateIndexingState(indexing: Indexer.Indexing): Boolean {
when (indexing) {
is Indexer.Indexing.Indeterminate -> {
logD("Updating state to $indexing")
setContentText(context.getString(R.string.lbl_indexing))
setProgress(0, 0, true)
return true
}
is Indexer.Indexing.Songs -> {
// Only update the notification every 50 songs to prevent excessive updates.
if (indexing.current % 50 == 0) {
logD("Updating state to $indexing")
setContentText(
context.getString(R.string.fmt_indexing, indexing.current, indexing.total))
setProgress(indexing.total, indexing.current, false)
return true
}
}
}
return false
}
companion object {
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER"
}
}

View file

@ -32,7 +32,7 @@ class IndexerViewModel : ViewModel(), Indexer.Callback {
val state: StateFlow<Indexer.State?> = _state val state: StateFlow<Indexer.State?> = _state
init { init {
indexer.addCallback(this) indexer.registerCallback(this)
} }
fun reindex() { fun reindex() {
@ -44,6 +44,6 @@ class IndexerViewModel : ViewModel(), Indexer.Callback {
} }
override fun onCleared() { override fun onCleared() {
indexer.removeCallback(this) indexer.unregisterCallback(this)
} }
} }

View file

@ -20,8 +20,6 @@ package org.oxycblt.auxio.music
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.oxycblt.auxio.util.contentResolverSafe import org.oxycblt.auxio.util.contentResolverSafe
/** /**
@ -48,15 +46,11 @@ class MusicStore private constructor() {
callbacks.remove(callback) callbacks.remove(callback)
} }
suspend fun updateLibrary(newLibrary: Library?) { fun updateLibrary(newLibrary: Library?) {
// Ensure we are on the main thread when updating the library, as callbacks expect to synchronized(this) {
// run in an stable app thread. library = newLibrary
withContext(Dispatchers.Main) { for (callback in callbacks) {
synchronized(this) { callback.onLibraryChanged(library)
library = newLibrary
for (callback in callbacks) {
callback.onLibraryChanged(library)
}
} }
} }
} }

View file

@ -105,12 +105,12 @@ class PlaybackStateManager private constructor() {
} }
} }
/** Remove a [PlaybackStateManager.Callback] bound to this instance. */ /** Remove a [Callback] bound to this instance. */
fun removeCallback(callback: Callback) { fun removeCallback(callback: Callback) {
callbacks.remove(callback) callbacks.remove(callback)
} }
/** Register a [PlaybackStateManager.Controller] with this instance. */ /** Register a [Controller] with this instance. */
fun registerController(controller: Controller) { fun registerController(controller: Controller) {
if (BuildConfig.DEBUG && this.controller != null) { if (BuildConfig.DEBUG && this.controller != null) {
logW("Controller is already registered") logW("Controller is already registered")
@ -129,7 +129,7 @@ class PlaybackStateManager private constructor() {
} }
} }
/** Unregister a [PlaybackStateManager.Controller] with this instance. */ /** Unregister a [Controller] with this instance. */
fun unregisterController(controller: Controller) { fun unregisterController(controller: Controller) {
if (BuildConfig.DEBUG && this.controller !== controller) { if (BuildConfig.DEBUG && this.controller !== controller) {
logW("Given controller did not match current controller") logW("Given controller did not match current controller")

View file

@ -81,7 +81,7 @@
<android.widget.ImageButton <android.widget.ImageButton
android:id="@+id/widget_repeat" android:id="@+id/widget_repeat"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn" android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn" android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_change_repeat" android:contentDescription="@string/desc_change_repeat"
@ -94,7 +94,7 @@
<android.widget.ImageButton <android.widget.ImageButton
android:id="@+id/widget_skip_prev" android:id="@+id/widget_skip_prev"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn" android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn" android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_skip_prev" android:contentDescription="@string/desc_skip_prev"
@ -122,7 +122,7 @@
<android.widget.ImageButton <android.widget.ImageButton
android:id="@+id/widget_skip_next" android:id="@+id/widget_skip_next"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn" android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn" android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_skip_next" android:contentDescription="@string/desc_skip_next"
@ -135,7 +135,7 @@
<android.widget.ImageButton <android.widget.ImageButton
android:id="@+id/widget_shuffle" android:id="@+id/widget_shuffle"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn" android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn" android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_shuffle" android:contentDescription="@string/desc_shuffle"

View file

@ -79,7 +79,7 @@
<android.widget.ImageButton <android.widget.ImageButton
android:id="@+id/widget_skip_prev" android:id="@+id/widget_skip_prev"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn" android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn" android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_skip_prev" android:contentDescription="@string/desc_skip_prev"
@ -107,7 +107,7 @@
<android.widget.ImageButton <android.widget.ImageButton
android:id="@+id/widget_skip_next" android:id="@+id/widget_skip_next"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn" android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn" android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_skip_next" android:contentDescription="@string/desc_skip_next"

View file

@ -68,7 +68,7 @@
<android.widget.ImageButton <android.widget.ImageButton
android:id="@+id/widget_skip_prev" android:id="@+id/widget_skip_prev"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn" android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn" android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_skip_prev" android:contentDescription="@string/desc_skip_prev"
@ -94,7 +94,7 @@
<android.widget.ImageButton <android.widget.ImageButton
android:id="@+id/widget_skip_next" android:id="@+id/widget_skip_next"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn" android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn" android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_skip_next" android:contentDescription="@string/desc_skip_next"

View file

@ -55,7 +55,7 @@
<android.widget.ImageButton <android.widget.ImageButton
android:id="@+id/widget_repeat" android:id="@+id/widget_repeat"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn" android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn" android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_change_repeat" android:contentDescription="@string/desc_change_repeat"
@ -68,7 +68,7 @@
<android.widget.ImageButton <android.widget.ImageButton
android:id="@+id/widget_skip_prev" android:id="@+id/widget_skip_prev"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn" android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn" android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_skip_prev" android:contentDescription="@string/desc_skip_prev"
@ -95,7 +95,7 @@
<android.widget.ImageButton <android.widget.ImageButton
android:id="@+id/widget_skip_next" android:id="@+id/widget_skip_next"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn" android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn" android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_skip_next" android:contentDescription="@string/desc_skip_next"
@ -108,7 +108,7 @@
<android.widget.ImageButton <android.widget.ImageButton
android:id="@+id/widget_shuffle" android:id="@+id/widget_shuffle"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn" android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn" android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_shuffle" android:contentDescription="@string/desc_shuffle"

View file

@ -49,7 +49,7 @@
</style> </style>
<!-- A variant of PlaybackButton that plays along with AppWidget restrictions. --> <!-- A variant of PlaybackButton that plays along with AppWidget restrictions. -->
<style name="Widget.Auxio.PlaybackButton.AppWidget" parent="Widget.AppCompat.Button.Borderless"> <style name="Widget.Auxio.MaterialButton.AppWidget" parent="Widget.AppCompat.Button.Borderless">
<item name="android:minHeight">@dimen/size_btn</item> <item name="android:minHeight">@dimen/size_btn</item>
<item name="android:background">@drawable/ui_remote_ripple</item> <item name="android:background">@drawable/ui_remote_ripple</item>
</style> </style>