Visitas: 637  
Tiempo total: 14 días con 2:6:55 hrs  

El método común es utilizando REST con resultados escritos en JSON pero mientras avanzas en el proyecto y empiezas a hacer pruebas, te das cuenta que las imágenes que tienen una mejor resolución generan una cadena de texto (imágenes en base64) bastante larga, tanto que ni Android puede almacenar la imagen en una sola variable String.

Problema en Android

En un servidor Php ese problema pasa desapercibido, pero en una aplicación móvil debemos de optimizar todo, para que el resultado final no empiece a utilizar toda la memoria del dispositivo móvil.

Carga y descarga

El problema de la imagen base64 puede darse tanto en la descarga como en la carga. Por ejemplo, si tenemos una dirección URL que a través de un token nos pueda responder con el nickname, nombre, apellido, correo electrónico e imagen: todo estará bien si el usuario en la interface web cargo una imagen de baja resolución, o bien si el propio sistema web tiene restricciones o redimensiona la imagen enviada automáticamente. Caso contrario, el dispositivo móvil recibirá una respuesta JSON de más de 15mb, con una respuesta de este tamaño, la aplicación Android dejara de funcionar.

Solución

Es simple: crear dos versiones de la imagen cuando el usuario carga una imagen en el sistema web: la imagen original y una vista previa para dispositivos móviles, esta vista previa debe de tener un tamaño de 320px de ancho.

En el momento en que el usuario actualiza la imagen en su dispositivo móvil, se envía la imagen utilizando el método POST y no consumiendo servicios web. Cuando el servidor recibe la imagen, debe de guardar la imagen original y la vista previa para dispositivos móviles.

Mi problema fue, no tener esto en cuenta en el momento en que empecé a desarrollar un sistema web, realizar estos cambios cuando ya todo estaba terminado, costo tiempo de desarrollo y de pruebas de nuevo, y como todo desarrollador sabe: para realizar cambios en el código finalizado, se debe de tener en cuenta que otras funciones o resultados se ven afectados con ese cambio en el código.

Android

Esta es la función Android que envía una imagen alojada en la memoria del dispositivo a una página web utilizando el método POST y utilizando el formulario multipart/form-data.

public String enviar_imagen(String imagenURL, String urlString){
HttpURLConnection conn = null;
DataOutputStream dos = null;
DataInputStream inStream = null;
String lineEnd = "\r\n";
String twoHyphens = "--";
String boundary = "*****";
int bytesRead, bytesAvailable, bufferSize;
byte[] buffer;
int maxBufferSize = 1*1024*1024*1024;
try
{
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap bitmap = BitmapFactory.decodeFile(imagenURL, options);
/**/
ExifInterface ei = new ExifInterface(imagenURL);
int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);

switch(orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:

bitmap=RotateBitmap(bitmap, 90);
break;
case ExifInterface.ORIENTATION_ROTATE_180:
bitmap=RotateBitmap(bitmap, 180);
break;
// etc.
}
/**/
OutputStream os = new FileOutputStream(imagenURL);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
os.flush();
os.close();
/**/
FileInputStream fileInputStream = new FileInputStream(new File(imagenURL));
/**/
java.net.URL url = new java.net.URL(urlString);
conn = (HttpURLConnection) url.openConnection();
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
String post="POST";
conn.setRequestMethod(post);
String keep="Keep-Alive";
String connection="Connection";
conn.setRequestProperty(connection, keep);
String multipart="multipart/form-data;boundary="+boundary;
String imagen = "Content-Disposition: form-data; name=\"imagen\";filename=\"" + imagenURL + "\"" + lineEnd+"Content-type: image/"+MimeTypeMap.getFileExtensionFromUrl(imagenURL.toLowerCase())+";"+lineEnd;
/**/
long contentLength = fileInputStream.available();
int longitud = Integer.parseInt(String.valueOf(contentLength));
/**/
conn.setChunkedStreamingMode(longitud);
conn.setRequestProperty("Content-Type", multipart);
dos = new DataOutputStream( conn.getOutputStream() );
dos.writeBytes(twoHyphens + boundary + lineEnd);
dos.writeBytes(imagen);
dos.writeBytes(lineEnd);
/**/
bytesAvailable = fileInputStream.available();
bufferSize = Math.min(bytesAvailable, maxBufferSize);
buffer = new byte[bufferSize];
bytesRead = fileInputStream.read(buffer, 0, bufferSize);
fileInputStream.close();
/**/
int totalRead = 0;
/**/
int buffersend=bytesRead/100;
/**/
dos.flush();
while (bytesRead > 0)
{
if(bytesRead-buffersend<0)
buffersend=bytesRead;
dos.write(buffer, totalRead, buffersend);
dos.flush();
bytesRead-=buffersend;
totalRead += buffersend;
int progress = (int) (totalRead * (100/(double) contentLength));
MainActivity.config.progreso.setProgress(progress);
}
dos.writeBytes(lineEnd);
dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
dos.flush();
dos.close();
}
catch(SocketException ex)
{}
catch (MalformedURLException ex)
{}
catch (IOException ioe)
{}

String str="NOT OK";
try {
inStream = new DataInputStream ( conn.getInputStream() );
String test="";
while (( test = inStream.readLine()) != null)
{
str=test;
}
inStream.close();
}
catch (IOException ioex)
{}
return str;
}

Las partes en este código que son necesarias explicar debido a su importancia (aparte de la propia función de enviar el formulario), son las siguientes:

/**/
ExifInterface ei = new ExifInterface(imagenURL);
int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);

switch(orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:

bitmap=RotateBitmap(bitmap, 90);
break;
case ExifInterface.ORIENTATION_ROTATE_180:
bitmap=RotateBitmap(bitmap, 180);
break;
// etc.
}
/**/

El código anterior permite guardar en la imagen que vamos a enviar, la orientación que tenía cuando la capturamos, de este modo se observara correctamente en la aplicación web.

/**/
long contentLength = fileInputStream.available();
int longitud = Integer.parseInt(String.valueOf(contentLength));
/**/
conn.setChunkedStreamingMode(longitud);

El código anterior permite enviar la imagen sin calcular la longitud exacta de caracteres que se van a enviar, esto es necesario en los formularios HTML porque de esta manera, el servidor que recibe la petición podrá responder que porcentaje de la información se ha recibido. En resumen, setChunkedStreamingMode es la instrucción clave a agregar en el código que estas escribiendo.

MainActivity.config.progreso.setProgress(progress);

La instrucción anterior guarda el progreso de la carga de la imagen en la barra de progreso, que definimos de la siguiente manera (en una clase externa):

MainActivity.config.progreso = (ProgressBar) findViewById(R.id.pgb_progreso);

Por último observamos que la función devuelve NOT OK o lo que el servidor destino devuelve, en mi caso siempre es OK. También observamos en el código que la imagen se envía por partes y no por completo, esto fue decisión personal (programación extrema) aunque aconsejo enviar la imagen completamente, de esta manera se evitara ver una imagen guardada con posibles errores.

Detalles a tener en cuenta

La función Android es enviar_imagen(String imagenURL, String urlString), en este caso imagenURL es la dirección de la imagen en la tarjeta de memoria o en la memoria interna del teléfono. La variable urlString es la dirección del servidor PHP que recibe la imagen POST, la cual puede ser solo una que recibe varios valores a la vez:

http://elConspirador.com/codeIgniter/ImagenForm/{token}/{tipo}/{id}/

En este caso, la URL que recibe el método POST es /codeIgniter/ImagenForm pero al mismo tiempo recibe el token del usuario, el tipo puede utilizarse de la siguiente manera:

  • Usuario: guarda la imagen en la tabla usuario
  • Ejemplar: guarda la imagen en la tabla nombre_cientifico
  • Colecta: guarda la imagen en la tabla colecta

En este ejemplo, el ultimo parámetro representa el id del registro a actualizar (si es usuario, el token sirve para actualizar la imagen de perfil), pero en cambio, también se puede estar actualizando la imagen de otro perfil establecida en el parámetro id.

Mostrar el progreso en Android

Si programamos un botón que -solo- llama a esta función, la aplicación Android se detendrá hasta que la imagen se cargue completamente en el servidor PHP, lo cual es completamente inservible.

Para llamar a la función descrita aquí, es necesario utilizar o crear un nuevo proceso. Supongamos que la función está en la clase webservice ws=new webservice(); en este caso se llama utilizando la instruccion ws.enviar_imagen(imagenURL, urlString); utilizando un nuevo proceso, de la siguiente manera:

Thread thread = new Thread() {
public void run() {
Looper.prepare();
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
ws.enviar_imagen(imagenURL, urlString);
}
}, 10);

Looper.loop();
}
};
thread.start();

Si recordamos, la función actualiza el progreso de la carga de la imagen en la barra de progreso que nosotros establecimos, debido a esto, debemos de reiniciarla antes de crear el proceso anterior:

MainActivity.config.progreso.setProgress(0);
MainActivity.config.progreso.setMax(100);

Conclusión

Guardar una imagen desde un dispositivo movil Android en un servidor externo es algo que no se debe de hacer convirtiendo la imagen a texto base64, es necesario también tener en cuenta otros aspectos que todo programador cinco estrellas debe de tener.