martes, 18 de marzo de 2008

Problema con backCommand en Netbeans Mobility Pack

Una de las funcionalidades más interesante de esta herramienta es el editor visual del flujo.

Permite rápidamente manejar el cambio de pantallas y la navegación.

Sin embargo los comandos "Back" agregados con este método tienen un comportamiento que puede no ser el deseado.

Cada vez que se cambia de pantalla, el midlet guarda la pantalla a la que se va a acceder y la que se está dejando en una HashTable.
Al ejecutar un comando "Back" se llama a switchToPreviousDisplayable(), el cual recupera para la pantalla actual cual fue la anterior.
El problema es que cuando ejecuta switchToPreviousDisplayable() también escribe en la HashTable de manera que queda switcheando en forma permanente entre dos pantallas.

Es decir
1) Se ingresa a la pantalla A
2) Se ingresa a la pantalla B
3) Se ingresa a la pantalla C
4) Se presiona back y se regresa a la pantalla B
5) Se presiona back y se direcciona a la pantalla C de nuevo!

De esta forma el flujo no vuelve a la pantalla A y si se presiona Back queda alternando siempre entre B y C.

En muchos casos sería preferible que al presionar back sucesivamente llevara a todas las pantallas recorridas en la forma explicada al final de Uso de la clase java.util.Stack

Lamentablemente tanto el código de switchToPreviousDisplayable() como switchDisplayable() no se puede modificar desde el editor.

La mejor solución sería cambiar el comportamiento de NetBeans para que genere otro código. Y la solución más rápida sin perder la funcionalidad del editor de flujo es sobreescribir la HashTable para que no considere los cambios hechos por switchToPreviousDisplayable() .


public void switchDisplayable(Alert alert, Displayable nextDisplayable) {
// write pre-switch user code here
Displayable onePrevious = null;
Displayable twoPrevious = null;
try {
Displayable current = getDisplay().getCurrent();
onePrevious = (Displayable)__previousDisplayables.get(current);
twoPrevious = (Displayable)__previousDisplayables.get(onePrevious);
} catch(Exception e) {

}

Display display = getDisplay();
Displayable __currentDisplayable = display.getCurrent();
if (__currentDisplayable != null && nextDisplayable != null) {
__previousDisplayables.put(nextDisplayable, __currentDisplayable);
}
if (alert == null) {
display.setCurrent(nextDisplayable);
} else {
display.setCurrent(alert, nextDisplayable);
}
// write post-switch user code here
if (onePrevious != null) {
if (nextDisplayable == onePrevious) {
if (twoPrevious != null) {
__previousDisplayables.put(nextDisplayable, twoPrevious);
}
}
}
}
Lo que hace es antes de salir del procedimiento "restaurar" la referencia original si es que se está volviendo a una pantalla anterior.

Notese que en realidad no captura el comando "Back" si no que lo deduce cuando la pantalla a la que se va a acceder coincide con la previa. Esto puede no aplicar en el caso en que se acceda a una misma pantalla más de una vez en un mismo flujo de navegación.

Otra solución sería encapsular todo el código de este método en un if (1==2) y reescribirlo a gusto.

Problema con ChoiceGroup en Netbeans Mobility Pack

Me topé con un inconveniente al usar Netbeans Mobility Pack. La causa es el código que genera cuando creamos una lista en el editor visual de pantallas.
Cuando agregamos elementos a un ChoiceGroup se genera el siguiente código


public ChoiceGroup getCgToAccount() {
if (cgToAccount == null) {
// write pre-init user code here
cgToAccount = new ChoiceGroup("To Account", Choice.POPUP);
cgToAccount.append("Choice Element 1", null);
cgToAccount.append("Choice Element 2", null);
cgToAccount.setFitPolicy(Choice.TEXT_WRAP_DEFAULT);
cgToAccount.setSelectedFlags(new boolean[] { false, false });
cgToAccount.setFont(0, null);
cgToAccount.setFont(1, null);
// write post-init user code here
}
return cgToAccount;
}


Si más tarde debemos modificar la lista de valores con el método append(value, null), la aplicación falla mostrando :

java.lang.ArrayIndexOutOfBoundsException
at javax.microedition.lcdui.ChoiceGroup.insertImpl(ChoiceGroup.java:1377)
at javax.microedition.lcdui.ChoiceGroup.append(+25)


Esto aparentemente sería un bug de Java, el cual al llamar a .setFont de alguna forma "congela" el tamaño de la lista y no pueden agregarse nuevos elementos sin recibir el error. Hay un tema en el foro de SDN que explica este bug en detalle: CLDC and MIDP - Problem in changing List control's font.

Por suerte el problema no se presenta si no existe una llamada a .setFont así es que para el caso de NetBeans la solución es borrar todos los elementos del ChoiceGroup y agregarlos por código. Esto por supuesto en el caso de que necesitemos modificarlos durante la ejecución.

sábado, 15 de marzo de 2008

Netbeans + Mobility Pack

Después de haber practicado un poco con Sun Java Wireless Toolkit, decidí buscar un reemplazo para el bloc de notas como editor.

Las opciones de IDE más mencionadas en la web para desarrollar en J2ME son Netbeans y Eclipse. Después de revisar varios foros decidí comenzar con Netbeans y su Mobility Pack.

Buscaba un editor que me permita diseñar las pantallas y formularios más rápido y/o más comodamente.

Me impresionó su editor visual que permite no solo manejar la interfaz si no también el flujo de la aplicación.

Permite crear los formularios, asignarles comandos, y acciones a esos comandos. Con todo lo diseñado va creando el código del midlet dejando protegidas las líneas generadas y dejando zonas libres para insertar el código propio. De esta forma libera en gran medida del trabajo repetitivo.




Otras funciones interesantes son:
- Agrega automáticamente las cláusulas "import" con el botón derecho.
- Permite renombrar y mover de paquetes a clases, métodos, etc. con la opción de "Refactoring", que cambia el nombre en todas las partes del código donde existan referencias.

Acostumbrado a trabajar con otro tipo de herramientas quede sorprendido por este IDE.

Dejo el link a una presentación para ver lo cómodo que resulta:
Visual Mobile Designer and Web Services in NetBeans IDE 6.0

jueves, 13 de marzo de 2008

Simular un celular con Firefox

Muchos sitios tienen un sistema de detección del navegador para mostar el contenido para PCs o celulares según desde donde se acceda. Caso típico es Google, Gmail, etc.

Esto se consigue examinando la "User Agent String" que una identificación que pasa el navegador al servidor del sitio web.

Así por ejemplo la de mi Firefox es:
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.3) Gecko/20070309 Firefox/2.0.0.3

La de InternetExplorer puede ser:
Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; InfoPath.1; FDM)

Mediante esta identificación un sitio puede mostrar características compatibles con tal o cual navegador o puede proveer un versión más reducida para la navegación por celular, etc.

A veces necesitamos o queremos navegar un sitio desde la PC para acceder al contenido para celulares, por ejemplo para descargar Google Maps para móviles en la PC.

En esos casos podemos cambiar el user agent string de Firefox para que pretenda ser un teléfono celular.

Lo primero es obtener un UAS de un teléfono. La del Sony Ericsson w580 es:
SonyEricssonW580i/R6BC Browser/NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1

Para averiguar la cadena de un navegador se puede ingresar a:
http://www.tarkasys.com/ua/ua.php

En ese enlace se muestra solamente la cadena del navegador con el cual accedimos. Si ingresamos a esa página desde el celular nos dirá su cadena.

Luego en la barra de direcciones de Firefox tenemos que tipear about:config que abre una lista de preferencias.

Hacer click con el botón derecho en la lista, seleccionar "New", "String" e ingresar "general.useragent.override" como nombre de la preferencia y el User Agent String que queramos en el valor.

Con esto convertimos a Firefox en un celular y podemos acceder al contenido móvil como este:

miércoles, 12 de marzo de 2008

Leer y escribir archivos en J2ME

En muchos casos se necesita leer y escribir archivos a las unidades de almacenamiento del teléfono. Uno de los escenarios posibles es la exportación e importación de datos.

En esta nota voy a mostrar breves ejemplos de como realizar estas tareas utilizando la API FileConnection.

La clase FileConnection forma parte de los paquetes opcionales a J2ME. Por ese motivo al crear un nuevo proyecto en Sun Wireless Toolkit, al momento de elegir la API, debemos marcar:
Target Platform = JTWI y tildar PDA Profile for J2ME (JSR 75)


Hay que destacar que al ser una especificación optativa puede no estar presente en todos los teléfonos o dispositivos. En general todos los teléfonos actuales lo soportan, pero no es así para los modelos viejos.

En el emulador el sistema de archivos "virtual" se encuentra en la carpeta "..j2mewtk\2.5.2\appdb\DefaultColorPhone\filesystem\".

Dentro de esta carpeta podemos encontrar otra llamada root1. Cada carpeta en "filesystem" es tomada como una unidad o root por el emulador. En el télefono cada root corresponderá a un dispositivo de almacenamiento diferente, en la mayoría de los casos uno será la memoria interna del teléfono y otro la tarjeta de memoria.

El primer ejemplo trata de como listar todos los roots disponibles. Esto se logra utilizando la clase FileSystemRegistry:

public void SelectRoot() {
Enumeration rootListEnum = FileSystemRegistry.listRoots();
listRoots = new List("Elija el root:", Choice.EXCLUSIVE);
while (rootListEnum.hasMoreElements()) {
String rootName = (String) rootListEnum.nextElement();
listRoots.append(rootName, null);
}
...


Este fragmento utiliza la función listRoots() que devuelve una lista de los roots y luego la carga en un control de lista para mostrarla en la pantalla.

Para listar el contenido de un directorio:

Display.getDisplay(this).setCurrent(WaitForm);
Thread t = new Thread() {
public void run() {
try{

FileConnection myFc = (FileConnection)
Connector.open("file:///" + currentRoot);
Enumeration fileEnum = myFc.list("*", false);
while(fileEnum.hasMoreElements()) {
String fileName = (String) fileEnum.nextElement();
listContents.append(fileName, null);
}
} catch (Throwable th) {
System.out.println(th.toString());
}
}
};
t.start();



Las tres primeras líneas son obligatorias cuando estemos utilizando un CommandListener por lo expuesto en Acceso a archivos y threads, su finalidad es evitar que la aplicación se cuelgue si hay un problema en las operaciones de manejo de archivos.

En lo demás este caso es similar al de listado de roots, utilizando el método "list".

Finalmente para escribir un archivo debemos utlizar además un Data Stream:


Thread t = new Thread() {
public void run() {
try{

FileConnection conn = (FileConnection) Connector.open(writeFile, Connector.READ_WRITE);
if (!conn.exists()) {
conn.create();
}
OutputStream outSt = conn.openOutputStream();
PrintStream printSt = new PrintStream(outSt);
printSt.println(writeContents);
conn.close();

} catch(IOException e) {
System.out.println("IOException in WriteToFile: " + e.toString());
} catch(SecurityException e) {
System.out.println("SecurityException in WriteToFile: " + e.toString());
}
}
};
t.start();


La referencia de este paquete opcional se encuentra en:
JSR-000075 PDA Optional Packages for the J2METM Platform. Ahí puede descargarse las especificaciones de FileConnection con todas sus clases y métodos.
Y una serie de artículos sobre esto en:
Mobile Service Architecture - JSR 75



El código completo de esta clase ejemplo es:
import javax.microedition.midlet.MIDlet;
import javax.microedition.io.file.*;
import java.io.*;
import java.util.*;
import javax.microedition.io.*;
import javax.microedition.lcdui.*;

public class FileDemo extends MIDlet implements CommandListener {
List listRoots, listContents;
private String currentRoot, currentPath, writeFile, writeContents;
private Command cmOK,cmExit, cmNew;
private Form WaitForm;

public void FileDemo() {
}

public void startApp() {
WaitForm = new Form("Espere por favor...");
WaitForm.setCommandListener(this);
cmExit = new Command("Exit", Command.EXIT, 1);
cmNew = new Command("New", Command.ITEM,2);
SelectRoot();
}

public void pauseApp() {
}

public void destroyApp(boolean condition) {
notifyDestroyed();
}

public void SelectRoot() {
Enumeration rootListEnum = FileSystemRegistry.listRoots();
listRoots = new List("Elija el root:", Choice.EXCLUSIVE);
while (rootListEnum.hasMoreElements()) {
String rootName = (String) rootListEnum.nextElement();
listRoots.append(rootName, null);
}
cmOK = new Command("OK", Command.ITEM, 1);

listRoots.addCommand(cmOK);
listRoots.addCommand(cmExit);
listRoots.setCommandListener(this);
Display.getDisplay(this).setCurrent(listRoots);


}

public void ShowContents() {
listContents = new List("Contenidos de " + currentRoot, Choice.EXCLUSIVE);
Display.getDisplay(this).setCurrent(WaitForm);
Thread t = new Thread() {
public void run() {
try{

FileConnection myFc = (FileConnection)
Connector.open("file:///" + currentRoot);
Enumeration fileEnum = myFc.list("*", false);
while(fileEnum.hasMoreElements()) {
String fileName = (String) fileEnum.nextElement();
listContents.append(fileName, null);
}
} catch (Throwable th) {
System.out.println(th.toString());
}
}
};
t.start();
listContents.addCommand(cmExit);
listContents.addCommand(cmNew);
listContents.setCommandListener(this);
Display.getDisplay(this).setCurrent(listContents);

}

public void WriteToFile(String fileName, String contents) {
writeFile = fileName;
writeContents = contents;

Thread t = new Thread() {
public void run() {
try{

FileConnection conn = (FileConnection) Connector.open(writeFile, Connector.READ_WRITE);
if (!conn.exists()) {
conn.create();
}
OutputStream outSt = conn.openOutputStream();
PrintStream printSt = new PrintStream(outSt);
printSt.println(writeContents);
conn.close();

} catch(IOException e) {
System.out.println("IOException in WriteToFile: " + e.toString());
} catch(SecurityException e) {
System.out.println("SecurityException in WriteToFile: " + e.toString());
}
}
};
t.start();
}

public void commandAction(Command cm, Displayable d) {
//System.out.println("En commandAction");
if (cm == cmOK && d == listRoots) {
System.out.println("Elegiste: " + listRoots.getString(listRoots.getSelectedIndex()));
currentRoot = listRoots.getString(listRoots.getSelectedIndex());
ShowContents();
} else if (cm == cmExit) {
destroyApp(false);
} else if (cm == cmNew) {
WriteToFile("file:///" + currentRoot + "newfile.txt","Sample, sample, sample");
}
}
}

martes, 11 de marzo de 2008

Acceso a archivos y threads

Corriendo un ejemplo de escritura de archivos en Sun Java Wireless Toolkit sucedía que la aplicación se colgaba al momento de abrir el archivo.

En la consola se mostraba este mensaje:
Warning: To avoid potential deadlock, operations that may block, such as
networking, should be performed in a different thread than the
commandAction() handler.


El mismo problema aparecía en el ejemplo de lectura de un directorio que estaba escribiendo. En ambos casos la aplicación se colgaba al llamar a Connector.open.

La advertencia en cuestión quedaría traducida a:
"Para evitar posibles puntos muertos, las operaciones que pueden bloquear, como conexiones de red, deben ser ejecutadas en un hilo (thread) diferente que el administrador de commandAction()"

El "commandAction() handler" sería la clase que especificamos para manejar la interacción con el usuario mediante el método setCommandListener.

Como un conexión de red o un acceso a un archivo puede dejar inutilizable la interfaz de usuario, debe correrse en un thread separado.

Una muy buena explicación de esto puede encontrarse en:
Networking, User Experience, and Threads

En cuanto a la solución de este error en particular fue cambiar este código:


public void ShowContents() {
listContents = new List("Contenidos de " + currentRoot, Choice.EXCLUSIVE);

try{

FileConnection myFc = (FileConnection)
Connector.open("file:///" + currentRoot);
Enumeration fileEnum = myFc.list("*", false);
while(fileEnum.hasMoreElements()) {
String fileName = (String) fileEnum.nextElement();
listContents.append(fileName, null);
}
} catch (Throwable th) {
System.out.println(th.toString());
}

listContents.addCommand(cmExit);
listContents.setCommandListener(this);
Display.getDisplay(this).setCurrent(listContents);
}

Por este:

public void ShowContents() {
listContents = new List("Contenidos de " + currentRoot, Choice.EXCLUSIVE);
Display.getDisplay(this).setCurrent(WaitForm);
Thread t = new Thread() {
public void run() {
try{

FileConnection myFc = (FileConnection)
Connector.open("file:///" + currentRoot);
Enumeration fileEnum = myFc.list("*", false);
while(fileEnum.hasMoreElements()) {
String fileName = (String) fileEnum.nextElement();
listContents.append(fileName, null);
}
} catch (Throwable th) {
System.out.println(th.toString());
}
}
};
t.start();
listContents.addCommand(cmExit);
listContents.setCommandListener(this);
Display.getDisplay(this).setCurrent(listContents);

}

De acuerdo a lo explicado en el artículo citado.

Para ver un ejemplo de como listar roots y escribir archivos ir a:
Leer y escribir archivos en J2ME

Uso de la clase java.util.Stack en flujo de aplicaciones J2ME

Una pila (stack en inglés) es una estructura de datos de tipo LIFO (del inglés Last In First Out, último en entrar, primero en salir) que permite almacenar y recuperar datos. Se aplica en multitud de ocasiones en informática debido a su simplicidad y ordenación implícita en la propia estructura.

Para el manejo de los datos se cuenta con dos operaciones básicas: apilar (push), que coloca un objeto en la pila, y su operación inversa, retirar (o desapilar, pop), que retira el último elemento apilado. Ver Pila (estructura de datos) en Wikipedia.

Java provee una implementación de esta estructura de datos en la clase java.util.Stack que cuenta con los métodos empty, peek, pop, push y search.

Push: agrega un elemento a la pila.
Pop: recupera el último elemento y lo elimina.
Peek: recupera el último elemento sin eliminarlo de la pila.

Un ejemplo de utilización de esta clase puede ser para almacenar el flujo de una aplicación J2ME como se ve en este ejemplo adaptado del código de Cash2ME:


package net.sf.cash2me;

import java.util.Stack;

import javax.microedition.lcdui.Displayable;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;

public class Cash2ME extends MIDlet {

private Displayable currentScreen; // La pantalla actual
private Stack screenStack; // contiene todas las pantallas previas

/**
* Recibe un objeto Displayable y lo configura como Current de Display, es decir
* lo muestra en la pantalla.
* @param screen El objeto Displayable a mostrar.
*/
private void setCurrent(Displayable screen) {
if (currentScreen == null || screen != currentScreen) {
if (screenStack == null)
screenStack = new Stack();
screenStack.push(currentScreen); //almacena la pantalla actual en la pila.
currentScreen = screen;
}
Display.getDisplay(this).setCurrent(currentScreen); //muestra la pantalla recibida
}

/**
* Muestra la pantalla anterior.
*/
public void showPrevious() {
if (screenStack != null) {
/*Controla si hay más de una pantalla en la pila*/
Displayable screen = screenStack.size() > 1
/*Si hay más de una pantalla en la pila recupera la última y la elimina de la pila.*/
? (Displayable)screenStack.pop()
/* Si queda solo una, utiliza el método peek para recuperar la última pantalla
sin borrarla de la pila.*/

: (Displayable)screenStack.peek();
if (screen != null)
Display.getDisplay(this).setCurrent(currentScreen = screen);
}
}
}


En este ejemplo en lugar de recurrir directamente al método Display.setCurrent se recurre a estos métodos para llevar un "historial" de las pantallas recorridas. De esta forma cada vez que a lo largo de la aplicación se muestra una pantalla nueva, primero se guarda la pantalla actual en la pila y luego se muestra la nueva.

Cuando la aplicación necesita volver a la pantalla anterior, recupera la última pantalla, que estará arriba de la pila, la muestra y la elimina, usando el método pop.

Si se llega al final de la pila, quedando una sola pantalla en la pila, se utiliza el método peek para recuperar dicha pantalla sin eliminarla.

Más Información
Otro ejemplo de pilas en java: How do I use stacks in Java?
Referencia de la clase Stack: Class Stack