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