J2ME, FileConnection API. Acceso a tarjetas de memorias desde MIDlets
En la actualidad, debido a la naturaleza multimedia de los terminarles móviles (teléfonos, PDAs, etc.), es muy normal que admitan tarjetas de memoria externas (SD, MultimediaCard, Memory Stick, etc.) que incrementan notablemente sus capacidades de almacenamiento.
En este tutorial voy a intentar hacer una introducción al API FileConection (JSR75) de J2ME que nos permite acceder a estas memorias desde aplicaciones para dispositivos 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.
índice de contenidos
Introducción
La gran mayoría de las aplicaciones necesitan guardar información en memorias permanentes cuando la aplicación finalice.
CLDC, nos proporciona a través de las clases del paquete javax.microedition.io
de un mecanismo estándar de almacenamiento de bloques de bytes (RecordStore
) que sólo pueden ser leídos, modificados y borrados por el MIDlet que lo creó.
Aunque en la mayoría de las aplicaciones este mecanismo es más que suficiente, en otras se ve la necesidad de tener un tratamiento más completo que permita la lectura, la escritura, el borrardo y la búsqueda de archivos y carpetas.
Entre otras cosas, esto último lo que podemos realizar a través del API FileConnection.
FileConnection es una especificación opcional, por lo que la implementación de esta en cada terminal depende del fabricante.
El API
Es un API ligero y sencillo, definido en el paquete javax.microedition.io.file
.
javax.microedition.io.file.FileSystemRegistry
A través de esta clase podemos enumerar las raices de sistemas de ficheros presentes en el terminal.
Para hacer una analogía, podría ser como la ventana "MI PC" de los sistemas operativos Windows en donde se muestran las unidades físicas y lógicas presentes sistema.
Además a través de esta clase podemos registrar y desregistrar oyentes (Listener) que serán invocados cuando estas raices cambien. Por ejemplo, cuando se extraiga la tarjeta de memoria.
public static boolean addFileSystemListener(FileSystemListener listener)
Registra un FileSystemListener
que será notificado cuando varíen las raices de los sistemas de archivos.
public static boolean removeFileSystemListener(FileSystemListener listener)
Desregistra un FileSystemListener
que será notificado cuando varíen las raices de los sistemas de archivos.
public static java.util.Enumeration listRoots()
Proporciona una enumeración de java.lang.String
con las cadenas de conexión necesarias para el acceso a cada uno de las raices de los sistemas de ficheros disponibles.
Por ejemplo, si devolviese "SDCard/" podríamos acceder a ese sistema de ficheros mediante la instrucción:FileConnection fs = (FileConnection) Connector.open("file:///SDCard/")
javax.microedition.io.file.FileSystemListener
Es una interfaz que deberán implementar las clases que deseen ser registradas. Contiene un sólo método que será invocado por el sistema cuando se añada o elimine la tarjeta de memoria.
public void rootChanged(int eventType, java.lang.String rootName)
EventType podrá ser una de las dos constantes estáticas definidas en la interface: ROOT_ADDED
o ROOT_REMOVED
javax.microedition.io.file.FileConnection
Se trata de una interfaz que será implementada por cada dispositivo y que nos proporciona decenas de métodos para crear, leer, escribir, consultar y eliminar información relacionada con archivos y directorios. Debido a la extensión de la misma sólo voy a enumerar algunos de ellos, debiendo dirigirse usted a la especificación oficial si necesita más información.
public long availableSize()
Devuelve el número de bytes dispobibles en la memoria.
public long directorySize(boolean includeSubDirs) throws java.io.IOException
Devuelve el número de bytes que ocupa un directorio teniendo en cuenta o no los subdirectorios.
public void fileSize() throws java.io.IOException
Devuelve el número de bytes que ocupa el fichero.
public java.util.Enumeration list() throws java.io.IOException
Devuelve una enumeracion de java.lang.String de
todos los archivos y carpetas que contiene el directorio actual.
Los directorios se distinguen fácilmente de los archivos normales por que acaban con el caracter "/".
public void setFileConnection(java.lang.String fileName) throws java.io.IOException
Sirve para reutilizar instancias de FileConnection y así no malgastar recursos. Puede usarse el nombre ".." para ir al directorio padre.
public void rename(java.lang.String newName) throws java.io.IOException
Renombra un archivo o carpeta.
public void delete() throws java.io.IOException
Elimina un archivo o carpeta.
public java.io.InputStream openInputStream() throws java.io.IOException
Obtiene un java.io.InputStream
para leer el archivo.
public java.io.OutputStream openOutputStream() throws java.io.IOException
Obtiene un java.io.OutputStream
para escribir información en el archivo.
El ejemplo: Explorando las tarjetas de memoria y abriendo un archivo de texto.
En este ejemplo vamos a hacer una aplicación que nos permita navegar por todas las carpetas de todas las memorias así como abrir y mostrar un archivo de texto.
Para el desarrollo de aplicaciones para móviles, yo personalmente utilizo el IDE NetBeans con la extensión Mobility
package autentia.tutoriales.fileconnections; import javax.microedition.lcdui.*; import javax.microedition.midlet.*; /** * Clase principal del ejemplo. * Explorador de ficheros para terminales móviles * @author Carlos García. Autentia * @see http://www.mobiletest.es */ public class FileExplorerMIDlet extends javax.microedition.midlet.MIDlet { private FileSelector fileSelector; /** * Constructor */ public FileExplorerMIDlet(){ this.fileSelector = new FileSelector(this); } /* * Punto de inicio del MIDlet * @see javax.microedition.midlet.MIDlet#startApp() */ public void startApp(){ Displayable current = Display.getDisplay(this).getCurrent(); if (current == null){ // Averiguamos si el dispositivo implementa la especificación FileConnection boolean isFCAvailable = System.getProperty("microedition.io.file.FileConnection.version") != null; if (! isFCAvailable) { Alert splashScreen = new Alert(null, "El terminal no permite esta característica", null, AlertType.INFO); Display.getDisplay(this).setCurrent(splashScreen); return; } Display.getDisplay(this).setCurrent(this.fileSelector); } else { Display.getDisplay(this).setCurrent(current); } } /* * @see javax.microedition.midlet.MIDlet#pauseApp() */ public void pauseApp(){ // No se requiere la realización de ninguna tarea aqui cuando el MIDlet es pausado. } /* * Liberamos recursos y salimos * @see javax.microedition.midlet.MIDlet#destroyApp(boolean) */ public void destroyApp(boolean unconditional){ this.fileSelector.stop(); this.notifyDestroyed(); } }
package autentia.tutoriales.fileconnections; import java.io.*; import java.util.*; import javax.microedition.io.*; import javax.microedition.io.file.*; import javax.microedition.lcdui.*; import javax.microedition.midlet.MIDlet; /** * Explorador de ficheros para terminales móviles * @author Carlos García. Autentia * @see http://www.mobiletest.es */ public class FileSelector extends List implements CommandListener, FileSystemListener, Runnable { /** * Separador de ficheros */ private final static String FILE_SEPARATOR = (System.getProperty("file.separator") != null) ? System.getProperty("file.separator") : "/"; /** * Directorio padre */ private final static String UPPER_DIR = ".."; // Códigos de las posibles operaciones: Inicialización y apertura de ficheros private final static int LOAD_OP = 1; private final static int OPEN_OP = 2; private MIDlet midlet; private Vector roots; private FileConnection currentRoot; // Las tareas de I/O deben realizarse en un hilo independiente private int operationCode; private Thread task; // Atributos gráficos private Command cmdOpen, cmdExit; /** * Constructor * @param midlet Referencia al MIDlet */ public FileSelector(MIDlet midlet) { super("File Explorer", Choice.IMPLICIT); this.midlet = midlet; this.roots = new Vector(); this.createUI(); // Nos registramos para que el sistema nos notifique cuando se inserten o extraigan memorias externas. FileSystemRegistry.addFileSystemListener(FileSelector.this); // Mostramos todas las posibles raices de sistemas de ficheros. this.operationCode = FileSelector.LOAD_OP; task = new Thread(this); task.start(); } /** * Crea e inicializa el interfaz gráfico */ private void createUI(){ this.cmdOpen = new Command("Abrir", Command.ITEM, 1); this.cmdExit = new Command("Salir", Command.EXIT, 1); this.addCommand(this.cmdOpen); this.addCommand(this.cmdExit); this.setCommandListener(this); } /** * Este método será invocado cuando la aplicación sea cerrada */ public void stop() { /* Liberamos recursos eliminando la subcripción de que el sistema (AMS) nos notifique de las inserción o extración de una memoria externa */ FileSystemRegistry.removeFileSystemListener(this); } /* * El usuario hizo clic en un botón. * @see javax.microedition.lcdui.CommandListener#commandAction(javax.microedition.lcdui.Command, javax.microedition.lcdui.Displayable) */ public void commandAction(Command cmd, Displayable d) { if (cmd == this.cmdOpen) { this.operationCode = FileSelector.OPEN_OP; task = new Thread(this); task.start(); } else if (cmd == this.cmdExit) { this.midlet.notifyDestroyed(); } } /** * Una targeta de memoria externa ha sido insertada o extraída. Recargamos la información. * @see javax.microedition.io.file.FileSystemListener#rootChanged(int, java.lang.String) */ public void rootChanged(int state, String rootName) { this.operationCode = FileSelector.LOAD_OP; task = new Thread(this); task.start(); } /** * Borra todos los elementos de la pantalla */ private void deleteAll(){ int num = this.size(); for (int i = 0; i < num; i++){ this.delete(0); } } /** * Muestra en pantalla todos las raices encontradas */ private void displayAllRoots() { this.setTitle("Raices"); this.deleteAll(); Enumeration enum1 = this.roots.elements(); String root = null; while (enum1.hasMoreElements()) { root = (String) enum1.nextElement(); // El primer caracter es FileSelector.FILE_SEPARATOR. ==> lo descartamos. this.append(root.substring(1), null); } this.currentRoot = null; } /** * Inicializa 'roots' con todos las raices de sistemas de ficheros disponibles en el terminal */ private void loadRoots() { // Borramos los elementos que tuviese previamente this.roots.removeAllElements(); // Solicitamos el dispositivo que nos proporcione la URL de las raices de todos los sistemas de ficheros LóGICOS Enumeration roots = FileSystemRegistry.listRoots(); String root = null; while (roots.hasMoreElements()) { root = (String) roots.nextElement(); root = FileSelector.FILE_SEPARATOR + root; this.roots.addElement(root); } } /** * Abre el fichero o directorio seleccionado */ private void openSelected() throws java.io.IOException { int selectedIdx = this.getSelectedIndex(); String url = null; String selectedFile = null; if (selectedIdx < 0) { return; } selectedFile = this.getString(selectedIdx); // ¿ Desea abrir un directorio? if (selectedFile.endsWith(FileSelector.FILE_SEPARATOR) || selectedFile.endsWith("/")) { if (this.currentRoot == null) { this.currentRoot = (FileConnection) Connector.open("file:///" + selectedFile, Connector.READ); } else { this.currentRoot.setFileConnection(selectedFile); } this.displayCurrentRoot(); return; } // ¿ Desea ir al directorio anterior? if (selectedFile.equals(FileSelector.UPPER_DIR)) { if (this.roots.contains(this.currentRoot.getPath() + this.currentRoot.getName())) { this.displayAllRoots(); } else { this.currentRoot.setFileConnection(FileSelector.UPPER_DIR); this.displayCurrentRoot(); } return; } // Es un fichero, lo mostramos. url = this.currentRoot.getURL() + selectedFile; this.showTextFile(url); } /** * Muestra el contenido de un fichero en la pantalla del terminal. * @param url URL del fichero a mostrar. */ private void showTextFile(String url) throws java.io.IOException { FileConnection conn = null; DataInputStream input = null; byte[] bytes = null; try { // Leemos el fichero conn = (FileConnection) Connector.open(url, Connector.READ); input = conn.openDataInputStream(); bytes = this.readFully(input); // Borramos la pantalla this.deleteAll(); // Mostramos el contenido this.append(new String(bytes), null); conn.close(); } finally { if (conn != null){ conn.close(); } } } /** * (La forma de lectura aqui mostrada no es la más óptima) * @return Devuelve todos los bytes disponibles y cierra el stream */ public byte[] readFully(DataInputStream input) throws java.io.IOException { ByteArrayOutputStream bo = null; try { bo = new ByteArrayOutputStream(6144); // 6Kb while (true){ bo.write(input.readByte()); } } catch (java.io.IOException ex){ // No quitar } byte[] bytes = bo.toByteArray(); // Liberamos recursos try { bo.close(); } catch (Exception ex){} try { input.close(); } catch (Exception ex){} return bytes; } /** * Muestra el contenido del directorio actual */ private void displayCurrentRoot() { Vector dirs = new Vector(); Vector files = new Vector(); String current = null; try { // Mostramos información sobre la ruta en el título. this.setTitle(this.currentRoot.getURL()); // Inicializamos la pantalla this.deleteAll(); // Añadirmos la posibilidad de volver al directorio anterior this.append(FileSelector.UPPER_DIR, null); // Mostramos los directorios Enumeration enum1 = this.currentRoot.list("*", true); while (enum1.hasMoreElements()) { current = (String) enum1.nextElement(); if (current.endsWith(FileSelector.FILE_SEPARATOR)) { dirs.addElement(current); // Es un directorio } else { files.addElement(current); // Es un fichero } } this.showItems(dirs); // Añadimos los directorios a la pantalla this.showItems(files); // Añadimos los ficheros } catch (Exception e) { Alert alert = new Alert(null, e.getMessage(), null, AlertType.ERROR); Display.getDisplay(this.midlet).setCurrent(alert, this); } } /** * Muestra los elementos de tipo string almacenados en un vector */ private void showItems(Vector items){ Enumeration enum1 = items.elements(); String current = null; while (enum1.hasMoreElements()){ current = (String) enum1.nextElement(); this.append(current, null); } } /* * Ejecuta la tarea en segundo plano. * (En J2ME, todas las tareas de I/O deben ser en segundo plano.) * @see java.lang.Runnable#run() */ public void run() { if (this.operationCode == LOAD_OP) { FileSelector.this.loadRoots(); FileSelector.this.displayAllRoots(); } else if (this.operationCode == OPEN_OP) { FileSelector.this.openSelected(); } } }
Conclusiones y reflexiones
Es increíble la evolución de las capacidades de los dispositivos móviles. Actualmente es normal tener varios cientos de Mb de almacenamiento disponibles.
Aunque normalmente esta memoria es usada para el almacenamiento de imagen y sonido, dejando volar la imaginación se pueden crear aplicaciones que requieran grandes capacidades de almacenamiento y que antes eran inviables... Gestión de mapas, diccionarios, etc. Aun así deberiamos de poner todo nuestro empeño en hacer las cosas bien más aun en este tipo de aplicaciones en donde los recursos de CPU y memoría volátil son limitados.
Bueno, espero que os haya parecido interesante. Desde Autentia nos gusta compartir el conocimiento. aquí tenéis un poquito más de nuestra aportación.
Carlos García Pérez. Creador de MobileTest, un complemento educativo para los profesores y sus alumnos.
cgpcosmad@gmail.com
Buenas. mira estoy trabajando en una aplicación que tiene que poder leer un archivo y tambien editarlo, me podrían decir como sería el codigo para escribir o editar en el archivo