Skip to content

Fix AppWidget bitmap memory cap calculation and downsampling in ImageUtils - #510

Merged
ashnohe merged 4 commits into
android:mainfrom
imraytiong:fix/glance-widget-memory-cap
Oct 23, 2026
Merged

ashnohe merged 4 commits into
android:mainfrom
imraytiong:fix/glance-widget-memory-cap

Conversation

@imraytiong

Copy link
Copy Markdown
Contributor

Fixes two issues in samples/user-interface/appwidgets (ImageUtils.kt):

  1. Active WindowMetrics & 15 MB ceiling: getMaxWidgetMemoryAllowedSizeInBytes(context) previously used context.resources.displayMetrics, which over-allocates on multi-resolution and foldable displays when folded and does not cap at 15 MB (15_728_640 bytes). Updated to compute min(15_728_640L, 6L * bounds.width() * bounds.height()) from WindowManager.currentWindowMetrics.bounds on API 30+ (with displayMetrics fallback on API 26–29 and a 15 MB default fallback), matching AppWidgetServiceImpl.ensureWidgetViewsMemoryLimitLocked.
  2. Two-stage power-of-two inSampleSize & RGB_565 default: calculateInSampleSize previously used a single conjunctive (&&) loop that stopped early whenever either axis met the target size, and decodeSampledBitmapFromResource decoded in 4-byte ARGB_8888. Updated to a two-stage loop (Stage 1: viewable target dimensions; Stage 2: maxBytesPerImage ceiling) with Bitmap.Config.RGB_565 (2 B/px) by default and automatic 4 B/px accounting when the source image has an alpha channel (outConfig == Bitmap.Config.ARGB_8888).
  3. Unit tests: Added ImageUtilsTest.kt covering non-square aspect ratios, byte-budget ceilings, and foldable display caps.

…Utils

- Update getMaxWidgetMemoryAllowedSizeInBytes() to query WindowManager.currentWindowMetrics.bounds on API 30+ and enforce the 15 MiB foldable cover-screen ceiling (15_728_640 bytes) so widget bitmap budgets match AppWidgetServiceImpl.ensureWidgetViewsMemoryLimitLocked on Android 17 (API 37) and foldable/multi-resolution displays (b/437054491).
- Fix decodeSampledBitmapFromResource() to use a disjunctive (||) dimension check, a byte-budget ceiling loop, and RGB_565 decoding by default for opaque resources.
- Add unit tests in ImageUtilsTest verifying WindowMetrics budget caps and downsampling behavior.
assertTrue("Memory cap must be positive", reportedCap > 0)
assertTrue(
"Memory cap ($reportedCap B) must never exceed 15 MB (15,000,000 B) for foldable cover-screen safety",
reportedCap <= 15_000_000,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of hardcoded this value, can we use the CONSERVATIVE_MAX_BYTES defined in ImageUtils.kt, changing the visibility of that field.

e.g

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal const val CONSERVATIVE_MAX_BYTES = 15_000_000

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b9f89ab. Agreed — removed the hardcoded byte literals from ImageUtilsTest.kt, updated the first test to match the unclamped 6 * width * height calculation, and marked DEFAULT_MAX_BYTES_PER_IMAGE as @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal const val in ImageUtils.kt so the test references it directly.

* Loads the image resource as bitmap by scaling for the requested size.
* Loads the image resource as bitmap by scaling for the requested size and enforcing
* the per-image AppWidget memory ceiling.
* https://developer.android.com/topic/performance/graphics/load-bitmap#load-bitmap

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yikes to needing to do this logic as it's not my expertise. The link here refers to some recommended libs that do this effectively. Is that not something to put in practice here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two reasons we kept and fixed decodeSampledBitmapFromResource here:

  1. This method was already in ImageUtils.kt (and is mirrored directly into the glance-canonical-layouts agent skill reference file) as a zero-dependency helper for apps that decode local drawable resources without pulling in Coil or Glide, and its existing && loop had a bug where wide/tall images bypassed downsampling.
  2. Libraries like Coil (which the sample repositories use alongside ImageUtils.getMaxPossibleImageSize) only downsample by pixel dimensions (Size(width, height)) rather than enforcing a strict byte ceiling (maxBytesPerImage) when an alpha-channel PNG forces 4-byte ARGB_8888 under Android 17's RemoteViews bitmap memory limit.

// Conservative 15 MB ceiling below the cover-screen cap of common foldables (e.g. 15.71 MB
// on 1080x2424). The system latches the real cap at boot, so a device booted folded keeps
// the smaller cap after unfolding.
private const val CONSERVATIVE_MAX_BYTES = 15_000_000

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did we get this number? There isn't a service to get this? I think it would be of value to other devs, if this is a constant that they can trust a service to give.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We originally set 15_000_000 below the computed WindowMetrics value because Android's AppWidgetServiceImpl calculates mMaxWidgetBitmapMemory (6 * width * height, with a 15,000,000 byte fallback in AOSP) once at boot time and doesn't expose a public service/API to query that latched value. If a foldable boots while folded on its cover screen (e.g., 1080x2424 = 15.71 MB) and is later unfolded (2076x2152 = 26.81 MB in WindowMetrics), system_server still enforces the smaller boot-time cover-screen cap.

Following our review on the companion skill reference file, we updated getMaxWidgetMemoryAllowedSizeInBytes() in b9f89ab to return 6 * size.width * size.height directly (relying on Stage 1 physical view bounds downsampling + Stage 2 DEFAULT_MAX_BYTES_PER_IMAGE = 1_500_000 per image so up to 9 images stay under 13.5 MB across boot states).

@ashnohe

ashnohe commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Double check this matches the reference file in the skill as I think some changes were made there. Otherwise, lgtm

@imraytiong

Copy link
Copy Markdown
Contributor Author

Thanks @ashnohe! Updated ImageUtils.kt and ImageUtilsTest.kt in b9f89ab so ImageUtils.kt matches the submitted glance-canonical-layouts skill reference file.

@ashnohe
ashnohe merged commit 177c3b9 into android:main Oct 23, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants