Añadiendo fotografías en Mis Lugares

En este apartado seguiremos trabajando con el uso de intenciones aplicándolas a la aplicación Mis Lugares. En concreto permitiremos que el usuario pueda asignar fotografías a cada lugar utilizando ficheros almacenados en su dispositivo o la cámara.

Ejercicio: Añadiendo fotografías desde la galería

1.     En la clase VistaLugarActivity añade las siguientes constantes y atributos:

final static int RESULTADO_GALERIA = 2;
final static int RESULTADO_FOTO = 3;
private ImageView foto;  
val RESULTADO_GALERIA = 2  //poner antes de la clase
val RESULTADO_FOTO = 3 

Desde la actividad VistaLugarActivity llamamos a diferentes actividades y algunas de ellas nos tienen que devolver información. En estos casos llamamos a la actividad con startActivityForResult() pasándole un código que identifica la llamada. Cuanto esta actividad termine se llamará al método  onActivityResult(), que nos indicará el mismo código usado en la llamada. Como vamos a hacerlo con  tres actividades diferentes, hemos creado tres constantes, con los respectivos códigos de respuesta. Actuando de esta forma conseguimos un código más legible.

2.     En Java, en el método onCreate() añade antes de actualizarVistas():

foto = findViewById(R.id.foto);

3.     Busca en vista_lugar.xml el ImageView con descripción “logo galería” y añade el atributo:
 

android:onClick="ponerDeGaleria" 

4.     Añade la siguiente función en la actividad:

public void ponerDeGaleria(View view) {
   usoLugar.ponerDeGaleria(RESULTADO_GALERIA);
} 
fun ponerDeGaleria(view: View)= usoLugar.ponerDeGaleria(RESULTADO_GALERIA) 

5.     Añade el siguiente caso de uso a la clase CasosUsoLugar:

// FOTOGRAFÍAS
public void ponerDeGaleria(int codidoSolicitud) {
    String action;
    if (android.os.Build.VERSION.SDK_INT >= 19) { // API 19 - Kitkat
        action = Intent.ACTION_OPEN_DOCUMENT;
    } else {
        action = Intent.ACTION_PICK;
    }
    Intent intent = new Intent(action, 
                           MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("image/*");
    actividad.startActivityForResult(intent, RESULTADO_GALERIA);
} 
// FOTOGRAFÍAS
fun galeria(view: View) {
   val action= if (android.os.Build.VERSION.SDK_INT >= 19) { // API 19 - Kitkat
      Intent.ACTION_OPEN_DOCUMENT
   } else {
      Intent.ACTION_PICK
   }
   val i = Intent(action,MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply {
      addCategory(Intent.CATEGORY_OPENABLE)
      type = "image/*"
   }
   startActivityForResult(i, RESULTADO_GALERIA)
} 

Este método crea una intención indicando que queremos seleccionar contenido. El contendo será proporcionado por el Content Provider MediaStore, además le indicamos que nos interesan imágenes del almacenamiento externo. Tipicamente se abrirá la aplicación galería de fotos (u otra similar).  Observa, que se usan dos acciones diferentes: ACTION_OPEN_DOCUMENT solo está disponible a partir del API 19, tiene la ventaja que no requiere que la aplicación pida permiso de lectura. Cuando el usuario selecciona un fichero el Content Provider, dará a nuestra aplicación permiso de lectura (o incluso de escritura) pero solo para el archivo solicitado. No se considera una acción peligrosa dado que es el usuario quien selecciona el archivo a compartir con la aplicación. Si se ejecuta en un API anterior al 19, tendremos que usar ACTION_PICK, que sí que requiere dar permisos de lectura en la memoria externa. Una vez concedido este permiso la aplicación podría aprovechar y leer otros ficheros sin la intervención directa del usuario. Como necesitamos una respuesta usamos startActivityForResult() con el código adecuado.

6.    Si la versión  del dispositivo es anterior al API 19, vamos a tener que pedir permiso para leer ficheros de la memoria externa. En AndroidManifest.xml añade dentro de la etiqueta <manifest …> </manifest> el siguiente código:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> 

7.     En el método onActivityResult() añade la sección else if que se muestra:

if (requestCode == RESULTADO_EDITAR) {
   …
} else if (requestCode == RESULTADO_GALERIA) {
   if (resultCode == Activity.RESULT_OK) {
      usoLugar.ponerFoto(pos, data.getDataString(), foto);
   } else {
      Toast.makeText(this, "Foto no cargada",Toast.LENGTH_LONG).show();
   }
} 
if (requestCode == RESULTADO_EDITAR) {
   …
} else if (requestCode == RESULTADO_GALERIA) {
   if (resultCode == RESULT_OK) {
      usoLugar.ponerFoto(pos, data?.dataString ?: "", foto)
   } else {
      Toast.makeText(this, "Foto no cargada", Toast.LENGTH_LONG).show();
   }
}  

Comenzamos verificando que volvemos de la actividad lanzada por la intención anterior. Comprobamos que el usuario no ha cancelado la operación. En este caso, nos tiene que haber pasado en la intención de respuesta, data, una URI con la foto seleccionada. Esta URI puede ser del tipo file://…, content://…o http://… dependiendo de que aplicación haya resuelto esta intención. El siguiente paso consiste en modificar el contenido de la vista que muestra la foto, imageView, con esta URI. Lo hacemos en el método ponerFoto():

8.     Añade el siguiente CasosUsoLugar:

public void ponerFoto(int pos, String uri, ImageView imageView) {
   Lugar lugar = lugares.elemento(pos);  
   lugar.setFoto(uri);
   visualizarFoto(lugar, imageView);
}

public void visualizarFoto(Lugar lugar, ImageView imageView) {
   if (lugar.getFoto() != null && !lugar.getFoto().isEmpty()) {
      imageView.setImageURI(Uri.parse(lugar.getFoto()));
   } else {
      imageView.setImageBitmap(null);
   }
}  
fun ponerFoto(pos: Int, uri: String?, imageView: ImageView) {
   val lugar = lugares.elemento(pos)
   lugar.foto = uri ?: ""
   visualizarFoto(lugar, imageView)
}

fun visualizarFoto(lugar: Lugar, imageView: ImageView) {
   if (!(lugar.foto == null || lugar.foto.isEmpty())) {
      imageView.setImageURI(Uri.parse(uri))
   } else {
      imageView.setImageBitmap(null)
   }
} 

El primer método comienza obteniendo el lugar que corresponde al id para modificar la URI de su foto. Luego llama a visualizarFoto(). Este verifica que la URI que acabamos de asignar no está vacía. Si es así, la asigna al ImageView que nos han pasado para que la imagen se represente en pantalla. En caso contrario, se le asigna un Bitmap igual a null, que es equivalente a que no se represente ninguna imagen.

9.     Ya puedes ejecutar la aplicación. Si añades una fotografía a un lugar, esta se visualizará. Sin embargo, si vuelve a la lista de lugares y seleccionas el mismo lugar al que asignaste la fotografía, ésta ya no se representa. La razón es que no hemos visualizado la foto al crear la actividad.

10.  En el método actualizarVistas() añade la siguiente línea al final:

usoLugar.visualizarFoto(lugar, foto); 
11.  Verifica de nuevo el funcionamiento de la aplicación.
 

Ejercicio paso a paso: Añadiendo fotografías desde la cámara

1.     Añade los siguientes atributos a la clase VistaLugarActivity:

private Uri uriUltimaFoto; 
private lateinit var uriUltimaFoto: Uri 

Como veremos, necesitamos esta variable en dos métodos diferentes. Por lo tanto, la declaramos de forma global.

2.     Añade el siguiente método a la clase CasosUsoLugar:

public Uri tomarFoto(int codidoSolicitud) {
   try {
      Uri uriUltimaFoto;
      File file = File.createTempFile(
           "img_" + (System.currentTimeMillis()/ 1000), ".jpg" ,
           actividad.getExternalFilesDir(Environment.DIRECTORY_PICTURES));
      if (Build.VERSION.SDK_INT >= 24) {
         uriUltimaFoto = FileProvider.getUriForFile(
               actividad, "es.upv.jtomas.mislugares.fileProvider", file); 
      } else {
         uriUltimaFoto = Uri.fromFile(file);
      }
      Intent intent   = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
      intent.putExtra (MediaStore.EXTRA_OUTPUT, uriUltimaFoto);
      actividad.startActivityForResult(intent, codidoSolicitud);
      return uriUltimaFoto;
   } catch (IOException ex) {
      Toast.makeText(actividad, "Error al crear fichero de imagen", 
            Toast.LENGTH_LONG).show();
      return null;
   }
}  
fun tomarFoto(codidoSolicitud: Int): Uri? {
   try {
      val file = File.createTempFile(
         "img_" + System.currentTimeMillis() / 1000, ".jpg", 
         actividad.getExternalFilesDir(Environment.DIRECTORY_PICTURES) )
      val uriUltimaFoto = if (Build.VERSION.SDK_INT >= 24)
         FileProvider.getUriForFile(
            actividad, "es.upv.jtomas.mislugares.fileProvider", file )
      else Uri.fromFile(file)
      val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
      intent.putExtra(MediaStore.EXTRA_OUTPUT, uriUltimaFoto)
      actividad.startActivityForResult(intent, codidoSolicitud)
      return uriUltimaFoto
   } catch (ex: IOException) {
      Toast.makeText(actividad, "Error al crear fichero de imagen",
                     Toast.LENGTH_LONG).show()
      return null
   }
}  

Este método crea  una intención indicando que queremos capturar una imagen desde el dispositivo. Típicamente se abrirá la aplicación cámara de fotos. A esta intención vamos a añadirle un extra con una URI al fichero donde queremos que se almacene la fotografía. Para crear el fichero, se utiliza createTempFile() indicando nombre, extensión y directorio. El método currentTimeMillis() nos da el número de milisegundos transcurridos desde 1970. Al dividir entre 1000, tenemos el número de segundos. El objetivo que se persigue es que, al crear un nuevo fichero, su nombre nunca coincida con uno anterior. El directorio del fichero será el utilizado para almacenar fotos privadas en la memoria externa. Estos ficheros serán de uso exclusivo para tu aplicación. Además, si desístalas la aplicación este directorio será borrado. Si quieres que los ficheros sean de acceso público utiliza  getExternalStoragePublicDirectory().
 Una vez tenemos el fichero, hay dos alternativas para crear la URI. Si el API de Android donde se ejecuta la aplicación es 24 o superior, podemos crear el fichero asociado a un Content Provider nuestro. Esta acción no requiere solicitar permiso de escritura. Si el API es anterior al 24, no se dispone de esta acción, y el fichero será creado de forma convencional en la memoria externa. El inconveniente es que para realizar esta acción tendremos que pedir al usuario permiso de escritura en la memoria externa. Al concedernos este permiso, también podremos borrar o sobreescribir cualquier fichero que el usuario tenga en esta memoria. Por lo que muchos usuarios no querrán darnos este permiso. Finalmente se añade la extensión del fichero. Al final llamamos a startActivityForResult() con el código que nos han pasado.

3.     Si la versión mínima de API es anterior a 24, vamos a tener que pedir permiso para leer ficheros de la memoria externa. En AndroidManifest.xml añade dentro de la etiqueta <manifest …> </manifest> el siguiente código:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 

NOTA: Observa como en el ejercicio anterior hemos tenido que solicitar permiso para acceder a la memoria externa, pero en este no es necesario solicitar permiso para tomar una fotografía. La razón es que realmente nuestra aplicación no toma la fotografía directamente, si no que por medio de una intención lanzamos otra aplicación, que si que tiene este permiso.

4.     En un punto anterior hemos utilizado un Content Provider para almacenar ficheros. Para crearlo añade en AndroidManifest.xml dentro de la etiqueta <application …> </application> el siguiente código:

<provider
   android:name="androidx.core.content.FileProvider"
   android:authorities="es.upv.jtomas.mislugares.fileProvider"
   android:exported="false"
   android:grantUriPermissions="true">
   <meta-data  android:name="android.support.FILE_PROVIDER_PATHS"
               android:resource="@xml/file_paths" />
</provider>  

5.     Crea un nuevo recurso con nombre "file_paths.xml" en la carpeta res/ xml:

<paths>
   <external-files-path name="my_images" path="/" />
</paths> 

Has de cambiar es.upv.jtomas… por un identificador unido que incluya tu nombre o empresa y la aplicación. Ha de coincidir con el valor indicado en la función tomarFoto(). Cambia com.example.package.name por el paquete de la aplicación.

6.     Busca en vista_lugar.xml el ImageView con descripción “logo cámara” y añade el atributo:
android:onClick="tomarFoto"

7.     En VistaLugarActivity añade:

public void tomarFoto(View view) {
   uriUltimaFoto = usoLugar.tomarFoto(RESULTADO_FOTO) 
} 
fun tomarFoto(view: View) {
   uriUltimaFoto = usoLugar.tomarFoto(RESULTADO_FOTO) 
} 
8.     En el método onActivityResult() añade la sección else if que se muestra:
} else if (requestCode == RESULTADO_GALERIA
   …
} else if (requestCode == RESULTADO_FOTO) {
   if (resultCode == Activity.RESULT_OK && uriUltimaFoto!=null) {
      lugar.setFoto(uriUltimaFoto.toString());
      usoLugar.ponerFoto(pos, lugar.getFoto(), foto);
   } else {
      Toast.makeText(this, "Error en captura", Toast.LENGTH_LONG).show();
   }
} 
} else if (requestCode == RESULTADO_GALERIA) {
   … 
} else if (requestCode == RESULTADO_FOTO) {
   if (resultCode == Activity.RESULT_OK && uriUltimaFoto!=null) {
      lugar.foto = uriUltimaFoto.toString()
      usoLugar.ponerFoto(pos, lugar.foto, foto);
   } else {
      Toast.makeText(this, "Error en captura", Toast.LENGTH_LONG).show()
   }
} 

Comenzamos verificando que volvemos de la actividad lanzada por la intención anterior, que el usuario no ha cancelado la operación y que uriUltimaFoto ha sido inicializado. En este caso, se nos pasa información en la intención de respuesta, pero sabemos que en uriUltimaFoto está almacenada la URI con el fichero donde se ha almacenado la foto. Guardamos esta URI en el campo adecuado de lugar e indicamos que se guarde y se represente en la vista foto.
 

9.     Verifica de nuevo el funcionamiento de la aplicación.

NOTA: En algunos dispositivos puede aparecer un error de memoria si la cámara está configurada con mucha resolución. En estos casos puedes probar con la cámara delantera.
 

Ejercicio paso a paso: Añadiendo un botón para eliminar fotografías

1.     En el layout vista_lugar.xml añade el siguiente botón dentro del LinearLayout donde están los botones para la cámara y para galería:

<ImageView
    android:layout_width="40dp"
    android:layout_height="40dp"
    android:contentDescription="Eliminar foto"
    android:onClick="eliminarFoto"
    android:src="@android:drawable/ic_menu_close_clear_cancel" /> 
2.     Añade el siguiente método a la clase VistaLugarActivity:
public void eliminarFoto(View view) {
   usoLugar.ponerFoto(pos, "", foto); 
}  
fun eliminarFoto(view: View) = usoLugar.ponerFoto(pos, "", foto)  

3.     Verifica el funcionamiento del nuevo botón.

NOTA: Las fotografías introducidas por el usuario pueden tener muy diversas procedencias, pudiendo tener grandes tamaños. Cuando trabajas con fotografías es muy importante que tengas en cuenta que la memoria es un recurso limitado. Por lo tanto, es muy probable que cuando trates de cargar una imagen demasiado grande, tu aplicación se detenga, mostrando en el LogCat un error de memoria. Para resolver el problema se podría cargar la imagen a una resolución menor, adecuada para un dispositivo móvil. Para ello puedes utilizar una librería de carga de imágenes como Glide o Picasso. También puedes realizar este proceso siguiendo esta documentación: https://developer.android.com/topic/performance/graphics/load-bitmap

Práctica: Confirmar borrado fotografías

1.     Si lo deseas, puedes poner un cuadro de diálogo para confirmar la eliminación. Puedes basarte en la práctica «Un cuadro de diálogo para confirmar el borrado».