A crash from one line of image downsampling: inSampleSize starts at 0
To save memory, we changed the document thumbnails in our QR scanner app to load downsampled. In the next version, the app closed every time the document list opened. The cause was that BitmapFactory.Options.inSampleSize defaults to 0. The code we changed, the crash data, the fix, and how we checked for the same mistake elsewhere.
It started with a memory recommendation
On October 10, after looking at Play Console's memory-related recommendations, we went through our live apps and fixed places that decoded images at full size. The document list in our QR scanner app was one of them.
Each row in the document list has a small thumbnail. That thumbnail file turned out to be the original JPEG of the scanned page. To draw one small row, the app was loading the full-resolution image into memory.
The code we changed
We changed it to read the size first, then halve the image until the short side would drop below 320px.
BitmapFactory.Options bounds = new BitmapFactory.Options();
bounds.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, bounds);
BitmapFactory.Options opts = new BitmapFactory.Options();
int minDim = Math.min(bounds.outWidth, bounds.outHeight);
while (minDim / (opts.inSampleSize * 2) >= 320) opts.inSampleSize *= 2;
Bitmap bmp = BitmapFactory.decodeFile(path, opts);
The build passed, and the change shipped with other fixes in version 1.1.25 on October 11.
The app closed every time the document list opened
The next day, a new crash showed up in Crashlytics.
java.lang.ArithmeticException: divide by zero
at ...ui.document.DocumentAdapter.lambda$onBindViewHolder$1
Over the 7 days up to 9 AM on October 13 (KST), there were 27 events from 4 users, all on 1.1.25. If there was even one document with a thumbnail, the app closed the moment the document list opened. Thumbnail decoding runs on a background thread, and nothing there caught the exception, so the whole app went down.
The cause: the default is 0
In options created with new BitmapFactory.Options(), inSampleSize is 0. The decoder treats any value of 1 or less as 1, so a 0 is harmless when you only decode. That's why you normally never notice it's 0.
The problem was using that value directly in arithmetic. The first pass of the loop computes minDim / (0 * 2), which is minDim / 0.
The example in the Android documentation starts its calculation with a local variable, int inSampleSize = 1;. Our code used the options field as the loop variable directly and left out the line that starts it at 1.
Why testing didn't catch it is also clear. It was a change where "the screen stays the same and only the internals change", so we looked only at the build passing and shipped it without actually opening a document list that had thumbnails.
The fix
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = 1;
int minDim = Math.min(bounds.outWidth, bounds.outHeight);
while (minDim > 0 && minDim / (opts.inSampleSize * 2) >= 320) opts.inSampleSize *= 2;
- Start at 1.
- We also added
minDim > 0. If the file is broken and the size can't be read,outWidthandoutHeightcome back as -1. In that case we skip the downsampling and just decode it.
We submitted 1.1.26 on October 13. This time we installed the release build we were submitting on a real device, created a document from a gallery photo, and checked that the thumbnail appeared in the document list.
Is the same mistake anywhere else?
Our other apps also have downsampling code, so we searched all our live app code for inSampleSize * 2.
- All five places in our tick camera app set
inSampleSize = 1before the calculation. - The two other places in the QR scanner app (document viewer and page list) calculate with a local variable
int sample = 1;and assign it to the options at the end, so they weren't affected.
There was only one place with the problem.
Summary
BitmapFactory.Options.inSampleSizedefaults to 0. If you use it in a calculation, set it to 1 first.- If the size can't be read, you get -1. Handle values of 0 or less as well.
- Even a change that only touches the internals, like a memory fix, isn't done until you open the screen where that code runs.
- This time the version had Crashlytics, so we saw it within a day. On an older version, we'd have found it about a day later in Play vitals.
The app in this post is QR Code Scanner & Reader (Google Play).