J2ME, FileConnection API. Acceso a tarjetas de memorias desde MIDlets

1
25488

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

1 COMENTARIO

  1. 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

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad