查询 MediaStore:连接缩略图和图像(在 ID 上)

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

我正在为 Android 开发一个 "photo gallery" 类型的应用程序。它最初是 Udacity 开发 Android 应用程序的最终项目,因此它的整体结构(活动、内容提供者等)应该非常健全,并且被 Udacity/Google 接受认证。

然而,它还没有 100% 完成,我仍在努力改进它。

我想做的应该是相当straight-forward;将设备上的所有图像(作为缩略图)加载到 MainActivity 中的 GridView 中,DetailActivity 显示全尺寸图像 + 一些元数据(标题、大小、日期等)。

课程要求我们编写一个 ContentProvider,所以我有一个 query() 函数,它基本上从 MediaStore 获取数据,returns 一个指向 MainActivity 的 GridView 的光标。至少在我的设备上,(Sony Xperia Z1,Android 5.1.1)这 几乎 完美。有一些错误和怪癖,但总的来说,我可以在我的应用程序中始终找到 phone 上的所有图像,然后单击它们以查看详细信息。

但是,当我尝试在我朋友的 Sony Xperia Z3 上安装该应用程序时,一切都失败了。没有图像出现,虽然我显然检查过他 phone 上实际上有大约 100 张照片。在另一位朋友的 phone(全新的三星 S6)上也是如此 :-(

这是主要问题。在我的 phone 上,东西正常,"secondary" 错误涉及当相机拍摄新照片时,它不会自动加载到我的应用程序中(作为缩略图)。看来我需要弄清楚如何触发扫描,或者 load/generate 新拇指所需的任何东西。这在我的愿望清单上也很高。

正如我所说,我相信这一切真的应该非常简单,所以也许我所有的困难都表明我正在以完全错误的方式解决问题? 这是我的 query() 函数正在做的事情:

  1. 获取所有缩略图的光标,来自MediaStore.Media.Thumbnails.EXTERNAL_CONTENT_URI

  2. 获取所有图片的光标,来自MediaStore.Media.Images.EXTERNAL_CONTENT_URI

  3. 加入这些,在 MediaStore.Media.Thumbnails.IMAGE_ID = MediaStore.Media.Images._ID 使用 CursorJoiner

  4. return 结果 retCursor(在联接中生成)

-- 请查找完整代码

虽然这个 看起来 正确(对我来说),也许这真的不是解决这个问题的方法?顺便说一下,我加入了缩略图和图像,这样我就可以在 GridView 中显示一些元数据(例如拍摄日期)和缩略图。我已经确定了连接的问题,特别是因为如果我将其简化为仅将拇指加载到 GridView 中,那么这一切都可以正常工作——在我朋友的 phone 上也是如此。 (加载新照片除外。)

不知何故,我假设 IMAGE_ID_ID 始终一致是不正确的?我见过 post on AirPair, describing a similar gallery app, and there the tutorial actually goes about this slightly differently. Rather than attempting to join cursors, he gets the thumbnails cursor and iterates over it, adding data from Images using individual queries to the MediaStore... 但这是最​​有效的方法吗? - 然而,他的解决方案 确实 将缩略图加入 ID 上的相应图像:

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

总而言之,我需要以下方面的帮助:

好的,看来我终于想通了这一切。我想我会在这里分享给任何可能感兴趣的人。

我想达到什么目的?

  • 查询设备上的缩略图和图像(通过 MediaStore)
  • 将这些合并为一个游标,按降序排列(最新图像在最上面)
  • 处理缺少缩略图的情况

经过大量试验和错误,并尝试使用 MediaStore,我了解到缩略图 table (MediaStore.Images.Thumbnails) 不能指望是最新的,在任何给定时间。 图像缺少缩略图,反之亦然(孤立的缩略图)。特别是当相机应用程序拍摄新照片时,显然它不会立即创建缩略图。直到图库应用程序(或同等应用程序)打开,缩略图才会更新 table。

关于如何解决这个问题,我得到了各种有用的建议,主要集中在查询图像 table (MediaStore.Images.Media),然后以某种方式将带有缩略图的光标扩展一排一次。虽然这确实有效,但它导致应用程序非常慢,并且在我的设备上消耗了约 2000 张图像的大量内存。

确实应该 可以简单地 JOIN(左外连接)缩略图 table 和图像 table,这样我们就可以得到所有图像以及存在时的缩略图。否则,我们将缩略图 DATA 列留给 null,并自己生成那些特定的缺失缩略图。真正酷的是 将这些缩略图插入 到 MediaStore 中,但我还没有研究过。

所有这一切的主要问题是使用 CursorJoiner。出于某种原因,它要求两个游标都按 升序 排序,比方说在 ID 上。然而,这意味着最旧的图像优先,这确实是一个蹩脚的画廊应用程序。我发现 CursorJoiner 可以是 "fooled",但是,只需按 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))");

不过,只要行匹配,就可以正常工作(BOTH 情况)。但是,当您 运行 进入游标唯一的行时(LEFTRIGHT 情况),反向排序会扰乱 CursorJoiner class 的内部工作。但是,对左右游标进行简单的补偿就足以 "re-align" 连接,使其回到正轨。请注意 moveToNext()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;

然后,在 "PhotoAdapter" class 中,它为我的 GridView 创建元素,并将数据从 ContentProvider 返回的游标(上面的 retCursor)绑定到这些元素中,我按以下方式创建缩略图(当 thumb_path 字段为 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;
    }
}

接受的答案让我开始了这个问题,但它包含一些小错误。

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)

这些是倒退的。该文档自相矛盾,并且很可能是错误所在。来自 source code of CursorJoiner:

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

然后在源代码的结果枚举中:

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
}

所以我猜这就是你强制递增游标的原因。

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

CursorJoiner 中的迭代器会自动为您递增游标。

这应该是工作代码(此代码还将内部存储和外部存储合并为一个游标):

        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();

这是我的测试用例,它表明 CursorJoiner 不支持 descending 有序游标。然而,这在 CursorJoiner 源代码中有专门的记录,所以我并不是要批评,而只是展示如何规避(或破解)它。

测试用例显示升序假设如何需要 "flipping" 或反转 CursorJoiner 所做的所有选择(比较器结果、游标递增等)。接下来我真正想尝试的是直接修改 CursorJoiner class,尝试添加对 DESC 排序的支持。

请注意,关于按 ID*(-1) 排序的部分似乎并不是这项工作所必需的。在下面的示例中,我没有否定 ID 列(纯 DESC 排序,而不是 "pseudo-ASC" 负序列),它仍然有效。

测试用例

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!");

和输出:

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!