I discovered a really wonderful cocurrency issue the other day while I was working on the LinkedIn application. It appeared that when a user takes a photo to upload as part of a status update that the thumbnail would be incomplete. The image was stored on the filesystem correctly and would upload fine, but the thumbnail in the app would be cut off. More interestingly, the thumbnail in the phone's photos app was the same thumbnail and would also be cut-off. Something was causing thumbnails that were created in the LinkedIn app to not be generated properly. Here's an example of what I'm talking about:
This problem did not happen on kitkat or older phones, only on Lollipop. What I ended up discovering was an apparent race condition dealing with how photos are stored and retrieved from MediaStore.
It starts with how we as developers take photos on Android. To have a photo taken, an intent is sent to
MediaStore.ACTION_IMAGE_CAPTURE and include the path that we want the image to be saved to:
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); intent.putExtra(MediaStore.EXTRA_OUTPUT, newImageUri); fragment.startActivityForResult(intent, 0);
The newImageUri in this case would have been created probably like:
File photoFile = new File(Environment.getExternalStorageDirectory(), "someTempName.jpg"); Uri newImageUri = Uri.fromFile(photoFile);
This will tell Android to start capturing an image, and whatever image you get save it to the file photoFile so we can read it when you're done. So far, so good.
In our app, we do some additional processing on that file. We take the image and sometimes perform rotations and resizing taks. To do this, we use a simple AsyncTask on the results returned from this activity. The doInBackground has a section sort of like:
File photoFile = new File(Environment.getExternalStorageDirectory(), "someTempName.jpg"); Bitmap bmp = BitmapFactory.decodeFile(photoFile.getAbsolutePath(), null); Bitmap correctBmp = rotateBitmap(bmp, rotateAngle); Uri finalImage = Uri.parse(MediaStore.Images.Media.insertImage(context.getContentResolver(), correctedBmp, null, null)); correctedBmp.recycle(); f.delete(); // there's try/catch blocks in for all of this
The interesting bit in here is the MediaStore.Images.Media.insertImage() method. Looking through the source code, it actually creates an entry in the MediaStore table and then generates a thumbnail image. If the thumbnail can't be created or if something goes wrong it will delete the image and undo the table insert. However, before the thumbnail is generated that table insert has happened which is fairly important for the second part of this bug appearing.
On our share page, we have a GridView that shows all of the images you might want to post. It's backed by a type of CursorAdapter that's looking at a Cursor supplied by MediaStore for all of it's images. As soon as MediaStore inserts or deletes an image, the adapter gets notified so that it can update it's views. In our code, when we go to display the thumbnails for this GridView, we use another AsyncTask to retrieve the bitmap as you shouldn't do disk IO or long running operations on the UI thread. Since we have a lot of images appearing on this grid at a time, we execute with an executor for the AsyncTask so that it can use a ThreadPool and retrieve multiple thumbnails in parallel. There's a problem here however that seemed to show itself on Lollipop. Order of execution would appear like this:
- Image is taken from camera and stored to disk. (UI Thread)
- New image entry is written to table, Thumbnail generation started (Async 1)
- Adapter is notified of update to MediaStore (UI Thread)
- Adapter tries to get thumbnail for new image and launches AsyncTask (UI Thread)
- Thumbnail is retrieved from MediaStore (Async 2)
- New image thumbnail generation completes (Async 1)
- Thumbnail is displayed to user (UI Thread)
The ordering of 5 and 6 is wrong. You can't get a thumbnail for an image that hasn't been fully generated yet, but that's what the system tries to do. If half of the thumbnail has been written to disk, getting the thumbnail will retrieve those bytes as if it were the full thumbnail. When it's displayed to the user, it looks like the thumbnail is cut-off and incomplete. In addition, getting the thumbnail seems to either stop the write process or store the retrieved partial thumbnail as the actual full thumbnail causing all other calls in any app to see this cutoff thumbnail.
I imagine that before Lollipop there was some sort of lock on the file descriptor that prevented reading from the file until writing was complete. This doesn't seem to be the case any more which causes concurrency issues. For us, the solution was to make Thumbnail loading happen on a synchronous AsyncTask (basically without an executor). This actually works pretty well as after the first load since all of the images are in a bitmap cache. Also the images are quite small and retrieval doesn't take a significant amount of time. Ideally Android would have a write lock on the thumbnail file for retrieving it, but until then this workaround is sufficient.