Profile cover photo
Profile photo
Israel Urdiales
74 followers -
Web dev & sysAdmin
Web dev & sysAdmin

74 followers
About
Israel's posts

Post has shared content

Post has attachment

Post has attachment

Post has attachment

Post has attachment

Post has attachment

Post has attachment

Post has attachment

Post has attachment

Post has shared content
Programar a ciegas, o por prueba y error, el peor defecto de la "ingeniería del software"

Un rant rápido

Estaba acostumbrado a "programar en la jungla", HTML que funcionan en un navegador y no en otro, javascripts que fallan, versiones malas de jQuery, agujeros de seguridad, DDoS, consultas muy lentas a la base de datos porque no dominas a fondo el SQL ni cómo gestiona los índices el gestor de base de datos. Ya tenía mucho de programación de prueba y error, pero al final descubres una lógica, qué funciona, los patrones... y te acostumbras.

Ahora estoy poseído programando en Android para un proyecto que comenzamos. Quería "rascarme un grano", y no se me ocurrió mejor manera que hacerlo yo sólo, aunque llevaba 20 años sin tocar nada de Java (que cambió muchísimo), y sin idea de Android.

Antes de comenzar a programar la primera línea, fui muy responsable, compré varios libros "buenos y caros" (llevo nueve leídos en un mes, mas dos de Java), pasé muchísimas horas leyendo http://developer.android.com/, probando e intentando entender los ejemplos, etc. etc. En resumen, estudié e intenté, con mucho esfuerzo, comprender bien los patrones de progamación buenos, y la lógica detrás de cada clase importante del API (imposibles todas, hay demasiado, más las de Java estándar).

Aunque el API y la forma de programar (por actividades, sin que el ciclo de vida de la aplicación esté bajo tu control) son muy divertidas a intelectualmente potentes, me parecía todo una construcción endeble: 15 versiones del API, cada uno con características nuevas que no funcionan en las anteriores, salvo las que están en las "support libraries", que no son justamente las que más necesitas.

Por ejemplo, te dicen continuamente "pensamos en los desarrolladores, bla bla bla, sóis el alma, os quitamos trabajo de encima". Con ello justifican tantas versiones nuevas con caracteristicas cada vez más rebuscadas. Si el AsyncTask empotrados en clases anónimas con el casi abuso de los "generics" de Java, o los Adapters (que lo explican de mil formas, salvo para lo más habitual, cursores al SQLite), ahora te encuenras con los Fragments o los Loaders. ¡Hostia! Me costó menos entender el tema de mi tesis doctoral.

¿Funcionan o no funcionan en versiones anteriores? ¿están en el "support package" para dar compatibilidad? Puf, te dejo el trabajo de averiguarlo, pero te quito otro: lo más nuevo a "impresionante" que introdujeron son la interfaz Holo, las barra de acciones (Action Bar) que reemplazan al menú, la gestión de cuentas de usuario, la actividad de edición de prefrencias, el "Share Builder"... pero ninguna de ellas funciona en las versiones anteriores.

Así, te tomas el trabajo de verificar la versión del API para no incluir código no compatible con las versiones anteriores. Como el de la barra de acciones... En una actividad compleja la oculto directamente, sólo uso las funciones tradicionales del menú. En Gingerbread funciona correctamente, pero resulta que el el Honeycomb e ICS en el menú no aparece nada... porque había ocultado el "action bar". Y me dí cuenta sólo probando en teléfonos diferentes, ninguno de los libros, ni los manuales me indicó (al menos claramente) que pasaba eso.

Pura programación de prueba y error, sin lógica alguna, sin explicaciones.

Pero no es lo peor. En los libros y documentación oficial puedes leer lo maravillosa que es la nueva edición de preferencias, y lo fácil y potente que es. Te lo recomiendan, ¡porque quita trabajo al programador!... justo antes de decirte que si prorgamas para teléfonos pre ICS (o sea, el 99% de los teléfonos en el mercado), debes también implementarla con el método antiguo. ¡Doble trabajo!, pero maravilloso, y útil, para masturbarse mirando algo que hiciste y no sirve al 99%.

También dicen en todos los libros y documentación: el API de gestión de cámara es maravilloso, puedes lograr "awesome user experiences" empotrando tu propia cámara en tu aplicación. Pensé que si dicen eso, debe ser casi una obligación, y en ello me metí. Pufff, cientos de horas, otras vez, de prueba y error.

Al principio piensas que debe ser simple, si todo el mundo lo hace, y en las primeras pruebas ya vez que el preview te queda invertido, o deformado. Arreglas eso, y cuando después de decenas de horas lo logras, vas a mirar las fotos, y aparecen en cualquier posición menos en la que toca. Así empiezas a mirar todo lo que hay, y te encuentras que tienes que tratar:

1. Posición natural del teléfono/tablet (portrait para teléfonos, landscape para tablets) que definen los "0 grados" de rotación del teléfono.

2. Rotación de la cámara respecto a la posición natural del teléfono (habitualmente 0 para tablets, 90 grados para teléfonos).

3. Tamaño del preview (¡amigo! da para medio libro sólo eso) para que corresponda con los tamaños disponibles para el hardware.

4. Rotación del preview, que depende de las rotación 1 y 2, pero que además van en sentido inverso. La cámara cuenta los grados de una forma, el teléfono a la inversa.

5. El tipo de cámara, si es la "back facing" (la normal) cuenta los grados y rotación de una forma, la "face facing" (la que apunta a tu cara) lo cuenta de otra forma, y además está "flipped".

Todas las rotaciones anteriores debes tenerla en cuenta para el preview, y además para:

6. Rotación de la imagen/fotografía, que va en su "propio" espacio.

Buscas funciones que las calculen, hay incluso una función de ejemplo "oficial" en http://developer.android.com/reference/android/hardware/Camera.Parameters.html#setRotation(int)

Además de que te enteras que tienes que poner un callback para el sensor de rotación en tiempo real del teléfono (que consume CPU y batería), está mal, no calcula la rotación correctamente (la parte de la fórmula que está mal es la que está abajo)

if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
mImageRotation = (info.orientation - orientation + 360) % 360;
} else { // back-facing camera
mImageRotation = (info.orientation + orientation) % 360;
}


Luego de pasar decenas de horas probando con un Nexus logras encontrar la fórmula y combinación correcta de sumas, restas y módulos. Te felicitas, se ve bien. Pero vas a probar en otro teléfono y te encuentras con todas las fotos giradas.

¡Me cago en la madre! Si decían que era fácil.

Luego de otras decenas de horas encuentro el problema:

7. La rotación que se pone en el EXIF de la fotografía.

Así, el Galaxy Nexus siempre guarda con rotación 0 en el EXIF, es decir, las rota adecuadamente según los grados que se le indica en el función comentada antes. Pero otros teléfonos (como el Sony Xperia Neo, o el Xoom) no rotan la imagen, la guardan tal cuál han sido capturadas y pone ese ángulo en el EXIF.

Vale, pero si está en el EXIF, ¿por qué se ven mal cuando las abro y muestro en el programa?

Luego de horas y horas, encuentro que las funciones de "Bitmap factory" que se usan (y no hay otras) para leer o grabar las imágenes, descartan los datos del EXIF, completamente.

Buscando en Google (que siempre te lleva a Stackoverflow -parecen spammer-seo de todo lo que tenga que ver con programación de Android) lees un comentario sobre eso, y las clases para manipular el EXIF.

¡Me cago en la madre que lo parió! ¡Eso debería estar en letras rojas por todos sitios!

Pero bueno, si todos dicen que es fácil, y en todos los ejemplos de los libros no te muestran ni comentan estos problemas, supongo que el burro soy yo. Y no cuento los trucos que hay que hacer para rotarla y dejarla en jpeg que se vea bien... porque es imposible leer el EXIF desde el buffer en memoria que devuelve la cámara. Debes grabarlo en un fichero, y luego leer el EXIF desde ese fichero (o no encontré la forma de hacerlo, a pesar que busqué mucho).

No acaban aquí las cosas, casi todo lo medianamente complejo que necesitas hacer para una aplicación que aproveche bien los recursos del teléfono obligan a que te olvides de aplicar la reflexión para convertirte simplemente en un ratón de laboratorio.

Por ejemplo, has arreglado lo de la cámara e imágenes, funciona perfectamente. Pero luego tienes que gestionar el área donde se muestra el preview (en un SurfaceView), y ponerle botones de "overlay" (como esos que véis de flash, focus, etc.). Puf, otra odisea, en contra de lo que te dicen los manuales.

Lo primero, si quieres hacer overlay debes poner el SurfaceView por debajo de otro Layout (es decir, como hermano, pero en posiciones previas). Pero para ello necesitas que ambos sean hijos de otro Layout. ¿Cuál usar? Por los tipos de layout que hay, el más adecuado es el FrameLayout, peor en la documentación dice explícitamente que el FrameLayout sólo sirve para un hijo. Pruebas con otros, no va, miras ejemplos de libros, unos te recomiendan (con ejemplos oficiales de Google) que hagas una subclase de ViewGroup. Vale lo haces, pero en cuanto intentas implementar funciones como onMeasure te dicen que no puedes llamar a super.onMeasure() (o onLayout, o onDraw) porque ViewGroup es una clase abstracta, debes implementarlo todo en tu función.

Vale, dejas todo, tiras el código, y buscas más ejemplos, y caes en uno de los demos oficiales sobre SurfaceView (pero aplicado a 3D y GL) donde funciona... pero para contener al SurfaceView y a los otros layout y botones, usa un FrameLayout... ¡que en la documentación dice que sólo sirve para un hijo!

Malditos hijos.

Empiezas de nuevo, ahora haciendo una subclase de FrameLayout para contener a todas las vistas de tu "cámara". La parte más importante es reprogramar el onMeasure() y onLayout() para que les des el tamaño que toca al SurfaceView, ya que éste depende de los tamaños de preview que permite la cámara, cosa que suelen pasar por alto en los ejemplos.

Miras los ejemplos, implementas el onMeasure(), el "contrato" dices que tienes que calcular el tamaño y llamar a la función setMeasuredDimension(width, height), que no debes llamar al super.onMeasure().

Lo haces, y no te funciona. Sin errores, ni avisos, simplemente no se ven los botones en overlay que deberían estar. Pasas decenas de horas debuggeando, pones Log.d() por todos sitios, pides el tamaño del Layout que contienen a los botones, te salen los tamaños correctos. Pero no va.

Al final reprogramas de nuevo con sólo las funciones básicas sin llamar a nada de cámara o lógica más complicada. Después de horas y horas de prueba y error, encuentras que la única forma que funciones correctamente es llamando primero a super.onMeasure() y luego a setMeasureDimension.

Era ese el problema, ¡lo que decían que no hay que hacer!, y sólo descubierto por prueba y error (y eso que gasté más de 300€ sólo en libros de programación Android).

Cuando crees resuelto ese problema, pasas a onLayout() para poder fijar correctamente el tamaño para que sea exactamente igual a los tamaños ofrecidos por la cámara. Y de nuevo, casi lo mismo. Te dicen que debes llamar tú al onLayout() para todos los hijos, cosa que no logré hacer que funcione, sólo iba con el super.onLayout(), pero como dejaba mal el tamaño del SurfaceView, inmediatamente a continuación he de llamar a mi propia función:

surfaceLayout(l, t, r, b);

¡Ah!, pero no lo hagas dentro del if (changed) que proponen (en ejemplos y libros), porque no funciona, cuando se termina de configurar el SurfaceView llamas a resquestLayout(), pero tu función nunca se ejecuta porque no cambiaron las dimensiones de los layouts anteriores. Debes hacerlo siempre:


@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
super.onLayout(changed, l, t, r, b);
}
surfaceLayout(l, t, r, b);
}
Y controlar con tus propias variables que ya has hecho el layout, para no generar llamada sen cascada innecesarias:

protected void surfaceLayout(int l, int t, int r, int b) {
if (mPreviewSize == null || mSurfaceLayoutDone) return;
...
}


Podría continuar con más, pero creo que la idea está clara: la mayor parte del tiempo los problemas fueron solucionados por prueba y error. Algo bastante patético para una [presunta y wannabe] ingeniería que está basada en una ciencia puramente formal, lógica y de creación humana.

¿Os imagináis que las oras ingenierías, o incluso el hardware, funcionasen de la misma forma? Entiendo que el principal problema es de la complejidad, que no se da en objetos físicos, pero estamos haciendo muy poco para minimizar los efectos de esa compejidad. Estamos agregando capas de abstracción cada vez más complejas... y llenas de incertidumbres que ni se encuentran en la mecánica cuántica. Los ejemplos que cité son ejemplos. Creo que los chicos de Android deberían dejar de sacar tantas versiones nuevas del API (ya vamos por la 15), y solucionar antes todos estos problemas, limpiar la basura, dejar que maduren las cosas, que se escriban libros y código de ejemplos que realmente sirvan para la generalidad de los casos y no sólo para ejemplos simples y con una versión determinada.

Me considero un programador relativamente bueno, apasionado por la informática y la programación (me puedo pasar 24 horas programando sin parar, y muy feliz de poder hacerlo), soy reflexivo (lo contrario que pica códgio), no escatimo gastos en libros, leo todo lo que puedo... y aún así me sentí un estupido. O esto de prueba y error no es para mí (espero que sea lo segundo).

Lo que no entiendo es que la mayoría de programadores acepten esta situación (¡qué es lo que aplauden en Google cada vez que presentan un nuevo API con nuevas chorradas incompatibles con la mayoría, millones, de teléfonos). Quizás es que sea más fácil la prueba y error que "competir" dominando realmente lo que se está haciendo. Pero si es así, que nos cojan ionizados cuando todo se nos venga encima.

En realidad creo que debemos empezar a rebelarnos contra situación a las que el "mercado informático" nos mete. No hablo por temas mercantilistas o sindicales, solamente de lo que nos atañe: no vamos a construir una ingeniería, ni una profesión, ni siquiera una "artesanía" madura y atractiva si sólo podemos hacer ingeniería por prueba y error. Es de locos.

PS1: Instagram se quitó de encima muchos de los problemas de gestión de imágenes que comento haciendo que sea cuadrada (supongo que el iOS debe tener problemas similares). Pero como no conozco ninguna cámara de Android que permita imágenes (y preview) cuadrados, lo que hacen es suporponer una capa que tape parte de la imagen, y luego la recortan por software (podéis comprobarlo en la galería, las originales tomadas con la cámara de Instagram son todas rectangulares). Una chapuza muy creativa, pero no deja de ser chapuza innecesaria.


PS2: Cuando había casi acabao este post, intento agregar una imagen arrastrando, al situarme sobre esta caja cambia el borde, así que asumo que acepta la imagen para subirla (como se hace en Menéame), con la sorpresa que el navegador abrió el URL la imagen, y perdí todo lo que había escrito. Menos mal que había hecho copy&paste, unos 15 minutos antes, a un documento en GDocs. Aún así tuve que volver a escribir desde la función que está más arriba, no había ni "borrador", a pesar que Google lo hace hasta en Gmail. Otro ejemplo de la ilógica y maltrato a los usuarios a los que estamos tan acostumbrados.
PhotoPhotoPhoto
3 Photos - View album
Wait while more posts are being loaded