查询MediaStore:通过ID连接缩略图和图片

10
我正在为 Android 开发一款“照片库”类型的应用程序。它最初是 Udacity 的 Developing Android Apps 课程的期末项目,因此其整体结构(活动、内容提供者等)应该非常稳健,并已被 Udacity/Google 认证通过。
然而,它仍然没有完全完成,我仍在努力改进它。
我想要做的事情应该非常简单;在 MainActivity 中将设备上的所有图像(作为缩略图)加载到 GridView 中,使用 DetailActivity 显示全尺寸图像和一些元数据(标题、大小、日期等)。
该课程要求我们编写 ContentProvider,因此我有一个 query() 函数,实质上从 MediaStore 获取数据,并返回一个光标到 MainActivity 的 GridView。在我的设备上至少(Sony Xperia Z1,Android 5.1.1),这几乎完美地工作。存在一些错误和怪癖,但总体上我可以在我的应用程序中始终找到手机上的所有图像,并单击它们查看详细信息。
然而,当我试图在我的朋友的Sony Xperia Z3手机上安装该应用时,一切都失败了。没有图片显示出来,尽管我明显检查过他的手机上确实有大约100张照片。同样的问题也出现在另一个朋友的手机上(全新的三星S6):-(
这是主要问题。在我的手机上,一切正常,但“次要”的错误包括当相机拍摄新照片时,它不会自动加载到我的应用程序中(作为缩略图)。似乎我需要找出如何触发扫描或生成新缩略图所需的内容。这也是我愿望清单上的重点之一。
正如我所说,我相信所有这些都应该非常简单,所以也许我所有的困难表明我完全错误地解决了问题?以下是我的query()函数正在执行的操作:
  1. MediaStore.Media.Thumbnails.EXTERNAL_CONTENT_URI获取所有缩略图的光标。

  2. MediaStore.Media.Images.EXTERNAL_CONTENT_URI获取所有图像的光标。

  3. 使用CursorJoinerMediaStore.Media.Thumbnails.IMAGE_ID = MediaStore.Media.Images._ID上连接这些光标。

  4. 返回连接产生的结果retCursor

-- 请在此前一篇文章中找到完整代码。

虽然这看起来(对我来说)是正确的,但也许这不是正确的方法?顺便说一下,我正在连接缩略图和图像,以便可以在GridView中显示一些元数据(例如拍摄日期)以及缩略图。我已经确定了问题所在,特别是因为如果我仅将缩略图加载到GridView中,则所有内容都可以正常工作--包括在我朋友的手机上。 (除了加载新照片之外。)

不知怎么的,我一直以为IMAGE_ID_ID总是一致的,但事实并非如此?我看到了一个在AirPair上的帖子,描述了一个类似的画廊应用程序,在那里教程实际上略有不同。他没有尝试连接游标,而是获取缩略图游标并迭代它,使用单独的查询从MediaStore添加Images数据...但这是最有效的方法吗? - 尽管如此,他的解决方案确实将缩略图与相应的图像连接在了一起:

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

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

  • 我是否正确地查询了MediaStore?
  • 根据ID连接缩略图和图像是否安全,它们始终稳定/同步吗?
  • 我的应用程序如何自动生成/获取新图像的缩略图?

1
相关:https://dev59.com/uHA65IYBdhLWcg3wsg2B - tskulbru
那么,连接游标可能并不比仅获取图像并迭代游标以获取每个缩略图更加优越? - joakimk
所以我理解我的问题的答案是直接查询图像表,并只在辅助查询中单独获取缩略图。我明白并非总是存在缩略图,特别是当给定图像的表被更新时。 - joakimk
3个回答

10

好的,看起来我终于弄清楚了所有这些。我想在这里分享一下,供其他有兴趣的人参考。

我想实现什么?

  • 通过MediaStore查询设备上的缩略图和图片
  • 将它们合并成一个游标,按降序排序(最新的图片在顶部)
  • 处理缺少缩略图的情况

经过多次尝试,以及与MediaStore的交互,我发现缩略图表(MediaStore.Images.Thumbnails)不能保证随时是最新的。会有缺少缩略图的图片,反之亦然(孤立的缩略图)。特别是当相机应用拍摄新照片时,显然并不会立即创建缩略图。只有在打开图库应用程序(或类似应用程序)后,缩略图表才会更新。

我得到了各种有用的建议,主要是围绕着仅查询图像表(MediaStore.Images.Media),然后以某种方式逐行扩展游标以获取缩略图。虽然这确实有效,但对于我的设备上约2000个图像而言,它导致应用程序非常缓慢,并消耗大量内存。

实际上,只需使用左外连接将缩略图表与图像表JOIN(左外连接),即可获得所有图像和这些图像的缩略图(如果存在)。否则,我们将缩略图DATA列保留为null,并自动生成那些特定缺失的缩略图。真正酷的是,实际上可以将这些缩略图插入到MediaStore中,但我还没有研究过。

所有这些的主要问题是使用CursorJoiner。由于某种原因,它要求两个游标按升序排序,比如按ID排序。然而,这意味着最旧的图像首先出现,这确实使得画廊应用程序变得糟糕。不过,我发现可以通过仅按ID*(-1)排序来“欺骗”CursorJoiner,从而允许降序。

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类的内部机制。然而,在左右游标上进行简单的补偿就足以“重新对齐”连接,并使其恢复正常。请注意使用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”类中,该类为我的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;
    }
}

0

这是我的测试用例,它展示了CursorJoiner不支持降序排序的缺乏。然而,在CursorJoiner源代码中明确记录了这一点,所以我并不是在批评,而只是展示如何规避(或者说“黑客”)这个问题。

这个测试用例展示了假设升序排序会导致CursorJoiner需要“翻转”或反转所有选择(比较器结果、游标增量等)。接下来我想尝试直接修改CursorJoiner类,尝试添加对DESC排序的支持。

请注意,似乎将ID按*(-1)排序可能并不是必需的才能使其工作。在下面的示例中,我没有取反ID列(仅使用普通的DESC排序,而非带有负序列的“伪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!

0

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

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)

这些是反过来的。文档自相矛盾,很可能是错误发生的地方。从CursorJoiner源代码中:

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

在源代码中的Result枚举中,然后返回翻译的文本:
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();

我注意到文档中有错别字(左右混淆),但这并不是导致问题的原因。如果光标按升序排序,一切都能按照描述正常工作。然而,当“欺骗”连接器处理降序时,一切都会被颠倒。由于这些评论字段太小了,我将在下面发布一个新答案,并提供更多细节。 - joakimk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接