Consultar el MediaStore: Unir miniaturas e imágenes (en ID)

Estoy trabajando en una aplicación de tipo "galería de fotos" para Android. Comenzó como un proyecto final para el desarrollo de aplicaciones de Android en Udacity, por lo que es la estructura general (actividades, contentproviders etc) debe ser muy sólido, y fue aceptado para la certificación por Udacity / Google.

Sin embargo, todavía no está terminado al 100%, y todavía estoy tratando de mejorar en él.

Lo que quiero hacer realmente debería ser bastante sencillo; Cargar todas las imágenes en el dispositivo (como miniaturas) en un GridView en el MainActivity, con un DetailActivity que muestra la imagen a tamaño completo + algunos metadatos (título, tamaño, fecha etc).

El curso requería que escribiéramos un ContentProvider, así que tengo una función query () que esencialmente obtiene datos del MediaStore y devuelve un cursor a GridView de MainActivity. En mi dispositivo, al menos, (Sony Xperia Z1, Android 5.1.1) esto funciona casi perfectamente. Hay algunos errores y peculiaridades, pero por lo general puedo encontrar todas las imágenes en mi teléfono en mi aplicación, y haga clic en ellas para ver los detalles.

Sin embargo, cuando intenté instalar la aplicación en la Sony Xperia Z3 de mi amigo, todo falló. No se presentó ninguna imagen, aunque obviamente comprobé que había en realidad ~ 100 fotos en su teléfono. Lo mismo en el teléfono de otro amigo (nuevo Samsung S6) 🙁

Este es el problema principal. En mi teléfono, donde las cosas funcionan, los errores "secundarios" implican cuando una nueva foto es tomada por la cámara, no se carga automáticamente en mi aplicación (como una miniatura). Parece que necesito averiguar cómo desencadenar una exploración, o lo que sea necesario para cargar / generar nuevos pulgares. Eso también es bastante alto en mi lista de deseos.

Como he dicho, estoy seguro de que todo esto realmente debe ser bastante simple, así que tal vez todas mis dificultades indican que estoy abordando el problema de la manera totalmente equivocada? Esto es lo que está haciendo mi función query ():

  1. Obtener un cursor de todas las miniaturas, de MediaStore.Media.Thumbnails.EXTERNAL_CONTENT_URI

  2. Obtener un cursor de todas las imágenes, desde MediaStore.Media.Images.EXTERNAL_CONTENT_URI

  3. Unir estos, en MediaStore.Media.Thumbnails.IMAGE_ID = MediaStore.Media.Images._ID con un CursorJoiner

  4. retCursor el retCursor resultante (como se produce en la unión)

– por favor, busque el código completo en este post anterior.

Aunque esto parece correcto (para mí), tal vez no es realmente la manera de ir sobre esto? Estoy uniendo pulgares e imágenes, por cierto, de tal manera que puedo mostrar algunos metadatos (por ejemplo, fecha tomada) junto con la miniatura, en el GridView. He identificado el problema a la unión, en particular, porque si lo simplifico para sólo cargar los pulgares en el GridView, entonces todo funciona bien – también en el teléfono de mi amigo. (Con la excepción de cargar nuevas fotos.)

De alguna manera, mi suposición de que IMAGE_ID y _ID siempre son consistentes no es correcta? He visto un post en AirPair , que describe una aplicación de galería similar, y allí el tutorial en realidad va sobre esto un poco diferente. En lugar de intentar unir cursores, obtiene el cursor de las miniaturas e itera sobre él, agregando datos de las imágenes usando consultas individuales al MediaStore … ¿Pero es esa la forma más eficiente de hacer esto? – Sin embargo, su solución unir la miniatura a la imagen correspondiente en ID:

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

En resumen, necesito ayuda con lo siguiente:

  • ¿Estoy consultando el MediaStore correctamente?
  • ¿Es seguro unir los pulgares y las imágenes, en el ID – que será estable / sincronizado en todo momento?
  • ¿Cómo puede mi aplicación generar / recuperar automáticamente las miniaturas de las nuevas imágenes?

OK, así que parece que finalmente terminé todo esto. Pensé compartir esto aquí, para cualquier otra persona que podría estar interesado.

¿Qué estoy tratando de lograr?

  • Consultar miniaturas e imágenes en el dispositivo (a través de MediaStore)
  • Únete a ellos en un solo cursor, ordenado descendente (las imágenes más recientes en la parte superior)
  • Manejar el caso de las miniaturas que faltan

Después de un montón de pruebas y errores, y jugar con el MediaStore, he aprendido que la tabla de miniaturas (MediaStore.Images.Thumbnails) no se puede esperar que esté al día, en un momento dado. Habrá imágenes que faltan imágenes en miniatura, y viceversa (miniaturas huérfanas). Especialmente cuando la aplicación de cámara toma una nueva foto, al parecer no crea inmediatamente una miniatura. No hasta que se abra la aplicación Galería (o equivalente), se actualizará la tabla de miniaturas.

Tengo varias sugerencias útiles sobre cómo trabajar conmigo alrededor de este problema, centrado principalmente en sólo consultar la tabla de imágenes (MediaStore.Images.Media) y, de alguna manera, ampliar el cursor con las miniaturas de una fila a la vez. Mientras que hizo el trabajo, causó la aplicación extremadamente lenta y consumió mucha memoria para ~ 2000 imágenes en mi dispositivo.

Realmente debería ser posible simplemente JOIN (unión externa izquierda) la tabla de miniaturas con la tabla de imágenes, de modo que obtengamos todas las imágenes y las miniaturas cuando éstas existan. De lo contrario, dejamos la columna de DATOS en miniatura a null , y solo generamos las miniaturas que faltan en particular. Lo que sería realmente genial es insertar realmente esas miniaturas en el MediaStore, pero que no he mirado todavía.

El principal problema con todo esto fue usar el CursorJoiner . Por alguna razón, requiere que ambos cursores sean ordenados en orden ascendente , digamos en ID. Sin embargo, eso significa que las imágenes más antiguas primero, lo que realmente hace una aplicación crappy galería. Encontré que el CursorJoiner puede ser "engañado", sin embargo, en permitir el orden descendente simplemente pidiendo por 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))"); 

Mientras que las filas emparejan para arriba, sin embargo, esto trabaja muy bien (el BOTH caso). Pero cuando se ejecuta en filas donde el cursor es único (los casos LEFT o RIGHT ) el orden inverso desordena el funcionamiento interno de la clase CursorJoiner. Sin embargo, una simple compensación en los cursores izquierdo y derecho es suficiente para "volver a alinear" la unión, volviéndola a la pista. Tenga en cuenta las moveToNext() y 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; 

Luego, en la clase "PhotoAdapter", que crea elementos para mi GridView y enlaza los datos en estos desde el cursor devuelto por el ContentProvider ( retCursor anterior), creo una miniatura de la siguiente manera (cuando el campo thumb_path es 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; } } 

La respuesta aceptada me inició en esta pregunta, pero contiene un par de pequeños errores.

 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) 

Estos son hacia atrás. La documentación se contradice, y es probable que se cometió el error. Del código fuente de CursorJoiner :

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

Luego en el enum de Resultado del código fuente:

 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 } 

Así que estoy adivinando esto es la razón por la que fueron la fuerza de incrementar los cursores.

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

El iterador en CursorJoiner incrementa automáticamente los cursores para usted.

Este debe ser el código de trabajo (este código también combinará el almacenamiento interno y el almacenamiento externo en un solo cursor):

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

Aquí está mi caso de prueba, que demuestra la falta de apoyo en CursorJoiner para los cursores ordenados descendentes . Esto, sin embargo, se documenta específicamente en el código fuente CursorJoiner, así que no estoy tratando de criticar, sino simplemente mostrar cómo esto puede ser eludido (o hackeado).

El caso de prueba muestra cómo la asunción de orden ascendente hace que sea necesario "invertir", o invertir, todas las opciones hechas por el CursorJoiner (resultado comparador, incremento de cursores, etc.). Lo que realmente me gustaría probar a continuación es modificar la clase CursorJoiner directamente, para intentar agregar soporte para ordenar DESC.

Tenga en cuenta que parece que la parte sobre el pedido por ID * (- 1) tal vez no es estrictamente necesario para que esto funcione. En el ejemplo siguiente, no negé las columnas ID (ordenación DESC simple, no "pseudo-ASC" con secuencias negativas), y todavía funciona.

Caso de prueba

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

Y la salida:

 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! 
  • Listar todas las imágenes de la cámara en Android
  • Error de cursor Android: "asegúrese de que el cursor se inicialice correctamente antes de acceder a los datos de él ..."
  • Uso del URI de contenido con ACTION_VIDEO_CAPTURE
  • Archivos no eliminados al borrar y aún mostrados en Listview
  • Android: EXTERNAL_CONTENT_URI es suficiente para una galería de fotos?
  • Android - Selector de imágenes no funciona en Kindle Fire?
  • Cómo guardar la ruta de la imagen utilizando las preferencias compartidas
  • cómo obtener todos los archivos de audio de la tarjeta sd Android
  • Cómo limitar la duración de MediaStore en Android 7
  • Mediastore para Android: ¿Cómo recuperar de forma eficiente todas las canciones de un determinado género?
  • Android: cómo obtener una miniatura de vídeo de un archivo privado para su aplicación?
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.