我正在开发适用于Android的“照片库”型应用。它最初是作为Udacity开发Android应用程序的最终项目,所以它的整体结构(活动,内容提供商等)应该非常合理,并且它被Udacity / Google接受认证。
然而,它仍然没有100%完成,我仍然在努力改进它。
我想要做的事情应该非常简单;将设备上的所有图像(缩略图)加载到MainActivity中的GridView中,使用DetailActivity显示完整大小的图像+一些元数据(标题,大小,日期等)。
本课程要求我们编写一个ContentProvider,所以我有一个query()函数,它基本上从MediaStore中获取数据,并将游标返回给MainActivity的GridView。在我的设备上,至少,(索尼Xperia Z1,Android 5.1.1)这几乎几乎。有一些错误和怪癖,但总的来说,我可以在我的应用程序中不断找到手机上的所有图像,然后点击它们查看详细信息。
然而,当我尝试在朋友的Sony Xperia Z3上安装应用程序时,一切都失败了。没有图像显示,虽然我明显检查他的手机上实际上有~100张照片。在另一位朋友的手机上(全新的三星S6): - (
这是主要问题。在我的手机上,当东西工作时,“次要”错误涉及当相机拍摄新照片时,它不会自动加载到我的应用程序中(作为缩略图)。我似乎需要弄清楚如何触发扫描,或者加载/生成新拇指所需的任何内容。我的愿望清单上也是如此。
正如我所说,我相信所有这一切都应该非常简单,所以也许我所有的困难都表明我正以完全错误的方式接近问题? 以下是我的query()函数正在执行的操作:
从MediaStore.Media.Thumbnails.EXTERNAL_CONTENT_URI
从MediaStore.Media.Images.EXTERNAL_CONTENT_URI
使用MediaStore.Media.Thumbnails.IMAGE_ID = MediaStore.Media.Images._ID
CursorJoiner
上加入这些内容
返回结果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);
总之,我需要以下方面的帮助:
答案 0 :(得分:7)
我想要实现的目标是什么?
经过大量试验和错误,以及使用MediaStore,我已经了解到缩略图表(MediaStore.Images.Thumbnails)在任何给定时间都不可能是最新的。 将成为缺少缩略图的图像,反之亦然(孤立的缩略图)。特别是当相机应用拍摄新照片时,显然它不会立即创建缩略图。直到Gallery应用程序(或同等版本)打开后,缩略图表才会更新。
关于如何解决这个问题,我得到了各种有用的建议,主要集中在查询图像表(MediaStore.Images.Media),然后,不知何故,一次一行地用缩略图扩展光标。虽然这确实有效,但它导致应用程序非常慢,并且在我的设备上消耗了大量内存〜2000个图像。
否则,我们将缩略图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
情况)。但是当你遇到任何游标都是唯一的行(LEFT
或RIGHT
情况)时,反向排序会混淆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" 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;
}
}
答案 1 :(得分: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)
这些是向后的。文档与自身相矛盾,很可能是错误发生的地方。来自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();
答案 2 :(得分: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!