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"
// Media
// TODO: Dumpster this for Media3
implementation "androidx.media:media:1.6.0"
// Preferences

View file

@ -44,8 +44,6 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
*
* TODO: Add multi-select
*
* TODO: Bug test runtime rescanning
*
* @author OxygenCobalt
*/
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.recycler.Header
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.logD
import org.oxycblt.auxio.util.logW
@ -113,7 +113,7 @@ class DetailViewModel(application: Application) :
currentGenre.value?.let(::refreshGenreData)
}
private val songGuard = GenerationGuard()
private val songGuard = TaskGuard()
fun setSongId(id: Long) {
if (_currentSong.value?.run { song.id } == id) return
@ -123,6 +123,7 @@ class DetailViewModel(application: Application) :
}
fun clearSong() {
songGuard.newHandle()
_currentSong.value = null
}
@ -161,9 +162,9 @@ class DetailViewModel(application: Application) :
private fun generateDetailSong(song: Song) {
_currentSong.value = DetailSong(song, null)
viewModelScope.launch(Dispatchers.IO) {
val generation = songGuard.newHandle()
val handle = songGuard.newHandle()
val info = generateDetailSongInfo(song)
songGuard.yield(generation)
songGuard.yield(handle)
_currentSong.value = DetailSong(song, info)
}
}
@ -220,7 +221,7 @@ class DetailViewModel(application: Application) :
// 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
// 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 byDisc = songs.groupBy { it.disc ?: 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.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.IndexerViewModel
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.playback.PlaybackViewModel
@ -75,7 +75,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
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.
private val storagePermissionLauncher: ActivityResultLauncher<String> by lifecycleObject {
@ -135,10 +135,10 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
// --- VIEWMODEL SETUP ---
collectImmediately(homeModel.songs, homeModel.isFastScrolling, ::updateFab)
collect(homeModel.recreateTabs, ::handleRecreateTabs)
collectImmediately(homeModel.currentTab, ::updateCurrentTab)
collectImmediately(indexerModel.state, ::handleIndexerState)
collectImmediately(indexerModel.libraryExists, homeModel.isFastScrolling, ::updateFab)
collectImmediately(indexerModel.indexerState, ::handleIndexerState)
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()
if (isFastScrolling || songs.isEmpty()) {
if (!hasLoaded || isFastScrolling) {
binding.homeFab.hide()
} else {
binding.homeFab.show()

View file

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

View file

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

View file

@ -23,16 +23,19 @@ import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.system.Indexer
/**
* A ViewModel representing the current music indexing state.
* A ViewModel representing the current indexing state.
* @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 _state = MutableStateFlow<Indexer.State?>(null)
val state: StateFlow<Indexer.State?> = _state
private val _indexerState = MutableStateFlow<Indexer.State?>(null)
/** 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 {
indexer.registerCallback(this)
@ -43,7 +46,11 @@ class IndexerViewModel : ViewModel(), Indexer.Callback {
}
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() {

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)
/** 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.
// Removable storage is represented with the UUID.
return if (volume.isInternalCompat) {
if (volume.isInternalCompat) {
"${DOCUMENT_URI_PRIMARY_NAME}:${relativePath}"
} else {
"${(volume.uuidCompat ?: return null).uppercase()}:${relativePath}"
volume.uuidCompat?.let { uuid -> "${uuid}:${relativePath}" }
}
}
companion object {
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.settings.Settings
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.logE
import org.oxycblt.auxio.util.logW
@ -64,7 +64,7 @@ class Indexer {
private var lastResponse: Response? = null
private var indexingState: Indexing? = null
private var guard = GenerationGuard()
private var guard = TaskGuard()
private var controller: Controller? = null
private var callback: Callback? = null
@ -133,21 +133,21 @@ class Indexer {
suspend fun index(context: Context) {
requireBackgroundThread()
val generation = guard.newHandle()
val handle = guard.newHandle()
val notGranted =
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_DENIED
if (notGranted) {
emitCompletion(Response.NoPerms, generation)
emitCompletion(Response.NoPerms, handle)
return
}
val response =
try {
val start = System.currentTimeMillis()
val library = indexImpl(context, generation)
val library = indexImpl(context, handle)
if (library != null) {
logD(
"Music indexing completed successfully in " +
@ -163,7 +163,7 @@ class Indexer {
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
* 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.
*/
@Synchronized
fun cancelLast() {
logD("Cancelling last job")
val generation = guard.newHandle()
emitIndexing(null, generation)
}
@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)
}
}
val handle = guard.newHandle()
emitIndexing(null, handle)
}
/**
* Run the proper music loading process. [generation] must be a truthful value of the generation
* calling this function.
* Run the proper music loading process. [handle] must be a truthful handle of the task calling
* this function.
*/
private fun indexImpl(context: Context, generation: Long): MusicStore.Library? {
emitIndexing(Indexing.Indeterminate, generation)
private fun indexImpl(context: Context, handle: Long): MusicStore.Library? {
emitIndexing(Indexing.Indeterminate, handle)
// 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.
@ -256,7 +215,7 @@ class Indexer {
mediaStoreBackend
}
val songs = buildSongs(context, backend, generation)
val songs = buildSongs(context, backend, handle)
if (songs.isEmpty()) {
return null
}
@ -290,7 +249,7 @@ class Indexer {
* [buildGenres] functions must be called with the returned list so that all songs are properly
* 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()
var songs =
@ -299,7 +258,7 @@ class Indexer {
"Successfully queried media database " +
"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
@ -403,6 +362,47 @@ class Indexer {
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. */
sealed class State {
data class Indexing(val indexing: Indexer.Indexing) : State()

View file

@ -102,13 +102,7 @@ class PlaybackPanelFragment :
binding.playbackRepeat.setOnClickListener { playbackModel.incrementRepeatMode() }
binding.playbackSkipPrev.setOnClickListener { playbackModel.prev() }
binding.playbackPlayPause.apply {
// Abuse the play/pause FAB (see style definition for more info)
post { binding.playbackPlayPause.stateListAnimator = null }
setOnClickListener { playbackModel.invertPlaying() }
}
binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlaying() }
binding.playbackSkipNext.setOnClickListener { playbackModel.next() }
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
* 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 {
val restored =
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.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.clamp
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
@ -42,6 +41,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* when the active track changes.
*
* @author OxygenCobalt
*
* TODO: Convert a low-level audio processor capable of handling any kind of PCM data.
*/
class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
private data class Gain(val track: Float, val album: Float)
@ -235,7 +236,8 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() {
sample =
(sample * volume)
.toInt()
.clamp(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt())
.coerceAtLeast(Short.MIN_VALUE.toInt())
.coerceAtMost(Short.MAX_VALUE.toInt())
.toShort()
buffer.putLeShort(sample)
}

View file

@ -400,6 +400,9 @@ class PlaybackStateManager private constructor() {
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 oldPosition = positionMs

View file

@ -44,8 +44,9 @@ import org.oxycblt.auxio.util.logD
*
* @author OxygenCobalt
*
* TODO: Update textual metadata first, then cover metadata later. Janky, yes, but also resolves
* some coherency issues.
* TODO: Queue functionality
*
* TODO: Remove the player callback once smooth seeking is implemented
*/
class MediaSessionComponent(
private val context: Context,
@ -60,10 +61,12 @@ class MediaSessionComponent(
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 settings = Settings(context, this)
private val notification = NotificationComponent(context, mediaSession.sessionToken)
private val provider = BitmapProvider(context)
@ -138,10 +141,18 @@ class MediaSessionComponent(
builder.putString(MediaMetadataCompat.METADATA_KEY_DATE, song.album.year.toString())
}
// Normally, android expects one to provide a URI to the metadata instance instead of
// a full blown bitmap. In practice, this is not ideal in the slightest, as we cannot
// provide any user customization or quality of life improvements with a flat URI.
// Instead, we load a full size bitmap and use it within it's respective fields.
// Cover loading is a mess. Android expects you to provide a clean, easy URI for it to
// leverage, but Auxio cannot do that as quality-of-life features like scaling or
// 1:1 cropping could not be used
//
// 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(
song,
object : BitmapProvider.Target {
@ -262,10 +273,16 @@ class MediaSessionComponent(
private fun invalidateSessionState() {
logD("Updating media session playback state")
// Position updates arrive faster when you upload a state that is different, as it
// forces the system to re-poll the position.
// FIXME: For some reason however, positions just DON'T UPDATE AT ALL when you
// change from FROM THE APP ONLY WHEN THE PLAYER IS PAUSED.
// There are two unfixable issues with this code:
// 1. If the position is changed while paused (from the app), the position just won't
// update unless I re-post the notification. However, I cannot do such without being
// 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
val state =
PlaybackStateCompat.Builder()
@ -278,10 +295,6 @@ class MediaSessionComponent(
.build())
.setBufferedPosition(player.bufferedPosition)
state.setState(PlaybackStateCompat.STATE_NONE, player.bufferedPosition, 1.0f)
mediaSession.setPlaybackState(state.build())
val playerState =
if (playbackManager.isPlaying) {
PlaybackStateCompat.STATE_PLAYING

View file

@ -68,8 +68,6 @@ import org.oxycblt.auxio.widgets.WidgetProvider
*
* TODO: Android Auto
*
* TODO: Get MediaSessionConnector (or the media3 equivalent) working or die trying
*
* @author OxygenCobalt
*/
class PlaybackService :
@ -248,7 +246,7 @@ class PlaybackService :
override fun loadSong(song: Song?) {
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")
player.stop()
stopAndSave()
@ -281,6 +279,7 @@ class PlaybackService :
}
if (hasPlayed) {
logD("Updating notification")
if (!foregroundManager.tryStartForeground(notification)) {
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
* 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
* path-based excluded system to a volume-based excluded system at the same time. These are both
* rolled into this conversion.
* In 2.4.0, Auxio switched to a system based on SharedPreferences, also switching from a path-based
* excluded system to a volume-based excluded system at the same time. These are both rolled into
* this conversion.
*/
fun handleExcludedCompat(context: Context, storageManager: StorageManager): List<Directory> {
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.
val primaryVolume =
storageManager.storageVolumesCompat.find { it.isInternalCompat } ?: return emptyList()
val primaryDirectory =
(primaryVolume.directoryCompat ?: return emptyList()) + File.separatorChar
return db.readPaths().map { path ->
return LegacyExcludedDatabase(context).readPaths().map { path ->
val relativePath = path.removePrefix(primaryDirectory)
Log.d("Auxio.SettingsCompat", "Migrate $path -> $relativePath")
Directory(primaryVolume, relativePath)

View file

@ -31,7 +31,7 @@ import androidx.recyclerview.widget.RecyclerView
import coil.Coil
import org.oxycblt.auxio.R
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.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.replaygain.PreAmpCustomizeDialog
@ -51,21 +51,17 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
* @author OxygenCobalt
*
* 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")
class SettingsListFragment : PreferenceFragmentCompat() {
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?) {
super.onViewCreated(view, savedInstanceState)
preferenceManager.onDisplayPreferenceDialogListener = this
preferenceScreen.children.forEach(::recursivelyHandlePreference)
preferenceScreen.children.forEach(::setupPreference)
// Make the RecycleView edge-to-edge capable
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) }
}
getString(R.string.set_key_restore_state) ->
playbackModel.restorePlaybackState { restored ->
playbackModel.tryRestorePlaybackState { restored ->
if (restored) {
context?.showToast(R.string.lbl_state_restored)
} else {
@ -141,15 +137,14 @@ class SettingsListFragment : PreferenceFragmentCompat() {
return true
}
/** Recursively handle a preference, doing any specific actions on it. */
private fun recursivelyHandlePreference(preference: Preference) {
private fun setupPreference(preference: Preference) {
val settings = Settings(requireContext())
if (!preference.isVisible) return
if (preference is PreferenceCategory) {
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
* if state was entered.
* Try to enter a foreground state. Returns false if already in foreground, returns true if
* state was entered.
*/
fun tryStartForeground(notification: ServiceNotification): Boolean {
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
* if state was stopped.
* Try to stop a foreground state. Returns false if already in backend, returns true if state
* was stopped.
*/
fun tryStopForeground(): Boolean {
if (!isForeground) {

View file

@ -26,7 +26,8 @@ import androidx.core.app.NotificationCompat
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
*/
abstract class ServiceNotification(context: Context, info: ChannelInfo) :
@ -35,7 +36,6 @@ abstract class ServiceNotification(context: Context, info: ChannelInfo) :
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
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.text.format.DateUtils
import androidx.core.math.MathUtils
import java.lang.reflect.Field
import java.lang.reflect.Method
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
* used, while on release builds, the unsafe assertion operator [!!] ]is used
*/
fun <T> unlikelyToBeNull(value: T?): T {
return if (BuildConfig.DEBUG) {
fun <T> unlikelyToBeNull(value: T?) =
if (BuildConfig.DEBUG) {
requireNotNull(value)
} else {
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.
@ -80,22 +75,21 @@ fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy {
}
/**
* A generation-based abstraction that allows cheap cooperative multi-threading in shared object
* contexts. Every new task should call [newHandle], while every running task should call [check] or
* [yield] depending on the context.
* An abstraction that allows cheap cooperative multi-threading in shared object contexts. Every new
* task should call [newHandle], while every running task should call [check] or [yield] depending
* on the context.
*
* @author OxygenCobalt
*/
class GenerationGuard {
class TaskGuard {
private var currentHandle = 0L
/**
* Returns a new handle to the calling task while invalidating the generations of the previous
* task.
* Returns a new handle to the calling task while invalidating the handle of the previous task.
*/
@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
/**

View file

@ -21,7 +21,6 @@ import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import coil.request.ImageRequest
import coil.size.Size
import coil.transform.RoundedCornersTransformation
import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.BitmapProvider
@ -112,10 +111,7 @@ class WidgetComponent(private val context: Context) :
// bitmap on very large screens.
.size(minOf(metrics.widthPixels, metrics.heightPixels, 1024))
} else {
this@WidgetComponent.logD("Doing API 21 cover load")
// 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)
builder
}
}

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">
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<corners android:radius="@dimen/spacing_large" />
<corners android:radius="@dimen/spacing_huge" />
<solid android:color="?attr/colorPrimary" />
</shape>
</item>

View file

@ -127,18 +127,18 @@
app:layout_constraintStart_toEndOf="@+id/playback_repeat"
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"
style="@style/Widget.Auxio.FloatingActionButton.PlayPause"
style="@style/Widget.Auxio.Button.PlayPause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_play_24" />
tools:icon="@drawable/ic_play_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/playback_skip_next"

View file

@ -19,7 +19,7 @@
<org.oxycblt.auxio.image.StyledImageView
android:id="@+id/playback_cover"
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_constraintEnd_toStartOf="@+id/playback_song_container"
app:layout_constraintHorizontal_bias="0.5"
@ -33,7 +33,7 @@
android:id="@+id/playback_song_container"
android:layout_width="0dp"
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_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
@ -104,7 +104,7 @@
style="@style/Widget.Auxio.Button.Icon.Large"
android:layout_width="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"
app:icon="@drawable/ic_repeat_off_24"
app:iconTint="@color/sel_accented"
@ -118,32 +118,32 @@
style="@style/Widget.Auxio.Button.Icon.Large"
android:layout_width="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"
app:icon="@drawable/ic_skip_prev_24"
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
app:layout_constraintEnd_toStartOf="@+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"
style="@style/Widget.Auxio.FloatingActionButton.PlayPause"
style="@style/Widget.Auxio.Button.PlayPause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_pause_24" />
tools:icon="@drawable/ic_pause_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/playback_skip_next"
style="@style/Widget.Auxio.Button.Icon.Large"
android:layout_width="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"
app:icon="@drawable/ic_skip_next_24"
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
@ -155,7 +155,7 @@
style="@style/Widget.Auxio.Button.Icon.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_large"
android:layout_marginStart="@dimen/spacing_huge"
android:contentDescription="@string/desc_shuffle"
app:icon="@drawable/sel_shuffle_state_24"
app:iconTint="@color/sel_accented"

View file

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

View file

@ -127,18 +127,18 @@
app:layout_constraintStart_toEndOf="@+id/playback_repeat"
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"
style="@style/Widget.Auxio.FloatingActionButton.PlayPause"
style="@style/Widget.Auxio.Button.PlayPause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_play_24" />
tools:icon="@drawable/ic_play_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/playback_skip_next"

View file

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

View file

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

View file

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

View file

@ -110,18 +110,18 @@
app:layout_constraintStart_toEndOf="@+id/playback_repeat"
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"
style="@style/Widget.Auxio.FloatingActionButton.PlayPause"
style="@style/Widget.Auxio.Button.PlayPause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_play_24" />
tools:icon="@drawable/ic_play_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/playback_skip_next"

View file

@ -11,7 +11,7 @@
style="@style/Widget.Auxio.TextView.Item.Primary"
android:layout_width="0dp"
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:gravity="center"
android:maxLines="@null"
@ -29,7 +29,7 @@
style="@style/Widget.Auxio.Button.Icon.Small"
android:layout_width="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"
app:icon="@drawable/ic_delete_24"
app:layout_constraintBottom_toBottomOf="parent"

View file

@ -35,7 +35,7 @@
style="@style/Widget.Auxio.Button.Icon.Small"
android:layout_width="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"
app:icon="@drawable/ic_handle_24"
app:layout_constraintBottom_toBottomOf="@+id/tab_icon"

View file

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

View file

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

View file

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

View file

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

View file

@ -3,10 +3,11 @@
<!-- Spacing Namespace | Dimens for padding/margin attributes -->
<dimen name="spacing_tiny">4dp</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_mid_large">24dp</dimen>
<dimen name="spacing_large">32dp</dimen>
<dimen name="spacing_mid_large">20dp</dimen>
<dimen name="spacing_large">24dp</dimen>
<dimen name="spacing_huge">32dp</dimen>
<dimen name="spacing_tiny_inv">-4dp</dimen>
<dimen name="spacing_small_inv">-8dp</dimen>

View file

@ -2,10 +2,6 @@
<resources>
<!-- 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">
<item name="android:checkedTextViewStyle">@style/Widget.Auxio.Dialog.CheckedTextView</item>
<item name="buttonBarPositiveButtonStyle">@style/Widget.Material3.Button.TextButton.Dialog

View file

@ -29,38 +29,6 @@
<item name="trackCornerRadius">@dimen/size_corners_medium</item>
</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="">
<item name="android:layout_width">@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.FloatingActionButton.PlayPause" parent="Widget.Material3.FloatingActionButton.Large.Secondary">
<!--
Abuse this floating action button to act more like an old-school auxio button.
This is for two reasons:
1. We upscale the play icon to 32dp, so the total FAB size also needs to increase to
compensate.
2. For some reason elevation behaves strangely in the playback panel, so we disable it.
TODO: I think I could just make a tonal button instead of this
-->
<item name="maxImageSize">@dimen/size_icon_large</item>
<item name="fabCustomSize">@dimen/size_play_pause_button</item>
<item name="fabSize">normal</item>
<item name="android:elevation">0dp</item>
<item name="elevation">0dp</item>
<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>
</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:minWidth">@dimen/size_btn</item>
<item name="android:minHeight">@dimen/size_btn</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: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 name="Widget.Auxio.FloatingActionButton.Adaptive" parent="Widget.Material3.FloatingActionButton.Primary">

View file

@ -1,5 +1,6 @@
# 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
Auxio has a couple of core systems or concepts that should be understood when working with the codebase.