Mostrando entradas con la etiqueta J2ME. Mostrar todas las entradas
Mostrando entradas con la etiqueta J2ME. Mostrar todas las entradas

lunes, 17 de mayo de 2010

J2ME, Java Wireless Message API (WMA)

Sin lugar a dudas, la mensajería instantánea a través de mensajes cortos (SMS) es una de las formas de comunicación más extendida y aceptadas por la sociedad.
En este tutorial voy a intentar hacer una introducción de las características más importantes que nos proporciona Java para el envío y recepción de SMS desde aplicaciones para móviles (MIDLets). Se presupone que el lector ya posee conocimientos básicos de programación (J2ME, MIDP, CLDC), compilación e instalación de MIDLets.

Introducción a WMA

WMA, son las siglas de Wireless Message API (curiosamente también lo son de uno de los formatos de audio de Windows, Windows Media Audio), una extensión de la especificaciones del CLDC y MIDP para el envio, la recepción y la gestión de SMS desde MIDLets.
A pesar de ser una extensión opcional, la gran mayoría de los terminales la llevan instalada y lista para ser usuada desde nuestras aplicaciones J2ME.
WMA es una especificación no una implementación. Su implementación dependerá del terminal y del protocolo de comunicación que use (GSM, CDMA, etc). Por supuesto, nosotros como desarrolladores podemos abstraernos de esto último.
Versiones de la especificación WMA:
Versión 1.0: Especificación inicial. Describe las funcionales básicas para el envío y la recepción de SMS. (JSR 120)
Versión 1.1: Una ampliación de la especificación 1.0, para soportar el nuevo modelo de seguridad del MIDP 2.0.
Versión 2.0: Una ampliación a las anteriores para la gestión de mensajes multimedia (MMS). (JSR 205)

Introducción al API WMA

El API está compuesto exclusivamente de interfaces ubicadas bajo el páquete javax.wireless.messaging. Estas interfaces son:
javax.wireless.messaging.Message: Define la funcionalidad genérica de todos los tipos de mensajes. Permite:
  1. Especificar el destinatario del mensaje. public void setAddress(String addr)
  2. Obtener el emisor del mensaje. public String getAddress()
  3. Obtener la fecha de envio del mensaje. java.util.Date getTimestamp()
javax.wireless.messaging.TextMessage: Representa a un mensaje de texto.
Hereda la funcionalidad de javax.wireless.messaging.Message añadiendo los métodos public void setPayloadText(String data) y public String getPayloadText() para especificar u obtener los datos del mensaje.
javax.wireless.messaging.BinaryMessage: Representa a un mensaje binario.
Hereda la funcionalidad de javax.wireless.messaging.Message añadiendo los métodos public void setPayloadData(byte[] data) y public byte[] getPayloadData() para especificar u obtener los datos del mensaje.
javax.wireless.messaging.MessageListener: Oyente de mensajes entrantes.
Esta interfaz es útil en javax.wireless.messaging.MessageConnection que funcionan en modo servidor. (Será explicada más adelante.)
javax.wireless.messaging.MessageConnection: Interfaz a través de la cual se realiza el envío y la recepción de mensajes. (Será explicada más adelante.)

¿Qué pasos tengo que realizar para enviar un mensaje (SMS)?

  1. Obtener un MessageConnection en modo cliente (Se verá más adelante).
  2. Crear el mensaje a través de la interfaz MessageConnection.
  3. Especificar el contenido y el destinatario del mensaje.
  4. Usar el método send de la interfaz MessageConnection para enviar el mensaje.

¿Qué pasos tengo que realizar para recibir un mensaje (SMS) de forma asíncrona?

  1. Obtener un MessageConnection en modo servidor (Se verá más adelante).
  2. Implementar la interfaz MessageListener en una de nuestras clases.
  3. Asociar la clase anterior al MessageConnection.
  4. Cuando el método notifyIncomingMessage() de la interfaz MessageListener sea invocado, significa que hemos recibido un mensaje.
  5. Deberemos invocar el método receive() de la interfaz MessageConnection en un hilo independiente.
  6. Realizar el tratamiento del mensaje.

¿Qué pasos tengo que realizar para recibir un mensaje (SMS) de forma síncrona?

  1. Obtener un MessageConnection en modo servidor (Se verá más adelante).
  2. Invocar el método receive() de la interfaz MessageConnection en un hilo independiente (Es un método bloqueante).
  3. Realizar el tratamiento del mensaje.

En J2ME, todas las comunicaciones que requieren los MIDLets con el exterior (Bluetooth, Socket, Http, etc.) se obtienen a través la clase javax.microedition.io.Connector que forma parte del CLDC. Esta clase devuelve una instancia de una clase que implementa la interfaz javax.microedition.io.Connection para el modo de comunicación deseada. Pues bien, la interface javax.wireless.messaging.MessageConnection no es más que un javax.microedition.io.Connection para comunicación via SMS.
Básicamente a través de está inteface podemos crear, enviar y recibir SMS tanto de forma síncrona como asíncrona.

¿Cómo obtengo javax.wireless.messaging.MessageConnection?

Los javax.wireless.messaging.MessageConection pueden funcionar de dos modos:
  1. En modo cliente: Sólo sirve para enviar SMS a un destinatario.
  2. En modo servidor: Sirve para recibir y tratar los SMS que son dirigidos hacia el. En esté modo también se pueden enviar SMS.
La forma de especificar el modo de funcionamiento deseado se realiza a través de la cadena de conexión que se le pasa a la clase javax.microedition.io.Connector.
Ejemplos de como especificar el modo cliente:
MessageConnection conn = (MessageConnection) javax.microedition.io.Connector.open("sms://+34666666666");
MessageConnection conn = (MessageConnection) javax.microedition.io.Connector.open("sms://+34666666666:5555"); En ambos casos el SMS sería enviado al teléfono 666666666 de España (por el prefijo +34), pero con una importante diferencia. En el primer caso, el SMS sería tratado por la aplicación que por defecto tiene instalada el teléfono, mientras que, en el segundo caso, el SMS sería tratado por la aplicación que esté escuchando en ese puerto.
Ejemplo de como especificar el modo servidor:
MessageConnection conn = (MessageConnection) javax.microedition.io.Connector.open("sms://:5555");
Los SMS que llegen al puerto 5555 serán atendidos por el MIDLet. En realidad, "5555" NO es un puerto sino un IDENTIFICADOR que le indica a la plataforma java que desea tratar los SMS que llegen al terminal con ese identificador.

Métodos de la clase:

public javax.wireless.messaging.Message newMessage(java.lang.String type, java.lang.String address)
Crea un nuevo mensaje para ser enviado.
En el argumento type especificamos el tipo de mensaje que deseamos enviar:
   MessageConnection.TEXT_MESSAGE para mensajes de texto
   MessageConnection.BINARY_MESSAGE para mensajes binarios.
En el argumento address debemos especificar la dirección del destinatario del mensaje. Normalmente, su número de teléfono.
public int numberOfSegments(javax.wireless.messaging.Message msg)
Devuelve el número de segmentos que son necesarios para enviar la información a través de la red.
Por ejemplo, si queremos enviar el Quijote via SMS pues seguro que son necesarios más de un segmento (un segmento es igual a un SMS como lo conocen los usuarios) y este método nos devolvería o bien un número grande o bien el valor 0 indicando que no se puede enviar la información deseada.
public javax.wireless.messaging.Message receive() throws IOException, InterruptedIOException
Devuelve un mensaje enviado a nuestra aplicación. Hay que tener varias cosas importantes en mente:
  1. Es un método bloqueante, por lo que generalmente deberá ser invocado en un hilo distinto al hilo principal donde está ejecutandose el Midlet.
  2. Nuestra aplicación es responsable de guardar la información recibida a memoria no volátil en caso de ser necesario.
public void send(javax.wireless.messaging.Message msg) throws IOException, InterruptedIOException
Envia el mensaje al destinatario.
Este método debe ser invocado en un hilo distinto al hilo principal donde está ejecutandose el Midlet.
Para especificar el destinatario del mensaje se debe usar el método setAddress(java.lang.String addr) definido en la interfaz javax.wireless.messaging.Message de la que heradan javax.wireless.messaging.TextMessage y javax.wireless.messaging.BinaryMessage.
public void setMessageListener(javax.wireless.messaging.MessageListener l) throws IOException
Registrar una clase que implemante la interfaz javax.wireless.messaging.MessageListener, el plataforma J2ME invocará el método notifyIncomingMessage() cuando reciba un mensaje.
Este método sólo tiene sentido para javax.wireless.messaging.MessageConnection que funcionen en modo servidor

Ejemplo 1. Envío de un SMS en modo texto.

En este ejemplo, vamos a hacer una aplicación que envie el texto que introduzca el usuario en un área de texto a un destinatario fijo.
Para el desarrollo de aplicaciones para móviles, yo personalmente utilizo el IDE NetBeans con la extensión Mobility

  1. package autentia.tutoriales.wma;  
  2. import javax.microedition.lcdui.*;  
  3. improt javax.microedition.midlet.*;  
  4.   
  5. /** 
  6.  * MIDLet de Ejemplo del uso del API WMA (Wireless Messagin API)  
  7.  * @author Carlos García. Autentia. 
  8.  */   
  9. public class WMAMidlet extends javax.microedition.midlet.MIDlet {  
  10.     private boolean started;  
  11.       
  12.     /** 
  13.      * Constructor 
  14.      */  
  15.     public WMAMidlet(){  
  16.         this.started = false;  
  17.     }  
  18.   
  19.     /*  
  20.      * @see javax.microedition.midlet.MIDlet#startApp() 
  21.      */  
  22.     protected void startApp() throws MIDletStateChangeException {  
  23.            
  24.         // Este método puede ser incovado varias veces mientras la aplicación se esté ejecutando.   
  25.         // Por ejemplo, si se está ejecutando la aplicación y nos llega una llamada   
  26.         // entrante, la aplicación generalmente es pausada y esté método será   
  27.         // invocado cuando la llamada finalize.  
  28.           
  29.         if (! this.started){      
  30.             this.started = true;  
  31.             Display.getDisplay(this).setCurrent(new WMAMainForm(this));  
  32.         }  
  33.     }  
  34.       
  35.     /*  
  36.      * @see javax.microedition.midlet.MIDlet#destroyApp(boolean) 
  37.      */  
  38.     protected void destroyApp(boolean arg0) throws MIDletStateChangeException {  
  39.         this.destroyApp(true);  
  40.     }  
  41.   
  42.     /*  
  43.      * @see javax.microedition.midlet.MIDlet#pauseApp() 
  44.      */  
  45.     protected void pauseApp() {  
  46.         // En este ejemplo no se requiere ninguna tarea cuando el MIDLet es pausado.  
  47.     }     
  48.   
  49. }  

  1. package autentia.tutoriales.wma;  
  2.   
  3. import java.io.*;  
  4. import javax.microedition.io.*;  
  5. import javax.microedition.lcdui.*;  
  6. import javax.wireless.messaging.*;  
  7.   
  8.   
  9.   
  10. /** 
  11.  * Ventana principal de la aplicación 
  12.  * @author Carlos García. Autentia 
  13.  */   
  14. public class WMAMainForm extends javax.microedition.lcdui.TextBox   
  15.             implements javax.microedition.lcdui.CommandListener {  
  16.       
  17.     /** 
  18.      * Referencia al MIDLet 
  19.      */  
  20.     private javax.microedition.midlet.MIDlet midlet;  
  21.       
  22.     /** 
  23.      * Envia el SMS 
  24.      */  
  25.     private javax.microedition.lcdui.Command cmdSend;  
  26.       
  27.     /** 
  28.      * Finaliza la aplicación 
  29.      */  
  30.     private javax.microedition.lcdui.Command cmdExit;  
  31.       
  32.     /** 
  33.      * Constructor 
  34.      * Ventana principal de la aplicación 
  35.      */  
  36.     public WMAMainForm(javax.microedition.midlet.MIDlet midlet) {  
  37.         super("Mensaje a enviar"""166, TextField.ANY);  
  38.         this.midlet = midlet;  
  39.           
  40.         this.createUI();  
  41.     }  
  42.   
  43.     /** 
  44.      * Crea y configura el interfaz gráfico de la ventana. 
  45.      */  
  46.     private void createUI(){  
  47.         this.setTicker(new Ticker("Autentia Real Business Solutions"));  
  48.         this.cmdSend = new Command("Enviar", Command.OK,   1);  
  49.         this.cmdExit   = new Command("Salir",  Command.STOP, 1);  
  50.           
  51.         this.addCommand(cmdSend);         
  52.         this.addCommand(cmdExit);  
  53.         this.setCommandListener(this);        
  54.     }  
  55.       
  56.       
  57.     /**  
  58.       * El usuario desea enviar el SMS  
  59.      */   
  60.     private void sendSMSClick() throws java.io.IOException {   
  61.         MessageConnection conn = null;   
  62.         TextMessage msg = null;   
  63.         try {   
  64.             // Paso 1: Obtenemos una implementación del Connection que se encargará de enviar el SMS   
  65.             conn = (MessageConnection) Connector.open("sms://+34699221570");   
  66.               
  67.             // Paso 2: Creamos el SMS   
  68.             msg = (TextMessage) conn.newMessage(MessageConnection.TEXT_MESSAGE);  
  69.   
  70.             // Paso 3: Establecemos el contenido del SMS   
  71.             msg.setPayloadText(this.getString());   
  72.               
  73.             // Paso 4: Enviamos el SMS   
  74.             conn.send(msg);   
  75.         } finally {   
  76.             // Paso 5: IMPORTANTE Cerramos la conexión   
  77.             this.closeQuietly(conn);   
  78.             conn = null;   
  79.         }   
  80.     }  
  81.   
  82.       /** 
  83.        * Cierra un Connection ignorando todas las posibles excepciones 
  84.        */  
  85.         private void closeQuietly(javax.microedition.io.Connection conn){  
  86.             try {  
  87.                 conn.close();  
  88.             } catch (Exception ex){  
  89.                 // Nada  
  90.             }  
  91.         }    
  92.           
  93.     /*  
  94.      * Receptor de eventos del UI (User Interface) 
  95.      * @see javax.microedition.lcdui.CommandListener#commandAction(javax.microedition.lcdui.Command, javax.microedition.lcdui.Displayable) 
  96.      */  
  97.     public void commandAction(Command arg0, Displayable arg1) {  
  98.         try {  
  99.             if (arg0 == cmdSend){  
  100.                 this.sendSMSClick();  
  101.             } else if (arg0 == cmdExit){  
  102.                 this.midlet.notifyDestroyed();  
  103.             }  
  104.         } catch (Exception ex){  
  105.             // En caso de error modificamos el texto de la ventana con el mensaje  
  106.             this.setString(ex.toString());  
  107.         }  
  108.     }  
  109. }  

Ejemplo 2. Envío de un SMS en modo binario.

Los pasos para enviar información en modo binario son los mismos que para el envio de SMS en modo texto.
Los mensajes binarios son representados bajo la clase javax.wireless.messaging.BinaryMessage. La principal diferencia entre esta clase y javax.wireless.messaging.TextMessage es el método para especificar el contenido del mensaje a enviar. En este último caso, el contenido es un array de bytes y es especificado a través del método setPayloadData.
  1. /** 
  2. * El usuario desea enviar el SMS en modo binario 
  3. */  
  4.   
  5. private void sendBinarySMSClick() throws java.io.IOException {  
  6.     MessageConnection conn = null;  
  7.     BinaryMessage msg = null;  
  8.     ByteArrayOutputStream bout = null;  
  9.       
  10.     try {  
  11.         // Paso 1: Obtenemos una implementación del Connection que se encargará de enviar el SMS   
  12.         conn = (javax.wireless.messaging.MessageConnection) Connector.open("sms://+34699221570");   
  13.           
  14.         // Paso 2: Creamos el SMS   
  15.         msg = (BinaryMessage) conn.newMessage(MessageConnection.BINARY_MESSAGE);  
  16.           
  17.         // Paso 3: Establecemos el contenido del SMS con algunos datos de prueba. Por ejemplo, datos de una persona.   
  18.         bout = new ByteArrayOutputStream();   
  19.         dout = new DataOutputStream(bout);   
  20.         dout.writeBoolean(true);  // ¿Soltero?   
  21.         dout.writeByte(55); // Edad   
  22.         dout.writeUTF("Madrid"); // Provincia de nacimiento   
  23.         dout.writeUTF("España"); // País.   
  24.         dout.writeLong(888883311L); // DNI   
  25.         msg.setPayloadData(bout.toByteArray());   
  26.           
  27.         // Paso 4: Enviamos el SMS   
  28.         conn.send(msg);   
  29.     } finally {   
  30.         // Paso 5: IMPORTANTE. Cerramos las objetos, liberando recursos   
  31.         this.closeQuietly(bout);   
  32.         this.closeQuietly(dout);   
  33.         this.closeQuietly(conn);   
  34.         dout = null;   
  35.         dout = null;   
  36.         conn = null;   
  37.     }   
  38. }  
  39.   
  40.    /** 
  41.     * Cierra un OutputStream ignorando todas las posibles excepciones 
  42.     */          
  43. private void closeQuietly(java.io.OutputStream out){  
  44.        try {  
  45.            out.close();  
  46.        } catch (Exception ex){  
  47.            // Nada  
  48.        }  
  49. }  
  50.          
  51.    /** 
  52.     * Cierra un Connection ignorando todas las posibles excepciones 
  53.     */  
  54. private void closeQuietly(javax.microedition.io.Connection conn){  
  55.        try {  
  56.            conn.close();  
  57.        } catch (Exception ex){  
  58.            // Nada  
  59.        }  
  60. }     

Introducción a Push Registry.

A partir de la especificación MIDP 2.0, se añadió una potente característica a la plataforma J2ME que consiste en que nuestras aplicaciones puedan ser iniciadas por eventos externos o temporizadores.
Por ejemplo, nuestra aplicación puede ser iniciada cuando reciba un SMS en un determinado puerto (identificador).

Información interesante

A continuación os presento una tabla con información relacionada con el juego número de SMS necesarios para enviar información.

Referencia:  http://java.sun.com/products/wma/index.jsp Por lo general los mensajes de texto se envian con el juego de caracteres del GSM-7 bit, y sólo cuando el mensaje tiene caracteres que no pueden ser codificados con ese juego de caracteres se usará el UCS-2.

Conclusiones y reflexiones

En comparación con otros, este API es bastante simple y fácil de utilizar.
Sabiendo utilizar esta y otras tecnologías como las que os presentamos en Autentia a través de nuestros tutoriales, se pueden hacer sistemas interesantes y útiles.
No olvideis que esto es sólo una introducción, asi que si necesitais más información debeis dirigiros a las páginas oficiales de la especificación.
Espero que os haya parecido interesante este tutorial.

Ejemplo de conexión de JavaME con periférico por puerto serial/USB

Todos los equipos Nextel con Java ME tienen la capacidad de establecer una conexión de datos por el puerto serial o mini USB desde una aplicación Java.
En la terminal receptora, es simplemente necesario poder detectar al equipo conectado como si fuese un modem.
Es muy sencillo. El código adjunto ofrece un ejemplo.
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.io.Connector;
import javax.microedition.io.CommConnection;
import java.io.IOException;
import java.io.DataOutputStream; public class SocketTest extends MIDlet implements CommandListener {
    private boolean midletPaused = false;
    private Command exitCommand;
    private Command okCommand;
    private Form form;
    private StringItem stringItem;
    /**
     * The SocketTest constructor.
     */

    public SocketTest() {
    }
    /**
     * Initilizes the application.
     * It is called only once when the MIDlet is started. The method is called before the startMIDlet method.
     */

    private void initialize() {
    }
    /**
     * Performs an action assigned to the Mobile Device - MIDlet Started point.
     */

    public void startMIDlet() {
        switchDisplayable(null, getForm());
    }
    /**
     * Performs an action assigned to the Mobile Device - MIDlet Resumed point.
     */

    public void resumeMIDlet() {
    }
    /**
     * Switches a current displayable in a display. The display  instance is taken from  getDisplay method. This method is used by all actions in the design for switching displayable.
     * @param alert the Alert which is temporarily set to the display; if null, then nextDisplayable is set immediately
     * @param nextDisplayable the Displayable to be set
     */

    public void switchDisplayable(Alert alert, Displayable nextDisplayable) {
        Display display = getDisplay();
        if (alert == null) {
            display.setCurrent(nextDisplayable);
        } else {
            display.setCurrent(alert, nextDisplayable);
        }
    }
    /**
     * Called by a system to indicated that a command has been invoked on a particular displayable.
     * @param command the Command that was invoked
     * @param displayable the Displayable where the command was invoked
     */

    public void commandAction(Command command, Displayable displayable) {
        if (displayable == form) {
            if (command == exitCommand) {
                exitMIDlet();
            } else if (command == okCommand) {
                openSocketConnection();
            }
        }
    }
    /**
     * Returns an initiliazed instance of exitCommand component.
     * @return the initialized component instance
     */

    public Command getExitCommand() {
        if (exitCommand == null) {
            exitCommand = new Command("Exit", Command.EXIT, 0);
        }
        return exitCommand;
    }
    /**
     * Returns an initiliazed instance of form component.
     * @return the initialized component instance
     */

    public Form getForm() {
        if (form == null) {
            form = new Form("Prueba", new Item[] { getStringItem() });
            form.addCommand(getExitCommand());
            form.addCommand(getOkCommand());
            form.setCommandListener(this);
        }
        return form;
    }
    /**
     * Returns an initiliazed instance of stringItem component.
     * @return the initialized component instance
     */

    public StringItem getStringItem() {
        if (stringItem == null) {
            stringItem = new StringItem("Prueba", "Prueba conexion serial");
        }
        return stringItem;
    }
    /**
     * Returns an initiliazed instance of okCommand component.
     * @return the initialized component instance
     */

    public Command getOkCommand() {
        if (okCommand == null) {
            okCommand = new Command("Ok", Command.OK, 0);
        }
        return okCommand;
    }
    /**
     * Returns a display instance.
     * @return the display instance.
     */

    public Display getDisplay () {
        return Display.getDisplay(this);
    }
    /**
     * Exits MIDlet.
     */

    public void exitMIDlet() {
        switchDisplayable (null, null);
        destroyApp(true);
        notifyDestroyed();
    }
    /**
     * Called when MIDlet is started.
     * Checks whether the MIDlet have been already started and initialize/starts or resumes the MIDlet.
     */

    public void startApp() {
        if (midletPaused) {
            resumeMIDlet ();
        } else {
            initialize ();
            startMIDlet ();
        }
        midletPaused = false;
    }
    /**
     * Called when MIDlet is paused.
     */

    public void pauseApp() {
        midletPaused = true;
    }
    /**
     * Called to signal the MIDlet to terminate.
     * @param unconditional if true, then the MIDlet has to be unconditionally terminated and all resources has to be released.
     */

    public void destroyApp(boolean unconditional) {
    }
    public void openSocketConnection() {
        try {
        CommConnection cc = (CommConnection)Connector.open("comm:com0;baudrate=19200");
        DataOutputStream dos = cc.openDataOutputStream();
        dos.writeChars("Sample output");
        dos.flush();
        cc.close();
        }
        catch (IOException ex){
            Alert alert = new Alert("Exception: ", ex.getMessage() ,null,AlertType.ERROR);
            switchDisplayable(alert,getDisplay().getCurrent());
        }
    }
}

miércoles, 12 de mayo de 2010

J2ME Tutorial Parte X Final

Certificados de Seguridad en MIDP 2.0

 OBJETIVOS
Una de las características que hacen de MIDP una buena plataforma para dispositivos móviles es la seguridad que ofrece. La seguridad se ve incrementada en MIDP 2.0 y podemos considerar que las siguientes fuentes de dicha seguridad son:
  • La seguridad implícita en el modelo de programación de Java (máquina virtual de Java).
  • La nueva interfaz javax.microedition.pki que permite trabajar con certificados de seguridad para autentificar la información en conexiones seguras. Esto resulta imprescindible en MIDP 2.0 ya que implementa en el paquete javax.microedition.io conexiones seguras (tanto sobre HTTP como sobre sockets).
  • La posibilidad que ofrece de definir distintos permisos y dominios de protección en el entorno de desarrollo J2ME Wireless Toolkit 2.0
Con la implementación de protocolos de conexión seguros cada vez son más la aplicaciones que deben ser descargadas desde un servidor para ser ejecutadas en nuestro terminal. Cobra, por tanto, una gran importancia el hecho de poder asegurar que el código descargado sea exactamente el que queremos y que no presente un comportamiento malicioso. El código descargado será especialmente peligroso cuando desconocemos su autor, su fin o su procedencia. Como ya se dijo anteriormente, Java ofrece un alto grado de seguridad en este aspecto a través de su máquina virtual, ya que el código descargado no se ejecutará directamente sobre el sistema operativo sino sobre dicha máquina virtual.
Sin embargo, partiendo de la base de que nunca conseguiremos un sistema cien por cien seguro, la máquina virtual de Java no será suficiente y se deben implementar nuevos mecanismos de seguridad. Es en este punto dónde MIDP 2.0 supone un considerable avance respecto a su predecesor. Como ya se ha dicho nos permite definir distintos dominios protección, cada uno de ellos con un nivel de seguridad determinado. MIDP 2.0 define muy vagamente cómo deben ser definidos estos dominios pero sí sugiere que estén basados en firmas criptográficas y certificados de seguridad.
MIDP 2.0 define a través del MIDP X.509 Certificate Profile (perfil de certificados X.509 de MIDP) cómo debe ser el formato y la utilización de los certificados. Las aplicaciones DEBEN soportar los certificados X.509 (son los más extendidos y se denominan así porque se definen en la recomendación X.509 de CCITT) y el algoritmo de cifrado RSA utilizando SHA-1. Además, PUEDEN soportar (o no) cualquier otro tipo de certificados (dependerá de la implementación).

 CERTIFICADOS DE SEGURIDAD
A la hora de establecer una conexión segura, por ejemplo para descargar una aplicación en nuestro terminal móvil, debemos estar seguros de la identidad del otro participante en la conexión o del autor o propietario que nos ofrece la aplicación. La solución adoptada es la de utilizar certificados de seguridad y consiste básicamente en autenticar mutuamente a ambos extremos de la comunicación a través de una autoridad de certificación en la que confían ambos. Así pues, la Autoridad de certificación actúa como un tercero de confianza en la comunicación entre clientes, cliente y servidor, etc.
El escenario en el que nos encontramos es el siguiente:
  • A pretende establecer una conexión segura con B (o viceversa, B propone la conexión).
  • A no puede garantizar que es B realmente quien se encuentra en el otro extremo de la comunicación, es decir, B no está autenticado frente a A.
  • La autoridad de certificación AC reconoce a B y por lo tanto, puede garantizar que es quien dice ser.
  • AC es una autoridad de certificación reconocida por A.

Figura1
Figura 1: Establecimiento de conexión segura e intercambio de certificado


El proceso de autenticación es el siguiente: cuando se va a establecer la conexión segura, por ejemplo, cuando A va a descargar en el terminal una aplicación desde el servidor B, solicitará que le sea enviado el certificado de seguridad. Una vez recibido, el usuario A comprobará cuál es la autoridad de certificación que expidió el certificado (en nuestro caso es AC) y si es una autoridad de certificación reconocida se lo reenviará. Si AC reconoce el certificado como el propio de B estará en condiciones de autenticarle frente a A y, dado que éste confía en la autoridad de certificación, aceptará el establecimiento de la conexión.
Por último comentar que los certificados contienen información adicional importante en la autenticación de usuarios (como por ejemplo marcas de tiempo o "timestamps" con las que hacer que los certificados tengan un tiempo de validez finito). Algunos de estos datos se verán con más detalle al tratar el interfaz Certificate.

 EL INTERFAZ Certificate

El interfaz Certificate abstrae una serie de datos contenidos en los certificados como son: el tema, el emisor, el tipo, la versión, el número de serie, el algoritmo de firmado digital, fechas de validez y número de serie.
Ya se ha explicado que los certificados de seguridad se intercambian a la hora de establecer una conexión segura. Si volvemos a la página de este tutorial referente al paquete javax.microedition.io veremos que cualquier conexión segura (HTTPS o SSL) tiene asociado un objeto SecurityInfo. Es en este objeto donde podemos encontrar el certificado recibido y en función del cuál se pudo establecer correctamente la conexión segura:
HttpsConnection conexionSegura = (HttpsConnection)Connector.open("........");
SecurityInfo si = conexionSegura.getSecurityInfo();
Certificate c = si.getServerCertificate();
Los distintos métodos que ofrece este interfaz están encaminados a poder acceder a los distintos datos que ofrece el certificado:
  • getIssuer(): devuelve el nombre del emisor del certificado. El valor devuelto NO DEBE ser null.
  • getNotAfter(): devuelve la fecha, en milisegundos, a partir de la cuál el certificado dejará de ser válido. Devolverá un número positivo (long) o la constante Long.MAX_LONG_VALUE si el certificado no tiene fecha de caducidad.
  • getNotBefore(): devuelve el tiempo, en milisegundos, a partir del cuál se podrá aceptar el certificado. Devolverá un número positivo (long) o cero si la validez del certificado no está sujeta a restricciones temporales.
  • getSerialNumber(): devuelve un String que contiene el número de serie del certificado. Dado que dicho número de serie está codificado en binario, será necesario darle el formato adecuado para que pueda ser imprimido. Por tanto la salida de este método será un String con valores hexadecimales separados por ":".
  • getSigAlgName(): devuelve el nombre del algoritmo utilizado para firmar el certificado.
  • getSubject(): devuelve el tema del certificado. El valor devuelto NO DEBE ser null.
  • getType(): devuelve un String indicando de que tipo de certificado se trata. Para certificados X.509 el valor devuelto es "X.509". El valor devuelto NO DEBE ser null.
  • getVersion(): indica cuál es la versión del certificado. A los certificados X.509 les corresponde el valor 2. El valor devuelto NO DEBE ser null.

J2ME Tutorial Parte IX

PROGRAMACIÓN DEL API DE JUEGOS

 OBJETIVOS
El API de juegos de MIDP 2.0 ofrece una serie de capacidades que no ofrecía MIDP 1.0 y que simplifican enormemente la creación de juegos 2D. Una de sus principales y más ventajosas características es que es enormemente compacto ya que, a pesar de ser una herramienta muy robusta y potente, tan solo consta de cinco nuevas clases que podemos encontrar en javax.microedition.lcdui.game. Estas cinco clases extienden las capacidades gráficas de MIDP de modo que se convierten en una herramienta fundamental para el programador de juegos.
El objetivo de este tutorial es familiarizar al lector con estas nuevas clases y con la funcionalidad que ofrecen. Como primer paso para conseguirlo introducimos en este punto cuál es el concepto fundamental que introduce el API de juegos: la pantalla puede estar descompuesta en distintas capas o estratos (layers) de tal forma que podemos tratar cada una de ellas como una entidad independiente. Conociendo a fondo las clases que se exponen a continuación podremos comprender esta nueva filosofía y ponerla en práctica. 

 GameCanvas
GameCanvas es una subclase de Canvas que ofrece, además de las heredadas de Canvas, nuevas capacidades específicas para la programación de juegos en J2ME. Por ejemplo, ofrece la posibilidad de crear animaciones rápidas y libres de parpadeo o la posibilidad de examinar el estado de las teclas del dispositivo.
Antes de conocer con más detalle qué nos ofrece la clase GameCanvas veamos como era la tarea de programar un juego con animaciones en MIDP 1.0, es decir, utilizando la clase Canvas. A lo más que podríamos llegar es a una solución que presentase el siguiente esquema:

public class JuegoCanvas extends Canvas implements Runnable {

  public void run(){
    while (true) {
      // Actualizar el estado del juego
      repaint();
     // Esperar
    }
  }

  public void paint(Graphics g) {
    // Pintar imagenes
  }

  protected void keyPressed(int keyCode) {

    // Codigo de respuesta a la 
    // pulsacion de una tecla
  }

}
La solución es cuando menos poco eficiente y difícilmente obtendremos el resultado deseado. El método run(), ejecutado por un hilo, actualiza el estado del juego. Además, al llamar al método repaint() el sistema llamará al método paint() con lo que se ejecutará otro hilo encargado de pintar las imágenes correspondientes en la pantalla. Por otro lado, cuando se produzca la pulsación de alguna tecla, el sistema llamará a keyPressed() y lo hará utilizando otro hilo independiente de los dos anteriores.
Por tanto, tenemos tres hilos diferentes y no habrá manera de que uno de ellos conozca que está ocurriendo con los otros dos en otras partes de la aplicación, con lo que el resultado puede ser una animación defectuosa. Además, si el tiempo que necesita la pantalla para refrescarse es mayor que el tiempo entre llamadas al método repaint(), el resultado será completamente inaceptable para el jugador.

GameCanvas ofrece la posibilidad de implementar un juego completo, incluyendo toda su funcionalidad, con un simple bucle controlado por un único hilo. Esto es así ya que permite que los procesos de pintado y gestión de eventos de teclado tengan un nivel de transparencia mayor para el programador. Los mecanismos mediante los cuales GameCanvas nos permite abstraernos en cierta medida de estas tareas son:
  • La posibilidad de acceder directamente al objeto Graphics y de disponer de un buffer específico en el que se podrán representar y tratar las imágenes antes de mostrarlas en la pantalla del dispositivo.
  • La técnica de "polling" (encuesta). Permite acceder directamente al estado de las teclas del dispositivo.

OFF-SCREEN BUFFER

Para poder entender los métodos de la clase GameCanvas debemos introducir primero el concepto de off-screen buffer. Es una representación de la pantalla que no es visible sino que está almacenada en memoria. A cada instancia de GameCanvas se le asigna en exclusiva un buffer, por lo que es preferible reutilizar un GameCanvas para no desperdiciar la memoria del dispositivo. El tamaño del buffer es el mismo que el del GameCanvas.
El buffer se rellena con pixels en blanco al inicializarlo y sólo el objeto GameCanvas puede modificar su contenido. Estas modificaciones se ejecutan sobre el contenido del buffer, pero no son visibles hasta que se indica que su contenido se vuelque a la pantalla, es decir, funciona como una "pantalla virtual". Veremos a continuación como podemos llevar a cabo estas acciones utilizando los métodos de la clase GameCanvas.

Los métodos de la clase GameCanvas son:
  • flushGraphics(): vuelca el contenido del buffer a la pantalla sin modificar el contenido de éste. El tamaño del área volcada es el mismo que el del GameCanvas.
    El método no finaliza hasta que la imagen ha sido volcada a la pantalla así que la aplicación puede mostrar inmediatamente otra imagen para continuar con la animación. Si el GameCanvas no está visible o no se puede realizar el volcado porque el sistema está ocupado, el método finalizará inmediatamente.

  • flushGraphics(int x, int y, int width, int height): El funcionamiento es idéntico al del método anterior con la salvedad de que en este caso se vuelca en la pantalla únicamente el contenido de la región del buffer especificada. Si especificamos una zona que sale fuera de los límites del GameCanvas sólo se volcará la parte que esté dentro de dichos límites. Si la altura o anchura especificadas son menores que 1 pixel no se volcará ninguna imagen.

  • getGraphics(): devuelve el objeto Graphics que representa la imagen almacenada en el off-screen buffer. Cada vez que se invoca este método sobre una misma instancia de GameCanvas se devolverá un objeto Graphics diferente aunque todos representarán al mismo buffer.
    Sobre este objeto Graphics podemos utilizar todos los métodos que están disponibles en dicha clase, al igual que hacíamos en MIDP 1.0, con una importante diferencia: ahora podemos pintar o modificar imágenes en segundo plano, de manera totalmente transparente al jugador, y cuando hayamos terminado mostrarlas en la pantalla observando tan solo el resultado final. Si se tratase de un objeto Graphics perteneciente a un objeto Canvas las acciones que se realizaran se mostrarían inmediatamente en la pantalla del dispositivo.

  • getKeyStates(): devuelve un entero en el que se codifica qué teclas están pulsadas y cuáles no. Cada bit del entero devuelto representa una tecla y se codificará de la siguiente manera:

    • 1 - Si la tecla está siendo pulsada o lo ha sido alguna vez desde que se hizo la última llamada a este método.
    • 0 - Si la tecla no está siendo pulsada y no se ha pulsado ninguna vez desde la última llamada a este método.

    Este comportamiento de "cerrojo" asegura que cualquier pulsación, por rápida que sea, será capturada independientemente del intervalo de tiempo que utilicemos para verificar periódicamente si ha habido alguna pulsación.

  • paint(Graphics g):pinta el GameCanvas, más concretamente su objeto Graphics. Como ya se ha explicado anteriormente al invocar este método se pintará en el GameCanvas lo que hayamos especificado y esto se ejecutará sobre el contenido del buffer. Para que sea visible debemos invocar el método flushGraphics().
Una vez conocemos todos los métodos de los que dispone la clase GameCanvas podemos ver el esquema de un juego genérico utilizando esta clase en lugar de Canvas. Conviene prestar atención a las ventajas que presenta este código respecto al que vimos anteriormente:
public class JuegoGameCanvas extends GameCanvas implements Runnable {

  public void run() {
    Graphics g = getGraphics();
    while (true) {
      // Actualizar el estado del juego.
      int keyState = getKeyStates();
      
      // Responder a la pulsación de teclas,
      // repintando aquí por ejemplo.
      flushGraphics();
      // Esperamos.
    }
  }
}

 Layer
Layer es la clase básica en el API de juegos de MIDP 2.0. Es una clase abstracta que representa un elemento visual cualquiera del juego y que tiene como propiedades la posición, el tamaño y la posibilidad de hacerla o no visible. Si consideramos que un juego consiste básicamente en un fondo con una serie de elementos animados, resulta muy cómodo crear este tipo de escenas utilizando "capas"(Layers), desplazándolas y haciéndolas visibles o invisibles de manera independiente. Además, el hecho de ser una clase abstracta permite al programador implementar subclases que ofrezcan una funcionalidad más específica.

Métodos en la clase Layer:

  • getHeight(): devuelve la altura del objeto Layer en pixels.

  • getWidth(): devuelve la anchura del objeto Layer en pixels.

  • getX(): devuelve la coordenada horizontal de la esquina superior izquierda del objeto Layer.

  • getY(): devuelve la coordenada vertical de la esquina superior izquierda del objeto Layer.

  • isVisible(): indica si el objeto Layer es visible (true) o no (false).

  • move(int distX, int distY): Mueve el objeto Layer horizontalmente una distancia distX verticalmente una distancia distY. Las coordenadas del objeto están sujetas a "¿wrapping?" si las distancias especificadas hacen que se sobrepasen los valores Integer.MIN_VALUE o Integer.MAX_VALUE.

  • setPosition(int x, int y): mueve el Layer de tal manera que su esquina superior izquierda se situe en el punto (x,y). Si no invocamos este método por defecto estará situado en (0,0).

  • paint(Graphics g): pinta el objeto Layer si está en modo visible, no haciendo nada en caso contrario. Todas las subclases de Layer deben implementar este método. Dichas implementaciones son las responsables de comprobar si el objeto Layer está o no visible. Los atributos del objeto Graphics no se ven alterados como resultado de invocar este método.

  • setVisible(boolean visible): indica si el objeto Layer es visible o no. Es importante principalmente a la hora de invocar el método paint(Graphics g) ya que como se acaba de ver este método sólo se ejecutará cuando el Layer sea visible.

 LayerManager
La clase LayerManager es la que nos permite controlar y tratar conjuntamente con una serie de objetos Layer que formen parte de la misma aplicación. La manera de hacerlo es manteniendo una lista ordenada en la que los Layers pueden ser insertados, accedidos o eliminados.
Esta clase complementa todas las facilidades que Layer nos ofrecía, como la de poder tratar cada elemento gráfico del juego como un estrato o capa independiente del resto. Si no existiera algún mecanismo que permitiese gestionar de forma ordenada tantos elementos gráficos independientes, el concepto de estrato supondría un aumento en la complejidad del proceso de programación de juegos. LayerManager es una superestructura que almacena una serie de objetos Layer y tiene acceso a todos los datos de cada uno de ellos: posición, profundidad (es decir, si se superpone a otro Layer o al revés), estado, etc.

Figura1
Figura 1: Layer Manager


La forma en que se almacenan los objetos Layer no es arbitraria. Están indexados, garantizando así su ordenación en función de la profundidad de cada uno. Es decir, la posición 0 corresponde al Layer más superficial, el más cercano al usuario. Del mismo modo, la última posición corresponderá a la capa más profunda sobre la que se superponen las demás. Esta ordenación se mantendrá incluso en el caso de que se elimine un Layer almacenado, en cuyo caso se reajustan las posiciones de manera que no existan huecos. De acuerdo con esto, el objeto LayerManager representado en la figura anterior daría lugar a una imagen como la siguiente:

Figura2
Figura 2: Imagen construida con un objeto Layer Manager




VENTANA VISIBLE (View Window)

Este nuevo concepto permite controlar el tamaño y la posición (relativa al sistema de coordenadas del objeto LayerManager) de la región visible. Es especialmente interesante cuando una capa contiene una imagen mayor que el tamaño de la pantalla del dispositivo, como por ejemplo un fondo. De esta manera se podrán generar con gran facilidad efectos de barrido (scrolling) y panorámica (panning). Se puede especificar el tamaño de la ventana visible, determinando así el tamaño de la imagen que verá el usuario.

Figura3
Figura 3: Ventana visible (View Window)


Para mostrar la ventana visible utilizaremos el método paint(Graphics g, int x, int y) que permite indicar en qué posición queremos que pinte la ventana visible. No se modifica el contenido de ésta, simplemente se le indica una posición relativa distinta a la que tiene por defecto (0,0). Este método se explica con más detalle más adelante.


Los métodos de la clase LayerManager son:

  • append(Layer l) añade el Layer especificado a un LayerManager. Le asigna el mayor índice, el que correspondea las capas más profundas. si el LayerManager ya contenía el mismo Layer éste será borrado antes de almacenarlo.

  • getLayerAt(int): devuelve el objeto Layer almacenado en la posición especificada.

  • getSize() devuelve el número de Layers almacenados en un LayerManager.

  • insert(Layer l, int index): inserta el Layer especificado en un LayerManager en la posición indicada por index. Si ya había sido añadido con anterioridad será borrado antes de insertarlo.

  • paint(Graphics g, int x, int y): como ya vimos este método dibuja en la pantalla la actual ventana visible del LayerManager en la posición indicada con las coordenadas x e y. Estas coordenadas son relativas a la posición del objeto Graphics, por lo que desplazarlo supone que la ventana visible se desplace también (pero no su contenido).
    Representará todos los objetos Layer almacenados en orden decreciente, componiendo la escena completa tal y cómo fue diseñada. Las imágenes que estén totalmente fuera de la ventana visible o que pertenezcan a un Layer en estado invisible no serán pintadas.

  • remove(Layer l): elimina del LayerManager el Layer especificado. Si dicho Layer no se encuentra este método no hace nada.

  • setViewWindow(int x, int y, int width, int height): indica cuál es la ventana visible desde el momento que se invoca este método. Será lo que se dibujará en la pantalla al invocar el método paint(....). Hasta que se ejecuta este método por primera vez la ventana visible empieza en (0,0) con una anchura y una altura igual a Integer.MAX_VALUE.

 TiledLayer
TiledLayer es una clase que hereda de Layer y que representa un elemento visual compuesto por un conjunto de celdas, cada una de las cuales tiene asociada una imagen que denominaremos baldosa (tile). Tener un objeto TiledLayer equivale a tener una serie de piezas de un rompecabezas que podremos colocar a nuestro gusto para obtener una imagen con el aspecto deseado. Esto resulta especialmente útil para imágenes de gran tamaño, como puede ser el fondo de un juego ya que no será necesario tener una imagen extremadamente grande, sino que podremos obtenerlo con una serie de celdas combinadas y repetidas convenientemente.


CÓMO CREAR UN OBJETO TiledLayer. CELDAS Y BALDOSAS

El método constructor de la clase TiledLayer es TiledLayer(int m, int n, Image i, int tileWidth, int tileHeight). Al invocarlo se divide la imagen indicada en baldosas de un tamaño indicado por tileWidth y tileHeight. Al mismo tiempo se genera un conjunto de celdas que conforman una matriz con m filas y n columnas. El siguiente código muestra cómo sería el proceso:
Image image = Image.createImage("/board.png");
  TiledLayer tiledLayer = new TiledLayer(10, 10, image, 16, 16);
  • Celdas
El tamaño de todas las celdas es el mismo (fijado por el tamaño de las baldosas) y la manera de acceder a cada una de ellas es igual a la de cualquier matriz, especificar la fila y la columna en la que se encuentra. El contenido de cada una de estas celdas se indicará mediante un entero que hace referencia a una baldosa en concreto (más adelante veremos cómo se numeran las baldosas). Al inicializarse todas las celdas contienen un 0 lo que significa que está vacía y que será transparente al representarla en la pantalla.

  • Baldosas
Como ya se ha dicho, la imagen fuente se divide en baldosas. No se especifica el número de éstas que queremos sino el tamaño que deben tener. Una vez que hemos descompuesto una imagen en una serie de imágenes más pequeñas necesitamos un mecanismo para acceder a cada una de ellas. Al invocar el método constructor de TiledLayer se asigna inmediatamente un entero a cada baldosa generada. La correspondiente a la esquina superior izquierda es la número 1 y se siguen asignando números consecutivos por filas.

Figura4
Figura 4: Tiled Layer


Cabe hacer una clasificación importante:
    • Baldosas estáticas: son aquellas que tienen asociada una imagen fija que no cambiará durante toda la ejecución.
    • Baldosas dinámicas o animadas: son aquellas que están asociadas a una imagen que puede variar durante el juego. Es muy útil para variar el aspecto de una porción importante de la pantalla sin necesidad de ir cambiando las celdas una por una. Tan solo hay que declarar varias baldosas dinámicas y variar cuando convenga la imagen a la que hacen referencia.
      IMPORTANTE: los identificadores de baldosas dinámicas son siempre enteros NEGATIVOS.


Los métodos que podemos encontrar en la clase TiledLayer son:

  • createAnimatedTile(int staticTileIndex): crea una baldosa dinámica nueva a partir de una estática. El método devuelve el índice que identifica a la nueva baldosa.

  • fillCells(int col, int row, int numCols, int numRows, int tileIndex): rellena una región de la matriz de celdas con la baldosa especificada. Puede ser una baldosa animada, estática o transparente (identificador 0).

  • setAnimatedTile(int animatedTileIndex, int staticTileIndex): asocia a una baldosa animada una determinada baldosa estática. Como ya se ha dicho, a lo largo de la ejecución del programa podemos cambiar el contenido de una baldosa dinámica y la manera de hacerlo es utilizando este método.

  • getAnimatedTile(int animatedTileIndex): devuelve la baldosa a la que hace referencia una determinada baldosa dinámica.

  • setCell(int col, int row, int tileIndex): fija el contenido de una celda asignándole una baldosa que puede ser de cualquier tipo.

  • getCell(int col, int row): devuelve el índice de la baldosa (sea del tipo que sea) que se encuentra en la celda especificada.

  • getCellHeight(): devuelve la altura de las celdas (recordad que es igual para todas ellas).

  • getCellWidth(): devuelve la anchura de las celdas (también idéntica para todas las celdas). Para obtener la altura o anchura total del objeto TiledLayer hay que invocar los métodos Layer.getHeight() y Layer.getWidth();

  • getColumns(): devuelve el número de columnas de la matriz de celdas del objeto TiledLayer.

  • getRows(): devuelve el número de filas de la matriz de celdas del objeto TiledLayer.

  • setStaticTileSet(Image i, int tileWidth, int tileHeight): cambia la imagen fuente sobre la que se creó el objeto TiledLayer. Si el número de baldosas que se generan con la nueva imagen es el mismo que ya existía, los identificadores serán los mismos por lo que el contenido de la matriz de celdas se conservará. Si no es así, todas las celdas perderán su contenido que será puesto a cero (como si se creara un nuevo objeto TiledLayer).

  • paint(Graphics g): dibuja en pantalla el contenido del objeto TiledLayer. No presenta ninguna variación respecto al método paint(....) que vimos en la clase Layer puesto que TiledLayer es una subclase.
El siguiente código muestra un ejemplo de como crear y asignar contenido a un objeto TiledLayer:
// Imagen que contiene las baldosas
 
 imagen = Image.createImage(< nombre imagen >);

 // Inicializacion del objeto TiledLayer

 tl = new TiledLayer(20,11,imagen,16,16);

 // Asignacion de contenido celda por celda

 int[] map = {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
              1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
              1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,    
              1,1,6,1,1,1,1,6,6,1,1,1,1,1,1,1,1,1,1,6,
              1,5,3,4,1,1,5,3,3,4,1,1,1,1,1,1,1,1,5,3,
              5,3,3,3,4,5,3,3,3,3,4,1,1,1,5,3,3,3,3,3,
              3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
              3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
              2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
              2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
              2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2}
            
 for (int i = 0; i < map.length; i++) {

   int column = i % 20;

   int row = (i - column) / 20;

   tl.setCell(column, row, map[i]);

 }

 // Se fija el estado visible

 tl.setVisible(true);

 Sprite
Sprite es otra subclase de Layer y se podría decir que es el concepto dual de TiledLayer. Si antes generábamos a partir de una imagen varias baldosas, ahora, a partir de varias imágenes (frames) construirmos una única imagen animada.


CÓMO CREAR UN OBJETO Sprite. GENERANDO UNA ANIMACIÓN

Para generar el objeto Sprite que nos proporcione la animación deseada utilizaremos la siguiente expresión:
Sprite sprite = new Sprite( imagen, frameWidth, frameHeight);

Siendo:

imagen: objeto Image creado a partir de un fichero fuente en el que estarán todos los frames que vayamos a necesitar para crear la animación.

frameWidth,frameHeight: dimensiones de cada frame.
      Lo que hacemos al crear el Sprite es dividir la imagen en fragmentos (frames) del tamaño especificado. Por tanto, al diseñar el fichero fuente debemos colocar adecuadamente, controlando la posición y las dimensiones, los que posteriormente serán frames independientes a los que podremos acceder. Para permitir dicho acceso, a cada frame le es asignado un índice al ser creado (comenzando por el 0). Cabe mencionar aquí que podemos construir un Sprite a partir de otro anterior o crear un objeto no animado mediante la expresión:

      Sprite sprite = new Sprite(Image imagen);
      

      Crea el objeto Sprite definiendo un tamaño de frame igual a imagen.getWidth() e imagen.getHeight(), es decir, crea un único frame por lo que el Sprite será no animado.

        Figura5
        Figura 5: Sprite


        Una vez descompuesta la imagen fuente podemos definir la forma de la animación. El hecho de crear un objeto Sprite no significa que hayamos creado una animación fija e invariable. Podemos darle forma o modificarla a nuestro parecer indicando cuál es la secuencia de frames a reproducir refiriéndonos a cada frame por su índice. Por ejemplo, para el Sprite de la figura anterior podríamos crear el efecto de avanzar ({0,1,2,3}) o retroceder ({0,3,2,1}).


        MANIPULACIÓN DE IMÁGENES UTILIZANDO Sprite

        La clase Sprite nos permite manipular a más bajo nivel las imágenes que posteriormente mostraremos en la pantalla. Las actuaciones que podemos llevar a cabo son:

        • Definir un píxel de referencia
          Habitualmente, cuando queremos colocar en la pantalla cualquier elemento gráfico, especificamos su posición indicando las coordenadas dónde se situará la esquina superior izquierda de éste. Ahora que podemos trabajar con distintas capas (objetos Layer, ya sean de tipo TiledLayer o Sprite) y crear escenarios más complejos en los que intervienen un mayor número de elementos visuales independientes, resultaría una labor de encaje el conseguir que la ordenación de todos estos elementos fuera la adecuada para llegar al resultado deseado.La clase Sprite simplifica esta tarea ya que nos permite definir un pixel de referencia y colocarlo en el punto del espacio deseado. También permite conocer cuál es la posición del pixel de referencia y de esta manera concretar, sin más complicaciones, lo que se está mostrando en la pantalla.
        • Realizar transformaciones sobre la imagen fuente
          La posibilidad de modificar ciertas propiedades de la imagen resulta muy útil para aumentar el número de animaciones que podemos crear a partir de una única imagen fuente con un número concreto de frames.Podemos invertir las imágenes según distintos ejes de simetría (horizontal o vertical) o rotarlas un ángulo determinado (90º, 180º, 270º).
        Los métodos que nos ofrece la clase Sprite para ejecutar las acciones anteriomente citadas y algunas más son los siguientes:
        • collidesWith (Image image,int x, int y, boolean pixelLevel): comprueba si se ha producido una colisión entre el objeto Sprite y la imagen especificada cuya posición es indicada mediante los enteros x e y. El argumento pixelLevel indica si se debe utilizar o no detección por niveles de píxel. Esto es, la colisión sólo se detectará en caso de que los pixels tanto del Sprite como de la imagen sean opacos. En caso de no utilizar este tipo de detección tan sólo se comprobará si la intersección del área de colisión del Sprite y la imagen es no nula.
          Para que la colisión pueda ser detectada el objeto Sprite debe ser visible.

        • collidesWith(Sprite sprite, boolean pixelLevel): método análogo al anterior con la salvedad de que en este caso la colisión se detecta entre dos objetos Sprite. El funcionamiento de este método es idéntico al anterior.

        • collidesWith(TiledLayer tiledLayer, boolean pixelLevel): método idéntico a los dos anteriores, en este caso aplicado a la colisión con un objeto TiledLayer

        • defineCollisionRectangle(int x, int y, int width, int height): método que permite definir cuál es el tamaño y la posición del área de colisión (o rectángulo de colisión) del Sprite. A la hora de comprobar si existe o no colisión con algún otro elemento visual (utilizando cualquiera de los tres métodos anteriores) sólo se realizará la comprobación sobre el área que hayamos definido con este método. Por defecto este rectángulo está definido respecto a la posición (0,0) (esquina superior izquierda) y con un tamaño igual al del objeto.

        • defineReferencePixel(int x, int y): método con el que podemos definir cuál será el pixel de referencia del objeto Sprite. Para hacer referencia a un pixel concreto lo haremos a partir de su posición relativa respecto a la esquina superior izquierda. Por defecto el pixel de referencia será el (0,0). El hecho de cambiarlo no hará que el Sprite cambie de posición en la pantalla.
          Al llevar a cabo una transformación sobre el objeto Sprite el pixel de referencia se define en relación a la que inicialmente era la esquina superior izquierda.

        • getFrame(): devuelve el índice del frame que está siendo mostrado en ese instante.

        • getFrameSequenceLength(): devuelve el número de frames que forman la secuencia de imágenes. No es necesariamente el número de frames distintos que se generaron a partir de la imagen fuente ya que en una secuencia pueden aparecer frames repetidos.

        • getRawFrameCount(): devuelve el número de frames creados ("frames crudos") a partir de la imagen fuente.

        • getRefPixelX(): devuelve la coordenada x del pixel de referencia.

        • getRefPixelY(): devuelve la coordenada y del pixel de referencia.

        • nextFrame(): selecciona el siguiente frame en la secuencia del objeto Sprite. Es importante tener en cuenta que el funcionamiento de la secuencia de frames es circular por lo que si se ejecuta este método cuando nos encontremos en la última posición de la secuencia se pasará de nuevo a la primera.

        • paint(Graphics g): pinta el objeto Sprite en cuestión. Para que al invocar este método se dibuje deberá estar definido como visible.

        • prevFrame(): selecciona el frame correspondiente a la posición index de la secuencia de frames. anterior frame en la secuencia del objeto Sprite. Dado que, como ya dijimos, la secuencia de frames es circular, al invocar este método cuando nos encontremos en la primera posición de la secuencia se pasará directamente a la última.

        • setFrame(int index): selecciona un frame determinado que será indicado por el índice que le fue asignado al ser creado. Esta acción no será visible hasta que no se invoque el método paint(Graphics g).

        • setFrameSequence(int[] sequence): indica cuál es la secuencia de frames a reproducir. Todos los objetos Sprite son creados con una secuencia que por defecto reproduce los frames en orden creciente. Si al invocar este método lo hacemos pasando un argumento null se volverá a esta secuencia por defecto.

        • setImage(Image image, int frameWidth, int frameHeight): cambia la imagen fuente asociada al objeto Sprite. Se especifica el tamaño de los nuevos frames que se deben crear con lo que el número de éstos que se generen puede variar respecto al que ya existía. Se pueden dar dos casos:
          • Que la nueva imagen genere un número MAYOR o IGUAL de frames:
            En este caso la secuencia de frames que ya existía permanece invariable. Además, el frame que en ese momento esté seleccionado no se modificará hasta que no se invoque el método correspondiente.
          • Que la nueva imagen genere un número MENOR de frames: En este caso se pasará inmediatamente al frame que ahora tenga el índice 0. Adicionalmente, si la secuencia de frames que existía estaba definida por el programador se reseteará volviendo a la secuencia por defecto.


        • setRefPixelPosition(int x, int y): sitúa el objeto Sprite en una posición concreta de manera que su pixel de referencia se sitúe en la posición indicada por (x,y).

        • setTransform(int transform): aplica la transformación especificada al objeto Sprite.
          A la hora de aplicar una transformación se debe tener en cuenta que las propiedades del Sprite se verán afectadas. Por ejemplo cambián las dimensiones (lo que conocíamos como anchura ahora es altura y viceversa), varía el rectángulo de colisión ya que este permanece "anclado" al pixel de referencia, puede variar la posición del pixel de referencia, etc.
        El código mostrado a continuación crea y dota de contenido a un Sprite:
        // Crear un Sprite a partir de una imagen que contiene los frames
        
          sprite = new Sprite(,15,25);
        
          // Declarar cual es la secuencia de frames
        
          int[] sequence = {0,1,2,3};
        
          sprite.setFrameSequence(sequence);
        
          // Determinar el pixel de referencia y su posicion
        
          sprite.defineReferencePixel(refX,refY);
          sprite.setRefPixelPosition(coordXSprite,coordYSprite);
        
          // Hacer el Sprite visible
        
          sprite.setVisible(true);