Okay, so it seems I finally figured it all out. I thought that I would share this here, for everyone who might be interested.
What am I trying to achieve?
- Pictograms and images of requests on the device (via MediaStore)
- Attach them to one cursor in descending order (newest images on top)
- Handle missing thumbnails
After a lot of trial and error and working with MediaStore, I found out that the thumbnail table (MediaStore.Images.Thumbnails) cannot be updated at any time. There will be images, missing thumbnails, and vice versa (orphaned thumbnails). Especially when the camera application takes a new photo, apparently it does not immediately create a thumbnail. Until the Gallery application (or equivalent) is opened, the thumbnail table is updated.
I got a lot of useful tips on how to deal with this problem, mainly just to request a table of images (MediaStore.Images.Media), and then somehow expand the cursor using thumbnails one row per time. While it really worked, it caused the application to be extremely slow and consume a lot of memory for ~ 2000 images on my device.
In fact, it should be possible to simply SAVE (left outer join) a thumbnail table with an image table so that we get all the images and thumbnails when they exist. Otherwise, we leave the DATA column of the thumbnails to null and simply generate these individual missing thumbnails. What would be great is to actually paste these thumbnails into the MediaStore, but I haven't studied them yet.
The main problem with all this was the use of CursorJoiner . For some reason, it requires both cursors to be ordered in ascending order, let them say by ID. However, this means the oldest images that really make for a crappy gallery. I found that CursorJoiner can be "tricked", however, to allow a decreasing order by simply ordering the ID*(-1) :
Cursor c_thumbs = getContext().getContentResolver().query( MediaStore.Images.Thumnails.EXTERNAL_CONTENT_URI, null, null, null, "(" + MediaStore.Images.Thumnails.IMAGE_ID + "*(-1))"); Cursor c_images= getContext().getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, "(" + MediaStore.Images.Media._ID + "*(-1))");
As long as the lines match, this works fine ( BOTH case). But when you come across lines in which each of the cursors is unique ( LEFT or RIGHT cases), reverse ordering will ruin the inner workings of the CursorJoiner class. However, a simple compensation on the left and right cursors is enough to “reconfigure” the connection, returning it to the right track. Notice the calls to moveToNext() and moveToPrevious() .
// join these and return // the join is on images._ID = thumbnails.IMAGE_ID CursorJoiner joiner = new CursorJoiner( c_thumbs, new String[] { MediaStore.Images.Thumnails.IMAGE_ID }, // left = thumbnails c_images, new String[] { MediaStore.Images.Media._ID } // right = images ); String[] projection = new String{"thumb_path", "ID", "title", "desc", "datetaken", "filename", "image_path"}; MatrixCursor retCursor = new MatrixCursor(projection); try { for (CursorJoiner.Result joinerResult : joiner) { switch (joinerResult) { case LEFT: // handle case where a row in cursorA is unique // images is unique (missing thumbnail) // we want to show ALL images, even (new) ones without thumbnail! // data = null will cause a temporary thumbnail to be generated in PhotoAdapter.bindView() retCursor.addRow(new Object[]{ null, // data c_images.getLong(1), // image id c_images.getString(2), // title c_images.getString(3), // desc c_images.getLong(4), // date c_images.getString(5), // filename c_images.getString(6) }); // compensate for CursorJoiner expecting cursors ordered ascending... c_images.moveToNext(); c_thumbs.moveToPrevious(); break; case RIGHT: // handle case where a row in cursorB is unique // thumbs is unique (missing image) // compensate for CursorJoiner expecting cursors ordered ascending... c_thumbs.moveToNext(); c_images.moveToPrevious(); break; case BOTH: // handle case where a row with the same key is in both cursors retCursor.addRow(new Object[]{ c_thumbs.getString(1), // data c_images.getLong(1), // image id c_images.getString(2), // title c_images.getString(3), // desc c_images.getLong(4), // date c_images.getString(5), // filename c_images.getString(6) }); break; } } } catch (Exception e) { Log.e("myapp", "JOIN FAILED: " + e); } c_thumbs.close(); c_images.close(); return retCursor;
Then in the "PhotoAdapter" class, which creates the elements for my GridView and binds the data to them from the cursor returned from the ContentProvider ( retCursor above), I create a thumbnail like this (when the thumb_path field is null ):
String thumbData = cursor.getString(0); // thumb_path if (thumbData != null) { Bitmap thumbBitmap; try { thumbBitmap = BitmapFactory.decodeFile(thumbData); viewHolder.iconView.setImageBitmap(thumbBitmap); } catch (Exception e) { Log.e("myapp", "PhotoAdapter.bindView() can't find thumbnail (file) on disk (thumbdata = " + thumbData + ")"); return; } } else { String imgPath = cursor.getString(6); // image_path String imgId = cursor.getString(1); // ID Log.v("myapp", "PhotoAdapter.bindView() thumb path for image ID " + imgId + " is null. Trying to generate, with path = " + imgPath); try { Bitmap thumbBitmap = ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(imgPath), 512, 384); viewHolder.iconView.setImageBitmap(thumbBitmap); } catch (Exception e) { Log.e("myapp", "PhotoAdapter.bindView() can't generate thumbnail for image path: " + imgPath); return; } }