MediaStore query: combining thumbnails and images (by ID)

I am working on a photo gallery app for Android. It started as the final project for developing Android applications in Udacity, so its overall structure (actions, content providers, etc.) must be very reliable, and it was accepted for certification by Udacity / Google.

However, it is still not 100% complete, and I'm still trying to improve it.

What I want to do must be very straightforward; upload all the images on the device (in the form of thumbnails) to the GridView in MainActivity, with DetailActivity, which shows the full size image + some metadata (name, size, date, etc.).

The course required us to write a ContentProvider, so I have a query () function that essentially retrieves data from the MediaStore and returns the cursor to the MainActivity GridView. On my device, at least (Sony Xperia Z1, Android 5.1.1) this works almost perfectly. There are some mistakes and quirks, but by and large I can sequentially find all the images on my phone in my application and click on them to view the details.

However, when I tried to install the application on my friend Sony Xperia Z3, everything failed. No images appeared, although I obviously checked that there were actually 100 photos on his phone. The same on another friend's phone (new Samsung S6): - (

This is the main problem. On my phone where the material works, the "secondary" errors are related to the fact that the camera takes a new photo, it does not automatically load into my application (in the form of a thumbnail). It seems I need to figure out how to run a scan or something else that is needed to load / generate new thumbs. It is also quite high on my wish list.

As I said, I’m sure that all this should really be quite simple, so maybe all my difficulties indicate that I'm approaching the problem completely wrong? Here is what my query () function does:

  • get the cursor of all thumbnails, from MediaStore.Media.Thumbnails.EXTERNAL_CONTENT_URI

  • get the cursor of all images from MediaStore.Media.Images.EXTERNAL_CONTENT_URI

  • attach them to MediaStore.Media.Thumbnails.IMAGE_ID = MediaStore.Media.Images._ID using CursorJoiner

  • returns the received retCursor (as shown in the connection)

- find the full code in this previous post.

Although it looks right (for me), maybe it really is not? By the way, I join the thumbs and images so that in GridView I can show some metadata (for example, date) along with a thumbnail. I have identified a connection issue, in particular, because if I simplify this just to upload my thumbs to the GridView, then all this works fine - also on my mobile phone. (Except for uploading new photos.)

Somehow my assumption that IMAGE_ID and _ID always consistent is wrong? I saw a post on AirPair describing a similar application in the gallery, and in this tutorial almost everything about it is a little different. Instead of trying to join the cursors, it gets a thumbnail cursor and iterates over it, adding data from images using separate queries to MediaStore ... But is this the most efficient way to do this? - However, his solution really connects the thumbnail with the corresponding image on the ID:

 Cursor imagesCursor = context.getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, filePathColumn, MediaStore.Images.Media._ID + "=?", new String[]{imageId}, // NB! null); 

In general, I need help with the following:

  • Am I asking MediaStore correctly?
  • Is it safe to combine thumbs and images, on ID - will it always be stable / synchronous?
  • How does my application automatically generate / extract thumbnails of new images?
+5
source share
3 answers

Here is my test case that demonstrates the lack of support in CursorJoiner for decreasing ordered cursors. This, however, is documented in the source code of CursorJoiner, so I am not trying to criticize, but just show how it can be circumvented (or hacked).

The test example shows how the assumption of an increasing order makes the need to "flip" or reverse all the options made by CursorJoiner (result of a comparator, increased cursors, etc.). What I really would like to try is to modify the CursorJoiner class directly to try to add support for DESC ordering.

Please note that the order part by ID * (- 1), apparently, is not required to work. In the following example, I did not deny the identifier columns (a simple DESC order, not a “pseudo-ASC” with negative sequences), and it still works.

Test case

 String[] colA = new String[] { "_id", "data", "B_id" }; String[] colB = new String[] { "_id", "data" }; MatrixCursor cursorA = new MatrixCursor(colA); MatrixCursor cursorB = new MatrixCursor(colB); // add 4 items to cursor A, linked to cursor B // the data is ordered DESCENDING // all cases, LEFT/RIGHT/BOTH, are included cursorA.addRow(new Object[] { 5, "Item A", 1004 }); // BOTH cursorA.addRow(new Object[] { 4, "Item B", 1003 }); // LEFT cursorA.addRow(new Object[] { 3, "Item C", 1002 }); // BOTH cursorA.addRow(new Object[] { 2, "Item D", 1001 }); // LEFT cursorA.addRow(new Object[] { 1, "Item E", 1000 }); // BOTH cursorA.addRow(new Object[] { 0, "Item F", 500 }); // LEFT // similarily for cursorB (DESC) cursorB.addRow(new Object[] { 1004, "X" }); // BOTH cursorB.addRow(new Object[] { 1002, "Y" }); // BOTH cursorB.addRow(new Object[] { 999, "Z" }); // RIGHT cursorB.addRow(new Object[] { 998, "S" }); // RIGHT cursorB.addRow(new Object[] { 900, "A" }); // RIGHT cursorB.addRow(new Object[] { 1000, "G" }); // BOTH // join these on ID CursorJoiner cjoiner = new CursorJoiner( cursorA, new String[] { "B_id" }, // left = A cursorB, new String[] { "_id" } // right = B ); // enable workaround boolean desc = true; int count = 0; for (CursorJoiner.Result joinerResult : cjoiner) { Log.v("TEST", "Processing (left)=" + (cursorA.isAfterLast() ? "<empty>" : cursorA.getLong(2)) + " / (right)=" + (cursorB.isAfterLast() ? "<empty>" : cursorB.getLong(0))); // flip the CursorJoiner.Result (unless Result.BOTH, or either cursor is exhausted) if (desc && joinerResult != CursorJoiner.Result.BOTH && !cursorB.isAfterLast() && !cursorA.isAfterLast()) joinerResult = (joinerResult == CursorJoiner.Result.LEFT ? CursorJoiner.Result.RIGHT : CursorJoiner.Result.LEFT); switch (joinerResult) { case LEFT: // handle case where a row in cursorA is unique Log.v("TEST", count + ") join LEFT. cursorA is unique"); if (desc) { // compensate cursor increments if (!cursorB.isAfterLast()) cursorB.moveToPrevious(); if (!cursorA.isLast()) cursorA.moveToNext(); } break; case RIGHT: Log.v("TEST", count + ") join RIGHT. cursorB is unique"); // handle case where a row in cursorB is unique if (desc) { if (!cursorB.isLast()) cursorB.moveToNext(); if (!cursorA.isAfterLast()) cursorA.moveToPrevious(); } break; case BOTH: Log.v("TEST", count + ") join BOTH: " + cursorA.getInt(0) + "," + cursorA.getString(1) + "," + cursorA.getInt(2) + "/" + cursorB.getInt(0) + "," + cursorB.getString(1)); // handle case where a row with the same key is in both cursors break; } count++; } Log.v("TEST", "Join done!"); 

and conclusion:

 V/TEST: Processing (left)=5 / (right)=1004 V/TEST: 0) join BOTH: 4,Item A,1004/1004,X V/TEST: Processing (left)=4 / (right)=1002 V/TEST: 1) join LEFT. cursorA is unique V/TEST: Processing (left)=3 / (right)=1002 V/TEST: 2) join BOTH: 2,Item C,1002/1002,Y V/TEST: Processing (left)=2 / (right)=999 V/TEST: 3) join RIGHT. cursorB is unique V/TEST: Processing (left)=2 / (right)=998 V/TEST: 4) join RIGHT. cursorB is unique V/TEST: Processing (left)=2 / (right)=900 V/TEST: 5) join RIGHT. cursorB is unique V/TEST: Processing (left)=2 / (right)=1000 V/TEST: 6) join LEFT. cursorA is unique V/TEST: Processing (left)=1 / (right)=1000 V/TEST: 7) join BOTH: 0,Item D,1000/1000,F V/TEST: Processing (left)=0 / (right)=--- V/TEST: 8) join LEFT. cursorA is unique V/TEST: Join done! 
0
source

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; } } 
+6
source

The accepted answer made me start this question, but it contains a couple of small errors.

 case LEFT: // handle case where a row in cursorA is unique // images is unique (missing thumbnail) case RIGHT: // handle case where a row in cursorB is unique // thumbs is unique (missing image) 

This is back. The documentation is self-contradictory, and probably where the mistake was made. From the source code of CursorJoiner :

 case LEFT: // handle case where a row in cursorA is unique 

Then in the listing for the result from the source code:

 public enum Result { /** The row currently pointed to by the left cursor is unique */ RIGHT, /** The row currently pointed to by the right cursor is unique */ LEFT, /** The rows pointed to by both cursors are the same */ BOTH } 

So, I guess, so you were a force increasing cursors.

  //compensate for CursorJoiner expecting cursors ordered ascending... c_images.moveToNext(); c_thumbs.moveToPrevious(); 

An iterator in CursorJoiner automatically increments cursors for you.

This should be working code (this code will also combine the internal storage and external storage into one cursor):

  Cursor[] thumbs = new Cursor[2]; thumbs[0] = mActivity.getContentResolver().query( MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, new String[]{ MediaStore.Images.Thumbnails._ID , MediaStore.Images.Thumbnails.IMAGE_ID, MediaStore.Images.Thumbnails.DATA }, null, null, MediaStore.Images.Thumbnails.IMAGE_ID + "*(-1)" ); thumbs[1] = mActivity.getContentResolver().query( MediaStore.Images.Thumbnails.INTERNAL_CONTENT_URI, new String[]{ MediaStore.Images.Thumbnails._ID , MediaStore.Images.Thumbnails.IMAGE_ID, MediaStore.Images.Thumbnails.DATA }, null, null, MediaStore.Images.Thumbnails.IMAGE_ID + "*(-1)" ); Cursor thumbCursor = new MergeCursor(thumbs); Cursor[] cursors = new Cursor[2]; cursors[0] = mActivity.getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{ MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.ORIENTATION, MediaStore.Images.Media.BUCKET_DISPLAY_NAME, MediaStore.Images.Media.BUCKET_ID, MediaStore.Images.Media.MIME_TYPE }, null, null, MediaStore.Images.Media._ID + "*(-1)" ); cursors[1] = mActivity.getContentResolver().query( MediaStore.Images.Media.INTERNAL_CONTENT_URI, new String[]{ MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.ORIENTATION, MediaStore.Images.Media.BUCKET_DISPLAY_NAME, MediaStore.Images.Media.BUCKET_ID, MediaStore.Images.Media.MIME_TYPE }, null, null, MediaStore.Images.Media._ID + "*(-1)" ); Cursor photoCursor = new MergeCursor(cursors); CursorJoiner cursorJoiner = new CursorJoiner( thumbCursor, new String[]{ MediaStore.Images.Thumbnails.IMAGE_ID }, photoCursor, new String[]{ MediaStore.Images.Media._ID, } ); Cursor finalCursor= new MatrixCursor(new String[]{ MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.Images.Media.ORIENTATION, MediaStore.Images.Media.BUCKET_DISPLAY_NAME, MediaStore.Images.Media.BUCKET_ID, MediaStore.Images.Media.MIME_TYPE, "thumb_data" }); for (CursorJoiner.Result joinerResult : cursorJoiner) { switch (joinerResult) { case RIGHT: finalCursor.addRow(new Object[]{ photoCursor.getLong(photoCursor.getColumnIndex(MediaStore.Images.Media._ID)), photoCursor.getString(photoCursor.getColumnIndex(MediaStore.Images.Media.DATA)), photoCursor.getLong(photoCursor.getColumnIndex(MediaStore.Images.Media.ORIENTATION)), photoCursor.getString(photoCursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)), photoCursor.getLong(photoCursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID)), photoCursor.getString(photoCursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE)), null }); break; case BOTH: finalCursor.addRow(new Object[]{ photoCursor.getLong(photoCursor.getColumnIndex(MediaStore.Images.Media._ID)), photoCursor.getString(photoCursor.getColumnIndex(MediaStore.Images.Media.DATA)), photoCursor.getLong(photoCursor.getColumnIndex(MediaStore.Images.Media.ORIENTATION)), photoCursor.getString(photoCursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)), photoCursor.getLong(photoCursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID)), photoCursor.getString(photoCursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE)), thumbCursor.getString(thumbCursor.getColumnIndex(MediaStore.Images.Thumbnails.DATA)), }); break; } } photoCursor.close(); thumbCursor.close(); 
0
source

Source: https://habr.com/ru/post/1232269/


All Articles