From b4d9a197aff18a3f5e418fa54c5e90610525c059 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Thu, 5 Aug 2021 15:22:22 -0600 Subject: [PATCH] widgets: port responsiveness back to <31 Post widget responsive behavior back to Android 11 and below. This is not the nicest solution [mostly since the UI flickers when resizing], but it seems to work well enough. --- .../java/org/oxycblt/auxio/MainActivity.kt | 1 - .../org/oxycblt/auxio/ui/InterfaceUtils.kt | 3 - .../oxycblt/auxio/widgets/WidgetProvider.kt | 181 ++++++++++++------ app/src/main/res/layout/widget_full.xml | 1 + app/src/main/res/values-v29/styles_core.xml | 16 -- app/src/main/res/values/styles_core.xml | 4 +- 6 files changed, 122 insertions(+), 84 deletions(-) delete mode 100644 app/src/main/res/values-v29/styles_core.xml diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index c9f629775..538793770 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -44,7 +44,6 @@ class MainActivity : AppCompatActivity() { override fun onStart() { super.onStart() - // Start PlaybackService startService(Intent(this, PlaybackService::class.java)) // onNewIntent doesnt automatically call on startup, so call it here. 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 205a38849..81ccdcb30 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt @@ -28,7 +28,6 @@ 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 @@ -147,8 +146,6 @@ 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/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 42ebc9d09..930c445b0 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -7,6 +7,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.os.Build +import android.os.Bundle import android.util.SizeF import android.widget.RemoteViews import org.oxycblt.auxio.BuildConfig @@ -14,6 +15,7 @@ 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.isLandscape import org.oxycblt.auxio.ui.newMainIntent import org.oxycblt.auxio.widgets.forms.FullWidgetForm import org.oxycblt.auxio.widgets.forms.SmallWidgetForm @@ -36,71 +38,40 @@ class WidgetProvider : AppWidgetProvider() { 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) + applyDefaultViews(context, appWidgetManager) + requestUpdate(context) } /* * Update the widget based on the playback state. */ fun update(context: Context, playbackManager: PlaybackStateManager) { - val manager = AppWidgetManager.getInstance(context) - + val appWidgetManager = AppWidgetManager.getInstance(context) val song = playbackManager.song if (song == null) { - manager.applyViews(context, defaultViews(context)) + applyDefaultViews(context, appWidgetManager) 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 - ) + 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) - ) + // 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) + ) - 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)) - } + appWidgetManager.applyViewsCompat(context, views) } } @@ -110,12 +81,11 @@ class WidgetProvider : AppWidgetProvider() { fun reset(context: Context) { logD("Resetting widget") - val manager = AppWidgetManager.getInstance(context) - manager.applyViews(context, defaultViews(context)) + applyDefaultViews(context, AppWidgetManager.getInstance(context)) } @SuppressLint("RemoteViewLayout") - private fun defaultViews(context: Context): RemoteViews { + private fun applyDefaultViews(context: Context, manager: AppWidgetManager) { val views = RemoteViews(context.packageName, R.layout.widget_default) views.setOnClickPendingIntent( @@ -123,20 +93,107 @@ class WidgetProvider : AppWidgetProvider() { context.newMainIntent() ) - return views + manager.updateAppWidget(ComponentName(context, this::class.java), views) } - private fun AppWidgetManager.applyViews(context: Context, views: RemoteViews) { - val ids = getAppWidgetIds(ComponentName(context, this::class.java)) + override fun onAppWidgetOptionsChanged( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + newOptions: Bundle? + ) { + super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) - if (ids.isNotEmpty()) { - // Existing widgets found, update those - ids.forEach { id -> - updateAppWidget(id, views) - } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + // We can't resize the widget until we can generate the views, 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 + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Widgets are automatically responsive on Android 12, no need to do anything. + updateAppWidget( + ComponentName(context, WidgetProvider::class.java), + RemoteViews(views) + ) } else { - // No existing widgets found. Fall back to the name of the widget class - updateAppWidget(ComponentName(context, this@WidgetProvider::class.java), views) + // Otherwise, we try our best to backport the responsive behavior to older versions. + // This is mostly a guess based on RemoteView's documentation, and it has some + // problems [most notably UI jittering when resizing], but it works. 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. + + val ids = getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)) + + for (id in ids) { + val options = getAppWidgetOptions(id) + + if (options != null) { + var width: Int + var height: Int + + // AFAIK, Landscape mode uses MAX_WIDTH and MIN_HEIGHT, while Portrait + // uses MIN_WIDTH and MAX_HEIGHT + if (isLandscape(context.resources)) { + height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) + width = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) + } else { + width = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) + height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) + } + + logD("Assuming 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) { + candidates.add(size) + } + } + + val layout = candidates.maxByOrNull { it.height * it.width } + + if (layout != null) { + logD("Using widget layout $layout") + + updateAppWidget( + ComponentName(context, WidgetProvider::class.java), + views[layout] + ) + + continue + } + } + + // No layout works. Just use the smallest view. + + logD("No widget layout found") + + val minimum = requireNotNull( + views.minByOrNull { it.key.width * it.key.height }?.value + ) + + updateAppWidget(id, minimum) + } } } diff --git a/app/src/main/res/layout/widget_full.xml b/app/src/main/res/layout/widget_full.xml index e1f828db1..3a5ae61c7 100644 --- a/app/src/main/res/layout/widget_full.xml +++ b/app/src/main/res/layout/widget_full.xml @@ -21,6 +21,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" + android:elevation="@dimen/elevation_normal" android:background="?attr/colorSurface" android:orientation="vertical" android:padding="@dimen/spacing_medium"> diff --git a/app/src/main/res/values-v29/styles_core.xml b/app/src/main/res/values-v29/styles_core.xml deleted file mode 100644 index a8f0a408b..000000000 --- a/app/src/main/res/values-v29/styles_core.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values/styles_core.xml b/app/src/main/res/values/styles_core.xml index f10e40213..c26e78091 100644 --- a/app/src/main/res/values/styles_core.xml +++ b/app/src/main/res/values/styles_core.xml @@ -44,8 +44,8 @@