【问题标题】:Querying the MediaStore: Joining thumbnails and images (on ID)查询 MediaStore:加入缩略图和图像(在 ID 上)
【发布时间】:2015-09-25 12:20:05
【问题描述】:

我正在开发一款适用于 Android 的“照片库”类型的应用程序。它最初是 Udacity 开发 Android 应用程序的最终项目,因此它的整体结构(活动、内容提供者等)应该非常完善,并且被 Udacity/Google 接受了认证。

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

我想做的事情真的应该很直截了当;将设备上的所有图像(作为缩略图)加载到 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. 加入这些,在MediaStore.Media.Thumbnails.IMAGE_ID = MediaStore.Media.Images._ID 上使用CursorJoiner

  4. 返回结果retCursor(在连接中产生)

--请找到完整代码in this previous post.

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

不知何故,我认为IMAGE_ID_ID 始终一致的假设不正确?我见过post on AirPair,描述了一个类似的画廊应用程序,并且教程实际上对此略有不同。他没有尝试加入光标,而是获取缩略图光标并对其进行迭代,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?
  • 在 ID 上加入拇指和图像是否安全 - 是否始终稳定/同步?
  • 我的应用如何自动生成/获取新图像的缩略图?

【问题讨论】:

  • 那么,加入游标可能并不比仅获取图像并遍历游标以获取每个单独的缩略图优越吗?
  • 所以我认为我的问题的答案是直接查询图像表,并且仅在辅助查询中单独获取缩略图。据我了解,缩略图并不总是存在,表格会针对给定图像进行更新。

标签: android mediastore


【解决方案1】:

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

我想达到什么目的?

  • 查询设备上的缩略图和图像(通过 MediaStore)
  • 将这些加入到一个光标中,按降序排列(最新图像在顶部)
  • 处理丢失缩略图的情况

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

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

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

这一切的主要问题是使用CursorJoiner。出于某种原因,它要求两个游标都按升序排序,比方说在 ID 上。然而,这意味着最旧的图像优先,这确实是一个糟糕的画廊应用程序。我发现 CursorJoiner 可以被“欺骗”,但是,通过简单地按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 类的内部工作。但是,对左右光标进行简单的补偿就足以“重新对齐”连接,使其回到正轨。注意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;
    }
}

【讨论】:

    【解决方案2】:

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

    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
    

    然后在源代码的 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();
    

    【讨论】:

    • 我注意到文档中的拼写错误(左右混淆),但这不是导致问题的原因。如果游标按升序排序,则这一切都按描述进行。然而,当“愚弄”连接者来处理降序时,一切都被扭转了。我将在下面发布一个新答案,并提供更多详细信息(因为这些评论字段非常小)。
    【解决方案3】:

    这是我的测试用例,它表明 CursorJoiner 缺乏对 descending 有序游标的支持。然而,这在 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!
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-10-30
      • 1970-01-01
      • 2011-11-19
      • 2020-06-24
      • 2011-04-06
      • 2015-04-10
      • 2018-03-11
      相关资源
      最近更新 更多