Querying the MediaStore: Joining thumbnails and images (on ID)

前端 未结 3 785
情深已故
情深已故 2021-02-15 12:38

I\'m working on a \"photo gallery\"-type app for Android. It started as a Final Project for the Developing Android Apps at Udacity, so it\'s overall structure (activities, conte

3条回答
  •  名媛妹妹
    2021-02-15 13:03

    OK, so it seems I finally figured all this out. Thought I'd share this here, for anyone else who might be interested.

    What am I trying to achieve?

    • Query thumbnails and images on device (via MediaStore)
    • Join these into one cursor, ordered descending (newest images on top)
    • Handle the case of missing thumbnails

    After lots of trial and error, and playing around with the MediaStore, I've learned that the thumbnails table (MediaStore.Images.Thumbnails) can not be expected to be up-to-date, at any given time. There will be images missing thumbnails, and vice versa (orphaned thumbnails). Especially when the camera app takes a new photo, apparently it doesn't immediately create a thumbnail. Not until the Gallery app (or equivalent) is opened, is the thumbnail table updated.

    I got various helpful suggestions on how to work myself around this problem, mainly centered on just querying the images table (MediaStore.Images.Media) and then, somehow, extend the cursor with thumbnails one row at a time. While that did work, it caused the app to be extremely slow and consumed a lot of memory for ~2000 images on my device.

    It really should be possible to simply JOIN (left outer join) the thumbnails table with the images table, such that we get all images and the thumbnails when these exist. Otherwise, we leave the thumbnail DATA column to null, and just generate those particular missing thumbnails ourselves. What would be really cool is to actually insert those thumbnails into the MediaStore, but that I have not looked into yet.

    The main problem with all this was using the CursorJoiner. For some reason, it requires both cursors to be ordered in ascending order, let's say on ID. However, that means oldest images first, which really makes for a crappy gallery app. I found that the CursorJoiner can be "fooled", however, into permitting descending order by simply ordering by 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 rows match up, though, this works fine (the BOTH case). But when you run into rows where either cursor is unique (the LEFT or RIGHT cases) the reversed ordering messes up the inner workings of the CursorJoiner class. However, a simple compensation on the left and right cursors is sufficient to "re-align" the join, getting it back on track. Note the moveToNext() and moveToPrevious() calls.

    // 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 elements for my GridView and binds data into these from the cursor returned from the ContentProvider (retCursor above), I create a thumbnail in the following manner (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;
        }
    }
    

提交回复
热议问题