diff --git a/android/app/src/main/java/com/shonar/widget/ShonarWidget.kt b/android/app/src/main/java/com/shonar/widget/ShonarWidget.kt index e2f6579..22e7786 100644 --- a/android/app/src/main/java/com/shonar/widget/ShonarWidget.kt +++ b/android/app/src/main/java/com/shonar/widget/ShonarWidget.kt @@ -47,22 +47,41 @@ class ShonarRecordWidget : AppWidgetProvider() { companion object { 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( ctx: Context, phase: com.shonar.recording.RecordingSnapshot.Phase, ): RemoteViews { 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) { 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.setContentDescription( R.id.widget_button, ctx.getString(R.string.widget_recording), ) } 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.setContentDescription( 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) { - 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, - Intent(ctx, ShonarRecordWidget::class.java).setAction(ACTION_WIDGET_RECORD) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), + Intent(ctx, StartRecordingActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) } else {