From 200a3dfeafe8f252575ea82de73f148dc90281bf Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 23 Dec 2022 21:37:48 -0700 Subject: [PATCH] appwidget: redocument Redocument the appwidget (formerly widget) module. This commit also re-architectures the module somewhat to make further extension easier later on. --- README.md | 9 +- .../java/org/oxycblt/auxio/MainActivity.kt | 55 +-- .../java/org/oxycblt/auxio/MainFragment.kt | 2 + .../auxio/detail/AlbumDetailFragment.kt | 3 +- .../auxio/detail/ArtistDetailFragment.kt | 3 +- .../auxio/detail/GenreDetailFragment.kt | 3 +- .../oxycblt/auxio/detail/SongDetailDialog.kt | 3 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 2 +- .../auxio/playback/system/PlaybackService.kt | 2 +- .../java/org/oxycblt/auxio/widgets/Forms.kt | 215 --------- .../oxycblt/auxio/widgets/WidgetComponent.kt | 81 ++-- .../oxycblt/auxio/widgets/WidgetProvider.kt | 415 +++++++++++++----- .../org/oxycblt/auxio/widgets/WidgetUtil.kt | 97 ++++ ...idget_bar.xml => ui_widget_bar_system.xml} | 0 ...idget_bar.xml => ui_widget_bar_system.xml} | 0 ..._widget_bg.xml => ui_widget_bg_system.xml} | 0 app/src/main/res/layout/widget_large.xml | 2 +- app/src/main/res/layout/widget_medium.xml | 2 +- app/src/main/res/layout/widget_small.xml | 2 +- app/src/main/res/layout/widget_thin.xml | 2 +- app/src/main/res/layout/widget_wide.xml | 2 +- app/src/main/res/values-ar-rIQ/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-el/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fil/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-hr/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values-lt/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values/dimens.xml | 4 +- app/src/main/res/values/strings.xml | 2 +- app/src/main/res/xml-v31/widget_info.xml | 10 +- app/src/main/res/xml/widget_info.xml | 8 +- 43 files changed, 523 insertions(+), 437 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt rename app/src/main/res/drawable-v31/{ui_widget_bar.xml => ui_widget_bar_system.xml} (100%) rename app/src/main/res/drawable/{ui_widget_bar.xml => ui_widget_bar_system.xml} (100%) rename app/src/main/res/drawable/{ui_widget_bg.xml => ui_widget_bg_system.xml} (100%) 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 @@