all: cleanup

Semi-major code cleanup.
This commit is contained in:
OxygenCobalt 2022-07-11 11:29:34 -06:00
parent caa755c12f
commit 60b637e1ce
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
43 changed files with 288 additions and 307 deletions

View file

@ -82,7 +82,6 @@ dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version" implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
// Media // Media
// TODO: Dumpster this for Media3
implementation "androidx.media:media:1.6.0" implementation "androidx.media:media:1.6.0"
// Preferences // Preferences

View file

@ -44,8 +44,6 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
* *
* TODO: Add multi-select * TODO: Add multi-select
* *
* TODO: Bug test runtime rescanning
*
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {

View file

@ -39,7 +39,7 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.recycler.Header import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.GenerationGuard import org.oxycblt.auxio.util.TaskGuard
import org.oxycblt.auxio.util.application import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
@ -113,7 +113,7 @@ class DetailViewModel(application: Application) :
currentGenre.value?.let(::refreshGenreData) currentGenre.value?.let(::refreshGenreData)
} }
private val songGuard = GenerationGuard() private val songGuard = TaskGuard()
fun setSongId(id: Long) { fun setSongId(id: Long) {
if (_currentSong.value?.run { song.id } == id) return if (_currentSong.value?.run { song.id } == id) return
@ -123,6 +123,7 @@ class DetailViewModel(application: Application) :
} }
fun clearSong() { fun clearSong() {
songGuard.newHandle()
_currentSong.value = null _currentSong.value = null
} }
@ -161,9 +162,9 @@ class DetailViewModel(application: Application) :
private fun generateDetailSong(song: Song) { private fun generateDetailSong(song: Song) {
_currentSong.value = DetailSong(song, null) _currentSong.value = DetailSong(song, null)
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val generation = songGuard.newHandle() val handle = songGuard.newHandle()
val info = generateDetailSongInfo(song) val info = generateDetailSongInfo(song)
songGuard.yield(generation) songGuard.yield(handle)
_currentSong.value = DetailSong(song, info) _currentSong.value = DetailSong(song, info)
} }
} }
@ -220,7 +221,7 @@ class DetailViewModel(application: Application) :
// To create a good user experience regarding disc numbers, we intersperse // To create a good user experience regarding disc numbers, we intersperse
// items that show the disc number throughout the album's songs. In the case // items that show the disc number throughout the album's songs. In the case
// that the album does not have distinct disc numbers, we omit the header. // that the album does not have distinct disc numbers, we omit such a header
val songs = albumSort.songs(album.songs) val songs = albumSort.songs(album.songs)
val byDisc = songs.groupBy { it.disc ?: 1 } val byDisc = songs.groupBy { it.disc ?: 1 }
if (byDisc.size > 1) { if (byDisc.size > 1) {

View file

@ -46,8 +46,8 @@ import org.oxycblt.auxio.home.list.SongListFragment
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.IndexerViewModel
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.system.Indexer import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
@ -75,7 +75,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val navModel: NavigationViewModel by activityViewModels() private val navModel: NavigationViewModel by activityViewModels()
private val homeModel: HomeViewModel by androidActivityViewModels() private val homeModel: HomeViewModel by androidActivityViewModels()
private val indexerModel: IndexerViewModel by activityViewModels() private val indexerModel: MusicViewModel by activityViewModels()
// lifecycleObject builds this in the creation step, so doing this is okay. // lifecycleObject builds this in the creation step, so doing this is okay.
private val storagePermissionLauncher: ActivityResultLauncher<String> by lifecycleObject { private val storagePermissionLauncher: ActivityResultLauncher<String> by lifecycleObject {
@ -135,10 +135,10 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
collectImmediately(homeModel.songs, homeModel.isFastScrolling, ::updateFab)
collect(homeModel.recreateTabs, ::handleRecreateTabs) collect(homeModel.recreateTabs, ::handleRecreateTabs)
collectImmediately(homeModel.currentTab, ::updateCurrentTab) collectImmediately(homeModel.currentTab, ::updateCurrentTab)
collectImmediately(indexerModel.state, ::handleIndexerState) collectImmediately(indexerModel.libraryExists, homeModel.isFastScrolling, ::updateFab)
collectImmediately(indexerModel.indexerState, ::handleIndexerState)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
} }
@ -328,9 +328,9 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
} }
private fun updateFab(songs: List<Song>, isFastScrolling: Boolean) { private fun updateFab(hasLoaded: Boolean, isFastScrolling: Boolean) {
val binding = requireBinding() val binding = requireBinding()
if (isFastScrolling || songs.isEmpty()) { if (!hasLoaded || isFastScrolling) {
binding.homeFab.hide() binding.homeFab.hide()
} else { } else {
binding.homeFab.show() binding.homeFab.show()

View file

@ -36,7 +36,6 @@ import kotlin.math.abs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.recycler.EdgeRecyclerView import org.oxycblt.auxio.ui.recycler.EdgeRecyclerView
import org.oxycblt.auxio.util.canScroll import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.clamp
import org.oxycblt.auxio.util.getDimenOffsetSafe import org.oxycblt.auxio.util.getDimenOffsetSafe
import org.oxycblt.auxio.util.getDimenSizeSafe import org.oxycblt.auxio.util.getDimenSizeSafe
import org.oxycblt.auxio.util.getDrawableSafe import org.oxycblt.auxio.util.getDrawableSafe
@ -260,9 +259,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
val thumbAnchorY = thumbView.paddingTop val thumbAnchorY = thumbView.paddingTop
val popupTop = val popupTop =
(thumbTop + thumbAnchorY - popupAnchorY).clamp( (thumbTop + thumbAnchorY - popupAnchorY)
thumbPadding.top + popupLayoutParams.topMargin, .coerceAtLeast(thumbPadding.top + popupLayoutParams.topMargin)
height - thumbPadding.bottom - popupLayoutParams.bottomMargin - popupHeight) .coerceAtMost(
height - thumbPadding.bottom - popupLayoutParams.bottomMargin - popupHeight)
popupView.layout(popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight) popupView.layout(popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight)
} }
@ -358,7 +358,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
private fun scrollToThumbOffset(thumbOffset: Int) { private fun scrollToThumbOffset(thumbOffset: Int) {
val clampedThumbOffset = thumbOffset.clamp(0, thumbOffsetRange) val clampedThumbOffset = thumbOffset.coerceAtLeast(0).coerceAtMost(thumbOffsetRange)
val scrollOffset = val scrollOffset =
(scrollOffsetRange.toLong() * clampedThumbOffset / thumbOffsetRange).toInt() - (scrollOffsetRange.toLong() * clampedThumbOffset / thumbOffsetRange).toInt() -

View file

@ -25,22 +25,20 @@ 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.GenerationGuard import org.oxycblt.auxio.util.TaskGuard
/** /**
* 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.
* *
* Pretty much each service component needs to load bitmaps of some kind, but doing a blind image * Pretty much each service component needs to load bitmaps of some kind, but doing a blind image
* request with some target callbacks could result in overlapping requests causing unrelated * request with some target callbacks could result in overlapping requests causing incorrect
* updates. This class (to an extent) resolves this by keeping track of the current request and * updates. This class (to an extent) resolves this by adding several guards
* disposing of it every time a new request is created. This greatly reduces the surface for race
* conditions.
* *
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class BitmapProvider(private val context: Context) { class BitmapProvider(private val context: Context) {
private var currentRequest: Request? = null private var currentRequest: Request? = null
private var guard = GenerationGuard() private var guard = TaskGuard()
/** If this provider is currently attempting to load something. */ /** If this provider is currently attempting to load something. */
val isBusy: Boolean val isBusy: Boolean
@ -52,7 +50,7 @@ class BitmapProvider(private val context: Context) {
*/ */
@Synchronized @Synchronized
fun load(song: Song, target: Target) { fun load(song: Song, target: Target) {
val generation = guard.newHandle() val handle = guard.newHandle()
currentRequest?.run { disposable.dispose() } currentRequest?.run { disposable.dispose() }
currentRequest = null currentRequest = null
@ -64,12 +62,12 @@ class BitmapProvider(private val context: Context) {
.size(Size.ORIGINAL) .size(Size.ORIGINAL)
.target( .target(
onSuccess = { onSuccess = {
if (guard.check(generation)) { if (guard.check(handle)) {
target.onCompleted(it.toBitmap()) target.onCompleted(it.toBitmap())
} }
}, },
onError = { onError = {
if (guard.check(generation)) { if (guard.check(handle)) {
target.onCompleted(null) target.onCompleted(null)
} }
}) })

View file

@ -34,7 +34,9 @@ import org.oxycblt.auxio.util.contentResolverSafe
* a typical DB and a mem-cache, like Vinyl. But why would we do that when I've encountered no real * a typical DB and a mem-cache, like Vinyl. But why would we do that when I've encountered no real
* issues with the current system. * issues with the current system.
* *
* [Library] may not be available at all times, so leveraging [Callback] is recommended. * [Library] may not be available at all times, so leveraging [Callback] is recommended. Consumers
* should also be aware that [Library] may change while they are running, and design their work
* accordingly.
* *
* @author OxygenCobalt * @author OxygenCobalt
*/ */

View file

@ -23,16 +23,19 @@ import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.system.Indexer import org.oxycblt.auxio.music.system.Indexer
/** /**
* A ViewModel representing the current music indexing state. * A ViewModel representing the current indexing state.
* @author OxygenCobalt * @author OxygenCobalt
*
* TODO: Indeterminate state for Home + Settings
*/ */
class IndexerViewModel : ViewModel(), Indexer.Callback { class MusicViewModel : ViewModel(), Indexer.Callback {
private val indexer = Indexer.getInstance() private val indexer = Indexer.getInstance()
private val _state = MutableStateFlow<Indexer.State?>(null) private val _indexerState = MutableStateFlow<Indexer.State?>(null)
val state: StateFlow<Indexer.State?> = _state /** The current music indexing state. */
val indexerState: StateFlow<Indexer.State?> = _indexerState
private val _libraryExists = MutableStateFlow(false)
/** Whether a music library has successfully been loaded. */
val libraryExists: StateFlow<Boolean> = _libraryExists
init { init {
indexer.registerCallback(this) indexer.registerCallback(this)
@ -43,7 +46,11 @@ class IndexerViewModel : ViewModel(), Indexer.Callback {
} }
override fun onIndexerStateChanged(state: Indexer.State?) { override fun onIndexerStateChanged(state: Indexer.State?) {
_state.value = state _indexerState.value = state
if (state is Indexer.State.Complete && state.response is Indexer.Response.Ok) {
_libraryExists.value = true
}
} }
override fun onCleared() { override fun onCleared() {

View file

@ -51,15 +51,14 @@ data class Directory(val volume: StorageVolume, val relativePath: String) {
context.getString(R.string.fmt_path, volume.getDescriptionCompat(context), relativePath) context.getString(R.string.fmt_path, volume.getDescriptionCompat(context), relativePath)
/** Converts this dir into an opaque document URI in the form of VOLUME:PATH. */ /** Converts this dir into an opaque document URI in the form of VOLUME:PATH. */
fun toDocumentUri(): String? { fun toDocumentUri() =
// "primary" actually corresponds to the internal storage, not the primary volume. // "primary" actually corresponds to the internal storage, not the primary volume.
// Removable storage is represented with the UUID. // Removable storage is represented with the UUID.
return if (volume.isInternalCompat) { if (volume.isInternalCompat) {
"${DOCUMENT_URI_PRIMARY_NAME}:${relativePath}" "${DOCUMENT_URI_PRIMARY_NAME}:${relativePath}"
} else { } else {
"${(volume.uuidCompat ?: return null).uppercase()}:${relativePath}" volume.uuidCompat?.let { uuid -> "${uuid}:${relativePath}" }
} }
}
companion object { companion object {
private const val DOCUMENT_URI_PRIMARY_NAME = "primary" private const val DOCUMENT_URI_PRIMARY_NAME = "primary"

View file

@ -33,7 +33,7 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.GenerationGuard import org.oxycblt.auxio.util.TaskGuard
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 import org.oxycblt.auxio.util.logW
@ -64,7 +64,7 @@ class Indexer {
private var lastResponse: Response? = null private var lastResponse: Response? = null
private var indexingState: Indexing? = null private var indexingState: Indexing? = null
private var guard = GenerationGuard() private var guard = TaskGuard()
private var controller: Controller? = null private var controller: Controller? = null
private var callback: Callback? = null private var callback: Callback? = null
@ -133,21 +133,21 @@ class Indexer {
suspend fun index(context: Context) { suspend fun index(context: Context) {
requireBackgroundThread() requireBackgroundThread()
val generation = guard.newHandle() val handle = guard.newHandle()
val notGranted = val notGranted =
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_DENIED PackageManager.PERMISSION_DENIED
if (notGranted) { if (notGranted) {
emitCompletion(Response.NoPerms, generation) emitCompletion(Response.NoPerms, handle)
return return
} }
val response = val response =
try { try {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val library = indexImpl(context, generation) val library = indexImpl(context, handle)
if (library != null) { if (library != null) {
logD( logD(
"Music indexing completed successfully in " + "Music indexing completed successfully in " +
@ -163,7 +163,7 @@ class Indexer {
Response.Err(e) Response.Err(e)
} }
emitCompletion(response, generation) emitCompletion(response, handle)
} }
/** /**
@ -178,63 +178,22 @@ class Indexer {
/** /**
* "Cancel" the last job by making it unable to send further state updates. This will cause the * "Cancel" the last job by making it unable to send further state updates. This will cause the
* worker operating the job for that specific generation to cancel as soon as it tries to send a * worker operating the job for that specific handle to cancel as soon as it tries to send a
* state update. * state update.
*/ */
@Synchronized @Synchronized
fun cancelLast() { fun cancelLast() {
logD("Cancelling last job") logD("Cancelling last job")
val generation = guard.newHandle() val handle = guard.newHandle()
emitIndexing(null, generation) emitIndexing(null, handle)
}
@Synchronized
private fun emitIndexing(indexing: Indexing?, generation: Long) {
guard.yield(generation)
if (indexing == indexingState) {
// Ignore redundant states used when the backends just want to check for
// a cancellation
return
}
indexingState = indexing
// If we have canceled the loading process, we want to revert to a previous completion
// whenever possible to prevent state inconsistency.
val state =
indexingState?.let { State.Indexing(it) } ?: lastResponse?.let { State.Complete(it) }
controller?.onIndexerStateChanged(state)
callback?.onIndexerStateChanged(state)
}
private suspend fun emitCompletion(response: Response, generation: Long) {
guard.yield(generation)
// Swap to the Main thread so that downstream callbacks don't crash from being on
// a background thread. Does not occur in emitIndexing due to efficiency reasons.
withContext(Dispatchers.Main) {
synchronized(this) {
// Do not check for redundancy here, as we actually need to notify a switch
// from Indexing -> Complete and not Indexing -> Indexing or Complete -> Complete.
lastResponse = response
indexingState = null
val state = State.Complete(response)
controller?.onIndexerStateChanged(state)
callback?.onIndexerStateChanged(state)
}
}
} }
/** /**
* Run the proper music loading process. [generation] must be a truthful value of the generation * Run the proper music loading process. [handle] must be a truthful handle of the task calling
* calling this function. * this function.
*/ */
private fun indexImpl(context: Context, generation: Long): MusicStore.Library? { private fun indexImpl(context: Context, handle: Long): MusicStore.Library? {
emitIndexing(Indexing.Indeterminate, generation) emitIndexing(Indexing.Indeterminate, handle)
// Since we have different needs for each version, we determine a "Backend" to use // Since we have different needs for each version, we determine a "Backend" to use
// when loading music and then leverage that to create the initial song list. // when loading music and then leverage that to create the initial song list.
@ -256,7 +215,7 @@ class Indexer {
mediaStoreBackend mediaStoreBackend
} }
val songs = buildSongs(context, backend, generation) val songs = buildSongs(context, backend, handle)
if (songs.isEmpty()) { if (songs.isEmpty()) {
return null return null
} }
@ -290,7 +249,7 @@ class Indexer {
* [buildGenres] functions must be called with the returned list so that all songs are properly * [buildGenres] functions must be called with the returned list so that all songs are properly
* linked up. * linked up.
*/ */
private fun buildSongs(context: Context, backend: Backend, generation: Long): List<Song> { private fun buildSongs(context: Context, backend: Backend, handle: Long): List<Song> {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
var songs = var songs =
@ -299,7 +258,7 @@ class Indexer {
"Successfully queried media database " + "Successfully queried media database " +
"in ${System.currentTimeMillis() - start}ms") "in ${System.currentTimeMillis() - start}ms")
backend.buildSongs(context, cursor) { emitIndexing(it, generation) } backend.buildSongs(context, cursor) { emitIndexing(it, handle) }
} }
// Deduplicate songs to prevent (most) deformed music clones // Deduplicate songs to prevent (most) deformed music clones
@ -403,6 +362,47 @@ class Indexer {
return genres return genres
} }
@Synchronized
private fun emitIndexing(indexing: Indexing?, handle: Long) {
guard.yield(handle)
if (indexing == indexingState) {
// Ignore redundant states used when the backends just want to check for
// a cancellation
return
}
indexingState = indexing
// If we have canceled the loading process, we want to revert to a previous completion
// whenever possible to prevent state inconsistency.
val state =
indexingState?.let { State.Indexing(it) } ?: lastResponse?.let { State.Complete(it) }
controller?.onIndexerStateChanged(state)
callback?.onIndexerStateChanged(state)
}
private suspend fun emitCompletion(response: Response, handle: Long) {
guard.yield(handle)
// Swap to the Main thread so that downstream callbacks don't crash from being on
// a background thread. Does not occur in emitIndexing due to efficiency reasons.
withContext(Dispatchers.Main) {
synchronized(this) {
// Do not check for redundancy here, as we actually need to notify a switch
// from Indexing -> Complete and not Indexing -> Indexing or Complete -> Complete.
lastResponse = response
indexingState = null
val state = State.Complete(response)
controller?.onIndexerStateChanged(state)
callback?.onIndexerStateChanged(state)
}
}
}
/** Represents the current indexer state. */ /** Represents the current indexer state. */
sealed class State { sealed class State {
data class Indexing(val indexing: Indexer.Indexing) : State() data class Indexing(val indexing: Indexer.Indexing) : State()

View file

@ -102,13 +102,7 @@ class PlaybackPanelFragment :
binding.playbackRepeat.setOnClickListener { playbackModel.incrementRepeatMode() } binding.playbackRepeat.setOnClickListener { playbackModel.incrementRepeatMode() }
binding.playbackSkipPrev.setOnClickListener { playbackModel.prev() } binding.playbackSkipPrev.setOnClickListener { playbackModel.prev() }
binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlaying() }
binding.playbackPlayPause.apply {
// Abuse the play/pause FAB (see style definition for more info)
post { binding.playbackPlayPause.stateListAnimator = null }
setOnClickListener { playbackModel.invertPlaying() }
}
binding.playbackSkipNext.setOnClickListener { playbackModel.next() } binding.playbackSkipNext.setOnClickListener { playbackModel.next() }
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffled() } binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffled() }

View file

@ -284,9 +284,10 @@ class PlaybackViewModel(application: Application) :
/** /**
* Force restore the last [PlaybackStateManager] saved state, regardless of if a library exists * Force restore the last [PlaybackStateManager] saved state, regardless of if a library exists
* or not. * or not. [onDone] will be called with true if it was successfully done, or false if there was
* no state or if a library was not present.
*/ */
fun restorePlaybackState(onDone: (Boolean) -> Unit) { fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch { viewModelScope.launch {
val restored = val restored =
playbackManager.restoreState(PlaybackStateDatabase.getInstance(application)) playbackManager.restoreState(PlaybackStateDatabase.getInstance(application))

View file

@ -29,7 +29,6 @@ import kotlin.math.pow
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.clamp
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -42,6 +41,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* when the active track changes. * when the active track changes.
* *
* @author OxygenCobalt * @author OxygenCobalt
*
* TODO: Convert a low-level audio processor capable of handling any kind of PCM data.
*/ */
class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() { class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
private data class Gain(val track: Float, val album: Float) private data class Gain(val track: Float, val album: Float)
@ -235,7 +236,8 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
sample = sample =
(sample * volume) (sample * volume)
.toInt() .toInt()
.clamp(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()) .coerceAtLeast(Short.MIN_VALUE.toInt())
.coerceAtMost(Short.MAX_VALUE.toInt())
.toShort() .toShort()
buffer.putLeShort(sample) buffer.putLeShort(sample)
} }

View file

@ -400,6 +400,9 @@ class PlaybackStateManager private constructor() {
logD("Sanitizing state") logD("Sanitizing state")
// While we could just save and reload the state, we instead sanitize the state
// at runtime for better efficiency (and to sidestep a co-routine on behalf of the caller).
val oldSongId = song?.id val oldSongId = song?.id
val oldPosition = positionMs val oldPosition = positionMs

View file

@ -44,8 +44,9 @@ import org.oxycblt.auxio.util.logD
* *
* @author OxygenCobalt * @author OxygenCobalt
* *
* TODO: Update textual metadata first, then cover metadata later. Janky, yes, but also resolves * TODO: Queue functionality
* some coherency issues. *
* TODO: Remove the player callback once smooth seeking is implemented
*/ */
class MediaSessionComponent( class MediaSessionComponent(
private val context: Context, private val context: Context,
@ -60,10 +61,12 @@ class MediaSessionComponent(
fun onPostNotification(notification: NotificationComponent?) fun onPostNotification(notification: NotificationComponent?)
} }
val mediaSession = MediaSessionCompat(context, context.packageName).apply { isActive = true } private val mediaSession =
MediaSessionCompat(context, context.packageName).apply { isActive = true }
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
private val settings = Settings(context, this) private val settings = Settings(context, this)
private val notification = NotificationComponent(context, mediaSession.sessionToken) private val notification = NotificationComponent(context, mediaSession.sessionToken)
private val provider = BitmapProvider(context) private val provider = BitmapProvider(context)
@ -138,10 +141,18 @@ class MediaSessionComponent(
builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, song.album.year.toString()) builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, song.album.year.toString())
} }
// Normally, android expects one to provide a URI to the metadata instance instead of // Cover loading is a mess. Android expects you to provide a clean, easy URI for it to
// a full blown bitmap. In practice, this is not ideal in the slightest, as we cannot // leverage, but Auxio cannot do that as quality-of-life features like scaling or
// provide any user customization or quality of life improvements with a flat URI. // 1:1 cropping could not be used
// Instead, we load a full size bitmap and use it within it's respective fields. //
// Thus, we have two options to handle album art:
// 1. Load the bitmap, then post the notification
// 2. Post the notification with text metadata, then post it with the bitmap when it's
// loaded.
//
// Neither of these are good, but 1 is the only one that will work on all versions
// without the notification being eaten by rate-limiting.
provider.load( provider.load(
song, song,
object : BitmapProvider.Target { object : BitmapProvider.Target {
@ -262,10 +273,16 @@ class MediaSessionComponent(
private fun invalidateSessionState() { private fun invalidateSessionState() {
logD("Updating media session playback state") logD("Updating media session playback state")
// Position updates arrive faster when you upload a state that is different, as it // There are two unfixable issues with this code:
// forces the system to re-poll the position. // 1. If the position is changed while paused (from the app), the position just won't
// FIXME: For some reason however, positions just DON'T UPDATE AT ALL when you // update unless I re-post the notification. However, I cannot do such without being
// change from FROM THE APP ONLY WHEN THE PLAYER IS PAUSED. // rate-limited. I cannot believe android rate-limits media notifications when they
// have to be updated as often as they need to.
// 2. Due to metadata updates being delayed but playback remaining ongoing, the position
// will be wonky until we can upload a duration. Again, this ties back to how I must
// aggressively batch notification updates to prevent rate-limiting.
// Android 13 seems to resolve these, but I'm still stuck with these issues below that
// version.
// TODO: Add the custom actions for Android 13 // TODO: Add the custom actions for Android 13
val state = val state =
PlaybackStateCompat.Builder() PlaybackStateCompat.Builder()
@ -278,10 +295,6 @@ class MediaSessionComponent(
.build()) .build())
.setBufferedPosition(player.bufferedPosition) .setBufferedPosition(player.bufferedPosition)
state.setState(PlaybackStateCompat.STATE_NONE, player.bufferedPosition, 1.0f)
mediaSession.setPlaybackState(state.build())
val playerState = val playerState =
if (playbackManager.isPlaying) { if (playbackManager.isPlaying) {
PlaybackStateCompat.STATE_PLAYING PlaybackStateCompat.STATE_PLAYING

View file

@ -68,8 +68,6 @@ import org.oxycblt.auxio.widgets.WidgetProvider
* *
* TODO: Android Auto * TODO: Android Auto
* *
* TODO: Get MediaSessionConnector (or the media3 equivalent) working or die trying
*
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class PlaybackService : class PlaybackService :
@ -248,7 +246,7 @@ class PlaybackService :
override fun loadSong(song: Song?) { override fun loadSong(song: Song?) {
if (song == null) { if (song == null) {
// Clear if there's nothing to play. // Stop the foreground state if there's nothing to play.
logD("Nothing playing, stopping playback") logD("Nothing playing, stopping playback")
player.stop() player.stop()
stopAndSave() stopAndSave()
@ -281,6 +279,7 @@ class PlaybackService :
} }
if (hasPlayed) { if (hasPlayed) {
logD("Updating notification")
if (!foregroundManager.tryStartForeground(notification)) { if (!foregroundManager.tryStartForeground(notification)) {
notification.post() notification.post()
} }

View file

@ -70,19 +70,21 @@ fun handleAccentCompat(context: Context, prefs: SharedPreferences): Accent {
* was a dumb idea, as the choice of a full-blown database for a few paths was overkill, version * was a dumb idea, as the choice of a full-blown database for a few paths was overkill, version
* boundaries were not respected, and the data format limited us to grokking DATA. * boundaries were not respected, and the data format limited us to grokking DATA.
* *
* In 2.4.0, Auxio switched to a system based on SharedPreferences, also switching from a flat * In 2.4.0, Auxio switched to a system based on SharedPreferences, also switching from a path-based
* path-based excluded system to a volume-based excluded system at the same time. These are both * excluded system to a volume-based excluded system at the same time. These are both rolled into
* rolled into this conversion. * this conversion.
*/ */
fun handleExcludedCompat(context: Context, storageManager: StorageManager): List<Directory> { fun handleExcludedCompat(context: Context, storageManager: StorageManager): List<Directory> {
Log.d("Auxio.SettingsCompat", "Migrating old excluded database") Log.d("Auxio.SettingsCompat", "Migrating old excluded database")
val db = LegacyExcludedDatabase(context)
// /storage/emulated/0 (the old path prefix) should correspond to primary *emulated* storage. // /storage/emulated/0 (the old path prefix) should correspond to primary *emulated* storage.
val primaryVolume = val primaryVolume =
storageManager.storageVolumesCompat.find { it.isInternalCompat } ?: return emptyList() storageManager.storageVolumesCompat.find { it.isInternalCompat } ?: return emptyList()
val primaryDirectory = val primaryDirectory =
(primaryVolume.directoryCompat ?: return emptyList()) + File.separatorChar (primaryVolume.directoryCompat ?: return emptyList()) + File.separatorChar
return db.readPaths().map { path ->
return LegacyExcludedDatabase(context).readPaths().map { path ->
val relativePath = path.removePrefix(primaryDirectory) val relativePath = path.removePrefix(primaryDirectory)
Log.d("Auxio.SettingsCompat", "Migrate $path -> $relativePath") Log.d("Auxio.SettingsCompat", "Migrate $path -> $relativePath")
Directory(primaryVolume, relativePath) Directory(primaryVolume, relativePath)

View file

@ -31,7 +31,7 @@ import androidx.recyclerview.widget.RecyclerView
import coil.Coil import coil.Coil
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.TabCustomizeDialog import org.oxycblt.auxio.home.tabs.TabCustomizeDialog
import org.oxycblt.auxio.music.IndexerViewModel import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.dirs.MusicDirsDialog import org.oxycblt.auxio.music.dirs.MusicDirsDialog
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.replaygain.PreAmpCustomizeDialog import org.oxycblt.auxio.playback.replaygain.PreAmpCustomizeDialog
@ -51,21 +51,17 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
* @author OxygenCobalt * @author OxygenCobalt
* *
* TODO: Add option to not restore state * TODO: Add option to not restore state
*
* TODO: Disable playback state options when music is loading
*
* TODO: Indicate music loading in the "reload music" option???
*/ */
@Suppress("UNUSED") @Suppress("UNUSED")
class SettingsListFragment : PreferenceFragmentCompat() { class SettingsListFragment : PreferenceFragmentCompat() {
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val indexerModel: IndexerViewModel by activityViewModels() private val indexerModel: MusicViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
preferenceManager.onDisplayPreferenceDialogListener = this preferenceManager.onDisplayPreferenceDialogListener = this
preferenceScreen.children.forEach(::recursivelyHandlePreference) preferenceScreen.children.forEach(::setupPreference)
// Make the RecycleView edge-to-edge capable // Make the RecycleView edge-to-edge capable
view.findViewById<RecyclerView>(androidx.preference.R.id.recycler_view).apply { view.findViewById<RecyclerView>(androidx.preference.R.id.recycler_view).apply {
@ -125,7 +121,7 @@ class SettingsListFragment : PreferenceFragmentCompat() {
playbackModel.savePlaybackState { context?.showToast(R.string.lbl_state_saved) } playbackModel.savePlaybackState { context?.showToast(R.string.lbl_state_saved) }
} }
getString(R.string.set_key_restore_state) -> getString(R.string.set_key_restore_state) ->
playbackModel.restorePlaybackState { restored -> playbackModel.tryRestorePlaybackState { restored ->
if (restored) { if (restored) {
context?.showToast(R.string.lbl_state_restored) context?.showToast(R.string.lbl_state_restored)
} else { } else {
@ -141,15 +137,14 @@ class SettingsListFragment : PreferenceFragmentCompat() {
return true return true
} }
/** Recursively handle a preference, doing any specific actions on it. */ private fun setupPreference(preference: Preference) {
private fun recursivelyHandlePreference(preference: Preference) {
val settings = Settings(requireContext()) val settings = Settings(requireContext())
if (!preference.isVisible) return if (!preference.isVisible) return
if (preference is PreferenceCategory) { if (preference is PreferenceCategory) {
for (child in preference.children) { for (child in preference.children) {
recursivelyHandlePreference(child) setupPreference(child)
} }
} }

View file

@ -33,8 +33,8 @@ class ForegroundManager(private val service: Service) {
} }
/** /**
* Try to enter a foreground state. Returns false if already in foreground, returns true * Try to enter a foreground state. Returns false if already in foreground, returns true if
* if state was entered. * state was entered.
*/ */
fun tryStartForeground(notification: ServiceNotification): Boolean { fun tryStartForeground(notification: ServiceNotification): Boolean {
if (isForeground) { if (isForeground) {
@ -50,8 +50,8 @@ class ForegroundManager(private val service: Service) {
} }
/** /**
* Try to stop a foreground state. Returns false if already in backend, returns true * Try to stop a foreground state. Returns false if already in backend, returns true if state
* if state was stopped. * was stopped.
*/ */
fun tryStopForeground(): Boolean { fun tryStopForeground(): Boolean {
if (!isForeground) { if (!isForeground) {

View file

@ -26,7 +26,8 @@ import androidx.core.app.NotificationCompat
import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceSafe
/** /**
* Wrapper around [NotificationCompat.Builder] that automates parts of the notification setup. * Wrapper around [NotificationCompat.Builder] that automates parts of the notification setup, under
* the assumption that the notification will be used in a service.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
abstract class ServiceNotification(context: Context, info: ChannelInfo) : abstract class ServiceNotification(context: Context, info: ChannelInfo) :
@ -35,7 +36,6 @@ abstract class ServiceNotification(context: Context, info: ChannelInfo) :
init { init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = val channel =
NotificationChannel(info.id, context.getString(info.nameRes), info.importance) NotificationChannel(info.id, context.getString(info.nameRes), info.importance)

View file

@ -19,7 +19,6 @@ package org.oxycblt.auxio.util
import android.os.Looper import android.os.Looper
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.core.math.MathUtils
import java.lang.reflect.Field import java.lang.reflect.Field
import java.lang.reflect.Method import java.lang.reflect.Method
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
@ -37,16 +36,12 @@ fun requireBackgroundThread() {
* Sanitizes a nullable value that is not likely to be null. On debug builds, requireNotNull is * Sanitizes a nullable value that is not likely to be null. On debug builds, requireNotNull is
* used, while on release builds, the unsafe assertion operator [!!] ]is used * used, while on release builds, the unsafe assertion operator [!!] ]is used
*/ */
fun <T> unlikelyToBeNull(value: T?): T { fun <T> unlikelyToBeNull(value: T?) =
return if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
requireNotNull(value) requireNotNull(value)
} else { } else {
value!! value!!
} }
}
/** Shortcut to clamp an integer between [min] and [max] */
fun Int.clamp(min: Int, max: Int): Int = MathUtils.clamp(this, min, max)
/** /**
* Convert a [Long] of seconds into a string duration. * Convert a [Long] of seconds into a string duration.
@ -80,22 +75,21 @@ fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy {
} }
/** /**
* A generation-based abstraction that allows cheap cooperative multi-threading in shared object * An abstraction that allows cheap cooperative multi-threading in shared object contexts. Every new
* contexts. Every new task should call [newHandle], while every running task should call [check] or * task should call [newHandle], while every running task should call [check] or [yield] depending
* [yield] depending on the context. * on the context.
* *
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class GenerationGuard { class TaskGuard {
private var currentHandle = 0L private var currentHandle = 0L
/** /**
* Returns a new handle to the calling task while invalidating the generations of the previous * Returns a new handle to the calling task while invalidating the handle of the previous task.
* task.
*/ */
@Synchronized fun newHandle() = ++currentHandle @Synchronized fun newHandle() = ++currentHandle
/** Check if the given [handle] is still the one represented by this class. */ /** Check if the given [handle] is still the one stored by this class. */
@Synchronized fun check(handle: Long) = handle == currentHandle @Synchronized fun check(handle: Long) = handle == currentHandle
/** /**

View file

@ -21,7 +21,6 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Size
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.BitmapProvider import org.oxycblt.auxio.image.BitmapProvider
@ -112,10 +111,7 @@ class WidgetComponent(private val context: Context) :
// bitmap on very large screens. // bitmap on very large screens.
.size(minOf(metrics.widthPixels, metrics.heightPixels, 1024)) .size(minOf(metrics.widthPixels, metrics.heightPixels, 1024))
} else { } else {
this@WidgetComponent.logD("Doing API 21 cover load") builder
// Note: Explicitly use the "original" size as without it the scaling logic
// in coil breaks down and results in an error.
builder.size(Size.ORIGINAL)
} }
} }

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="@android:color/white"
android:pathData="M 19.005724,25.685114 V 6.3148861 h 5.744275 V 25.685114 Z m -11.7557234,0 V 6.3148861 H 12.994275 V 25.685114 Z" />
</vector>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="@android:color/white"
android:pathData="M 10.399999,25.400001 V 6.6000001 L 25.233332,16 Z M 13.266666,16 Z m 0,4.133334 L 19.833332,16 13.266666,11.866667 Z" />
</vector>

View file

@ -3,7 +3,7 @@
android:color="?attr/colorControlHighlight"> android:color="?attr/colorControlHighlight">
<item android:id="@android:id/background"> <item android:id="@android:id/background">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<corners android:radius="@dimen/spacing_large" /> <corners android:radius="@dimen/spacing_huge" />
<solid android:color="?attr/colorPrimary" /> <solid android:color="?attr/colorPrimary" />
</shape> </shape>
</item> </item>

View file

@ -127,18 +127,18 @@
app:layout_constraintStart_toEndOf="@+id/playback_repeat" app:layout_constraintStart_toEndOf="@+id/playback_repeat"
app:layout_constraintTop_toTopOf="@+id/playback_play_pause" /> app:layout_constraintTop_toTopOf="@+id/playback_play_pause" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.button.MaterialButton
android:id="@+id/playback_play_pause" android:id="@+id/playback_play_pause"
style="@style/Widget.Auxio.FloatingActionButton.PlayPause" style="@style/Widget.Auxio.Button.PlayPause"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/desc_play_pause" android:contentDescription="@string/desc_play_pause"
android:src="@drawable/sel_playing_state_24" app:icon="@drawable/sel_playing_state_24"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_play_24" /> tools:icon="@drawable/ic_play_24" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/playback_skip_next" android:id="@+id/playback_skip_next"

View file

@ -19,7 +19,7 @@
<org.oxycblt.auxio.image.StyledImageView <org.oxycblt.auxio.image.StyledImageView
android:id="@+id/playback_cover" android:id="@+id/playback_cover"
style="@style/Widget.Auxio.Image.Full" style="@style/Widget.Auxio.Image.Full"
android:layout_margin="@dimen/spacing_mid_large" android:layout_margin="@dimen/spacing_large"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/playback_song_container" app:layout_constraintEnd_toStartOf="@+id/playback_song_container"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"
@ -33,7 +33,7 @@
android:id="@+id/playback_song_container" android:id="@+id/playback_song_container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_mid_large" android:layout_marginEnd="@dimen/spacing_large"
app:layout_constraintBottom_toTopOf="@+id/playback_artist" app:layout_constraintBottom_toTopOf="@+id/playback_artist"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"
@ -104,7 +104,7 @@
style="@style/Widget.Auxio.Button.Icon.Large" style="@style/Widget.Auxio.Button.Icon.Large"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_large" android:layout_marginEnd="@dimen/spacing_huge"
android:contentDescription="@string/desc_change_repeat" android:contentDescription="@string/desc_change_repeat"
app:icon="@drawable/ic_repeat_off_24" app:icon="@drawable/ic_repeat_off_24"
app:iconTint="@color/sel_accented" app:iconTint="@color/sel_accented"
@ -118,32 +118,32 @@
style="@style/Widget.Auxio.Button.Icon.Large" style="@style/Widget.Auxio.Button.Icon.Large"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_large" android:layout_marginEnd="@dimen/spacing_huge"
android:contentDescription="@string/desc_skip_prev" android:contentDescription="@string/desc_skip_prev"
app:icon="@drawable/ic_skip_prev_24" app:icon="@drawable/ic_skip_prev_24"
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause" app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause" app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
app:layout_constraintTop_toTopOf="@+id/playback_play_pause" /> app:layout_constraintTop_toTopOf="@+id/playback_play_pause" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.button.MaterialButton
android:id="@+id/playback_play_pause" android:id="@+id/playback_play_pause"
style="@style/Widget.Auxio.FloatingActionButton.PlayPause" style="@style/Widget.Auxio.Button.PlayPause"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/desc_play_pause" android:contentDescription="@string/desc_play_pause"
android:src="@drawable/sel_playing_state_24" app:icon="@drawable/sel_playing_state_24"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_pause_24" /> tools:icon="@drawable/ic_pause_24" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/playback_skip_next" android:id="@+id/playback_skip_next"
style="@style/Widget.Auxio.Button.Icon.Large" style="@style/Widget.Auxio.Button.Icon.Large"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_large" android:layout_marginStart="@dimen/spacing_huge"
android:contentDescription="@string/desc_skip_next" android:contentDescription="@string/desc_skip_next"
app:icon="@drawable/ic_skip_next_24" app:icon="@drawable/ic_skip_next_24"
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause" app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
@ -155,7 +155,7 @@
style="@style/Widget.Auxio.Button.Icon.Large" style="@style/Widget.Auxio.Button.Icon.Large"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_large" android:layout_marginStart="@dimen/spacing_huge"
android:contentDescription="@string/desc_shuffle" android:contentDescription="@string/desc_shuffle"
app:icon="@drawable/sel_shuffle_state_24" app:icon="@drawable/sel_shuffle_state_24"
app:iconTint="@color/sel_accented" app:iconTint="@color/sel_accented"

View file

@ -20,7 +20,7 @@
<org.oxycblt.auxio.image.StyledImageView <org.oxycblt.auxio.image.StyledImageView
android:id="@+id/playback_cover" android:id="@+id/playback_cover"
style="@style/Widget.Auxio.Image.Full" style="@style/Widget.Auxio.Image.Full"
android:layout_margin="@dimen/spacing_mid_large" android:layout_margin="@dimen/spacing_large"
app:layout_constraintBottom_toTopOf="@+id/playback_song" app:layout_constraintBottom_toTopOf="@+id/playback_song"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -32,8 +32,8 @@
style="@style/Widget.Auxio.TextView.Primary" style="@style/Widget.Auxio.TextView.Primary"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_mid_large" android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_mid_large" android:layout_marginEnd="@dimen/spacing_large"
app:layout_constraintBottom_toTopOf="@+id/playback_artist" app:layout_constraintBottom_toTopOf="@+id/playback_artist"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -45,8 +45,8 @@
style="@style/Widget.Auxio.TextView.Secondary" style="@style/Widget.Auxio.TextView.Secondary"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_mid_large" android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_mid_large" android:layout_marginEnd="@dimen/spacing_large"
app:layout_constraintBottom_toTopOf="@+id/playback_album" app:layout_constraintBottom_toTopOf="@+id/playback_album"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -57,8 +57,8 @@
style="@style/Widget.Auxio.TextView.Secondary" style="@style/Widget.Auxio.TextView.Secondary"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_mid_large" android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_mid_large" android:layout_marginEnd="@dimen/spacing_large"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar" app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -81,7 +81,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium" android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium" android:layout_marginEnd="@dimen/spacing_medium"
android:layout_marginBottom="@dimen/spacing_mid_large" android:layout_marginBottom="@dimen/spacing_large"
app:layout_constraintBottom_toBottomOf="parent"> app:layout_constraintBottom_toBottomOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@ -93,7 +93,7 @@
style="@style/Widget.Auxio.Button.Icon.Large" style="@style/Widget.Auxio.Button.Icon.Large"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_large" android:layout_marginEnd="@dimen/spacing_huge"
android:contentDescription="@string/desc_change_repeat" android:contentDescription="@string/desc_change_repeat"
app:icon="@drawable/ic_repeat_off_24" app:icon="@drawable/ic_repeat_off_24"
app:iconTint="@color/sel_accented" app:iconTint="@color/sel_accented"
@ -107,32 +107,32 @@
style="@style/Widget.Auxio.Button.Icon.Large" style="@style/Widget.Auxio.Button.Icon.Large"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_large" android:layout_marginEnd="@dimen/spacing_huge"
android:contentDescription="@string/desc_skip_prev" android:contentDescription="@string/desc_skip_prev"
app:icon="@drawable/ic_skip_prev_24" app:icon="@drawable/ic_skip_prev_24"
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause" app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause" app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
app:layout_constraintTop_toTopOf="@+id/playback_play_pause" /> app:layout_constraintTop_toTopOf="@+id/playback_play_pause" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.button.MaterialButton
android:id="@+id/playback_play_pause" android:id="@+id/playback_play_pause"
style="@style/Widget.Auxio.FloatingActionButton.PlayPause" style="@style/Widget.Auxio.Button.PlayPause"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/desc_play_pause" android:contentDescription="@string/desc_play_pause"
android:src="@drawable/sel_playing_state_24" app:icon="@drawable/sel_playing_state_24"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_pause_24" /> tools:icon="@drawable/ic_pause_24" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/playback_skip_next" android:id="@+id/playback_skip_next"
style="@style/Widget.Auxio.Button.Icon.Large" style="@style/Widget.Auxio.Button.Icon.Large"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_large" android:layout_marginStart="@dimen/spacing_huge"
android:contentDescription="@string/desc_skip_next" android:contentDescription="@string/desc_skip_next"
app:icon="@drawable/ic_skip_next_24" app:icon="@drawable/ic_skip_next_24"
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause" app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
@ -144,7 +144,7 @@
style="@style/Widget.Auxio.Button.Icon.Large" style="@style/Widget.Auxio.Button.Icon.Large"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_large" android:layout_marginStart="@dimen/spacing_huge"
android:contentDescription="@string/desc_shuffle" android:contentDescription="@string/desc_shuffle"
app:icon="@drawable/sel_shuffle_state_24" app:icon="@drawable/sel_shuffle_state_24"
app:iconTint="@color/sel_accented" app:iconTint="@color/sel_accented"

View file

@ -127,18 +127,18 @@
app:layout_constraintStart_toEndOf="@+id/playback_repeat" app:layout_constraintStart_toEndOf="@+id/playback_repeat"
app:layout_constraintTop_toTopOf="@+id/playback_play_pause" /> app:layout_constraintTop_toTopOf="@+id/playback_play_pause" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.button.MaterialButton
android:id="@+id/playback_play_pause" android:id="@+id/playback_play_pause"
style="@style/Widget.Auxio.FloatingActionButton.PlayPause" style="@style/Widget.Auxio.Button.PlayPause"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/desc_play_pause" android:contentDescription="@string/desc_play_pause"
android:src="@drawable/sel_playing_state_24" app:icon="@drawable/sel_playing_state_24"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_play_24" /> tools:icon="@drawable/ic_play_24" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/playback_skip_next" android:id="@+id/playback_skip_next"

View file

@ -27,9 +27,9 @@
android:id="@+id/dirs_empty" android:id="@+id/dirs_empty"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/spacing_mid_large" android:paddingStart="@dimen/spacing_large"
android:paddingTop="@dimen/spacing_medium" android:paddingTop="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_mid_large" android:paddingEnd="@dimen/spacing_large"
android:paddingBottom="@dimen/spacing_medium" android:paddingBottom="@dimen/spacing_medium"
android:text="@string/err_no_dirs" android:text="@string/err_no_dirs"
android:textAlignment="center" android:textAlignment="center"
@ -40,8 +40,8 @@
style="@style/Widget.Auxio.TextView.Header" style="@style/Widget.Auxio.TextView.Header"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/spacing_mid_large" android:paddingStart="@dimen/spacing_large"
android:paddingEnd="@dimen/spacing_mid_large" android:paddingEnd="@dimen/spacing_large"
android:text="@string/set_dirs_mode" /> android:text="@string/set_dirs_mode" />
<com.google.android.material.divider.MaterialDivider <com.google.android.material.divider.MaterialDivider
@ -52,9 +52,9 @@
android:id="@+id/folder_mode_group" android:id="@+id/folder_mode_group"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_mid_large" android:layout_marginStart="@dimen/spacing_large"
android:layout_marginTop="@dimen/spacing_medium" android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_mid_large" android:layout_marginEnd="@dimen/spacing_large"
android:gravity="center" android:gravity="center"
app:checkedButton="@+id/dirs_mode_exclude" app:checkedButton="@+id/dirs_mode_exclude"
app:selectionRequired="true" app:selectionRequired="true"
@ -82,9 +82,9 @@
android:id="@+id/dirs_mode_desc" android:id="@+id/dirs_mode_desc"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_mid_large" android:layout_marginStart="@dimen/spacing_large"
android:layout_marginTop="@dimen/spacing_small" android:layout_marginTop="@dimen/spacing_small"
android:layout_marginEnd="@dimen/spacing_mid_large" android:layout_marginEnd="@dimen/spacing_large"
android:textAlignment="viewStart" android:textAlignment="viewStart"
tools:text="Mode description" /> tools:text="Mode description" />

View file

@ -14,7 +14,7 @@
android:id="@+id/with_tags_header" android:id="@+id/with_tags_header"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_mid_large" android:layout_marginStart="@dimen/spacing_large"
android:layout_marginTop="@dimen/spacing_medium" android:layout_marginTop="@dimen/spacing_medium"
android:text="@string/set_pre_amp_with" android:text="@string/set_pre_amp_with"
android:textAppearance="@style/TextAppearance.Auxio.TitleMediumLowEmphasis" android:textAppearance="@style/TextAppearance.Auxio.TitleMediumLowEmphasis"
@ -38,7 +38,7 @@
android:id="@+id/with_tags_ticker" android:id="@+id/with_tags_ticker"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_mid_large" android:layout_marginEnd="@dimen/spacing_large"
android:gravity="center" android:gravity="center"
android:minWidth="@dimen/size_pre_amp_ticker" android:minWidth="@dimen/size_pre_amp_ticker"
android:textAppearance="@style/TextAppearance.Auxio.LabelMedium" android:textAppearance="@style/TextAppearance.Auxio.LabelMedium"
@ -51,7 +51,7 @@
android:id="@+id/without_tags_header" android:id="@+id/without_tags_header"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_mid_large" android:layout_marginStart="@dimen/spacing_large"
android:layout_marginTop="@dimen/spacing_medium" android:layout_marginTop="@dimen/spacing_medium"
android:text="@string/set_pre_amp_without" android:text="@string/set_pre_amp_without"
android:textAppearance="@style/TextAppearance.Auxio.TitleMediumLowEmphasis" android:textAppearance="@style/TextAppearance.Auxio.TitleMediumLowEmphasis"
@ -75,7 +75,7 @@
android:id="@+id/without_tags_ticker" android:id="@+id/without_tags_ticker"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_mid_large" android:layout_marginEnd="@dimen/spacing_large"
android:gravity="center" android:gravity="center"
android:minWidth="@dimen/size_pre_amp_ticker" android:minWidth="@dimen/size_pre_amp_ticker"
android:textAppearance="@style/TextAppearance.Auxio.LabelMedium" android:textAppearance="@style/TextAppearance.Auxio.LabelMedium"
@ -88,9 +88,9 @@
android:id="@+id/pre_amp_notice" android:id="@+id/pre_amp_notice"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_mid_large" android:layout_marginStart="@dimen/spacing_large"
android:layout_marginTop="@dimen/spacing_medium" android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_mid_large" android:layout_marginEnd="@dimen/spacing_large"
android:text="@string/set_pre_amp_warning" android:text="@string/set_pre_amp_warning"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.Auxio.BodySmall" android:textAppearance="@style/TextAppearance.Auxio.BodySmall"

View file

@ -15,8 +15,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:paddingStart="@dimen/spacing_mid_large" android:paddingStart="@dimen/spacing_large"
android:paddingEnd="@dimen/spacing_mid_large" android:paddingEnd="@dimen/spacing_large"
android:showDividers="middle" android:showDividers="middle"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">

View file

@ -110,18 +110,18 @@
app:layout_constraintStart_toEndOf="@+id/playback_repeat" app:layout_constraintStart_toEndOf="@+id/playback_repeat"
app:layout_constraintTop_toTopOf="@+id/playback_play_pause" /> app:layout_constraintTop_toTopOf="@+id/playback_play_pause" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.button.MaterialButton
android:id="@+id/playback_play_pause" android:id="@+id/playback_play_pause"
style="@style/Widget.Auxio.FloatingActionButton.PlayPause" style="@style/Widget.Auxio.Button.PlayPause"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/desc_play_pause" android:contentDescription="@string/desc_play_pause"
android:src="@drawable/sel_playing_state_24" app:icon="@drawable/sel_playing_state_24"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_play_24" /> tools:icon="@drawable/ic_play_24" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/playback_skip_next" android:id="@+id/playback_skip_next"

View file

@ -11,7 +11,7 @@
style="@style/Widget.Auxio.TextView.Item.Primary" style="@style/Widget.Auxio.TextView.Item.Primary"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_mid_large" android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_medium" android:layout_marginEnd="@dimen/spacing_medium"
android:gravity="center" android:gravity="center"
android:maxLines="@null" android:maxLines="@null"
@ -29,7 +29,7 @@
style="@style/Widget.Auxio.Button.Icon.Small" style="@style/Widget.Auxio.Button.Icon.Small"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_mid_small" android:layout_marginEnd="@dimen/spacing_mid_medium"
android:contentDescription="@string/desc_music_dir_delete" android:contentDescription="@string/desc_music_dir_delete"
app:icon="@drawable/ic_delete_24" app:icon="@drawable/ic_delete_24"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View file

@ -35,7 +35,7 @@
style="@style/Widget.Auxio.Button.Icon.Small" style="@style/Widget.Auxio.Button.Icon.Small"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_mid_small" android:layout_marginEnd="@dimen/spacing_mid_medium"
android:contentDescription="@string/desc_tab_handle" android:contentDescription="@string/desc_tab_handle"
app:icon="@drawable/ic_handle_24" app:icon="@drawable/ic_handle_24"
app:layout_constraintBottom_toBottomOf="@+id/tab_icon" app:layout_constraintBottom_toBottomOf="@+id/tab_icon"

View file

@ -74,9 +74,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="@dimen/spacing_mid_small" android:paddingStart="@dimen/spacing_mid_medium"
android:paddingTop="@dimen/spacing_medium" android:paddingTop="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_mid_small" android:paddingEnd="@dimen/spacing_mid_medium"
android:paddingBottom="@dimen/spacing_medium"> android:paddingBottom="@dimen/spacing_medium">
<android.widget.ImageButton <android.widget.ImageButton

View file

@ -72,9 +72,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="@dimen/spacing_mid_small" android:paddingStart="@dimen/spacing_mid_medium"
android:paddingTop="@dimen/spacing_medium" android:paddingTop="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_mid_small" android:paddingEnd="@dimen/spacing_mid_medium"
android:paddingBottom="@dimen/spacing_medium"> android:paddingBottom="@dimen/spacing_medium">
<android.widget.ImageButton <android.widget.ImageButton

View file

@ -64,7 +64,7 @@
android:background="@drawable/ui_widget_bar" android:background="@drawable/ui_widget_bar"
android:backgroundTint="?attr/colorSurface" android:backgroundTint="?attr/colorSurface"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="@dimen/spacing_mid_small"> android:padding="@dimen/spacing_mid_medium">
<android.widget.ImageButton <android.widget.ImageButton
android:id="@+id/widget_skip_prev" android:id="@+id/widget_skip_prev"

View file

@ -51,7 +51,7 @@
android:background="@drawable/ui_widget_bar" android:background="@drawable/ui_widget_bar"
android:backgroundTint="?attr/colorSurface" android:backgroundTint="?attr/colorSurface"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="@dimen/spacing_mid_small"> android:padding="@dimen/spacing_mid_medium">
<android.widget.ImageButton <android.widget.ImageButton
android:id="@+id/widget_repeat" android:id="@+id/widget_repeat"

View file

@ -3,10 +3,11 @@
<!-- Spacing Namespace | Dimens for padding/margin attributes --> <!-- Spacing Namespace | Dimens for padding/margin attributes -->
<dimen name="spacing_tiny">4dp</dimen> <dimen name="spacing_tiny">4dp</dimen>
<dimen name="spacing_small">8dp</dimen> <dimen name="spacing_small">8dp</dimen>
<dimen name="spacing_mid_small">12dp</dimen> <dimen name="spacing_mid_medium">12dp</dimen>
<dimen name="spacing_medium">16dp</dimen> <dimen name="spacing_medium">16dp</dimen>
<dimen name="spacing_mid_large">24dp</dimen> <dimen name="spacing_mid_large">20dp</dimen>
<dimen name="spacing_large">32dp</dimen> <dimen name="spacing_large">24dp</dimen>
<dimen name="spacing_huge">32dp</dimen>
<dimen name="spacing_tiny_inv">-4dp</dimen> <dimen name="spacing_tiny_inv">-4dp</dimen>
<dimen name="spacing_small_inv">-8dp</dimen> <dimen name="spacing_small_inv">-8dp</dimen>

View file

@ -2,10 +2,6 @@
<resources> <resources>
<!-- ANDROID COMPONENT-SPECIFIC STYLES.--> <!-- ANDROID COMPONENT-SPECIFIC STYLES.-->
<!--
A dialog theme that doesn't suck. This is the only non-Material3 usage in the entire
project since the Material3 dialogs [and especially the button panel] are unusable.
-->
<style name="Theme.Auxio.Dialog" parent="ThemeOverlay.Material3.MaterialAlertDialog"> <style name="Theme.Auxio.Dialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
<item name="android:checkedTextViewStyle">@style/Widget.Auxio.Dialog.CheckedTextView</item> <item name="android:checkedTextViewStyle">@style/Widget.Auxio.Dialog.CheckedTextView</item>
<item name="buttonBarPositiveButtonStyle">@style/Widget.Material3.Button.TextButton.Dialog <item name="buttonBarPositiveButtonStyle">@style/Widget.Material3.Button.TextButton.Dialog

View file

@ -29,38 +29,6 @@
<item name="trackCornerRadius">@dimen/size_corners_medium</item> <item name="trackCornerRadius">@dimen/size_corners_medium</item>
</style> </style>
<style name="Widget.Auxio.Button.Icon.Base" parent="Widget.Material3.Button.TextButton">
<item name="iconPadding">0dp</item>
<item name="iconTint">?attr/colorControlNormal</item>
<item name="rippleColor">?attr/colorControlHighlight</item>
<item name="android:minWidth">@dimen/size_btn</item>
<item name="android:minHeight">@dimen/size_btn</item>
</style>
<style name="Widget.Auxio.Button.Icon.Small" parent="Widget.Auxio.Button.Icon.Base">
<item name="iconSize">@dimen/size_icon_small</item>
<item name="android:insetTop">@dimen/spacing_tiny</item>
<item name="android:insetBottom">@dimen/spacing_tiny</item>
<item name="android:insetLeft">@dimen/spacing_tiny</item>
<item name="android:insetRight">@dimen/spacing_tiny</item>
<item name="android:paddingStart">@dimen/spacing_small</item>
<item name="android:paddingEnd">@dimen/spacing_small</item>
<item name="android:paddingTop">@dimen/spacing_small</item>
<item name="android:paddingBottom">@dimen/spacing_small</item>
</style>
<style name="Widget.Auxio.Button.Icon.Large" parent="Widget.Auxio.Button.Icon.Base">
<item name="iconSize">@dimen/size_icon_large</item>
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>
<item name="android:insetLeft">0dp</item>
<item name="android:insetRight">0dp</item>
<item name="android:paddingStart">@dimen/spacing_small</item>
<item name="android:paddingEnd">@dimen/spacing_small</item>
<item name="android:paddingTop">@dimen/spacing_small</item>
<item name="android:paddingBottom">@dimen/spacing_small</item>
</style>
<style name="Widget.Auxio.Image.Small" parent=""> <style name="Widget.Auxio.Image.Small" parent="">
<item name="android:layout_width">@dimen/size_cover_compact</item> <item name="android:layout_width">@dimen/size_cover_compact</item>
<item name="android:layout_height">@dimen/size_cover_compact</item> <item name="android:layout_height">@dimen/size_cover_compact</item>
@ -203,20 +171,54 @@
<style name="Widget.Auxio.Button.Secondary" parent="Widget.Material3.Button.OutlinedButton" /> <style name="Widget.Auxio.Button.Secondary" parent="Widget.Material3.Button.OutlinedButton" />
<style name="Widget.Auxio.FloatingActionButton.PlayPause" parent="Widget.Material3.FloatingActionButton.Large.Secondary"> <style name="Widget.Auxio.Button.Icon.Base" parent="Widget.Material3.Button.TextButton">
<!-- <item name="iconPadding">0dp</item>
Abuse this floating action button to act more like an old-school auxio button. <item name="iconTint">?attr/colorControlNormal</item>
This is for two reasons: <item name="rippleColor">?attr/colorControlHighlight</item>
1. We upscale the play icon to 32dp, so the total FAB size also needs to increase to </style>
compensate.
2. For some reason elevation behaves strangely in the playback panel, so we disable it. <style name="Widget.Auxio.Button.Icon.Small" parent="Widget.Auxio.Button.Icon.Base">
TODO: I think I could just make a tonal button instead of this <item name="iconSize">@dimen/size_icon_small</item>
--> <item name="android:minWidth">@dimen/size_btn</item>
<item name="maxImageSize">@dimen/size_icon_large</item> <item name="android:minHeight">@dimen/size_btn</item>
<item name="fabCustomSize">@dimen/size_play_pause_button</item> <item name="android:insetTop">@dimen/spacing_tiny</item>
<item name="fabSize">normal</item> <item name="android:insetBottom">@dimen/spacing_tiny</item>
<item name="android:elevation">0dp</item> <item name="android:insetLeft">@dimen/spacing_tiny</item>
<item name="elevation">0dp</item> <item name="android:insetRight">@dimen/spacing_tiny</item>
<item name="android:paddingStart">@dimen/spacing_small</item>
<item name="android:paddingEnd">@dimen/spacing_small</item>
<item name="android:paddingTop">@dimen/spacing_small</item>
<item name="android:paddingBottom">@dimen/spacing_small</item>
</style>
<style name="Widget.Auxio.Button.Icon.Large" parent="Widget.Auxio.Button.Icon.Base">
<item name="iconSize">@dimen/size_icon_large</item>
<item name="android:minWidth">@dimen/size_btn</item>
<item name="android:minHeight">@dimen/size_btn</item>
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>
<item name="android:insetLeft">0dp</item>
<item name="android:insetRight">0dp</item>
<item name="android:paddingStart">@dimen/spacing_small</item>
<item name="android:paddingEnd">@dimen/spacing_small</item>
<item name="android:paddingTop">@dimen/spacing_small</item>
<item name="android:paddingBottom">@dimen/spacing_small</item>
</style>
<style name="Widget.Auxio.Button.PlayPause" parent="Widget.Material3.Button.TonalButton">
<item name="android:minWidth">@dimen/size_play_pause_button</item>
<item name="android:minHeight">@dimen/size_play_pause_button</item>
<item name="iconSize">@dimen/size_icon_large</item>
<item name="iconPadding">0dp</item>
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>
<item name="android:insetLeft">0dp</item>
<item name="android:insetRight">0dp</item>
<item name="android:paddingStart">@dimen/spacing_mid_large</item>
<item name="android:paddingEnd">@dimen/spacing_mid_large</item>
<item name="android:paddingTop">@dimen/spacing_mid_large</item>
<item name="android:paddingBottom">@dimen/spacing_mid_large</item>
<item name="shapeAppearanceOverlay">@style/ShapeAppearanceOverlay.Material3.FloatingActionButton</item>
</style> </style>
<style name="Widget.Auxio.FloatingActionButton.Adaptive" parent="Widget.Material3.FloatingActionButton.Primary"> <style name="Widget.Auxio.FloatingActionButton.Adaptive" parent="Widget.Material3.FloatingActionButton.Primary">

View file

@ -1,5 +1,6 @@
# Architecture # Architecture
This document is designed to provide an overview of Auxio's architecture and design decisions. It will be updated as Auxio changes. This document is designed to provide an overview of Auxio's architecture and design decisions. It will be updated as Auxio changes,
however it may not completely line up as parts of the codebase will change rapidly at times.
## Core Facets ## Core Facets
Auxio has a couple of core systems or concepts that should be understood when working with the codebase. Auxio has a couple of core systems or concepts that should be understood when working with the codebase.