diff --git a/README.md b/README.md
index ddd58df0d..c1fac9b7d 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-

+d
Auxio
A simple, rational music player for android.
@@ -46,9 +46,10 @@ I primarily built Auxio for myself, but you can use it too, I guess.
- Snappy UI derived from the latest Material Design guidelines
- Opinionated UX that prioritizes ease of use over edge cases
- Customizable behavior
-- Seamless artist system that unifies album artist and artist tags
-- Advanced media indexer with support for multiple artists, release types,
- precise/original dates, sort tags, and more
+- Support for disc numbers, multiple artists, release types,
+precise/original dates, sort tags, and more
+- Advanced artist system that unifies artists and album artists
+into a single entry
- SD Card-aware folder management
- Reliable playback state persistence
- Full ReplayGain support (On MP3, FLAC, OGG, OPUS, and MP4 files)
diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
index f7503eabe..77eaa6288 100644
--- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
+++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt
@@ -59,13 +59,11 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
setupTheme()
-
+ // Inflate the views after setting up the theme so that the theme attributes are applied.
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupEdgeToEdge(binding.root)
-
logD("Activity created")
}
@@ -76,6 +74,7 @@ class MainActivity : AppCompatActivity() {
startService(Intent(this, PlaybackService::class.java))
if (!startIntentAction(intent)) {
+ // No intent action to do, just restore the previously saved state.
playbackModel.startAction(InternalPlayer.Action.RestoreState)
}
}
@@ -85,6 +84,32 @@ class MainActivity : AppCompatActivity() {
startIntentAction(intent)
}
+ private fun setupTheme() {
+ val settings = Settings(this)
+ // Apply the theme configuration.
+ AppCompatDelegate.setDefaultNightMode(settings.theme)
+ // Apply the color scheme. The black theme requires it's own set of themes since
+ // it's not possible to modify the themes at run-time.
+ if (isNight && settings.useBlackTheme) {
+ logD("Applying black theme [accent ${settings.accent}]")
+ setTheme(settings.accent.blackTheme)
+ } else {
+ logD("Applying normal theme [accent ${settings.accent}]")
+ setTheme(settings.accent.theme)
+ }
+ }
+
+ private fun setupEdgeToEdge(contentView: View) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ contentView.setOnApplyWindowInsetsListener { view, insets ->
+ // Automatically inset the view to the left/right, as component support for
+ // these insets are highly lacking.
+ val bars = insets.systemBarInsetsCompat
+ view.updatePadding(left = bars.left, right = bars.right)
+ insets
+ }
+ }
+
/**
* Transform an [Intent] given to [MainActivity] into a [InternalPlayer.Action]
* that can be used in the playback system.
@@ -117,30 +142,6 @@ class MainActivity : AppCompatActivity() {
return true
}
- private fun setupTheme() {
- val settings = Settings(this)
- // Set up the current theme.
- AppCompatDelegate.setDefaultNightMode(settings.theme)
- // Set up the color scheme. Note that the black theme has it's own set
- // of styles since the color schemes cannot be modified at runtime.
- if (isNight && settings.useBlackTheme) {
- logD("Applying black theme [accent ${settings.accent}]")
- setTheme(settings.accent.blackTheme)
- } else {
- logD("Applying normal theme [accent ${settings.accent}]")
- setTheme(settings.accent.theme)
- }
- }
-
- private fun setupEdgeToEdge(contentView: View) {
- WindowCompat.setDecorFitsSystemWindows(window, false)
- contentView.setOnApplyWindowInsetsListener { view, insets ->
- val bars = insets.systemBarInsetsCompat
- view.updatePadding(left = bars.left, right = bars.right)
- insets
- }
- }
-
companion object {
private const val KEY_INTENT_USED = BuildConfig.APPLICATION_ID + ".key.FILE_INTENT_USED"
}
diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
index 7e2c51e96..aebd69e53 100644
--- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt
@@ -69,6 +69,8 @@ class MainFragment :
override fun onCreateBinding(inflater: LayoutInflater) = FragmentMainBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
+ super.onBindingCreated(binding, savedInstanceState)
+
// --- UI SETUP ---
val context = requireActivity()
// Override the back pressed callback so we can map back navigation to collapsing
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt
index 82f14fefb..bd2bf8859 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt
@@ -75,6 +75,7 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
+ // --- UI SETUP --
binding.detailToolbar.apply {
inflateMenu(R.menu.menu_album_detail)
setNavigationOnClickListener { findNavController().navigateUp() }
@@ -84,10 +85,8 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd
binding.detailRecycler.adapter = detailAdapter
// -- VIEWMODEL SETUP ---
-
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setAlbumUid(args.albumUid)
-
collectImmediately(detailModel.currentAlbum, ::updateAlbum)
collectImmediately(detailModel.albumList, detailAdapter::submitList)
collectImmediately(
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt
index f9af946bb..f4cc64c39 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt
@@ -75,6 +75,7 @@ class ArtistDetailFragment : ListFragment(), DetailAdapte
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
+ // --- UI SETUP ---
binding.detailToolbar.apply {
inflateMenu(R.menu.menu_genre_artist_detail)
setNavigationOnClickListener { findNavController().navigateUp() }
@@ -84,10 +85,8 @@ class ArtistDetailFragment : ListFragment(), DetailAdapte
binding.detailRecycler.adapter = detailAdapter
// --- VIEWMODEL SETUP ---
-
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setArtistUid(args.artistUid)
-
collectImmediately(detailModel.currentArtist, ::updateItem)
collectImmediately(detailModel.artistList, detailAdapter::submitList)
collectImmediately(
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt
index 62fa4f937..4c0ee9090 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt
@@ -74,6 +74,7 @@ class GenreDetailFragment : ListFragment(), DetailAdapter
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
+ // --- UI SETUP ---
binding.detailToolbar.apply {
inflateMenu(R.menu.menu_genre_artist_detail)
setNavigationOnClickListener { findNavController().navigateUp() }
@@ -83,10 +84,8 @@ class GenreDetailFragment : ListFragment(), DetailAdapter
binding.detailRecycler.adapter = detailAdapter
// --- VIEWMODEL SETUP ---
-
// DetailViewModel handles most initialization from the navigation argument.
detailModel.setGenreUid(args.genreUid)
-
collectImmediately(detailModel.currentGenre, ::updateItem)
collectImmediately(detailModel.genreList, detailAdapter::submitList)
collectImmediately(
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt
index e93f22894..7fe0792cb 100644
--- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt
+++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt
@@ -57,14 +57,13 @@ class SongDetailDialog : ViewBindingDialogFragment() {
}
private fun updateSong(song: DetailSong?) {
- val binding = requireBinding()
-
if (song == null) {
// Song we were showing no longer exists.
findNavController().navigateUp()
return
}
+ val binding = requireBinding()
if (song.properties != null) {
// Finished loading Song properties, populate and show the list of Song information.
binding.detailLoading.isInvisible = true
diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
index 0ad6e5c36..05efb148c 100644
--- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
+++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
@@ -106,6 +106,7 @@ class HomeFragment :
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
+ // --- UI SETUP ---
binding.homeAppbar.addOnOffsetChangedListener(this)
binding.homeToolbar.setOnMenuItemClickListener(this)
@@ -152,7 +153,6 @@ class HomeFragment :
binding.homeFab.setOnClickListener { playbackModel.shuffleAll() }
// --- VIEWMODEL SETUP ---
-
collect(homeModel.shouldRecreate, ::handleRecreate)
collectImmediately(homeModel.currentTabMode, ::updateCurrentTab)
collectImmediately(homeModel.songLists, homeModel.isFastScrolling, ::updateFab)
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
index 48e5d1154..a45323a1a 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
@@ -451,7 +451,7 @@ class PlaybackService :
playbackManager.changePlaying(false)
stopAndSave()
}
- WidgetProvider.ACTION_WIDGET_UPDATE -> widgetComponent.update()
+ WidgetProvider.ACTION_WIDGET_UPDATE -> widgetComponent.updateNowPlaying()
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt b/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt
deleted file mode 100644
index 74a9a0680..000000000
--- a/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * Copyright (c) 2021 Auxio Project
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package org.oxycblt.auxio.widgets
-
-import android.content.Context
-import android.os.Build
-import android.view.View
-import android.widget.RemoteViews
-import androidx.annotation.LayoutRes
-import org.oxycblt.auxio.R
-import org.oxycblt.auxio.playback.state.RepeatMode
-import org.oxycblt.auxio.playback.system.PlaybackService
-import org.oxycblt.auxio.settings.Settings
-import org.oxycblt.auxio.util.newBroadcastPendingIntent
-import org.oxycblt.auxio.util.newMainPendingIntent
-
-/**
- * The default widget is displayed whenever there is no music playing. It just shows the message "No
- * music playing".
- */
-fun createDefaultWidget(context: Context) = createViews(context, R.layout.widget_default)
-
-/**
- * The thin widget is a weird outlier widget intended to work well on strange launchers or landscape
- * grid launchers that allow really thin widget sizing.
- */
-fun createThinWidget(context: Context, state: WidgetComponent.WidgetState) =
- createViews(context, R.layout.widget_thin)
- .applyRoundingToBackground(context)
- .applyMeta(context, state)
- .applyBasicControls(context, state)
-
-/**
- * The small widget is for 2x2 widgets and just shows the cover art and playback controls. This is
- * generally because a Medium widget is too large for this widget size and a text-only widget is too
- * small for this widget size.
- */
-fun createSmallWidget(context: Context, state: WidgetComponent.WidgetState) =
- createViews(context, R.layout.widget_small)
- .applyRoundingToBar(context)
- .applyCover(context, state)
- .applyBasicControls(context, state)
-
-/**
- * The medium widget is for 2x3 widgets and shows the cover art, title/artist, and three controls.
- * This is the default widget configuration.
- */
-fun createMediumWidget(context: Context, state: WidgetComponent.WidgetState) =
- createViews(context, R.layout.widget_medium)
- .applyRoundingToBackground(context)
- .applyMeta(context, state)
- .applyBasicControls(context, state)
-
-/** The wide widget is for Nx2 widgets and is like the small widget but with more controls. */
-fun createWideWidget(context: Context, state: WidgetComponent.WidgetState) =
- createViews(context, R.layout.widget_wide)
- .applyRoundingToBar(context)
- .applyCover(context, state)
- .applyFullControls(context, state)
-
-/** The large widget is for 3x4 widgets and shows all metadata and controls. */
-fun createLargeWidget(context: Context, state: WidgetComponent.WidgetState): RemoteViews =
- createViews(context, R.layout.widget_large)
- .applyRoundingToBackground(context)
- .applyMeta(context, state)
- .applyFullControls(context, state)
-
-private fun createViews(context: Context, @LayoutRes layout: Int): RemoteViews {
- val views = RemoteViews(context.packageName, layout)
- views.setOnClickPendingIntent(android.R.id.background, context.newMainPendingIntent())
- return views
-}
-
-private fun RemoteViews.applyRoundingToBackground(context: Context): RemoteViews {
- if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
- setInt(android.R.id.background, "setBackgroundResource", R.drawable.ui_widget_bg_round)
- } else {
- setInt(android.R.id.background, "setBackgroundResource", R.drawable.ui_widget_bg)
- }
-
- return this
-}
-
-private fun RemoteViews.applyRoundingToBar(context: Context): RemoteViews {
- if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
- setInt(R.id.widget_controls, "setBackgroundResource", R.drawable.ui_widget_bar_round)
- } else {
- setInt(R.id.widget_controls, "setBackgroundResource", R.drawable.ui_widget_bar)
- }
-
- return this
-}
-
-private fun RemoteViews.applyMeta(
- context: Context,
- state: WidgetComponent.WidgetState
-): RemoteViews {
- applyCover(context, state)
-
- setTextViewText(R.id.widget_song, state.song.resolveName(context))
- setTextViewText(R.id.widget_artist, state.song.resolveArtistContents(context))
-
- return this
-}
-
-private fun RemoteViews.applyCover(
- context: Context,
- state: WidgetComponent.WidgetState
-): RemoteViews {
- if (state.cover != null) {
- setImageViewBitmap(R.id.widget_cover, state.cover)
- setContentDescription(
- R.id.widget_cover,
- context.getString(R.string.desc_album_cover, state.song.album.resolveName(context)))
- } else {
- setImageViewResource(R.id.widget_cover, R.drawable.ic_remote_default_cover_24)
- setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover))
- }
-
- return this
-}
-
-private fun RemoteViews.applyPlayPauseControls(
- context: Context,
- state: WidgetComponent.WidgetState
-): RemoteViews {
- // Controls are timeline elements, override the layout direction to RTL
- setInt(R.id.widget_controls, "setLayoutDirection", View.LAYOUT_DIRECTION_LTR)
-
- setOnClickPendingIntent(
- R.id.widget_play_pause,
- context.newBroadcastPendingIntent(PlaybackService.ACTION_PLAY_PAUSE))
-
- // Like the Android 13 media controls, use a circular fab when paused, and a squircle fab
- // when playing.
-
- val icon: Int
- val container: Int
-
- if (state.isPlaying) {
- icon = R.drawable.ic_pause_24
- container = R.drawable.ui_remote_fab_container_playing
- } else {
- icon = R.drawable.ic_play_24
- container = R.drawable.ui_remote_fab_container_paused
- }
-
- setImageViewResource(R.id.widget_play_pause, icon)
- setInt(R.id.widget_play_pause, "setBackgroundResource", container)
-
- return this
-}
-
-private fun RemoteViews.applyBasicControls(
- context: Context,
- state: WidgetComponent.WidgetState
-): RemoteViews {
- applyPlayPauseControls(context, state)
-
- setOnClickPendingIntent(
- R.id.widget_skip_prev, context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_PREV))
-
- setOnClickPendingIntent(
- R.id.widget_skip_next, context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_NEXT))
-
- return this
-}
-
-private fun RemoteViews.applyFullControls(
- context: Context,
- state: WidgetComponent.WidgetState
-): RemoteViews {
- applyBasicControls(context, state)
-
- setOnClickPendingIntent(
- R.id.widget_repeat,
- context.newBroadcastPendingIntent(PlaybackService.ACTION_INC_REPEAT_MODE))
-
- setOnClickPendingIntent(
- R.id.widget_shuffle,
- context.newBroadcastPendingIntent(PlaybackService.ACTION_INVERT_SHUFFLE))
-
- val shuffleRes =
- when {
- state.isShuffled -> R.drawable.ic_shuffle_on_24
- else -> R.drawable.ic_shuffle_off_24
- }
-
- val repeatRes =
- when (state.repeatMode) {
- RepeatMode.NONE -> R.drawable.ic_repeat_off_24
- RepeatMode.ALL -> R.drawable.ic_repeat_on_24
- RepeatMode.TRACK -> R.drawable.ic_repeat_one_24
- }
-
- setImageViewResource(R.id.widget_shuffle, shuffleRes)
- setImageViewResource(R.id.widget_repeat, repeatRes)
-
- return this
-}
diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt
index ebf14fc9f..22c9dcd55 100644
--- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt
+++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt
@@ -36,39 +36,31 @@ import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.logD
/**
- * A wrapper around each [WidgetProvider] that plugs into the main Auxio process and updates the
- * widget state based off of that. This cannot be rolled into [WidgetProvider] directly, as it may
- * result in memory leaks if [PlaybackStateManager]/[Settings] gets created and bound to without
- * being released.
+ * A component that manages the state of all of Auxio's widgets.
+ * This is kept separate from the AppWidgetProviders themselves to prevent possible memory
+ * leaks and enable the main functionality to be extended to more widgets in the future.
+ * @param context [Context] required to manage AppWidgetProviders.
* @author Alexander Capehart (OxygenCobalt)
*/
class WidgetComponent(private val context: Context) :
PlaybackStateManager.Callback, Settings.Callback {
private val playbackManager = PlaybackStateManager.getInstance()
private val settings = Settings(context, this)
- private val widget = WidgetProvider()
+ private val widgetProvider = WidgetProvider()
private val provider = BitmapProvider(context)
init {
playbackManager.addCallback(this)
-
- if (playbackManager.isInitialized) {
- update()
- }
}
- /*
- * Force-update the widget.
+ /**
+ * Update [WidgetProvider] with the current playback state.
*/
- fun update() {
- // Updating Auxio's widget is unlike the rest of Auxio for a few reasons:
- // 1. We can't use the typical primitives like ViewModels
- // 2. The component range is far smaller, so we have to do some odd hacks to get
- // the same UX.
+ fun updateNowPlaying() {
val song = playbackManager.song
if (song == null) {
logD("No song, resetting widget")
- widget.update(context, null)
+ widgetProvider.update(context, null)
return
}
@@ -83,7 +75,7 @@ class WidgetComponent(private val context: Context) :
override fun onConfigRequest(builder: ImageRequest.Builder): ImageRequest.Builder {
val cornerRadius =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- // Android 12, always round the cover with the app widget's inner radius
+ // Android 12, always round the cover with the widget's inner radius
context.getDimenPixels(android.R.dimen.system_app_widget_inner_radius)
} else if (settings.roundMode) {
// < Android 12, but the user still enabled round mode.
@@ -93,8 +85,6 @@ class WidgetComponent(private val context: Context) :
0
}
- // We resize the image in a such a way that we don't hit the RemoteView size
- // limit, which is the size of an RGB_8888 bitmap 1.5x the screen size.
val metrics = context.resources.displayMetrics
val sw = metrics.widthPixels
val sh = metrics.heightPixels
@@ -104,55 +94,70 @@ class WidgetComponent(private val context: Context) :
// to work around a bug in Android 13 where the bitmaps aren't pooled
// properly, massively reducing the memory size we can work with.
builder
- .size(computeSize(sw, sh, 10f))
+ .size(computeWidgetImageSize(sw, sh, 10f))
.transformations(
SquareFrameTransform.INSTANCE,
RoundedCornersTransformation(cornerRadius.toFloat()))
} else {
// Divide by two to really make sure we aren't hitting the memory limit.
- builder.size(computeSize(sw, sh, 2f))
+ builder.size(computeWidgetImageSize(sw, sh, 2f))
}
}
override fun onCompleted(bitmap: Bitmap?) {
- val state = WidgetState(song, bitmap, isPlaying, repeatMode, isShuffled)
- widget.update(context, state)
+ val state = PlaybackState(song, bitmap, isPlaying, repeatMode, isShuffled)
+ widgetProvider.update(context, state)
}
})
}
- private fun computeSize(sw: Int, sh: Int, modifier: Float) =
+ /**
+ * Get the recommended image size to load for use.
+ * @param sw The current screen width
+ * @param sh The current screen height
+ * @param modifier Modifier to reduce the image size.
+ * @return An image size that is guaranteed not to exceed the widget bitmap memory limit.
+ */
+ private fun computeWidgetImageSize(sw: Int, sh: Int, modifier: Float) =
+ // Maximum size is 1/3 total screen area * 4 bytes per pixel. Reverse
+ // that to obtain the image size.
sqrt((6f / 4f / modifier) * sw * sh).toInt()
- /*
- * Release this instance, removing the callbacks and resetting all widgets
+ /**
+ * Release this instance, preventing any further events from updating the widget instances.
*/
fun release() {
provider.release()
settings.release()
- widget.reset(context)
+ widgetProvider.reset(context)
playbackManager.removeCallback(this)
}
// --- CALLBACKS ---
- override fun onIndexMoved(index: Int) = update()
- override fun onNewPlayback(index: Int, queue: List, parent: MusicParent?) = update()
- override fun onStateChanged(state: InternalPlayer.State) = update()
- override fun onShuffledChanged(isShuffled: Boolean) = update()
- override fun onRepeatChanged(repeatMode: RepeatMode) = update()
+ // Hook all the major song-changing updates + the major player state updates
+ // to updating the "Now Playing" widget.
+ override fun onIndexMoved(index: Int) = updateNowPlaying()
+ override fun onNewPlayback(index: Int, queue: List, parent: MusicParent?) = updateNowPlaying()
+ override fun onStateChanged(state: InternalPlayer.State) = updateNowPlaying()
+ override fun onShuffledChanged(isShuffled: Boolean) = updateNowPlaying()
+ override fun onRepeatChanged(repeatMode: RepeatMode) = updateNowPlaying()
override fun onSettingChanged(key: String) {
if (key == context.getString(R.string.set_key_cover_mode) ||
key == context.getString(R.string.set_key_round_mode)) {
- update()
+ updateNowPlaying()
}
}
- /*
- * An immutable condensed variant of the current playback state, used so that PlaybackStateManager
- * does not need to be queried directly.
+ /**
+ * A condensed form of the playback state that is safe to use in AppWidgets.
+ * @param song [PlaybackStateManager.song]
+ * @param cover A pre-loaded album cover [Bitmap] for [song].
+ * @param isPlaying [PlaybackStateManager.playerState]
+ * @param repeatMode [PlaybackStateManager.repeatMode]
+ * @param isShuffled [PlaybackStateManager.isShuffled]
*/
- data class WidgetState(
+ data class PlaybackState(
val song: Song,
val cover: Bitmap?,
val isPlaying: Boolean,
diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt
index aa47c9bde..9c449e404 100644
--- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt
+++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt
@@ -25,73 +25,30 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.SizeF
+import android.view.View
import android.widget.RemoteViews
import org.oxycblt.auxio.BuildConfig
-import org.oxycblt.auxio.util.isLandscape
-import org.oxycblt.auxio.util.logD
-import org.oxycblt.auxio.util.logW
+import org.oxycblt.auxio.R
+import org.oxycblt.auxio.playback.state.RepeatMode
+import org.oxycblt.auxio.playback.system.PlaybackService
+import org.oxycblt.auxio.settings.Settings
+import org.oxycblt.auxio.util.*
/**
- * Auxio's one and only appwidget. This widget follows a more unorthodox approach, effectively
- * packing what could be considered multiple widgets into a single responsive widget.
- *
- * This widget is also able to backport it's responsive behavior to android versions below 12,
- * albeit with some issues, such as UI jittering and a layout not being picked when the orientation
- * changes. This is tolerable.
- *
- * For more specific details about these sub-widgets, see Forms.kt.
- *
+ * The [AppWidgetProvider] for the "Now Playing" widget. This widget shows the current
+ * playback state alongside actions to control it.
* @author Alexander Capehart (OxygenCobalt)
*/
class WidgetProvider : AppWidgetProvider() {
- /*
- * Update the widget based on the playback state.
- */
- fun update(context: Context, state: WidgetComponent.WidgetState?) {
- if (state == null) {
- reset(context)
- return
- }
-
- // Map each widget form to the cells where it would look at least okay.
- val views =
- mapOf(
- SizeF(180f, 100f) to createThinWidget(context, state),
- SizeF(180f, 152f) to createSmallWidget(context, state),
- SizeF(272f, 152f) to createWideWidget(context, state),
- SizeF(180f, 272f) to createMediumWidget(context, state),
- SizeF(272f, 272f) to createLargeWidget(context, state))
-
- val awm = AppWidgetManager.getInstance(context)
-
- try {
- awm.updateAppWidgetCompat(context, views)
- } catch (e: Exception) {
- logW("Unable to update widget: $e")
- awm.updateAppWidget(
- ComponentName(context, this::class.java), createDefaultWidget(context))
- }
- }
-
- /*
- * Revert this widget to its default view
- */
- fun reset(context: Context) {
- logD("Resetting widget")
-
- AppWidgetManager.getInstance(context)
- .updateAppWidget(ComponentName(context, this::class.java), createDefaultWidget(context))
- }
-
- // --- OVERRIDES ---
-
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
- reset(context)
requestUpdate(context)
+ // Revert to the default layout for now until we get a response from WidgetComponent.
+ // If we don't, then we will stick with the default widget layout.
+ reset(context)
}
override fun onAppWidgetOptionsChanged(
@@ -101,80 +58,322 @@ class WidgetProvider : AppWidgetProvider() {
newOptions: Bundle?
) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
-
+ // Another adaptive layout backport for API 21+: We are unable to immediately update
+ // the layout ourselves when the widget dimensions change, so we need to request
+ // an update from WidgetComponent first.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
- logD("Requesting new view from PlaybackService")
-
- // We can't resize the widget until we can generate the views, so request an update
- // from PlaybackService.
requestUpdate(context)
}
}
+ /**
+ * Update the currently shown layout based on the given [WidgetComponent.PlaybackState]
+ * @param context [Context] required to update the widget layout.
+ * @param state [WidgetComponent.PlaybackState] to show, or null if no playback is going
+ * on.
+ */
+ fun update(context: Context, state: WidgetComponent.PlaybackState?) {
+ if (state == null) {
+ // No state, use the default widget.
+ reset(context)
+ return
+ }
+
+ // Create and configure each possible layout for the widget. These dimensions seem
+ // arbitrary, but they are actually the minimum dimensions required to fit all of
+ // the widget elements, plus some leeway for text sizing.
+ val views =
+ mapOf(
+ SizeF(180f, 100f) to newThinLayout(context, state),
+ SizeF(180f, 152f) to newSmallLayout(context, state),
+ SizeF(272f, 152f) to newWideLayout(context, state),
+ SizeF(180f, 272f) to newMediumLayout(context, state),
+ SizeF(272f, 272f) to newLargeLayout(context, state))
+
+ // Manually update AppWidgetManager with the new views.
+ val awm = AppWidgetManager.getInstance(context)
+ val component = ComponentName(context, this::class.java)
+ try {
+ awm.updateAppWidgetCompat(context, component, views)
+ } catch (e: Exception) {
+ // Layout update failed, gracefully degrade to the default widget.
+ logW("Unable to update widget: $e")
+ reset(context)
+ }
+ }
+
+ /**
+ * Revert to the default layout that displays "No music playing".
+ * @param context [Context] required to update the widget layout.
+ */
+ fun reset(context: Context) {
+ logD("Using default layout")
+ AppWidgetManager.getInstance(context)
+ .updateAppWidget(ComponentName(context, this::class.java), newDefaultLayout(context))
+ }
+
// --- INTERNAL METHODS ---
+ /**
+ * Request an update from [WidgetComponent].
+ * @param context [Context] required to send update request broadcast.
+ */
private fun requestUpdate(context: Context) {
logD("Sending update intent to PlaybackService")
val intent = Intent(ACTION_WIDGET_UPDATE).addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
context.sendBroadcast(intent)
}
- private fun AppWidgetManager.updateAppWidgetCompat(
- context: Context,
- views: Map
- ) {
- check(views.isNotEmpty()) { "Must provide a non-empty map" }
+ // --- LAYOUTS ---
- val name = ComponentName(context, WidgetProvider::class.java)
+ /**
+ * Create and configure a [RemoteViews] for [R.layout.widget_default], intended for situations
+ * where no other widget layout is applicable.
+ * @param context [Context] required to create the [RemoteViews].
+ */
+ private fun newDefaultLayout(context: Context) =
+ newRemoteViews(context, R.layout.widget_default)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- // Widgets are automatically responsive on Android 12, no need to do anything.
- updateAppWidget(name, RemoteViews(views))
+ /**
+ * Create and configure a [RemoteViews] for [R.layout.widget_thin], intended for extremely
+ * small grid sizes on phones in landscape mode.
+ * @param context [Context] required to create the [RemoteViews].
+ */
+ private fun newThinLayout(context: Context, state: WidgetComponent.PlaybackState) =
+ newRemoteViews(context, R.layout.widget_thin)
+ .setupBackground(context)
+ .setupPlaybackState(context, state)
+ .setupTimelineControls(context, state)
+
+ /**
+ * Create and configure a [RemoteViews] for [R.layout.widget_small], intended to be a
+ * modestly-sized default layout for most devices.
+ * @param context [Context] required to create the [RemoteViews].
+ */
+ private fun newSmallLayout(context: Context, state: WidgetComponent.PlaybackState) =
+ newRemoteViews(context, R.layout.widget_small)
+ .setupBar(context)
+ .setupCover(context, state)
+ .setupTimelineControls(context, state)
+
+ /**
+ * Create and configure a [RemoteViews] for [R.layout.widget_medium], intended to be
+ * a taller widget that shows more information about the currently playing song.
+ * @param context [Context] required to create the [RemoteViews].
+ */
+ private fun newMediumLayout(context: Context, state: WidgetComponent.PlaybackState) =
+ newRemoteViews(context, R.layout.widget_medium)
+ .setupBackground(context)
+ .setupPlaybackState(context, state)
+ .setupTimelineControls(context, state)
+
+ /**
+ * Create and configure a [RemoteViews] for [R.layout.widget_wide], intended to be
+ * a wider version of [R.layout.widget_small] that shows additional controls.
+ * @param context [Context] required to create the [RemoteViews].
+ */
+ private fun newWideLayout(context: Context, state: WidgetComponent.PlaybackState) =
+ newRemoteViews(context, R.layout.widget_wide)
+ .setupBar(context)
+ .setupCover(context, state)
+ .setupFullControls(context, state)
+
+ /**
+ * Create and configure a [RemoteViews] for [R.layout.widget_large], intended to be
+ * a wider version of [R.layout.widget_medium] that shows additional controls.
+ * @param context [Context] required to create the [RemoteViews].
+ */
+ private fun newLargeLayout(context: Context, state: WidgetComponent.PlaybackState) =
+ newRemoteViews(context, R.layout.widget_large)
+ .setupBackground(context)
+ .setupPlaybackState(context, state)
+ .setupFullControls(context, state)
+
+ /**
+ * Set up the control bar in a [RemoteViews] layout that contains one. This is a kind of
+ * "floating" drawable that sits in front of the cover and contains the controls.
+ * @param context [Context] required to set up the view.
+ */
+ private fun RemoteViews.setupBar(context: Context): RemoteViews {
+ // Below API 31, enable a rounded bar only if round mode is enabled.
+ // On API 31+, the bar should always be round in order to fit in with other widgets.
+ val background = if (Settings(context).roundMode &&
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ R.drawable.ui_widget_bar_round
} else {
- // Otherwise, we try our best to backport the responsive behavior to older versions.
- // This seems to work well enough on most launchers.
-
- // Each widget has independent dimensions, so we iterate through them all
- // and do this for each.
- for (id in getAppWidgetIds(name)) {
- val options = getAppWidgetOptions(id)
-
- val width: Int
- val height: Int
-
- // Landscape/Portrait modes use different dimen bounds
- if (context.isLandscape) {
- width = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH)
- height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
- } else {
- width = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
- height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)
- }
-
- logD("Assuming true widget dimens are ${width}x$height")
-
- // Find the layout with the greatest area that fits entirely within
- // the widget. This is what we will use. Fall back to the smallest layout
- // otherwise.
- val candidates = mutableListOf()
-
- for (size in views.keys) {
- if (size.width <= width && size.height <= height) {
- candidates.add(size)
- }
- }
-
- val layout =
- candidates.maxByOrNull { it.height * it.width }
- ?: views.minBy { it.key.width * it.key.height }.key
-
- logD("Using widget layout $layout ${views.contains(layout)}")
- updateAppWidget(id, views[layout])
- }
+ R.drawable.ui_widget_bar_system
}
+ setBackgroundResource(R.id.widget_controls, background)
+ return this
+ }
+
+ /**
+ * Set up the background in a [RemoteViews] layout that contains one. This is largely
+ * self-explanatory, being a solid-color background that sits behind the cover and controls.
+ * @param context [Context] required to set up the view.
+ */
+ private fun RemoteViews.setupBackground(context: Context): RemoteViews {
+ // Below API 31, enable a rounded background only if round mode is enabled.
+ // On API 31+, the background should always be round in order to fit in with other
+ // widgets.
+ val background = if (Settings(context).roundMode &&
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+ R.drawable.ui_widget_bar_round
+ } else {
+ R.drawable.ui_widget_bar_system
+ }
+ setBackgroundResource(android.R.id.background, background)
+ return this
+ }
+
+ /**
+ * Set up the album cover in a [RemoteViews] layout that contains one.
+ * @param context [Context] required to set up the view.
+ * @param state Current [WidgetComponent.PlaybackState] to display.
+ */
+ private fun RemoteViews.setupCover(
+ context: Context,
+ state: WidgetComponent.PlaybackState
+ ): RemoteViews {
+ if (state.cover != null) {
+ setImageViewBitmap(R.id.widget_cover, state.cover)
+ setContentDescription(
+ R.id.widget_cover,
+ context.getString(R.string.desc_album_cover, state.song.album.resolveName(context)))
+ } else {
+ // We are unable to use the typical placeholder cover with the song item due to
+ // limitations with the corner radius. Instead use a custom-made album icon as the
+ // placeholder.
+ setImageViewResource(R.id.widget_cover, R.drawable.ic_remote_default_cover_24)
+ setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover))
+ }
+
+ return this
+ }
+
+ /**
+ * Set up the album cover, song title, and artist name in a [RemoteViews] layout that
+ * contains them.
+ * @param context [Context] required to set up the view.
+ * @param state Current [WidgetComponent.PlaybackState] to display.
+ */
+ private fun RemoteViews.setupPlaybackState(
+ context: Context,
+ state: WidgetComponent.PlaybackState
+ ): RemoteViews {
+ setupCover(context, state)
+ setTextViewText(R.id.widget_song, state.song.resolveName(context))
+ setTextViewText(R.id.widget_artist, state.song.resolveArtistContents(context))
+ return this
+ }
+
+ /**
+ * Set up the play/pause button in a [RemoteViews] layout that contains one.
+ * @param context [Context] required to set up the view.
+ * @param state Current [WidgetComponent.PlaybackState] to display.
+ */
+ private fun RemoteViews.setupBasicControls(
+ context: Context,
+ state: WidgetComponent.PlaybackState
+ ): RemoteViews {
+ // Hook the play/pause button to the play/pause broadcast that will be recognized
+ // by PlaybackService.
+ setOnClickPendingIntent(
+ R.id.widget_play_pause,
+ context.newBroadcastPendingIntent(PlaybackService.ACTION_PLAY_PAUSE))
+
+ // Set up the play/pause button appearance. Like the Android 13 media controls, use
+ // a circular FAB when paused, and a squircle FAB when playing. This does require us
+ // to disable the ripple animation sadly, as it will glitch when this is used. The
+ // shape change should act as a similar signal.
+ val icon: Int
+ val background: Int
+ if (state.isPlaying) {
+ icon = R.drawable.ic_pause_24
+ background = R.drawable.ui_remote_fab_container_playing
+ } else {
+ icon = R.drawable.ic_play_24
+ background = R.drawable.ui_remote_fab_container_paused
+ }
+
+ setImageViewResource(R.id.widget_play_pause, icon)
+ setBackgroundResource(R.id.widget_play_pause, background)
+
+ return this
+ }
+
+ /**
+ * Set up the play/pause and skip previous/next button in a [RemoteViews] layout that
+ * contains them.
+ * @param context [Context] required to set up the view.
+ * @param state Current [WidgetComponent.PlaybackState] to display.
+ */
+ private fun RemoteViews.setupTimelineControls(
+ context: Context,
+ state: WidgetComponent.PlaybackState
+ ): RemoteViews {
+ // Timeline controls contain the basic controls, set those up
+ setupBasicControls(context, state)
+ // Timeline elements should always be left-to-right.
+ setLayoutDirection(R.id.widget_controls, View.LAYOUT_DIRECTION_LTR)
+ // Hook the skip buttons to the respective broadcasts that can be recognized
+ // by PlaybackService.
+ setOnClickPendingIntent(
+ R.id.widget_skip_prev, context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_PREV))
+ setOnClickPendingIntent(
+ R.id.widget_skip_next, context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_NEXT))
+ return this
+ }
+
+ /**
+ * Set up the play/pause, skip previous/next, and repeat/shuffle buttons in a [RemoteViews]
+ * that contains them.
+ * @param context [Context] required to set up the view.
+ * @param state Current [WidgetComponent.PlaybackState] to display.
+ */
+ private fun RemoteViews.setupFullControls(
+ context: Context,
+ state: WidgetComponent.PlaybackState
+ ): RemoteViews {
+ // Full controls contain timeline controls, make are set those.
+ setupTimelineControls(context, state)
+
+ // Hook the repeat/shuffle buttons to the respective broadcasts that can
+ // be recognized by PlaybackService.
+ setOnClickPendingIntent(
+ R.id.widget_repeat,
+ context.newBroadcastPendingIntent(PlaybackService.ACTION_INC_REPEAT_MODE))
+ setOnClickPendingIntent(
+ R.id.widget_shuffle,
+ context.newBroadcastPendingIntent(PlaybackService.ACTION_INVERT_SHUFFLE))
+
+ // Set up the repeat/shuffle buttons. When working with RemoteViews, we will
+ // need to hard-code different accent tinting configurations, as stateful drawables
+ // are unsupported.
+ val repeatRes =
+ when (state.repeatMode) {
+ RepeatMode.NONE -> R.drawable.ic_repeat_off_24
+ RepeatMode.ALL -> R.drawable.ic_repeat_on_24
+ RepeatMode.TRACK -> R.drawable.ic_repeat_one_24
+ }
+ setImageViewResource(R.id.widget_repeat, repeatRes)
+
+ val shuffleRes =
+ when {
+ state.isShuffled -> R.drawable.ic_shuffle_on_24
+ else -> R.drawable.ic_shuffle_off_24
+ }
+ setImageViewResource(R.id.widget_shuffle, shuffleRes)
+
+ return this
}
companion object {
+ /**
+ * Broadcast when [WidgetProvider] desires to update it's widget with new
+ * information. Responsible background tasks should intercept this and relay
+ * the message to [WidgetComponent].
+ */
const val ACTION_WIDGET_UPDATE = BuildConfig.APPLICATION_ID + ".action.WIDGET_UPDATE"
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt
new file mode 100644
index 000000000..32382643b
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt
@@ -0,0 +1,97 @@
+package org.oxycblt.auxio.widgets
+
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
+import android.content.Context
+import android.os.Build
+import android.util.SizeF
+import android.widget.RemoteViews
+import androidx.annotation.DrawableRes
+import androidx.annotation.IdRes
+import androidx.annotation.LayoutRes
+import org.oxycblt.auxio.util.isLandscape
+import org.oxycblt.auxio.util.logD
+import org.oxycblt.auxio.util.newMainPendingIntent
+
+/**
+ * Create a [RemoteViews] instance with the specified layout and an automatic click handler
+ * to open the Auxio activity.
+ * @param context [Context] required to create [RemoteViews].
+ * @param layoutRes Resource ID of the layout to use. Must be compatible with [RemoteViews].
+ * @return A new [RemoteViews] instance with the specified configuration.
+ */
+fun newRemoteViews(context: Context, @LayoutRes layoutRes: Int): RemoteViews {
+ val views = RemoteViews(context.packageName, layoutRes)
+ views.setOnClickPendingIntent(android.R.id.background, context.newMainPendingIntent())
+ return views
+}
+
+/**
+ * Set the background resource of a [RemoteViews] View.
+ * @param viewId The ID of the view to update.
+ * @param drawableRes The resource ID of the drawable to set the background to.
+ */
+fun RemoteViews.setBackgroundResource(@IdRes viewId: Int, @DrawableRes drawableRes: Int) {
+ setInt(viewId, "setBackgroundResource", drawableRes)
+}
+
+/**
+ * Set the layout direction of a [RemoteViews] view.
+ * @param viewId The ID of the view to update.
+ * @param layoutDirection The layout direction to apply to the view,
+ */
+fun RemoteViews.setLayoutDirection(@IdRes viewId: Int, layoutDirection: Int) {
+ setInt(viewId, "setLayoutDirection", layoutDirection)
+}
+
+/**
+ * Update the app widget layouts corresponding to the given [AppWidgetProvider] [ComponentName]
+ * with an adaptive layout, in a version-compatible manner.
+ * @param context [Context] required to backport adaptive layout behavior.
+ * @param component [ComponentName] of the app widget layout to update.
+ * @param views Mapping between different size classes and [RemoteViews] instances.
+ * @see RemoteViews
+ */
+fun AppWidgetManager.updateAppWidgetCompat(
+ context: Context,
+ component: ComponentName,
+ views: Map
+) {
+ check(views.isNotEmpty()) { "Must provide a non-empty map" }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ // Can use adaptive widgets from API 31+.
+ updateAppWidget(component, RemoteViews(views))
+ } else {
+ // Backport adaptive widgets to API 21+.
+ // Each app widget has independent dimensions, so we iterate through them all
+ // and do this for each.
+ for (id in getAppWidgetIds(component)) {
+ val options = getAppWidgetOptions(id)
+
+ // Depending on the device orientation, the size of the app widget will be
+ // composed differently.
+ val width: Int
+ val height: Int
+ if (context.isLandscape) {
+ // Width will be larger in landscape, so use MAX_WIDTH.
+ width = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH)
+ height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
+ } else {
+ // Height will be larger in portrait, so use MAX_HEIGHT.
+ width = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
+ height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)
+ }
+ logD("Assuming dimens are ${width}x$height")
+
+ // Find the layout with the greatest area that fits entirely within
+ // the app widget. This is what we will use. Fall back to the smallest layout
+ // otherwise.
+ val layout = views.keys.filter { it.width <= width && it.height <= height }
+ .maxByOrNull { it.height * it.width } ?:
+ views.minBy { it.key.width * it.key.height }.key
+ logD("Using layout $layout ${views.contains(layout)}")
+
+ updateAppWidget(id, views[layout])
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v31/ui_widget_bar.xml b/app/src/main/res/drawable-v31/ui_widget_bar_system.xml
similarity index 100%
rename from app/src/main/res/drawable-v31/ui_widget_bar.xml
rename to app/src/main/res/drawable-v31/ui_widget_bar_system.xml
diff --git a/app/src/main/res/drawable/ui_widget_bar.xml b/app/src/main/res/drawable/ui_widget_bar_system.xml
similarity index 100%
rename from app/src/main/res/drawable/ui_widget_bar.xml
rename to app/src/main/res/drawable/ui_widget_bar_system.xml
diff --git a/app/src/main/res/drawable/ui_widget_bg.xml b/app/src/main/res/drawable/ui_widget_bg_system.xml
similarity index 100%
rename from app/src/main/res/drawable/ui_widget_bg.xml
rename to app/src/main/res/drawable/ui_widget_bg_system.xml
diff --git a/app/src/main/res/layout/widget_large.xml b/app/src/main/res/layout/widget_large.xml
index b3feb70b1..2ef525fa3 100644
--- a/app/src/main/res/layout/widget_large.xml
+++ b/app/src/main/res/layout/widget_large.xml
@@ -4,7 +4,7 @@
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@drawable/ui_widget_bg"
+ android:background="@drawable/ui_widget_bg_system"
android:backgroundTint="?attr/colorSurface"
android:theme="@style/Theme.Auxio.Widget">
diff --git a/app/src/main/res/layout/widget_medium.xml b/app/src/main/res/layout/widget_medium.xml
index ddcddd9a0..4dc4c394c 100644
--- a/app/src/main/res/layout/widget_medium.xml
+++ b/app/src/main/res/layout/widget_medium.xml
@@ -4,7 +4,7 @@
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@drawable/ui_widget_bg"
+ android:background="@drawable/ui_widget_bg_system"
android:backgroundTint="?attr/colorSurface"
android:theme="@style/Theme.Auxio.Widget">
diff --git a/app/src/main/res/layout/widget_small.xml b/app/src/main/res/layout/widget_small.xml
index b84ea5bbb..2b2566812 100644
--- a/app/src/main/res/layout/widget_small.xml
+++ b/app/src/main/res/layout/widget_small.xml
@@ -61,7 +61,7 @@
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_gravity="center"
- android:background="@drawable/ui_widget_bar"
+ android:background="@drawable/ui_widget_bar_system"
android:backgroundTint="?attr/colorSurface"
android:orientation="horizontal"
android:padding="@dimen/spacing_mid_medium">
diff --git a/app/src/main/res/layout/widget_thin.xml b/app/src/main/res/layout/widget_thin.xml
index 2127870f1..a3201fc54 100644
--- a/app/src/main/res/layout/widget_thin.xml
+++ b/app/src/main/res/layout/widget_thin.xml
@@ -4,7 +4,7 @@
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@drawable/ui_widget_bg"
+ android:background="@drawable/ui_widget_bg_system"
android:backgroundTint="?attr/colorSurface"
android:baselineAligned="false"
android:orientation="horizontal"
diff --git a/app/src/main/res/layout/widget_wide.xml b/app/src/main/res/layout/widget_wide.xml
index c1dc8551f..f0b129ae5 100644
--- a/app/src/main/res/layout/widget_wide.xml
+++ b/app/src/main/res/layout/widget_wide.xml
@@ -48,7 +48,7 @@
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_gravity="center"
- android:background="@drawable/ui_widget_bar"
+ android:background="@drawable/ui_widget_bar_system"
android:backgroundTint="?attr/colorSurface"
android:orientation="horizontal"
android:padding="@dimen/spacing_mid_medium">
diff --git a/app/src/main/res/values-ar-rIQ/strings.xml b/app/src/main/res/values-ar-rIQ/strings.xml
index b1699cb21..b39639d0f 100644
--- a/app/src/main/res/values-ar-rIQ/strings.xml
+++ b/app/src/main/res/values-ar-rIQ/strings.xml
@@ -2,7 +2,7 @@
مشغل موسيقى بسيط ومعقول لنظام الاندرويد.
- عرض وتحكم بشتغيل الموسيقى
+ عرض وتحكم بشتغيل الموسيقى
إعادة المحاولة
منح
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index af8a00184..8f9c102fc 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -3,7 +3,7 @@
Jednoduchý, rozumný hudební přehrávač pro Android.
Načítání hudby
- Zobrazení a ovládání přehrávání hudby
+ Zobrazení a ovládání přehrávání hudby
Načítání vaší hudební knihovny…
Zkusit znovu
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 54b7e17ac..592e193c8 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -117,7 +117,7 @@
- %d Alben
Ein einfacher, rationaler Musikplayer für Android.
- Musikwiedergabe anzeigen und kontrollieren
+ Musikwiedergabe anzeigen und kontrollieren
Künstler
Album
Jahr
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 5ec5945f2..c912eddf0 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -100,7 +100,7 @@
Καθόλου φάκελοι
Μιά απλή, λογική εφαρμογή αναπαραγωγής μουσικής για Android.
Φόρτωση μουσικής
- Προβολή και έλεγχος αναπαραγωγής μουσικής
+ Προβολή και έλεγχος αναπαραγωγής μουσικής
Album
EP
EP
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 6dd253837..08fa9115e 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -2,7 +2,7 @@
Un reproductor de música simple y racional para Android.
- Ver y controlar la reproducción musical
+ Ver y controlar la reproducción musical
Reintentar
Permitir
diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml
index 4e42f3e16..33171fd06 100644
--- a/app/src/main/res/values-fil/strings.xml
+++ b/app/src/main/res/values-fil/strings.xml
@@ -94,7 +94,7 @@
Mga Single
Bigyan
Mga Genre
- Tignan at ayusin ang pagtugtog ng kanta
+ Tignan at ayusin ang pagtugtog ng kanta
Tema
Ibahin ang pagkakita at ayos ng mga library tab
Inilalapat ang pre-amp sa kasalukuyang ayos habang ito\'y tumutugtog
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 174c98f05..3de798889 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -94,7 +94,7 @@
Onglets de la bibliothèque
Un lecteur de musique simple et rationnel pour Android.
Chargement de musique
- Afficher et contrôler la lecture de la musique
+ Afficher et contrôler la lecture de la musique
Chargement de votre bibliothèque musicale…
Nom
Artiste
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index ac660f486..e02754791 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -171,7 +171,7 @@
Jednostavan i racionalan izvođač glazbe za Android.
Pokušaj ponovo
Učitavanje glazbe
- Pregledaj i upravljaj reprodukcijom glazbe
+ Pregledaj i upravljaj reprodukcijom glazbe
Dozvoli
Singl uživo
Singl remiks
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index 3f5262b0c..ff631265c 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -76,7 +76,7 @@
Album
Pemutar musik yang simpel dan rasional untuk android.
Pemuatan Musik
- Lihat dan kontrol pemutaran musik
+ Lihat dan kontrol pemutaran musik
Tahun
Durasi
Disk
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index e22f007e8..cdaee71d0 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -2,7 +2,7 @@
Un semplice, razionale lettore musicale per android.
- Vedi e gestisci la riproduzione musicale
+ Vedi e gestisci la riproduzione musicale
Riprova
Permetti
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 59a437cd4..217658d04 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -2,7 +2,7 @@
단순하고, 실용적인 안드로이드용 뮤직 플레이어입니다.
- 음악 재생 제어 및 상태 확인
+ 음악 재생 제어 및 상태 확인
재시도
허가
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index 7773ab3e8..b6bf30928 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -49,7 +49,7 @@
Naudokti grynai juodą tamsią temą
Paprastas, racionalus „Android“ muzikos grotuvas.
Muzika kraunama
- Peržiūrėti ir valdyti muzikos grojimą
+ Peržiūrėti ir valdyti muzikos grojimą
Žanrai
Bandykite dar kartą
Suteikti
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index b9f6d4b5e..a203c7270 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -114,7 +114,7 @@
Schijf %d
%d kbps
+%.1f dB
- Muziekweergave bekijken en regelen
+ Muziekweergave bekijken en regelen
%d Hz
Bekijk eigenschappen
Naam
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index decce6ed2..a6edb271d 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -215,7 +215,7 @@
Wyczyść zapytanie wyszukiwania
Nie można przywrócić stanu odtwarzania
Okładka gatunku %s
- Wyświetlanie oraz kontrolowanie odtwarzania muzyki
+ Wyświetlanie oraz kontrolowanie odtwarzania muzyki
Regulacja w oparciu o tagi
Regulacja bez tagów
Wzmocnienie dźwięku przez preamplifier jest nakładane na wcześniej ustawione wzmocnienie podczas odtwarzania
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 4103deac5..bb6cf3bca 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -201,7 +201,7 @@
Salvar lista de reprodução
Limpar lista de reprodução
Restaurar lista de reprodução
- Visualize e controle a reprodução de música
+ Visualize e controle a reprodução de música
Ação personalizada na barra de reprodução
Modo de repetição
Limpa a lista de reprodução salva anteriormente (se houver)
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 518cbbc7d..c0f00005d 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -2,7 +2,7 @@
Простой и рациональный музыкальный проигрыватель.
- Настройки воспроизведения
+ Настройки воспроизведения
Повторить попытку
Разрешить
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index c2a19b0e3..deb2c84fb 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -71,7 +71,7 @@
Boyut
Bit hızı
Örnek hızı
- Müzik çalmayı görüntüle ve kontrol et
+ Müzik çalmayı görüntüle ve kontrol et
Android için basit, rasyonel bir müzik çalar.
Müzik Yükleniyor
Müzik kitaplığınız yükleniyor…
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 94ae5050e..8cb075d36 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -2,7 +2,7 @@
一款简洁、克制的 Android 音乐播放器。
- 查看并控制音乐播放
+ 查看并控制音乐播放
重试
授予
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 5b705f331..0600e089c 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -48,6 +48,6 @@
88dp
128dp
- 180dp
- 100dp
+ 180dp
+ 100dp
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 24730dcba..6c72440c7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -147,7 +147,7 @@
- View and control music playback
+ View and control music playback
Loading your music library…
Monitoring your music library for changes…
Added to queue
diff --git a/app/src/main/res/xml-v31/widget_info.xml b/app/src/main/res/xml-v31/widget_info.xml
index 69fbc9c35..11fcafb44 100644
--- a/app/src/main/res/xml-v31/widget_info.xml
+++ b/app/src/main/res/xml-v31/widget_info.xml
@@ -1,11 +1,11 @@