Repository navigation
Fix AppWidget bitmap memory cap calculation and downsampling in ImageUtils - #510
Conversation
…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.
…en cap and clarify KDoc
| 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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Two reasons we kept and fixed decodeSampledBitmapFromResource here:
- 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.
- 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
|
Double check this matches the reference file in the skill as I think some changes were made there. Otherwise, lgtm |
Fixes two issues in
samples/user-interface/appwidgets(ImageUtils.kt):WindowMetrics& 15 MB ceiling:getMaxWidgetMemoryAllowedSizeInBytes(context)previously usedcontext.resources.displayMetrics, which over-allocates on multi-resolution and foldable displays when folded and does not cap at15 MB(15_728_640bytes). Updated to computemin(15_728_640L, 6L * bounds.width() * bounds.height())fromWindowManager.currentWindowMetrics.boundson API 30+ (withdisplayMetricsfallback on API 26–29 and a15 MBdefault fallback), matchingAppWidgetServiceImpl.ensureWidgetViewsMemoryLimitLocked.inSampleSize&RGB_565default:calculateInSampleSizepreviously used a single conjunctive (&&) loop that stopped early whenever either axis met the target size, anddecodeSampledBitmapFromResourcedecoded in 4-byteARGB_8888. Updated to a two-stage loop (Stage 1: viewable target dimensions; Stage 2:maxBytesPerImageceiling) withBitmap.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).ImageUtilsTest.ktcovering non-square aspect ratios, byte-budget ceilings, and foldable display caps.