Code, Freak and Videotape

Android: tomando fotos, rotando fotos.

La idea de este post es explicar un error que me ha traído serios problemas, y que es un problema evidente en android ya que aplicaciones como twitter lo tienen. El fallo ocurre al tomar una foto de la cámara  e intentar mostrarla en un ImageView. Si buscamos información sobre como tomar una foto en la documentación de google, nos encontraremos esto:

 private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100;

private Uri fileUri;

@Override

public void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.main);

    // create Intent to take a picture and return control to the calling application
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

    fileUri = getOutputMediaFileUri(MEDIA_TYPE_IMAGE);

    // create a file to save the image
    intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);

    // set the image file name
    // start the image capture Intent
    startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);

}

private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100;


@Override

protected void onActivityResult(int requestCode, int resultCode, Intent data) {

    if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE) {

        if (resultCode == RESULT_OK) {

            // Image captured and saved to fileUri specified in the Intent
            Toast.makeText(this, "Image saved to:\n" +

            data.getData(), Toast.LENGTH_LONG).show();

        } else if (resultCode == RESULT_CANCELED) {

            // User cancelled the image capture
        } else {

            // Image capture failed, advise user
        }

    }
}

 

Este código funciona bastante bien en casi todos los casos, como se puede ver se crea una uri en el media store(donde debe guardarse las fotos) para la nueva imagen, y se la pasa a la cámara para que guarde ahí los datos, además se reciben los datos a través del método getData() que nos devuelve la Uri con la imagen que ha tomado y todos felices. Pero no, parecía demasiado fácil y alguien en algún momento decidió romperlo, y nos encontramos con algunos móviles que el método getData() no nos devuelve una Uri si no que nos devuelve un bonito y rico null. Además de otro bonito bug que pondré más adelante para no liar la cosa.

Una vez que me encuentro con este bug necesito solucionarlo porque es un error muy gordo no poder tomar fotos, veo que a otras aplicaciones no les pasa, por lo que confirmo que existe algún work around para hacer que funcione, la solución que voy a poner aquí funciona en todos los móviles que he probado, pero supongo que no es la mejor, así que si alguien conoce alguna mejor a la mia, por favor ponedla en los comentarios.

Mi idea era buscar una solución que no implicara hacer soluciones específicas para un móvil en concreto, se puede hacer pero no me apetece ir metiendo reglas dispositivo a dispositivo en mi código. Por lo cual la opción de usar getData() no me molaba nada ya que me podría devolver null, uhmmm muy áspero esto.

Así que me decidí ver que había en la URI que me devolvía el media store. Ahí estaba mi foto subida como debía de ser, yo era feliz, no había problemas, hasta que en algunos móviles descubrimos que la foto está girada cuando la tomamos en portrait, sobretodo en lo móviles Samsung y Sony. Bien, estaba jodido, pero parecía que ya no tenia ningún null pointer, ahora mi problema era descubrir porque esta foto estaba girada.

Lo primero que fui a ver era la información de exif, esta información esta contenida en el jpg y se puede leer desde android, al acceder a la información con el siguiente código me di cuenta de que estaba  vacio el campo de rotación, uhmm estaba jodido porque no sabia la rotación de la imagen, y así poco iba a arreglar que no viniera girada. Con este código obtenemos la info del exif

// read jpg rotation
ExifInterface exif;

exif = new ExifInterface(Utils.getRealPathFromURI(
       mCaptureTmpImage, MyActivity.this));

String oriExif = exif
       .getAttribute(ExifInterface.TAG_ORIENTATION);

 

Donde mCaptureTempImage es la uri donde tenemos la imagen obtenida por la camara. Como veis se llama a un metodo llamado getRealPathFromUri, esto lo que hace es conseguir el nombre del fichero a partir de la URI. Os dejo el código.

 

    public static String getRealPathFromURI(Uri contentUri, Activity activity) {

        String[] proj = {
            MediaStore.Images.Media.DATA
        };

        Cursor cursor = activity.managedQuery(contentUri, proj, null, null,
                 null);

        int column_index = cursor
                 .getColumnIndexOrThrow(MediaStore.Images.Media.DATA);

        cursor.moveToFirst();
        return cursor.getString(column_index);
    }

El siguiente paso era ver de donde podía sacar esa información, ya que debía estar allí porque en la galería nativa del teléfono la foto salía correctamente. Como hemos dicho antes, android nos da acceso al media store, y además nos da acceso a la base de datos que contiene información sobre él, si accedemos a la información de la foto en la base de datos, podemos ver que existe el campo orientación y si lo obtenemos vemos que nos da la orientación correcta. 

En algunos móviles me di cuenta que ni así obtenía la orientación buena, pero vi que estos móviles además de crear la imagen que creaba yo para obtener la uri para intent, creaban otra imagen a parte en el media store. Si accedía a la info de la imagen que yo creaba la rotación valía 0 siempre, pero sin embargo si tomaba la información de la base de datos con respecto a la foto que tomaba él automáticamente venia bien la orientación, parecía que lo tenia. Aquí podemos ver como conseguir la información de la primera foto.


 public static String getLastPhotoUpload(Activity activity) {
        Cursor mGalleryCursor;

        // Do it in another thread to avoid slow load times
        String[] img = {
            MediaStore.Images.Media._ID,
            MediaStore.Images.Media.DATA
        };

        mGalleryCursor = activity.managedQuery(

        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, img, "", null,
                   "_id DESC"); // Last photos should appear before

        String id = null;
        if (mGalleryCursor != null && !mGalleryCursor.isClosed()) {
            try {

                int mIdIndex = mGalleryCursor
                      .getColumnIndex(MediaStore.Images.Media._ID);

                mGalleryCursor.moveToFirst();
                id = mGalleryCursor.getString(mIdIndex);

            } catch (IllegalStateException e) {
                Log.e(LOGTAG, "error taking photo ", e);
            }
        }
        return id;
    }

En resumen lo que hacía era tomar la foto, coger la foto que se incluía en el uri que yo creaba, por otro lado acceder a la primera foto de la base de datos, que es la que toma el móvil, y obtener la orientación buena. Ahora tenia que aplicar esta orientación a la imagen. La manera más fácil era aplicar la rotación a la imagen, pero no me gustaba esta solución ya que tenia que tener la imagen 2 veces en memoria, cosa que no me apetecía mucho ya que me podía dar algún problema en móviles “mierder”.  Por lo que mi primera opción fue intentar modificar la información del exif de la imagen y trabajar con esa imagen con él modificado. Este código lo hace:

    // write jpg rotation, sOrientation is with exif orientation format, view exifInterface doc
    ExifInterface exif = new ExifInterface(
        Utils.getRealPathFromURI(mCaptureTmpImage,
        MyActivity.this));

    exif.setAttribute(ExifInterface.TAG_ORIENTATION, sOrientation);
    exif.saveAttributes();

Primero decir que hay que tener cuidado con el método saveAttributes() ya que vuelve a crear la imagen; según pone en la documentación de android, y es bastante costoso. Pero eso no era lo peor, lo peor era que el ImageView ignoraba mis súplicas de que leyera el exif. No investigué mucho más por esta vía, es posible que se pueda hacer, pero me desquicié un poco además de que leía por stackOverflow, de gente que le desaparecía la info que habían guardado. Así que no me calenté mucho la cabeza y fui al método tradicional que aunque fuera algo costoso tenia más control sobre él.

Esta parte es bastante fácil, lo único que hice fue un método que recibía la imagen y la rotación de la misma, la carga escalada, no queremos imágenes de 4 megas y bonitos OutOfMemory. Y una vez que la tenemos escalada, creamos un buffer auxiliar donde vamos a pintar la imagen rotada. Ese código no os lo pongo para que este post no se haga eterno, además escalar y rotar una imagen es algo bastante común que podeis encontrar con una búsqueda en google. Si no lo encontráis puedo crear otra entrada con ello.

Ya lo tenia la imagen rotada y todo funcionando ahora solo falta hacer pruebas, me pongo a mirar en móviles, y parece que funciona hasta que después de unos 10 móviles pruebo el galaxy nexus y me encuentro de que este móvil no crea la copia automática de la cámara y además la imagen que creo en el media store, también tiene mal la información de la orientación en la bbdd. Solo me queda mirar si esta imagen traía bien la orientación si no, estaba fuckeado, tuve suerte, viene bien, miré en varios móviles más y por los que he visto o esta bien en la bbdd o está bien el exif. Por lo cual parece resuelto. Al final lo que he hecho a sido un código dual que lo que hace es mirar primero si la información esta bien en el exif, si no mirar en la bbdd, parece que funciona bien por ahora, pero me parece una cerdada. Éste es el código conjunto.


String lastPhotoUpload = Utils
    .getLastPhotoUpload(MyActivity.this);
   
mCaptureTmpImage = Uri.withAppendedPath(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    lastPhotoUpload);

// read jpg rotation
ExifInterface exif = new ExifInterface(
    Utils.getRealPathFromURI(mCaptureTmpImage,
    MyActivity.this));

String oriExif = exif
    .getAttribute(ExifInterface.TAG_ORIENTATION);

String orientation = "0";

//check the exif information, if dont exist read from database.
//the worst scenary if that image is not rotate and exif is good (0),
//but some handsets return always 0 :S
if ((oriExif == null) || (oriExif.equals(""))
    || oriExif.equals("0")) {

    orientation = Utils.getOrientation(lastPhotoUpload,
    MyActivity.this);

} else {

    // use exif info
    orientation = Utils.convertExifToDegree(oriExif);
}

//create the bitmap
Bitmap b = ImageUtils.resizeAndRotatePhoto(mCaptureTmpImage,
    Float.parseFloat(orientation));

Aquí os dejo el método que pasa de orientación exif a grados:

     public static String convertExifToDegree(String oriExif) {
        int oxif = Integer.parseInt(oriExif);


        switch (oxif) {
           case ExifInterface.ORIENTATION_ROTATE_90:
            return "90";

          case ExifInterface.ORIENTATION_ROTATE_180:
            return "180";

          case ExifInterface.ORIENTATION_ROTATE_270:
            return "270";

        default:
            break;
        }

        return "0";
    }

Si alguien conoce alguna solución mejor díganmela, seré muy feliz, ya que esta solución es muy costosa, creo un buffer y accedo varias veces a una base de datos :S. Sobre lo del otro bug que os decía al principio era lo de que creara dos fotos en la base de datos, una la que yo creaba y otra la que creaba el automáticamente al sacar la foto, no queda nada bonito tener 2 fotos en el media store, por lo que debía borrar una, el problema es que había móviles que solo guardaban una foto. por lo que tenia que ver cuantas fotos se habían creado antes del borrado, sencillo pero hay que tener cuidado con esas mierdas.

Bueno y con este cacho de mega entrada queda demostrado, que android no tiene nada de fragmentación, válgame el cielo. (para el lector lerdaco decir, ironic mode on).