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:
parent
6f8f333b72
commit
b4d9a197af
6 changed files with 122 additions and 84 deletions
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,71 +38,40 @@ 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,
|
bitmap,
|
||||||
bitmap,
|
playbackManager.isPlaying,
|
||||||
playbackManager.isPlaying,
|
playbackManager.isShuffling,
|
||||||
playbackManager.isShuffling,
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
Loading…
Reference in a new issue