@@ -14,7 +14,6 @@ import coil3.imageLoader
1414import coil3.request.CachePolicy
1515import coil3.request.ImageRequest
1616import coil3.request.allowHardware
17- import coil3.size.Dimension
1817import coil3.size.Precision
1918import coil3.size.Size
2019import coil3.toBitmap
@@ -32,6 +31,7 @@ import io.homeassistant.companion.android.widgets.ACTION_APPWIDGET_CREATED
3231import io.homeassistant.companion.android.widgets.BaseWidgetProvider.Companion.UPDATE_WIDGETS
3332import io.homeassistant.companion.android.widgets.EXTRA_WIDGET_ENTITY
3433import javax.inject.Inject
34+ import kotlin.math.sqrt
3535import kotlinx.coroutines.CancellationException
3636import kotlinx.coroutines.CoroutineScope
3737import kotlinx.coroutines.Dispatchers
@@ -82,8 +82,7 @@ class CameraWidget : AppWidgetProvider() {
8282 Timber .d(" Skipping widget update since network connection is not active" )
8383 return
8484 }
85- val views = getWidgetRemoteViews(context, appWidgetId)
86- views?.let { appWidgetManager.updateAppWidget(appWidgetId, it) }
85+ appWidgetManager.updateAppWidget(appWidgetId, getWidgetRemoteViews(context, appWidgetId))
8786 }
8887
8988 private suspend fun updateAllWidgets (context : Context ) {
@@ -108,7 +107,7 @@ class CameraWidget : AppWidgetProvider() {
108107 }
109108 }
110109
111- private suspend fun getWidgetRemoteViews (context : Context , appWidgetId : Int ): RemoteViews ? {
110+ private suspend fun getWidgetRemoteViews (context : Context , appWidgetId : Int ): RemoteViews {
112111 val updateCameraIntent = Intent (context, CameraWidget ::class .java).apply {
113112 action = UPDATE_IMAGE
114113 putExtra(AppWidgetManager .EXTRA_APPWIDGET_ID , appWidgetId)
@@ -174,7 +173,7 @@ class CameraWidget : AppWidgetProvider() {
174173 .diskCachePolicy(CachePolicy .DISABLED )
175174 .memoryCachePolicy(CachePolicy .DISABLED )
176175 .networkCachePolicy(CachePolicy .READ_ONLY )
177- .size(Size (getScreenWidth( ), Dimension . Undefined ))
176+ .size(getWidgetBitmapSize( AppWidgetManager .getInstance(context ), appWidgetId ))
178177 .precision(Precision .INEXACT )
179178 .build(),
180179 ).image?.toBitmap()?.let {
@@ -270,7 +269,56 @@ class CameraWidget : AppWidgetProvider() {
270269 // Enter relevant functionality for when the last widget is disabled
271270 }
272271
273- private fun getScreenWidth (): Int {
274- return Resources .getSystem().displayMetrics.widthPixels
272+ /* *
273+ * Returns a [Size] based on the widget's allocated dimensions, capped to stay within the
274+ * RemoteViews bitmap memory limit. Falls back to the full screen width and height when the
275+ * widget manager does not report a size.
276+ *
277+ * The system computes the limit as `6 * screenWidth * screenHeight`
278+ * (1.5× screen area × 4 bytes/pixel) in `AppWidgetServiceImpl.computeMaximumWidgetBitmapMemory`.
279+ * https://cs.android.com/android/platform/superproject/+/8b289e3d7cf7fd9242870758a894c6e9b4c3e655:frameworks/base/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java;l=513
280+ * There is no public API for this, so we replicate the formula and keep the camera bitmap
281+ * under roughly 90% of that limit to leave headroom for other bitmap work in the same
282+ * RemoteViews.
283+ */
284+ private fun getWidgetBitmapSize (appWidgetManager : AppWidgetManager , appWidgetId : Int ): Size {
285+ val res = Resources .getSystem()
286+ val screenWidth = res.displayMetrics.widthPixels
287+ val screenHeight = res.displayMetrics.heightPixels
288+ // System limit = 1.5 × screen area × 4 bytes/pixel (ARGB_8888).
289+ // Bytes-per-pixel cancels out when converting to pixels: 1.5 * w * h * 4 * 0.9 / 4 = 1.35 * w * h.
290+ // The 0.9 factor keeps a 10% margin for RemoteViews overhead.
291+ val maxPixels = (1.35 * screenWidth * screenHeight).toInt()
292+
293+ val options = appWidgetManager.getAppWidgetOptions(appWidgetId)
294+ val widthDp = options.getInt(AppWidgetManager .OPTION_APPWIDGET_MAX_WIDTH , 0 )
295+ val heightDp = options.getInt(AppWidgetManager .OPTION_APPWIDGET_MAX_HEIGHT , 0 )
296+
297+ val widthPx: Int
298+ val heightPx: Int
299+ if (widthDp <= 0 ) {
300+ widthPx = screenWidth
301+ heightPx = screenHeight
302+ } else {
303+ val density = res.displayMetrics.density
304+ widthPx = (widthDp * density).toInt()
305+ heightPx = if (heightDp > 0 ) (heightDp * density).toInt() else 0
306+ }
307+
308+ if (heightPx <= 0 ) {
309+ // Height unknown, derive a max height from the pixel budget so the
310+ // bitmap stays within limits even if the source image is unusually tall.
311+ val cappedWidth = minOf(widthPx, maxPixels / maxOf(screenHeight, 1 )).coerceAtLeast(1 )
312+ val maxHeight = (maxPixels / cappedWidth).coerceAtLeast(1 )
313+ return Size (cappedWidth, maxHeight)
314+ }
315+
316+ // Scale down proportionally if the bitmap would exceed the safe pixel budget
317+ return if (widthPx.toLong() * heightPx > maxPixels) {
318+ val scale = sqrt(maxPixels.toDouble() / (widthPx.toLong() * heightPx)).toFloat()
319+ Size ((widthPx * scale).toInt().coerceAtLeast(1 ), (heightPx * scale).toInt().coerceAtLeast(1 ))
320+ } else {
321+ Size (widthPx.coerceAtLeast(1 ), heightPx.coerceAtLeast(1 ))
322+ }
275323 }
276324}
0 commit comments