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.
This commit is contained in:
OxygenCobalt 2021-08-05 15:22:22 -06:00
parent 6f8f333b72
commit b4d9a197af
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 122 additions and 84 deletions

View file

@ -44,7 +44,6 @@ class MainActivity : AppCompatActivity() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
// Start PlaybackService
startService(Intent(this, PlaybackService::class.java)) startService(Intent(this, PlaybackService::class.java))
// onNewIntent doesnt automatically call on startup, so call it here. // onNewIntent doesnt automatically call on startup, so call it here.

View file

@ -28,7 +28,6 @@ import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.MainActivity import org.oxycblt.auxio.MainActivity
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.logD
import org.oxycblt.auxio.logE import org.oxycblt.auxio.logE
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -147,8 +146,6 @@ fun @receiver:AttrRes Int.resolveAttr(context: Context): Int {
resolvedAttr.data resolvedAttr.data
} }
logD(context.theme)
return color.toColor(context) return color.toColor(context)
} }

View file

@ -7,6 +7,7 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle
import android.util.SizeF import android.util.SizeF
import android.widget.RemoteViews import android.widget.RemoteViews
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
@ -14,6 +15,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.coil.loadBitmap import org.oxycblt.auxio.coil.loadBitmap
import org.oxycblt.auxio.logD import org.oxycblt.auxio.logD
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.ui.isLandscape
import org.oxycblt.auxio.ui.newMainIntent import org.oxycblt.auxio.ui.newMainIntent
import org.oxycblt.auxio.widgets.forms.FullWidgetForm import org.oxycblt.auxio.widgets.forms.FullWidgetForm
import org.oxycblt.auxio.widgets.forms.SmallWidgetForm import org.oxycblt.auxio.widgets.forms.SmallWidgetForm
@ -36,40 +38,24 @@ class WidgetProvider : AppWidgetProvider() {
appWidgetManager: AppWidgetManager, appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray appWidgetIds: IntArray
) { ) {
logD("Sending update intent to PlaybackService") applyDefaultViews(context, appWidgetManager)
requestUpdate(context)
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. * Update the widget based on the playback state.
*/ */
fun update(context: Context, playbackManager: PlaybackStateManager) { fun update(context: Context, playbackManager: PlaybackStateManager) {
val manager = AppWidgetManager.getInstance(context) val appWidgetManager = AppWidgetManager.getInstance(context)
val song = playbackManager.song val song = playbackManager.song
if (song == null) { if (song == null) {
manager.applyViews(context, defaultViews(context)) applyDefaultViews(context, appWidgetManager)
return 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. // FIXME: Fix the race conditions with the bitmap loading.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
loadBitmap(context, song) { bitmap -> loadBitmap(context, song) { bitmap ->
val state = WidgetState( val state = WidgetState(
song, song,
@ -79,28 +65,13 @@ class WidgetProvider : AppWidgetProvider() {
playbackManager.loopMode playbackManager.loopMode
) )
// Map each widget form to the rough dimensions where it would look nice. // Map each widget form to the rough dimensions where it would look at least okay.
// This might need to be adjusted.
val views = mapOf( val views = mapOf(
SizeF(110f, 110f) to SmallWidgetForm().createViews(context, state), SizeF(110f, 110f) to SmallWidgetForm().createViews(context, state),
SizeF(272f, 110f) to FullWidgetForm().createViews(context, state) SizeF(272f, 110f) to FullWidgetForm().createViews(context, state)
) )
manager.applyViews(context, RemoteViews(views)) appWidgetManager.applyViewsCompat(context, 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))
}
} }
} }
@ -110,12 +81,11 @@ class WidgetProvider : AppWidgetProvider() {
fun reset(context: Context) { fun reset(context: Context) {
logD("Resetting widget") logD("Resetting widget")
val manager = AppWidgetManager.getInstance(context) applyDefaultViews(context, AppWidgetManager.getInstance(context))
manager.applyViews(context, defaultViews(context))
} }
@SuppressLint("RemoteViewLayout") @SuppressLint("RemoteViewLayout")
private fun defaultViews(context: Context): RemoteViews { private fun applyDefaultViews(context: Context, manager: AppWidgetManager) {
val views = RemoteViews(context.packageName, R.layout.widget_default) val views = RemoteViews(context.packageName, R.layout.widget_default)
views.setOnClickPendingIntent( views.setOnClickPendingIntent(
@ -123,20 +93,107 @@ class WidgetProvider : AppWidgetProvider() {
context.newMainIntent() context.newMainIntent()
) )
return views manager.updateAppWidget(ComponentName(context, this::class.java), views)
} }
private fun AppWidgetManager.applyViews(context: Context, views: RemoteViews) { override fun onAppWidgetOptionsChanged(
val ids = getAppWidgetIds(ComponentName(context, this::class.java)) context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
newOptions: Bundle?
) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
if (ids.isNotEmpty()) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
// Existing widgets found, update those // We can't resize the widget until we can generate the views, request an update
ids.forEach { id -> // from PlaybackService.
updateAppWidget(id, views) 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<SizeF, RemoteViews>
) {
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 { } else {
// No existing widgets found. Fall back to the name of the widget class // Otherwise, we try our best to backport the responsive behavior to older versions.
updateAppWidget(ComponentName(context, this@WidgetProvider::class.java), views) // 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<SizeF>()
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)
}
} }
} }

View file

@ -21,6 +21,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:elevation="@dimen/elevation_normal"
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/spacing_medium"> android:padding="@dimen/spacing_medium">

View file

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Widget" parent="@android:style/Theme.DeviceDefault.DayNight">
<!-- I really don't care enough to make Auxio's theming correspond to widget theming.
It doesn't happen on Android 12 anyway, so why bother backporting it when its so
useless. -->
<item name="colorPrimary">?android:attr/colorAccent</item>
<item name="colorSecondary">?android:attr/colorAccent</item>
<item name="colorControlNormal">@color/control_color</item>
<item name="colorControlHighlight">?android:attr/colorControlHighlight</item>
<item name="colorSurface">@color/surface_color</item>
<item name="android:windowBackground">?attr/colorSurface</item>
<item name="android:colorBackground">?attr/colorSurface</item>
</style>
</resources>

View file

@ -44,8 +44,8 @@
</style> </style>
<!-- <!--
Theming widgets is technically possible below Android 10, but its too much of a hassle. Theming widgets is technically possible below Android 12, but I *really* don't care enough
Default to a light blue theme. to bother with it.
--> -->
<style name="Theme.Widget" parent="@style/Theme.Blue" /> <style name="Theme.Widget" parent="@style/Theme.Blue" />
</resources> </resources>