From 6d8f31f586c3280f81bfbfde8f470fe29d347088 Mon Sep 17 00:00:00 2001 From: Tymofiy Bortnyk Date: Mon, 29 Jun 2026 12:20:47 +0300 Subject: [PATCH] fix(widget): clamp stale saved dims and survive OOM in chart rasterizer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A second, distinct path to the "widget permanently stuck, new widget works" symptom: when onUpdate's launcher options come back 0 it falls back to the saved per-id dimensions (widget__width_px/height_px) used UNCLAMPED. An install upgraded from a build that stored the raw launcher size can carry an over-budget value there; Bitmap.createBitmap then throws OutOfMemoryError — which is an Error, not an Exception, so it escapes generateChartBitmap's catch (Exception) and crashes the in-process widget receiver on every update. - Clamp the saved-dims fallback through WidgetUtils.clampChartDimensions, so a stale huge value can no longer reach createBitmap. - Add a catch (OutOfMemoryError) to both widget bitmap rasterizers (renderSvgStringToBitmap, renderSvgToBitmap) so any unforeseen over-budget allocation degrades to null (placeholder / smaller render) instead of crashing. Distinct regime from the budget-rejection retry (other PR): that one is an IllegalArgumentException from updateAppWidget; this one is an OutOfMemoryError from createBitmap, which neither the existing catch nor the retry handles. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../meteogram/MeteogramWidgetProvider.kt | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt b/android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt index 95a94af..b44490f 100644 --- a/android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt +++ b/android/app/src/main/kotlin/org/bortnik/meteogram/MeteogramWidgetProvider.kt @@ -137,6 +137,12 @@ open class MeteogramWidgetProvider : AppWidgetProvider() { } catch (e: Exception) { Log.e(logTag, "Error rendering SVG string to bitmap", e) null + } catch (e: OutOfMemoryError) { + // A too-large bitmap allocation is an Error, not an Exception, so it + // would escape the catch above and crash the in-process receiver. + // Degrade to null (caller falls back to a smaller render/placeholder). + Log.e(logTag, "Out of memory rendering SVG string to bitmap (${width}x$height)", e) + null } } @@ -222,6 +228,11 @@ open class MeteogramWidgetProvider : AppWidgetProvider() { } catch (e: Exception) { Log.e(logTag, "Error rendering SVG", e) null + } catch (e: OutOfMemoryError) { + // See renderSvgStringToBitmap: OOM is an Error, caught here so an + // over-budget allocation degrades to null instead of crashing. + Log.e(logTag, "Out of memory rendering SVG (${width}x$height)", e) + null } } @@ -489,8 +500,13 @@ open class MeteogramWidgetProvider : AppWidgetProvider() { } else { val savedDims = getWidgetDimensions(widgetData, appWidgetId) if (savedDims != null) { - widthPx = savedDims.first - heightPx = savedDims.second + // Saved dims can predate clampChartDimensions (e.g. upgraded + // from a build that stored the raw launcher size), so clamp + // them too — a stale over-budget value here would otherwise + // OOM Bitmap.createBitmap and crash the in-process receiver. + val clampedSaved = WidgetUtils.clampChartDimensions(context, savedDims.first, savedDims.second) + widthPx = clampedSaved.first + heightPx = clampedSaved.second Log.d(logTag, "Widget $appWidgetId using saved dimensions: ${widthPx}x${heightPx}px") } else { widthPx = WidgetUtils.DEFAULT_WIDTH_PX