From 6f8f333b72194a11b1c4cc76797d3ae081a3a7b8 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Thu, 5 Aug 2021 10:17:33 -0600 Subject: [PATCH] widgets: make widget responsive The plans for widgets have changed somewhat. Instead of 4 or so variants, there will instead be one unified widget that chooses different layouts depending on its size. The first one added is the full widget, which shows more controls as long as theres enough space. --- app/src/main/AndroidManifest.xml | 4 +- .../java/org/oxycblt/auxio/MainActivity.kt | 3 +- .../auxio/playback/system/PlaybackService.kt | 10 +- .../org/oxycblt/auxio/ui/InterfaceUtils.kt | 5 +- .../org/oxycblt/auxio/widgets/BaseWidget.kt | 109 ------------- .../auxio/widgets/MinimalWidgetProvider.kt | 108 ------------- .../oxycblt/auxio/widgets/WidgetController.kt | 46 +++--- .../oxycblt/auxio/widgets/WidgetProvider.kt | 146 ++++++++++++++++++ .../org/oxycblt/auxio/widgets/WidgetState.kt | 17 ++ .../auxio/widgets/forms/FullWidgetForm.kt | 98 ++++++++++++ .../auxio/widgets/forms/SmallWidgetForm.kt | 64 ++++++++ .../oxycblt/auxio/widgets/forms/WidgetForm.kt | 20 +++ .../main/res/drawable/ic_loop_all_tinted.xml | 11 ++ .../main/res/drawable/ic_loop_one_tinted.xml | 11 ++ .../main/res/drawable/ic_shuffle_tinted.xml | 11 ++ app/src/main/res/layout-v31/widget_full.xml | 101 ++++++++++++ .../{widget_minimal.xml => widget_small.xml} | 21 +-- app/src/main/res/layout/widget_default.xml | 4 +- app/src/main/res/layout/widget_full.xml | 101 ++++++++++++ .../{widget_minimal.xml => widget_small.xml} | 26 ++-- app/src/main/res/values/strings.xml | 2 +- app/src/main/res/values/styles_ui.xml | 11 ++ app/src/main/res/xml-v31/widget_minimal.xml | 9 +- app/src/main/res/xml/widget_minimal.xml | 6 +- 24 files changed, 655 insertions(+), 289 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/widgets/BaseWidget.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/widgets/MinimalWidgetProvider.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/widgets/WidgetState.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/widgets/forms/FullWidgetForm.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/widgets/forms/SmallWidgetForm.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/widgets/forms/WidgetForm.kt create mode 100644 app/src/main/res/drawable/ic_loop_all_tinted.xml create mode 100644 app/src/main/res/drawable/ic_loop_one_tinted.xml create mode 100644 app/src/main/res/drawable/ic_shuffle_tinted.xml create mode 100644 app/src/main/res/layout-v31/widget_full.xml rename app/src/main/res/layout-v31/{widget_minimal.xml => widget_small.xml} (85%) create mode 100644 app/src/main/res/layout/widget_full.xml rename app/src/main/res/layout/{widget_minimal.xml => widget_small.xml} (81%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 09bc9ba60..fb8b9f303 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,8 +57,8 @@ android:roundIcon="@mipmap/ic_launcher_round" /> diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 649f0adba..c9f629775 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -19,7 +19,8 @@ import org.oxycblt.auxio.ui.isNight /** * The single [AppCompatActivity] for Auxio. - * TODO: Port widgets to non-12 android + * TODO: Migrate to colorAccent + * */ class MainActivity : AppCompatActivity() { private val playbackModel: PlaybackViewModel by viewModels() 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 4967f5cf3..ec788d938 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 @@ -42,8 +42,8 @@ import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.ui.getSystemServiceSafe -import org.oxycblt.auxio.widgets.BaseWidget import org.oxycblt.auxio.widgets.WidgetController +import org.oxycblt.auxio.widgets.WidgetProvider /** * A service that manages the system-side aspects of playback, such as: @@ -140,7 +140,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY) addAction(Intent.ACTION_HEADSET_PLUG) - addAction(BaseWidget.ACTION_WIDGET_UPDATE) + addAction(WidgetProvider.ACTION_WIDGET_UPDATE) registerReceiver(systemReceiver, this) } @@ -495,10 +495,8 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac } } - BaseWidget.ACTION_WIDGET_UPDATE -> { - widgets.initWidget( - intent.getIntExtra(BaseWidget.KEY_WIDGET_TYPE, -1) - ) + WidgetProvider.ACTION_WIDGET_UPDATE -> { + widgets.update() } } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt b/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt index afb0ee7d4..205a38849 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt @@ -28,6 +28,7 @@ import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.MainActivity import org.oxycblt.auxio.R +import org.oxycblt.auxio.logD import org.oxycblt.auxio.logE import kotlin.reflect.KClass @@ -101,7 +102,7 @@ fun @receiver:ColorRes Int.toColor(context: Context): Int { return try { ContextCompat.getColor(context, this) } catch (e: Resources.NotFoundException) { - logE("Attempted color load failed.") + logE("Attempted color load failed: ${e.stackTraceToString()}") // Default to the emergency color [Black] if the loading fails. ContextCompat.getColor(context, android.R.color.black) @@ -146,6 +147,8 @@ fun @receiver:AttrRes Int.resolveAttr(context: Context): Int { resolvedAttr.data } + logD(context.theme) + return color.toColor(context) } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/BaseWidget.kt b/app/src/main/java/org/oxycblt/auxio/widgets/BaseWidget.kt deleted file mode 100644 index 9ea8baf26..000000000 --- a/app/src/main/java/org/oxycblt/auxio/widgets/BaseWidget.kt +++ /dev/null @@ -1,109 +0,0 @@ -package org.oxycblt.auxio.widgets - -import android.annotation.SuppressLint -import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.widget.RemoteViews -import androidx.annotation.LayoutRes -import org.oxycblt.auxio.BuildConfig -import org.oxycblt.auxio.R -import org.oxycblt.auxio.logD -import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.ui.newMainIntent - -/** - * The base widget class for all widget implementations in Auxio. - */ -abstract class BaseWidget : AppWidgetProvider() { - abstract val type: Int - - protected open fun createViews(context: Context, @LayoutRes layout: Int): RemoteViews { - val views = RemoteViews(context.packageName, layout) - - views.setOnClickPendingIntent( - android.R.id.background, - context.newMainIntent() - ) - - return views - } - - /* - * Update views based off of the playback state. This job is asynchronous and should be - * notified as completed by calling [onDone] - */ - protected abstract fun updateViews( - context: Context, - playbackManager: PlaybackStateManager, - onDone: (RemoteViews) -> Unit - ) - - /* - * Update the widget based on the playback state. - */ - fun update(context: Context, playbackManager: PlaybackStateManager) { - val manager = AppWidgetManager.getInstance(context) - - // View updates are often async due to image loading, so only push the views - // when the callback is called. - updateViews(context, playbackManager) { views -> - manager.applyViews(context, views) - } - } - - /* - * Revert this widget to its default view - */ - fun reset(context: Context) { - logD("Resetting widget") - - val manager = AppWidgetManager.getInstance(context) - manager.applyViews(context, defaultViews(context)) - } - - override fun onUpdate( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetIds: IntArray - ) { - logD("Sending update intent to PlaybackService") - - appWidgetManager.applyViews(context, defaultViews(context)) - - val intent = Intent(ACTION_WIDGET_UPDATE) - .putExtra(KEY_WIDGET_TYPE, type) - .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) - - context.sendBroadcast(intent) - } - - @SuppressLint("RemoteViewLayout") - protected fun defaultViews(context: Context): RemoteViews { - return RemoteViews( - context.packageName, - R.layout.widget_default - ) - } - - private fun AppWidgetManager.applyViews(context: Context, views: RemoteViews) { - val ids = getAppWidgetIds(ComponentName(context, this::class.java)) - - if (ids.isNotEmpty()) { - // Existing widgets found, update those - ids.forEach { id -> - updateAppWidget(id, views) - } - } else { - // No existing widgets found. Fall back to the name of the widget class - updateAppWidget(ComponentName(context, this@BaseWidget::class.java), views) - } - } - - companion object { - const val ACTION_WIDGET_UPDATE = BuildConfig.APPLICATION_ID + ".action.WIDGET_UPDATE" - const val KEY_WIDGET_TYPE = BuildConfig.APPLICATION_ID + ".key.WIDGET_TYPE" - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/MinimalWidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/MinimalWidgetProvider.kt deleted file mode 100644 index c69b9f293..000000000 --- a/app/src/main/java/org/oxycblt/auxio/widgets/MinimalWidgetProvider.kt +++ /dev/null @@ -1,108 +0,0 @@ -package org.oxycblt.auxio.widgets - -import android.content.Context -import android.os.Build -import android.widget.RemoteViews -import org.oxycblt.auxio.R -import org.oxycblt.auxio.coil.loadBitmap -import org.oxycblt.auxio.logD -import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.playback.system.PlaybackService -import org.oxycblt.auxio.ui.newBroadcastIntent - -/** - * The minimal widget, which shows the primary song controls and basic playback information. - */ -class MinimalWidgetProvider : BaseWidget() { - override val type: Int get() = TYPE - - override fun createViews(context: Context, layout: Int): RemoteViews { - val views = super.createViews(context, layout) - - views.setOnClickPendingIntent( - R.id.widget_skip_prev, - context.newBroadcastIntent( - PlaybackService.ACTION_SKIP_PREV - ) - ) - - views.setOnClickPendingIntent( - R.id.widget_play_pause, - context.newBroadcastIntent( - PlaybackService.ACTION_PLAY_PAUSE - ) - ) - - views.setOnClickPendingIntent( - R.id.widget_skip_next, - context.newBroadcastIntent( - PlaybackService.ACTION_SKIP_NEXT - ) - ) - - return views - } - - override fun updateViews( - context: Context, - playbackManager: PlaybackStateManager, - onDone: (RemoteViews) -> Unit - ) { - val song = playbackManager.song - - if (song != null) { - logD("Updating view to ${song.name}") - - val views = createViews(context, R.layout.widget_minimal) - - // Update the metadata - views.setTextViewText(R.id.widget_song, song.name) - views.setTextViewText(R.id.widget_artist, song.album.artist.name) - - views.setInt( - R.id.widget_play_pause, - "setImageResource", - if (playbackManager.isPlaying) { - R.drawable.ic_pause - } else { - R.drawable.ic_play - } - ) - - // loadBitmap is async, hence the need for onDone - loadBitmap(context, song) { bitmap -> - if (bitmap != null) { - views.setBitmap(R.id.widget_cover, "setImageBitmap", bitmap) - views.setCharSequence( - R.id.widget_cover, "setContentDescription", - context.getString(R.string.description_album_cover, song.album.name) - ) - } else { - views.setInt(R.id.widget_cover, "setImageResource", R.drawable.ic_song) - views.setCharSequence( - R.id.widget_cover, "setContentDescription", - context.getString(R.string.description_placeholder_cover) - ) - } - - onDone(views) - } - } else { - logD("No song playing, reverting to default view") - - onDone(defaultViews(context)) - } - } - - companion object { - const val TYPE = 0xA0D0 - - fun new(): MinimalWidgetProvider? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - MinimalWidgetProvider() - } else { - null - } - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt index e828aa6c1..97610465f 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt @@ -1,54 +1,36 @@ package org.oxycblt.auxio.widgets import android.content.Context -import org.oxycblt.auxio.logD import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.settings.SettingsManager /** - * A wrapper around each widget subclass that manages which updates to deliver from the - * main process's [PlaybackStateManager] and [SettingsManager] instances to the widgets themselves. + * 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. */ class WidgetController(private val context: Context) : PlaybackStateManager.Callback, SettingsManager.Callback { private val playbackManager = PlaybackStateManager.getInstance() private val settingsManager = SettingsManager.getInstance() - private val minimal = MinimalWidgetProvider() + private val widget = WidgetProvider() init { playbackManager.addCallback(this) settingsManager.addCallback(this) } - /* - * Initialize a newly added widget. This usually comes from the WIDGET_UPDATE intent. - */ - fun initWidget(type: Int) { - logD("Updating new widget $type") - - when (type) { - MinimalWidgetProvider.TYPE -> minimal.update(context, playbackManager) - } - } - - /* - * Update every widget, regardless of whether it needs to or not. - */ fun update() { - logD("Updating all widgets") - - minimal.update(context, playbackManager) + widget.update(context, playbackManager) } /* * Release this instance, removing the callbacks and resetting all widgets */ fun release() { - logD("Resetting widgets") - - minimal.reset(context) + widget.reset(context) playbackManager.removeCallback(this) settingsManager.removeCallback(this) } @@ -56,20 +38,28 @@ class WidgetController(private val context: Context) : // --- PLAYBACKSTATEMANAGER CALLBACKS --- override fun onSongUpdate(song: Song?) { - minimal.update(context, playbackManager) + widget.update(context, playbackManager) } override fun onPlayingUpdate(isPlaying: Boolean) { - minimal.update(context, playbackManager) + widget.update(context, playbackManager) + } + + override fun onShuffleUpdate(isShuffling: Boolean) { + widget.update(context, playbackManager) + } + + override fun onLoopUpdate(loopMode: LoopMode) { + widget.update(context, playbackManager) } // --- SETTINGSMANAGER CALLBACKS --- override fun onShowCoverUpdate(showCovers: Boolean) { - minimal.update(context, playbackManager) + widget.update(context, playbackManager) } override fun onQualityCoverUpdate(doQualityCovers: Boolean) { - minimal.update(context, playbackManager) + widget.update(context, playbackManager) } } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt new file mode 100644 index 000000000..42ebc9d09 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -0,0 +1,146 @@ +package org.oxycblt.auxio.widgets + +import android.annotation.SuppressLint +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.SizeF +import android.widget.RemoteViews +import org.oxycblt.auxio.BuildConfig +import org.oxycblt.auxio.R +import org.oxycblt.auxio.coil.loadBitmap +import org.oxycblt.auxio.logD +import org.oxycblt.auxio.playback.state.PlaybackStateManager +import org.oxycblt.auxio.ui.newMainIntent +import org.oxycblt.auxio.widgets.forms.FullWidgetForm +import org.oxycblt.auxio.widgets.forms.SmallWidgetForm +import org.oxycblt.auxio.widgets.forms.WidgetForm + +/** + * Auxio's one and only appwidget. This widget follows a more unorthodox approach, effectively + * packing what could be considered 3 or 4 widgets into a single responsive widget. More specifically: + * + * - TODO?: For widgets 3x1 or lower, show a text-only view with minimal controls + * - TODO?: For widgets 4x1, show a minimized view with album art + * - For widgets Wx2 or higher, show an expanded view with album art and basic controls + * - For widgets 4x2 or higher, show a complete view with all playback controls + * + * For more specific details about these sub-widgets, see [WidgetForm]. + */ +class WidgetProvider : AppWidgetProvider() { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + logD("Sending update intent to PlaybackService") + + appWidgetManager.applyViews(context, defaultViews(context)) + + val intent = Intent(ACTION_WIDGET_UPDATE) + .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) + + context.sendBroadcast(intent) + } + + /* + * Update the widget based on the playback state. + */ + fun update(context: Context, playbackManager: PlaybackStateManager) { + val manager = AppWidgetManager.getInstance(context) + + val song = playbackManager.song + + if (song == null) { + manager.applyViews(context, defaultViews(context)) + return + } + + // What we do here depends on how we're responding to layout changes. + // If we are on Android 11 or below, then we use the current widget form and default + // to SmallWidgetForm if one couldn't be figured out. + // If we are using Android S, we use the standard method of creating each RemoteView + // instance and putting them into a Map with their size ranges. This isn't as nice + // as it means we have to always load album art, even with the text only widget forms. + // But it's still preferable than to the Pre-12 method. + + // FIXME: Fix the race conditions with the bitmap loading. + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + loadBitmap(context, song) { bitmap -> + val state = WidgetState( + song, + bitmap, + playbackManager.isPlaying, + playbackManager.isShuffling, + playbackManager.loopMode + ) + + // Map each widget form to the rough dimensions where it would look nice. + // This might need to be adjusted. + val views = mapOf( + SizeF(110f, 110f) to SmallWidgetForm().createViews(context, state), + SizeF(272f, 110f) to FullWidgetForm().createViews(context, state) + ) + + manager.applyViews(context, RemoteViews(views)) + } + } else { + loadBitmap(context, song) { bitmap -> + val state = WidgetState( + song, + bitmap, + playbackManager.isPlaying, + playbackManager.isShuffling, + playbackManager.loopMode + ) + + // TODO: Make sub-12 widgets responsive. + manager.applyViews(context, FullWidgetForm().createViews(context, state)) + } + } + } + + /* + * Revert this widget to its default view + */ + fun reset(context: Context) { + logD("Resetting widget") + + val manager = AppWidgetManager.getInstance(context) + manager.applyViews(context, defaultViews(context)) + } + + @SuppressLint("RemoteViewLayout") + private fun defaultViews(context: Context): RemoteViews { + val views = RemoteViews(context.packageName, R.layout.widget_default) + + views.setOnClickPendingIntent( + android.R.id.background, + context.newMainIntent() + ) + + return views + } + + private fun AppWidgetManager.applyViews(context: Context, views: RemoteViews) { + val ids = getAppWidgetIds(ComponentName(context, this::class.java)) + + if (ids.isNotEmpty()) { + // Existing widgets found, update those + ids.forEach { id -> + updateAppWidget(id, views) + } + } else { + // No existing widgets found. Fall back to the name of the widget class + updateAppWidget(ComponentName(context, this@WidgetProvider::class.java), views) + } + } + + companion object { + const val ACTION_WIDGET_UPDATE = BuildConfig.APPLICATION_ID + ".action.WIDGET_UPDATE" + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetState.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetState.kt new file mode 100644 index 000000000..392504ed3 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetState.kt @@ -0,0 +1,17 @@ +package org.oxycblt.auxio.widgets + +import android.graphics.Bitmap +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.playback.state.LoopMode + +/* + * A condensed variant of the current playback state, used so that PlaybackStateManager does not + * need to be queried directly. + */ +data class WidgetState( + val song: Song, + val albumArt: Bitmap?, + val isPlaying: Boolean, + val isShuffled: Boolean, + val loopMode: LoopMode, +) diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/forms/FullWidgetForm.kt b/app/src/main/java/org/oxycblt/auxio/widgets/forms/FullWidgetForm.kt new file mode 100644 index 000000000..a2a9af3e3 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/widgets/forms/FullWidgetForm.kt @@ -0,0 +1,98 @@ +package org.oxycblt.auxio.widgets.forms + +import android.content.Context +import android.widget.RemoteViews +import org.oxycblt.auxio.R +import org.oxycblt.auxio.playback.state.LoopMode +import org.oxycblt.auxio.playback.system.PlaybackService +import org.oxycblt.auxio.ui.newBroadcastIntent +import org.oxycblt.auxio.widgets.WidgetState + +class FullWidgetForm : WidgetForm(R.layout.widget_full) { + override fun createViews(context: Context, state: WidgetState): RemoteViews { + val views = super.createViews(context, state) + + views.setOnClickPendingIntent( + R.id.widget_loop, + context.newBroadcastIntent( + PlaybackService.ACTION_LOOP + ) + ) + + views.setOnClickPendingIntent( + R.id.widget_skip_prev, + context.newBroadcastIntent( + PlaybackService.ACTION_SKIP_PREV + ) + ) + + views.setOnClickPendingIntent( + R.id.widget_play_pause, + context.newBroadcastIntent( + PlaybackService.ACTION_PLAY_PAUSE + ) + ) + + views.setOnClickPendingIntent( + R.id.widget_skip_next, + context.newBroadcastIntent( + PlaybackService.ACTION_SKIP_NEXT + ) + ) + + views.setOnClickPendingIntent( + R.id.widget_shuffle, + context.newBroadcastIntent( + PlaybackService.ACTION_SHUFFLE + ) + ) + + views.setTextViewText(R.id.widget_song, state.song.name) + views.setTextViewText(R.id.widget_artist, state.song.album.artist.name) + + views.setImageViewResource( + R.id.widget_play_pause, + if (state.isPlaying) { + R.drawable.ic_pause + } else { + R.drawable.ic_play + } + ) + + if (state.albumArt != null) { + views.setImageViewBitmap(R.id.widget_cover, state.albumArt) + views.setCharSequence( + R.id.widget_cover, "setContentDescription", + context.getString(R.string.description_album_cover, state.song.album.name) + ) + } else { + views.setImageViewResource(R.id.widget_cover, R.drawable.ic_song) + views.setCharSequence( + R.id.widget_cover, + "setContentDescription", + context.getString(R.string.description_placeholder_cover) + ) + } + + // The main way the large widget differs from the other widgets is the addition of extra + // controls. However, since the context we use to load attributes is from the main process, + // attempting to dynamically color anything will result in an error. More duplicate + // resources it is, then. This is getting really tiring. + + val shuffleRes = if (state.isShuffled) + R.drawable.ic_shuffle_tinted + else + R.drawable.ic_shuffle + + val loopRes = when (state.loopMode) { + LoopMode.NONE -> R.drawable.ic_loop + LoopMode.ALL -> R.drawable.ic_loop_all_tinted + LoopMode.TRACK -> R.drawable.ic_loop_one_tinted + } + + views.setImageViewResource(R.id.widget_shuffle, shuffleRes) + views.setImageViewResource(R.id.widget_loop, loopRes) + + return views + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/forms/SmallWidgetForm.kt b/app/src/main/java/org/oxycblt/auxio/widgets/forms/SmallWidgetForm.kt new file mode 100644 index 000000000..951d1a34a --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/widgets/forms/SmallWidgetForm.kt @@ -0,0 +1,64 @@ +package org.oxycblt.auxio.widgets.forms + +import android.content.Context +import android.widget.RemoteViews +import org.oxycblt.auxio.R +import org.oxycblt.auxio.playback.system.PlaybackService +import org.oxycblt.auxio.ui.newBroadcastIntent +import org.oxycblt.auxio.widgets.WidgetState + +class SmallWidgetForm : WidgetForm(R.layout.widget_small) { + override fun createViews(context: Context, state: WidgetState): RemoteViews { + val views = super.createViews(context, state) + + views.setOnClickPendingIntent( + R.id.widget_skip_prev, + context.newBroadcastIntent( + PlaybackService.ACTION_SKIP_PREV + ) + ) + + views.setOnClickPendingIntent( + R.id.widget_play_pause, + context.newBroadcastIntent( + PlaybackService.ACTION_PLAY_PAUSE + ) + ) + + views.setOnClickPendingIntent( + R.id.widget_skip_next, + context.newBroadcastIntent( + PlaybackService.ACTION_SKIP_NEXT + ) + ) + + views.setTextViewText(R.id.widget_song, state.song.name) + views.setTextViewText(R.id.widget_artist, state.song.album.artist.name) + + views.setImageViewResource( + R.id.widget_play_pause, + if (state.isPlaying) { + R.drawable.ic_pause + } else { + R.drawable.ic_play + } + ) + + if (state.albumArt != null) { + views.setImageViewBitmap(R.id.widget_cover, state.albumArt) + views.setCharSequence( + R.id.widget_cover, "setContentDescription", + context.getString(R.string.description_album_cover, state.song.album.name) + ) + } else { + views.setImageViewResource(R.id.widget_cover, R.drawable.ic_song) + views.setCharSequence( + R.id.widget_cover, + "setContentDescription", + context.getString(R.string.description_placeholder_cover) + ) + } + + return views + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/forms/WidgetForm.kt b/app/src/main/java/org/oxycblt/auxio/widgets/forms/WidgetForm.kt new file mode 100644 index 000000000..0bdeec398 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/widgets/forms/WidgetForm.kt @@ -0,0 +1,20 @@ +package org.oxycblt.auxio.widgets.forms + +import android.content.Context +import android.widget.RemoteViews +import androidx.annotation.LayoutRes +import org.oxycblt.auxio.ui.newMainIntent +import org.oxycblt.auxio.widgets.WidgetState + +abstract class WidgetForm(@LayoutRes private val layout: Int) { + open fun createViews(context: Context, state: WidgetState): RemoteViews { + val views = RemoteViews(context.packageName, layout) + + views.setOnClickPendingIntent( + android.R.id.background, + context.newMainIntent() + ) + + return views + } +} diff --git a/app/src/main/res/drawable/ic_loop_all_tinted.xml b/app/src/main/res/drawable/ic_loop_all_tinted.xml new file mode 100644 index 000000000..81210d67a --- /dev/null +++ b/app/src/main/res/drawable/ic_loop_all_tinted.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_loop_one_tinted.xml b/app/src/main/res/drawable/ic_loop_one_tinted.xml new file mode 100644 index 000000000..65f97cc40 --- /dev/null +++ b/app/src/main/res/drawable/ic_loop_one_tinted.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_shuffle_tinted.xml b/app/src/main/res/drawable/ic_shuffle_tinted.xml new file mode 100644 index 000000000..89e1dbb46 --- /dev/null +++ b/app/src/main/res/drawable/ic_shuffle_tinted.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/layout-v31/widget_full.xml b/app/src/main/res/layout-v31/widget_full.xml new file mode 100644 index 000000000..cff265944 --- /dev/null +++ b/app/src/main/res/layout-v31/widget_full.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-v31/widget_minimal.xml b/app/src/main/res/layout-v31/widget_small.xml similarity index 85% rename from app/src/main/res/layout-v31/widget_minimal.xml rename to app/src/main/res/layout-v31/widget_small.xml index a053cb33a..a772f454a 100644 --- a/app/src/main/res/layout-v31/widget_minimal.xml +++ b/app/src/main/res/layout-v31/widget_small.xml @@ -7,7 +7,7 @@ android:orientation="vertical" android:theme="@style/Theme.Widget"> - - - - - + diff --git a/app/src/main/res/layout/widget_default.xml b/app/src/main/res/layout/widget_default.xml index 2333d9d41..e4df541a8 100644 --- a/app/src/main/res/layout/widget_default.xml +++ b/app/src/main/res/layout/widget_default.xml @@ -6,7 +6,7 @@ android:background="?attr/colorSurface" android:theme="@style/Theme.Widget"> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/widget_minimal.xml b/app/src/main/res/layout/widget_small.xml similarity index 81% rename from app/src/main/res/layout/widget_minimal.xml rename to app/src/main/res/layout/widget_small.xml index 5ff518aa8..31877a042 100644 --- a/app/src/main/res/layout/widget_minimal.xml +++ b/app/src/main/res/layout/widget_small.xml @@ -7,7 +7,7 @@ android:orientation="vertical" android:theme="@style/Theme.Widget"> - - - - + android:layout_marginTop="@dimen/spacing_medium"> A simple, rational music player for android. Music Playback - Minimal + See and control playing music Retry diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index bf0ccfd3f..aa44a8370 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -87,4 +87,15 @@ @font/inter_semibold ?attr/colorPrimary + + + + \ No newline at end of file diff --git a/app/src/main/res/xml-v31/widget_minimal.xml b/app/src/main/res/xml-v31/widget_minimal.xml index d33f98501..da6d4c4bb 100644 --- a/app/src/main/res/xml-v31/widget_minimal.xml +++ b/app/src/main/res/xml-v31/widget_minimal.xml @@ -1,13 +1,12 @@ - - \ No newline at end of file + android:widgetCategory="home_screen" /> \ No newline at end of file diff --git a/app/src/main/res/xml/widget_minimal.xml b/app/src/main/res/xml/widget_minimal.xml index a64f079fd..51a1db205 100644 --- a/app/src/main/res/xml/widget_minimal.xml +++ b/app/src/main/res/xml/widget_minimal.xml @@ -1,12 +1,10 @@ - - \ No newline at end of file + android:widgetCategory="home_screen" /> \ No newline at end of file