Widget: render mic+badge as bitmaps (Lawnchair themed-icons flattened drawable resources to gray)

This commit is contained in:
avi 2026-09-17 20:51:19 -05:00
commit 7f9b9c2e5c

View file

@ -47,22 +47,41 @@ class ShonarRecordWidget : AppWidgetProvider() {
companion object { companion object {
const val ACTION_WIDGET_RECORD = "com.shonar.widget.RECORD_TAP" const val ACTION_WIDGET_RECORD = "com.shonar.widget.RECORD_TAP"
/**
* Render the vector to a bitmap instead of handing the launcher a
* drawable resource id: themed-icon launchers (Lawnchair) re-tint any
* resource they can resolve, which flattened the navy+teal mic to a
* gray monochrome silhouette. A bitmap is opaque pixels — nothing to
* tint. (2026-09-17)
*/
private fun bitmapOf(ctx: Context, resId: Int, dp: Int): android.graphics.Bitmap {
val size = (dp * ctx.resources.displayMetrics.density).toInt().coerceAtLeast(64)
val d = androidx.core.content.ContextCompat.getDrawable(ctx, resId)
?: throw IllegalStateException("missing widget drawable $resId")
val bmp =
android.graphics.Bitmap.createBitmap(size, size, android.graphics.Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(bmp)
d.setBounds(0, 0, size, size)
d.draw(canvas)
return bmp
}
fun views( fun views(
ctx: Context, ctx: Context,
phase: com.shonar.recording.RecordingSnapshot.Phase, phase: com.shonar.recording.RecordingSnapshot.Phase,
): RemoteViews { ): RemoteViews {
val v = RemoteViews(ctx.packageName, R.layout.shonar_widget) val v = RemoteViews(ctx.packageName, R.layout.shonar_widget)
v.setImageViewResource(R.id.widget_icon, R.drawable.ic_widget_mic) v.setImageViewBitmap(R.id.widget_icon, bitmapOf(ctx, R.drawable.ic_widget_mic, 96))
when (phase) { when (phase) {
com.shonar.recording.RecordingSnapshot.Phase.RECORDING -> { com.shonar.recording.RecordingSnapshot.Phase.RECORDING -> {
v.setImageViewResource(R.id.widget_badge, R.drawable.ic_widget_badge_rec) v.setImageViewBitmap(R.id.widget_badge, bitmapOf(ctx, R.drawable.ic_widget_badge_rec, 28))
v.setViewVisibility(R.id.widget_badge, View.VISIBLE) v.setViewVisibility(R.id.widget_badge, View.VISIBLE)
v.setContentDescription( v.setContentDescription(
R.id.widget_button, ctx.getString(R.string.widget_recording), R.id.widget_button, ctx.getString(R.string.widget_recording),
) )
} }
com.shonar.recording.RecordingSnapshot.Phase.PAUSED -> { com.shonar.recording.RecordingSnapshot.Phase.PAUSED -> {
v.setImageViewResource(R.id.widget_badge, R.drawable.ic_widget_badge_paused) v.setImageViewBitmap(R.id.widget_badge, bitmapOf(ctx, R.drawable.ic_widget_badge_paused, 28))
v.setViewVisibility(R.id.widget_badge, View.VISIBLE) v.setViewVisibility(R.id.widget_badge, View.VISIBLE)
v.setContentDescription( v.setContentDescription(
R.id.widget_button, ctx.getString(R.string.widget_paused), R.id.widget_button, ctx.getString(R.string.widget_paused),
@ -77,10 +96,14 @@ class ShonarRecordWidget : AppWidgetProvider() {
} }
val tap: PendingIntent = if (phase == com.shonar.recording.RecordingSnapshot.Phase.IDLE) { val tap: PendingIntent = if (phase == com.shonar.recording.RecordingSnapshot.Phase.IDLE) {
PendingIntent.getBroadcast( // Invisible activity trampoline, NOT a broadcast: a cold
// process woken by a broadcast is battery-throttled (the
// banner could take ~10s); an activity start runs at full
// priority and gives startForegroundService its FGS
// exemption on Android 12+.
PendingIntent.getActivity(
ctx, 50, ctx, 50,
Intent(ctx, ShonarRecordWidget::class.java).setAction(ACTION_WIDGET_RECORD) Intent(ctx, StartRecordingActivity::class.java),
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
) )
} else { } else {