diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt b/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt new file mode 100644 index 000000000..aa9c43869 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt @@ -0,0 +1,116 @@ +package org.oxycblt.auxio.widgets + +import android.content.Context +import android.widget.RemoteViews +import androidx.annotation.LayoutRes +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.ui.newMainIntent + +private fun createBaseView( + context: Context, + @LayoutRes layout: Int, + state: WidgetState +): RemoteViews { + val views = RemoteViews(context.packageName, layout) + + views.setOnClickPendingIntent( + android.R.id.background, + context.newMainIntent() + ) + + 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.desc_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.desc_no_cover) + ) + } + + return views +} + +fun createSmallWidget(context: Context, state: WidgetState): RemoteViews { + return createBaseView(context, R.layout.widget_small, state) +} + +fun createFullWidget(context: Context, state: WidgetState): RemoteViews { + val views = createBaseView(context, R.layout.widget_full, state) + + // 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. This is getting really tiring. + + views.setOnClickPendingIntent( + R.id.widget_loop, + context.newBroadcastIntent( + PlaybackService.ACTION_LOOP + ) + ) + + views.setOnClickPendingIntent( + R.id.widget_shuffle, + context.newBroadcastIntent( + PlaybackService.ACTION_SHUFFLE + ) + ) + + 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/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 9d2996b21..de0d7a2d2 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -17,9 +17,6 @@ import org.oxycblt.auxio.logD import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.ui.isLandscape 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 @@ -28,7 +25,7 @@ import org.oxycblt.auxio.widgets.forms.WidgetForm * - 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]. + * For more specific details about these sub-widgets, see Forms.kt. */ class WidgetProvider : AppWidgetProvider() { override fun onUpdate( @@ -40,6 +37,21 @@ class WidgetProvider : AppWidgetProvider() { requestUpdate(context) } + override fun onAppWidgetOptionsChanged( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + newOptions: Bundle? + ) { + super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + // We can't resize the widget until we can generate the views, so request an update + // from PlaybackService. + requestUpdate(context) + } + } + /* * Update the widget based on the playback state. */ @@ -65,59 +77,14 @@ class WidgetProvider : AppWidgetProvider() { // Map each widget form to the rough dimensions where it would look at least okay. val views = mapOf( - SizeF(110f, 110f) to SmallWidgetForm().createViews(context, state), - SizeF(272f, 110f) to FullWidgetForm().createViews(context, state) + SizeF(110f, 110f) to createSmallWidget(context, state), + SizeF(250f, 110f) to createFullWidget(context, state) ) appWidgetManager.applyViewsCompat(context, views) } } - /* - * Revert this widget to its default view - */ - fun reset(context: Context) { - logD("Resetting widget") - - applyDefaultViews(context, AppWidgetManager.getInstance(context)) - } - - @SuppressLint("RemoteViewLayout") - private fun applyDefaultViews(context: Context, manager: AppWidgetManager) { - val views = RemoteViews(context.packageName, R.layout.widget_default) - - views.setOnClickPendingIntent( - android.R.id.background, - context.newMainIntent() - ) - - manager.updateAppWidget(ComponentName(context, this::class.java), views) - } - - override fun onAppWidgetOptionsChanged( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetId: Int, - newOptions: Bundle? - ) { - super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - // We can't resize the widget until we can generate the views, so request an update - // from PlaybackService. - requestUpdate(context) - } - } - - 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.applyViewsCompat( context: Context, views: Map @@ -134,10 +101,6 @@ class WidgetProvider : AppWidgetProvider() { // problems [most notably UI jittering when resizing]. Its better than just using // one layout though. It may be improved once Android 12's source is released. - // Theres a non-zero likelihood that this code ends up being copy-pasted all over - // by other apps that are trying to refresh their widgets for Android 12. So, if - // you're doing that than uh...hi. - // Each widget has independent dimensions, so we iterate through them all // and do this for each. val ids = getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)) @@ -159,13 +122,20 @@ class WidgetProvider : AppWidgetProvider() { height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) } - logD("Assuming widget dimens are ${width}x$height") + // Widget dimens pre-12 are weird. Basically, they correspond to columns + // but with 2 columns worth of DiP added for some insane reason. Take + // the dimens, normalize them into cells, and then turn them back into dimens. + // This is super lossy and may result in wonky layouts, but it *seems* to work. + width = normalizeDimen(width) + height = normalizeDimen(height) + + logD("Assuming true widget dimens are ${width}x$height") // Find layouts that fit into the widget val candidates = mutableListOf() for (size in views.keys) { - if (size.width < width && size.height < height) { + if (size.width <= width && size.height <= height) { candidates.add(size) } } @@ -195,6 +165,46 @@ class WidgetProvider : AppWidgetProvider() { } } + private fun normalizeDimen(dimen: Int): Int { + var cells = 0 + + while (70 * cells - 30 < dimen) { + cells++ + } + + return 70 * (cells - 2) - 30 + } + + /* + * Revert this widget to its default view + */ + fun reset(context: Context) { + logD("Resetting widget") + + applyDefaultViews(context, AppWidgetManager.getInstance(context)) + } + + @SuppressLint("RemoteViewLayout") + private fun applyDefaultViews(context: Context, manager: AppWidgetManager) { + val views = RemoteViews(context.packageName, R.layout.widget_default) + + views.setOnClickPendingIntent( + android.R.id.background, + context.newMainIntent() + ) + + manager.updateAppWidget(ComponentName(context, this::class.java), views) + } + + 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) + } + companion object { const val ACTION_WIDGET_UPDATE = BuildConfig.APPLICATION_ID + ".action.WIDGET_UPDATE" } 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 deleted file mode 100644 index 8f5231cee..000000000 --- a/app/src/main/java/org/oxycblt/auxio/widgets/forms/FullWidgetForm.kt +++ /dev/null @@ -1,98 +0,0 @@ -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.desc_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.desc_no_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 deleted file mode 100644 index 351052f03..000000000 --- a/app/src/main/java/org/oxycblt/auxio/widgets/forms/SmallWidgetForm.kt +++ /dev/null @@ -1,64 +0,0 @@ -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.desc_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.desc_no_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 deleted file mode 100644 index 0bdeec398..000000000 --- a/app/src/main/java/org/oxycblt/auxio/widgets/forms/WidgetForm.kt +++ /dev/null @@ -1,20 +0,0 @@ -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/ui_widget_preview.png b/app/src/main/res/drawable/ui_widget_preview.png index 685e2d133..800ee2741 100755 Binary files a/app/src/main/res/drawable/ui_widget_preview.png and b/app/src/main/res/drawable/ui_widget_preview.png differ diff --git a/app/src/main/res/layout/widget_full.xml b/app/src/main/res/layout/widget_full.xml index 6c17228e5..6f70d7fce 100644 --- a/app/src/main/res/layout/widget_full.xml +++ b/app/src/main/res/layout/widget_full.xml @@ -12,7 +12,7 @@ android:contentDescription="@string/desc_no_cover" android:src="@drawable/ic_song" /> - + - + match_parent 0dp + 1 ?attr/colorSurface centerCrop diff --git a/app/src/main/res/xml-v31/widget_minimal.xml b/app/src/main/res/xml-v31/widget_minimal.xml index 8c6c4f6dc..cd05a9984 100644 --- a/app/src/main/res/xml-v31/widget_minimal.xml +++ b/app/src/main/res/xml-v31/widget_minimal.xml @@ -6,7 +6,7 @@ android:minResizeHeight="110dp" android:previewLayout="@layout/widget_small" android:resizeMode="horizontal|vertical" - android:targetCellWidth="2" + android:targetCellWidth="3" android:targetCellHeight="2" android:updatePeriodMillis="0" 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 f3fa64dd3..26d09bb9f 100644 --- a/app/src/main/res/xml/widget_minimal.xml +++ b/app/src/main/res/xml/widget_minimal.xml @@ -1,8 +1,10 @@