Artigo Java Magazine 48 - Programação Java ME
Este projeto foi feito originalmente com o NetBeans Mobility Pack.
Na primeira parte deste minicurso (Edição 44) analisamos a plataforma Java ME como um todo. Depois nos especializamos no perfil MIDP, que é a fatia do leão do Java ME por ser o perfil dos onipresentes aparelhos de telefonia celular. Vimos na segunda parte as ferramentas do WTK e do IDE Eclipse e na terceira, os recursos do NetBeans e as APIs do MIDP para interface de usuário (LCDUI). Na quarta parte, demos uma pausa da prática para falar de desempenho e outros aspectos de implementação.
Saiba mais: Programação Java EE: Parte 3
O que ficou faltando? Bastante coisa, pois como vimos na parte inicial, o número de APIs disponíveis para Java ME é grande, muito embora o universo do Java ME seja bem mais “minimalista” que o do Java SE : temos menos APIs, sendo que cada API é menor e frequentemente mais genérica que as suas correspondentes do Java SE.
Ainda falta investigarmos pelo menos duas áreas de funcionalidade essencial. A primeira é a persistência de dados, sem a qual nenhuma plataforma computacional seria levada a sério (até o meu primeiro micro, em 1987, tinha memória de massa – em fita cassete, mas tinha!). A segunda, também crítica numa plataforma wireless, é a comunicação.
Retomando o projeto
Continuaremos trabalhando com o projeto Micro Mandel, um gerador de fractais desenvolvido na terceira parte desta série (Edição 46). Apresentaremos somente os trechos de código alterado, mas a compreensão deste artigo não depende dos anteriores. O projeto completo e atualizado está disponível no download deste artigo.
Para quem perdeu a terceira parte, o programa Micro Mandel tem uma estrutura muito simples. Temos um formulário de dados (que também é a tela inicial da MIDlet), implementado pela classe MicroMandel. Esse formulário permite editar diversos parâmetros para o cálculo do gráfico fractal. E uma segunda classe Fractal (um Canvas) é responsável pelo cálculo e exibição deste gráfico.
Este projeto foi feito originalmente com o NetBeans Mobility Pack, e continuei utilizando o mesmo ambiente ao atualizá-lo para este artigo. Usuários do EclipseME ou MTJ (ambos apresentados na segunda parte) ou de outros IDEs e plug-ins de suporte a Java ME, poderão utilizar o mesmo código sem problemas. Só a configuração de projetos (que é simples) ficará por conta do leitor.
Ao criar o projeto no NetBeans, recomendei que todas as APIs de extensão desnecessárias fossem desativadas no projeto. Agora será necessário alterar isso. Na página de propriedades do projeto, em Platform, ative a opção File Connection and PIM Optional Packages 1.0. (O download deste artigo inclui o projeto configurado para o NetBeans com o Mobility Pack.) Em outros IDEs poderá ser necessário fazer uma configuração semelhante, para poder usar a API File Connection (JSR-75). A RMS faz parte da MIDP, portanto não exige configurações adicionais.
Persistência
Muitas aplicações Java ME precisam armazenar dados persistentes – cujo tempo de vida excede o de processos individuais. Mas a primeira versão da MIDP foi criada sem nenhuma API para trabalhar com arquivos. Isso porque os dispositivos-alvo deste perfil, historicamente, não suportavam sistemas de arquivos (filesystems). A especificação exigia algum suporte à memória de massa, mas não eram exigidos diretórios, direitos de acesso, capacidade de acesso aleatório, extensão de arquivos preexistentes (append), locks, e outras facilidades comuns até nos sistemas de arquivos mais simples usados em PCs.
Devido a estas restrições, a MIDP 1.0 introduziu a RMS, uma API de persistência simples que podia ser implementada mesmo em dispositivos com organização de memória de massa rudimentar. Por um lado a RMS habilita a portabilidade entre aparelhos, cujas APIs nativas de persistência podem ser muito diferentes. Por outro, implementa por conta própria alguns recursos que a tornam uma API bastante conveniente (para os padrões dos aparelhos mais simples).
Nos aparelhos mais recentes, no entanto, o uso de sistemas de arquivos se universalizou. Praticamente todos utilizam algum sistema de arquivos convencional, tipicamente a FAT, para organizar dados, tanto nas memórias integradas quanto em cartões de expansão. Refletindo esta evolução, a JSR-75 inclui a File Connection, uma API que a maioria dos dispositivos Java ME recentes já suporta e permite manipular arquivos de maneira familiar.
A File Connection é uma extensão da GCF (Generic Connection Framework), uma API fundamental de I/O disponível desde o MIDP 1.0. Sim, precisamos mais uma vez de APIs totalmente novas, porque as APIs de I/O da Java SE (java.io, java.net e java.nio) não foram projetadas para uma plataforma limitada – são muito grandes. No lugar delas, a MIDP possui a GCF, que com um tamanho mínimo, oferece uma funcionalidade de I/O surpreendente e é muito simples de usar.
A especificação “guarda-chuva” JSR-248 (MSA: Mobile Systems Architecture) inclui a JSR-75, que assim passa a ser obrigatória em todos os novos dispositivos MSA (veja a Edição 45 para mais sobre a MSA).
Dito tudo isso, por que nos preocuparmos com a RMS? Por dois motivos. O primeiro é que a RMS ainda é relevante como menor denominador comum: todos os dispositivos MIDP a suportam, inclusive os mais velhos ou de baixo custo. O segundo é que a RMS também tem as suas vantagens. Assim, exercitaremos as duas APIs neste artigo, procurando um caso de uso ideal para cada uma.
Usando a File Connection (e a GCF)
Vamos começar usando a API de persistência mais moderna, a File Connection, apenas por ser a opção mais simples e familiar. O programa de exemplo MicroMandel produz um gráfico (Figura 1) que gostaríamos de armazenar num arquivo.
A Listagem 1 mostra o código que o MicroMandel precisa para implementar esta funcionalidade. O novo método save() é invocado dentro do tratador de eventos de teclado na classe Fractal, de forma que o acionamento da tecla * irá acionar esta operação.
protected void keyPressed (int key) {
...
switch (getGameAction(key)) {
...
case KEY_STAR:
new Thread(){ public void run() { save(fractal); }}.start();
return;
}
}
...
}
private void save () {
FileConnection conn = null;
try {
// Obtém pixels
int[] argb = new int[fractal.getHeight() * fractal.getWidth()];
fractal.getRGB(argb, 0, fractal.getWidth(), 0, 0,
fractal.getWidth(), fractal.getHeight());
// Abre arquivo e stream de saída
conn = (FileConnection)Connector.open(
System.getProperty("fileconn.dir.photos") +
Long.toString(System.currentTimeMillis(), 16) + ".img",
Connector.WRITE);
conn.create();
OutputStream fos = conn.openOutputStream();
// Converte pixels para bytes e grava, em partes de 4 Kb
ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
DataOutputStream dos = new DataOutputStream(baos);
for (int i = 0; i < argb.length; i += 1024) {
int max = Math.min(i + 1024, argb.length);
for (int j = i; j < max; ++j)
dos.writeInt(argb[j]);
fos.write(baos.toByteArray(), 0, (max – i) * 4);
baos.reset();
}
}
catch (IOException e) { e.printStackTrace(); }
finally {
if (conn != null) try { conn.close(); } catch (IOException e) {}
}
}
A API fundamental de I/O: GCF (e algo da java.io)
Vamos começar pela GCF (javax.microedition.io), que dá acesso a qualquer tipo de dispositivo de I/O – tanto para persistência quanto para comunicação – a partir de uma API única e muito simples. No centro da GCF, temos a classe Connector e a interface Connection. Os métodos Connector.openXxx() aceitam uma URL cujo formato varia de acordo com o tipo de conexão de I/O. Os tipos de conexão predefinidos incluem TCP e UDP, HTTP e HTTPS, TCP seguro (SSL/TLS), sockets (Server e Client), bem como portas seriais.
A Figura 2 mostra todas as classes, interfaces e exceções da GCF, e da File Connection (no canto inferior direito). Observe que a GCF é extremamente abstrata, possuindo quase só interfaces.
A interface-raiz Connection só define o método close(). As interfaces derivadas de InputConnection possuem métodos openInputStream() e openDataInputStream(), e as derivadas de OutputConnection incluem os métodos openOutputStream() e openDataOutputStream(). Observe que todos os tipos de Connection incluídos na GCF estendem ambas as interfaces, pois são bidirecionais.
Esses métodos retornam objetos java.io.[Data](Input|Output)Stream. A MIDP também inclui parte da java.io: o fundamental dos seus streams, readers e writers e exceções, que serão familiares a programadores de Java SE. Assim, a GCF não é uma substituição total da java.io; suas interfaces se integram com a parte da java.io que foi considerada adequada à MIDP (veja o quadro “Reuso de APIs: SE versus ME”).
A File Connection
Com a API File Connection (javax.microedition.io.file), temos também “conexões” para arquivos em filesystems. Para trabalhar com um arquivo, basta utilizar uma URL na forma “file:///”, já familiar aos usuários de browsers quando abrem páginas locais. O método Connector.open(), recebendo uma URL nesta forma, retornará um objeto FileConnection. A interface FileConnection suporta vários métodos específicos para arquivos, como mkdir() ou usedSize().
Observe que o nome da interface é um pouco impreciso: a FileConnection é equivalente à iava.io.File da Java SE, ou seja, é apenas uma representação de um path de arquivo, que pode inclusive não existir. O método Connector.open("file:///...") não abre o arquivo e não falha se o arquivo não existir. Somente quando chamarmos um dos métodos openXxxStream() sobre a FileConnection retornada, o arquivo será realmente aberto. No nosso exemplo, usamos openOutputStream().
Observe que este método retorna um DataOutputStream diretamente; não há necessidade de começar obtendo um InputStream e passá-lo para o construtor de um DataInputStream, como é tradicional em Java SE. Embora as APIs da Java ME sejam quase sempre mais “enxutas” que as da Java SE, elas às vezes oferecem alguns métodos auxiliares adicionais, quando isso ajuda a economizar código muito repetitivo nas aplicações.
Escolhendo diretórios de nomes de arquivo
Para determinar o diretório de gravação, consultamos a propriedade do sistema fileconn.dir.photos, que é padronizada pela especificação da File Connection e fornece o diretório preferencial para armazenamento de fotos geradas pela câmera do dispositivo (ou por outros meios). O uso destas propriedades evita a complicação de escolher um diretório, pois a estrutura do sistema de arquivos não é padronizada: o número de “raízes” e o nome de cada uma variam de um dispositivo para outro.
Num aparelho você pode descobrir que há uma raiz “file:///C:/” representando a memória integrada, “file:///D:/” para o cartão de expansão e assim por diante (imitando o padrão do DOS e Windows com letras de drives); e em outra máquina, poderá encontrar um filesystem a la Unix, usando por exemplo “file:///SDCard/” para o cartão de expansão. Estas raízes podem ser descobertas pela aplicação com FileSystemRegistry.listRoots(), e a partir delas você pode invocar métodos FileConnection.list() para navegar pela hierarquia de arquivos.
Para o nome do arquivo, criamos uma string única a partir do timestamp atual. É uma solução bastante crua; o ideal seria gerar nomes sequenciais como 0001, 0002 etc., mas procuramos não complicar o código. Observe que convertemos o timestamp (que tem 64 bits) para uma string hexadecimal, pois esta terá no máximo 16 caracteres e normalmente apenas 11. Isso é bom para evitar o truncamento dos nomes em gerenciadores de arquivo limitados por telas estreitas.
Por que não oferecer ao usuário uma GUI para escolha do diretório e digitação do nome do arquivo? Isso faria sentido na plataforma desktop, mas num dispositivo com limitações de entrada de dados como um celular, quanto menos digitação melhor. Seria OK dar ao usuário a opção (mas não a obrigação) de digitar nomes de arquivo. Se o usuário criar muitas imagens e quiser organizá-las com nomes bem escolhidos, provavelmente preferirá fazer isso após transferi-las para o seu PC.
Segurança
Observe que quando a MIDlet tentar gravar o arquivo, o dispositivo solicitará a permissão do usuário. É uma restrição de segurança do perfil MIDP, já que a File Connection (uma vez que seu uso seja autorizado) pode criar arquivos em número e tamanho arbitrário e acessar qualquer arquivo do filesystem[4]. Aplicações desenvolvidas profissionalmente desejarão eliminar ou reduzir ao máximo estes avisos de segurança. Não poderemos cobrir o assunto por completo neste artigo, mas para uma introdução veja o quadro “Configurando segurança para a File Connection”.
I/O em arquivo: cuidados com desempenho
Quanto à gravação em si, como a imagem gerada pelo MicroMandel já está disponível num atributo do tipo Image, usamos seu método getRGB() para obter um array no formato AARRGGBB (32 bits por pixel: 24 bpp de cor + 8 bpp de transparência). Gravamos estes pixels “brutos” no arquivo e estamos conversados. (E quanto a formatos gráficos “de verdade”, como PNG? Veja o quadro “A LCDUI e formatos de imagens”.)
O código apresentado é relativamente complexo, pois inclui algumas otimizações importantes. O problema é que a API Image.getRGB() retorna um int[]. Esta é uma representação útil para manipulações da imagem (pois cada posição do array é um pixel), mas inadequada para I/O, pois não existe um método OutputStream.write(int[]).
Uma solução seria anexar um DataOutputStream ao stream do arquivo e invocar writeInt() para cada pixel. Mas o desempenho disso seria péssimo. Experimentei esta opção, e o resultado foi que (no emulador) demorou 40 segundos para gravar o arquivo. Isso acontece porque (na resolução do DefaultColorPhone) o gráfico tem 291.840 bytes, exigindo o mesmo número de operações de I/O! Já num aparelho real, o desempenho disso provavelmente seria... melhor, porque a maioria desses aparelhos usa memória flash ao invés de discos rígidos. Mas isso é muito perigoso: você corre o risco de escrever código de I/O ineficiente, que parece ser rápido no seu aparelho, mas será horrivelmente lento em outro aparelho cuja memória de massa não seja RAM[5] (poderia ser um micro-drive, por exemplo).
Na plataforma Java SE, poderíamos usar classes com buffering como BufferedOutputStream. Mas estas classes não fazem parte da java.io da MIDP; por isso, se quisermos buffering temos que fazer isso à mão. Fizemos esta bufferização manual usando um ByteArrayOutputStream, o que nos permite continuar usando DataOutputStream para converter int em byte.
Mas ao invés de preencher o buffer de uma só vez (o que exigiria uma quantidade de memória temporária igual ao tamanho da imagem), decidimos criar um buffer de apenas 4 Kb, e fazer a conversão e o I/O em “fatias” de 4 Kb. Dessa forma, conseguimos uma combinação excelente entre desempenho (fazendo I/O em blocos grandes) e facilidade de programação (usando as APIs disponíveis para cada tarefa).
I/O e GUI
Você pode ter observado que no método keyPressed() do Canvas que desenha o fractal, onde invocamos save() quando a tecla * é pressionada, fazemos isso de forma assíncrona a partir de um thread. Se tentarmos realizar operações de I/O (de qualquer espécie, não só da File Connection) no mesmo thread de despacho de eventos da LCDUI, poderemos ter problemas. Modificando keyPressed() para fazer a invocação direta sem este thread, o emulador do WTK irá gerar uma assustadora mensagem de advertência no console (traduzindo):
Atenção: Para evitar deadlocks potenciais, operações que podem causar bloqueio, como comunicação por rede, devem ser executadas fora do thread de commandAction() [thread de eventos].
Com efeito, se você tentar executar esta operação, poderá haver um deadlock: se a aplicação não tiver as permissões e a assinatura digital corretas (veja quadro “Configurando segurança para a File Connection”), tudo trava quando o emulador solicitar a permissão de gravação.
O problema é que a chamada de I/O da File Connection é interceptada pelo runtime da MIDP, que apresenta uma tela de advertência/solicitação de permissão – ou seja, faz uma execução “reentrante” na LCDUI para ler a entrada do usuário. Só que o thread de eventos está travado, aguardando a operação de I/O! Para quem conhece a Swing é a mesma coisa: a LCDUI tem um design single-threaded e é preciso ter muito cuidado com o tipo e a duração das operações executadas no thread de eventos. (A classe Fractal utiliza um thread de cálculo separado para produzir a imagem.)
Nesse caso específico nosso problema era, a partir do thread de eventos, executar código que não pode rodar neste thread. Para isso basta criar um Thread. Se tivéssemos o problema oposto, ou seja, de estar rodando em outro thread mas ter que rodar algum código dentro do thread de eventos, poderíamos usar o método Display.callSerially(), que é equivalente ao método da Java SE SwingUtilities.invokeLater(). Ambos recebem como parâmetro uma implementação da interface Runnable, cujo método run() será eventualmente invocado no thread de eventos.
Outros recursos da GCF e da File Connection
Para os outros tipos de Connection, muito do que você aprendeu aqui sobre a FileConnection se aplica, devido à uniformidade da API. Há alguns tipos de Connection com características especiais, mas quem tem experiência com a java.io e java.net da Java SE irá reconhecer a maioria das especificidades.
Por exemplo, ao trabalhar com sockets, temos sempre um “socket cliente” e um “socket servidor”. O cliente é trivial: basta abrir um SocketConnection com Connector.open("socket://host:porta") e obter os streams para envio ou recepção de dados. Num servidor, abra um server socket (a URL é parecida: basta omitir o host), e faça um loop que invoca ServerSocketConnection.acceptAndOpen(). Este método retorna um SocketConnection que permite comunicar-se com o socket do cliente. Enfim, como dissemos, há pouca novidade para quem já programou a mesma coisa na Java SE.
Um caso especial – isto é, uma novidade – é a classe PushRegistry, que permite à aplicação se registrar como interessada em conexões entrantes, de forma passiva (ou seja, sem estar rodando). O método PushRegistry.registerConnection() permite fazer este registro dinamicamente, e há outros métodos para consultar e manipular o registro. Mas a melhor opção é fazer esse registro nos descritores da MIDlet (arquivos .jad e META-INF/MANIFEST.MF), com entradas como:
MIDlet-Push-1: socket://:999,
meupacote.MinhaMIDletDeChat, *
Feito este registro, se o dispositivo receber uma tentativa de conexão do tipo socket na porta 999, a classe registrada (meupacote.MinhaMIDletDeChat) será executada. O método startApp() deverá usar o método PushRegistry.listConnections(true) que, se a MIDlet foi iniciada devido a uma conexão entrante, retorna um array de URLs das conexões. Daí basta usar Connector.open() para abrir e tratar estas conexões. Observe que a grande vantagem do registro em descritores é que a MIDlet não precisa estar executando. Basta o dispositivo estar ligado e conectado.
Um recurso semelhante da File Connection é a FileSystemRegistry. Esta classe permite que a aplicação registre uma implementação de FileSystemListener, cujo método rootChanged() será invocado sempre que ocorrer alterações na lista de raízes do filesystem. Isso ocorre com frequência em dispositivos móveis, cujos usuários estão sempre inserindo e removendo cartões de memória “à quente”; cada cartão costuma ser representado por uma raiz separada no filesystem. Uma aplicação que faça uso intenso da File Connection, por exemplo um browser de arquivos, desejará ser notificada destas alterações, para evitar erros e para atualizar automaticamente uma GUI como uma árvore de diretórios. Diferentemente do PushRegistry, o FileSystemRegistry é controlado somente por código e opera somente quando a MIDlet está executando.
Usando a RMS
Vamos agora a uma necessidade de persistência diferente: preferências de aplicação. Nosso programa MicroMandel utiliza alguns parâmetros que customizam a geração do fractal – em especial, quatro coordenadas que delimitam a figura. Gostaríamos que, ao encerrar a execução da MIDlet, os últimos valores utilizados para estes parâmetros fossem gravados, de tal forma que a próxima execução irá carregá-los. Tudo funcionará como se o programa nem tivesse sido encerrado e reiniciado.
Poderíamos fazer isso com um arquivo de configuração tradicional, criado com a File Connection. Mas a File Connection não é suportada pelos dispositivos mais antigos. E a RMS também tem seus próprios truques, além de ser muito simples.
A RMS (Record Management System) modela uma base de dados estruturada em torno de dois conceitos simples: Store e Record. Uma MIDlet Suite pode possuir vários stores, identificados por nomes. Há um espaço de nomes único por Suite; não há “subdiretórios”. Assim, um store é algo equivalente a uma tabela num banco de dados relacional. Cada store pode possuir vários records, com um identificador numérico autogerado e único por store (isto é, uma chave). A Figura 3 modela a RMS.
Mas o paralelo com bancos relacionais termina por aí. Cada record é um simples array de bytes, e pode-se ter records de diferentes tamanhos misturados no mesmo store. Não há nada equivalente às colunas, chaves estrangeiras, projeção, seleção ou operações sobre conjuntos. (Mas veja a seção “A RMS para databases”, adiante.)
A Listagem 2 ilustra o uso da RMS para carregar e atualizar a configuração do MicroMandel. Usamos um store único chamado “config”, com um record único. Na primeira vez que executarmos o MicroMandel, o método load() não encontrará nenhum record (enumerateElements() retornará um iterado vazio) e a configuração não será carregada, permanecendo os defaults de código.
public void startApp() {
load();
}
public void destroyApp (boolean unconditional) {
save();
}
private void save () {
RecordStore store = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.writeDouble(Double.parseDouble(tfX1.getString()));
dos.writeDouble(Double.parseDouble(tfY1.getString()));
dos.writeDouble(Double.parseDouble(tfX2.getString()));
dos.writeDouble(Double.parseDouble(tfY2.getString()));
byte[] rec = baos.toByteArray();
store = RecordStore.openRecordStore("config", true);
RecordEnumeration re = store.enumerateRecords(null, null, false);
if (re.hasNextElement())
store.setRecord(re.nextRecordId(), rec, 0, rec.length);
else
store.addRecord(rec, 0, rec.length);
}
catch (RecordStoreException e) {}
catch (IOException e) {}
finally {
if (store != null)
try { store.closeRecordStore(); }
catch (RecordStoreException e) {}
}
}
private void load () {
RecordStore store = null;
try {
store = RecordStore.openRecordStore("config", true);
RecordEnumeration re = store.enumerateRecords(null, null, false);
if (re.hasNextElement()) {
DataInputStream dis =
new DataInputStream(new ByteArrayInputStream(re.nextRecord()));
tfX1.setString(Double.toString(dis.readDouble()));
tfX2.setString(Double.toString(dis.readDouble()));
tfY1.setString(Double.toString(dis.readDouble()));
tfY2.setString(Double.toString(dis.readDouble()));
}
}
catch (RecordStoreException e) {}
catch (IOException e) {}
finally {
if (store != null)
try { store.closeRecordStore(); }
catch (RecordStoreException e) {}
}
}
Quando o programa for encerrado, executamos o método save(), que ao detectar a inexistência de records, usará addRecord() para criar um record com a configuração atual. A partir da próxima execução de save() o record já existe; então faremos seu update com setRecord(). E quando reiniciarmos o programa, load() também encontrará este record e lerá dele a configuração. Para ler e gravar os dados, utilizamos novamente as classes de stream de java.io.
Note que ao contrário do que vimos com a File Connection, o dispositivo não irá pedir ao usuário nenhuma confirmação para dar à MIDlet o direito de gravar dados. A RMS é uma API mais segura, pois é mais gerenciada: cada MIDlet Suite tem seus stores privados e o dispositivo tem o direito de estabelecer uma cota (quota) máxima de espaço por Suite; portanto, os riscos de segurança associados a APIs de acesso a filesystems não existem. Para desenvolvedores de aplicações MIDP distribuídas pela internet, a RMS é muito mais tranquila que a File Connection, pois não irá “assustar” usuários potenciais com advertências de segurança.
Falando nesta cota máxima, a especificação relevante mais recente (MSA) exige que o dispositivo permita a cada MIDlet Suite criar pelo menos dez stores distintos. A MSA é recente e a primeira geração de produtos certificados só há pouco chegou ao mercado; a especificação anterior (JTWI) exigia somente cinco stores. Mas na prática, a maioria dos dispositivos atuais não impõe um limite artificial (além do espaço disponível no aparelho). E para o tamanho máximo de cada record, não existe nenhum padrão, mas também costuma valer o limite de espaço.
A RMS para databases
A RMS, em comparação com a File Connection, tem maior disponibilidade em aparelhos mais antigos e evita restrições de segurança (sem a trabalheira e o custo de usar uma assinatura digital). Mas a sua maior vantagem técnica é o fato de ter uma estrutura de database (e não de filesystem). Especificamente, records de qualquer tamanho são armazenados de forma muito densa em stores, e o acesso – sequencial ou aleatório – também é muito eficiente; todas as manipulações de records por ID beneficiam-se de índices.
Isso permite construir aplicações de banco de dados simples. Não fizemos isso neste artigo por motivo de espaço, e porque queríamos mostrar o cenário do seu uso para configurações (que é muito popular: praticamente todas as aplicações Java ME que têm configurações persistentes usam a RMS para isso). Mas vamos investigar o cenário de banco de dados com um exemplo fictício.
Imagine um programa de Ordens de Serviço (OS). Veja o modelo na Figura 1. Operadores em campo recebem notificações de novas OS pela rede wireless, e estas OS são gravadas num store. Cada objeto OrdemServico, por exemplo “Consertar o servidor”, é um record separado, com seu próprio ID. A aplicação irá cadastrar ações corretivas do operador (objetos Acao), como “Troquei o fusível”. Estas ações serão records separados (armazenados em outro store) porque cada OS pode ter N ações, e seria ruim gravar as ações nos records das OS. A solução para o relacionamento 1-N é usar uma chave invertida: nos records das ações, haverá um campo com o ID da sua OS-pai. Por exemplo, o layout completo deste record poderia ser {ID-ação, ID-OS, descricao, data}. Assim, a partir de uma ação, podemos localizar a OS associada de forma simples e rápida.
E se você desejar o oposto – obter todas as ações para determinada OS? Uma maneira é usar o método RecordStore.enumerateRecords(RecordFilter filter, RecordComparator comparator, boolean keepUpdated). Passando uma implementação da interface RecordFilter, você pode excluir do resultado todos os records de ações que não apontem para determinada OS. E se quiser receber os registros numa ordem específica, passe também uma implementação de RecordComparator.
Este recurso de filtro não é muito eficiente – equivale, em SQL, a um WHERE não-indexado que gera um TABLE SCAN (sendo obrigado a ler toda a tabela, ou no nosso caso todo o store, mesmo quando somente uns poucos records são desejados). Mas como a nossa memória de massa é quase certamente RAM flash, o tempo de acesso é muito rápido e isso acaba não sendo problema, a não ser que o store seja muito grande. Se isso for uma preocupação, existem soluções. A mais simples é armazenar, nos records de OS, uma lista dos IDs de todas as ações de cada OS. A mais sofisticada seria implementar um índice “ação por OS”, o que poderia ser feito num store separado (implementando métodos clássicos de indexação, como BTree). Mas se a sua necessidade chegar nesse ponto, recomendo usar um banco de dados imbatível para Java ME.
Completando a lista de recursos de banco de dados da RMS, podemos registrar com RecordStore.addRecordListener() um tratador de eventos – implementação de RecordListener – que será invocado quando acontecer qualquer alteração num determinado store. Conforme o tipo de alteração, será invocado um dos métodos recordAdded(), recordChanged() ou recordDeleted(). Esta funcionalidade é equivalente aos “triggers” de bancos relacionais.
Limites da RMS
Uma observação importante: se o seu celular tem, digamos, 20 Mb de memória integrada, é apenas esta memória que poderá ser usada para os stores. Não adianta ter um cartão de memória de 2 Gb – este só poderá ser usado pela File Connection.
A limitação parece grave, mas na prática, a maioria das aplicações de banco de dados para Java ME possui volumes de dados muito pequenos. As aplicações mais “sérias”, que a princípio poderiam acumular mais dados, normalmente são front-ends para um servidor que roda em plataforma Java SE ou EE. Não faz sentido ser de outra forma: a última coisa que você quer é perder gigabytes de dados armazenados numa base de dados local, porque o seu cachorro pegou o celular e mastigou o cartão de memória. E serão raros levantem a mão os leitores que fazem backup diário das memórias dos seus celulares ou PDAs... ah, nenhum... é o que eu achava!
Dito isto (e brincadeiras à parte), há cenários em que seria bom manter uma base de dados grande no dispositivo; por exemplo, para fazer cache local com dados do seu sistema corporativo, permitindo a consulta a estes dados em campo sem o custo de comunicação pela rede móvel. Mas a RMS não foi projetada para lidar com volumes muito grandes: mesmo que alguém consiga criar um store de 1Gb, aposto que o desempenho será bem ruim. Se você tiver esse tipo de necessidade, a dica novamente é avaliar soluções de persistência para Java ME mais avançadas.
Conclusões
Neste artigo, vimos as facilidades de persistência que elevam seu dispositivo ao patamar de uma plataforma mais séria, capaz de rodar aplicações de coleta ou visualização de dados, gerenciamento de informações pessoais, ou qualquer outra que precise de algum armazenamento de dados. Você pode utilizar tanto arquivos quanto bases de dados. As APIs da MIDP para isso não são tão completas quanto às da Java SE, mas possuem um conjunto de funcionalidades bem interessante, exibindo um design elegante e pragmático. Como é costume no mundo “micro”, estas APIs irão satisfazer bem as necessidades de muitas aplicações simples; para outras mais ambiciosas, um pouco de criatividade permitirá complementar ou contornar as limitações.
Reuso de API: SE versus ME
Sempre que dissermos que a Java ME compartilha APIs da Java SE, devemos subentender que não é necessariamente a mesma implementação que é compartilhada. Significa apenas que a Java ME terá classes compatíveis, com o mesmo nome e mesmos métodos públicos (talvez nem todos os métodos, mas um subconjunto). Mas a implementação pode ser muito diferente, seja devido a especificidades da plataforma subjacente, seja devido a necessidades de funcionalidade ou desempenho diferentes.
Comparando os fontes de algumas classes da Java SE (do JDK 6.0) com a Java ME (do phoneME), verifiquei que as classes do Java ME foram desenvolvidas tomando por base as classes da Java SE e procedendo com modificações necessárias na plataforma ME. Os métodos que existem em ambas as classes são quase sempre iguais ou muito parecidos, e a estrutura geral das classes (ex.: ordem dos métodos, e nomes de métodos e atributos privados) é idêntica. Mas também há várias diferenças.
Para citar um item simples e corriqueiro, o tratamento de exceções é sempre sacrificado: enquanto a versão da classe para Java SE gera uma exceção com uma mensagem explicativa e detalhada, a versão para Java ME tipicamente gera uma exceção sem qualquer mensagem. (O phoneME permite gerar as exceções completas se os fontes forem compilados com a opção VERBOSE_EXCEPTIONS, mas você só encontrará tais compilações em emuladores, nunca em dispositivos comerciais.)
Num exemplo mais elaborado, veja a seguir a diferença entre as implementações dos métodos Random.next() nas duas plataformas.
Java ME (phoneME):
private long seed;
synchronized protected int next(int bits) {
long nextseed = (seed * multiplier + addend) & mask;
seed = nextseed;
return (int)(nextseed >>> (48 - bits));
}
Java SE (JDK 6.0):
private final AtomicLong seed;
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
O método next() em ambos os trechos é um método privado que contém o algoritmo básico de geração de número aleatório, sendo invocado por métodos públicos (por exemplo, nextInt() é implementado, em ambas as plataformas, como “return next(32);”). Mas existe uma grande diferença entre as implementações SE e ME.
A versão para Java SE é mais escalável: ao invés de sincronização por locks (synchronized), utiliza uma variável AtomicLong e uma lógica de retry para garantir a sua corretude em aplicações multi-threaded sem fazer nenhum lock>*. Esta otimização não pode ser feita na Java ME porque depende da API java.util.concurrent, que só existe no Java SE 5 ou superior.
Mas mesmo que tal API existisse na Java ME, creio que o Random.next() do Java ME não teria essa otimização. Isso por um motivo simples: plataformas como celulares e PDAs não possuem várias CPUs, nem executam servidores com centenas de threads, portanto não se beneficiariam de otimizações dessa espécie. E o código não-otimizado do Java ME é menor, portanto é até melhor, considerando os requisitos de economia de espaço desta plataforma.
Configurando segurança para a File Connection
Para que a sua MIDlet possa acessar o filesystem sem aborrecimentos, você deve atribuir-lhe (1) permissões de segurança adequadas e (2) uma assinatura digital reconhecida pelo dispositivo.
A parte 1 é mais fácil. Os descritores da aplicação (tanto o META-INF/MANIFEST.MF quanto o .jad) devem possuir a seguinte linha:
MIDlet-Permissions: javax.microedition.io.Connector.file.write,
javax.microedition.io.Connector.file.read
No NetBeans Mobility Pack, ambos os descritores são gerados pelo build, portanto as permissões precisam ser configuradas no projeto. Na sua página de propriedades, em Application Descriptor > API Permissions, clique Add e selecione as duas permissões acima. A primeira é necessária para gravar arquivos, a segunda para ler. Você verá que existe um bom número de permissões para diversas outras APIs. Segurança é um dos itens que definitivamente não foi sacrificado na Java ME – e isso é sem dúvida uma das razões do sucesso desta plataforma.
Mas não basta declarar as permissões desejadas. O mais difícil é a parte 2: “assinar” o deploy da aplicação. Isso exige um certificado emitido por uma Autoridade Certificadora (CA), o que custa dinheiro. Mas para treinar, não precisamos disso. No NetBeans, ainda na página de propriedades do projeto, vá em Build > Signing e ative Sign distribution; utilize Keystore=Built-in Keystore e Alias=trusted.
Esse certificado predefinido trusted é como uma nota de R$100,00 do jogo Banco Imobiliário: serve para brincar, mas não será aceito, claro, por lojas e bancos de verdade. Se você adquirir um certificado real, poderá ter que exportá-lo para o emulador com Export Key into Java ME SDK/Platform/Emulator na mesma página de propriedades.
Na página Running do diálogo de propriedades, escolha Execute through OTA (Over The Air Provisioning). Esta opção fará o emulador instalar e executar a aplicação da mesma forma que um dispositivo real que tenha baixado a aplicação da rede: em especial, o arquivo .jad será lido e as suas propriedades de segurança serão consideradas. Se você abrir o arquivo .jad gerado pela compilação do projeto, verá que este possui duas linhas com dados criptografados. Estas linhas são a assinatura digital da aplicação. em resumo, elas garantem que a aplicação foi realmente gerada pela entidade possuidora de determinado certificado, e não foi corrompida ou modificada.
Depois de fazer tudo isso (ou o equivalente em outro IDE), execute a aplicação no emulador. Você verá que o comando de gravar arquivo funciona sem apresentar nenhum erro ou diálogo de segurança. Para verificar que a gravação teve sucesso, confira o arquivo .img em WTK_HOME/appdb/DefaultColorPhone/filesystem/root1/photos. Com o emulador DefaultColorPhone, cuja resolução útil é de 240x304, o arquivo terá 285 Kb.
Para completar esta seção, veja o que acontece com uma aplicação que utiliza APIs que exigem permissões especiais, mas não é configurada de forma apropriada. Primeiro, se esquecermos de acrescentar as permissões ao descritor, a gravação do arquivo irá falhar e o console do emulador reportará:
Uncaught exception java/lang/SecurityException:
Application not authorized to access the restricted API.
Isso mostra que a declaração das permissões é obrigatória. Agora, digamos que você declarou as permissões, mas não possui uma assinatura digital válida ou apropriada. Nas opções de assinatura digital, use Alias=untrusted para simular este cenário. Rode a MIDlet novamente e tente acionar a gravação do arquivo. Você verá uma advertência de segurança como a da Figura Q1.
A LCDUI e formatos de imagens
Como autor do MicroMandel, um recurso do qual senti falta na MIDP foi a capacidade de gravar imagens em formatos padronizados e modernos, como PNG, JPEG ou GIF. Infelizmente, nenhuma versão do perfil MIDP oferece APIs para isso (só é suportada a leitura de alguns formatos, especialmente PNG).
Acontece que nem todos os dispositivos possuem capacidade “nativa” de gravação de imagens em formatos padronizados e eficientes. Os mais novos suportam isso (em especial o formato JPEG), mas muitos aparelhos mais antigos só gravavam em formatos “brutos” (raw), proprietários.
A princípio, esta falta poderia ser compensada por um encoder puro-Java, mas daí o problema é que este código seria grande para os padrões das bibliotecas Java ME. O encoder de JPEG incluído na Java SE tem cerca de 200 Kb de arquivos .class – isso sem somar várias classes compartilhadas com outros encoders, nem outras APIs exclusivas da Java SE que este encoder usa. Isso é um horror, para os padrões tradicionais de tamanho de runtimes Java ME: os runtimes MIDP 2.0 mais modestos cabem em míseros 300 Kb de ROM. Mesmo nos dispositivos de última geração, com dezenas de megabytes de memória integrada (e cartões de 1 Gb ou mais), o problema é menor, mas também não há muita folga.
O mercado não se move tão rápido quanto a tecnologia e as APIs principais ainda não podem pressupor que todos os aparelhos terão essa capacidade. Porém, seríamos bem servidos por uma API de extensão, uma espécie de “ImageIO para Java ME”. Mas o que ocorre é que as APIs evoluem conforme a demanda do mercado. Basta ver, por exemplo, o número de APIs para gráficos avançados que são suportadas por todos os dispositivos mais recentes: SVG (JSR-226), OpenGL ES (JSR-239), M3G (Mobile 3D Graphics: JSR-184). Só para gráficos 3D temos duas! E a M3G 2.0 (JSR-297), com recursos como shaders, já está em andamento. Todo esse suporte existe devido ao sucesso do Java ME como plataforma para jogos.
Enfim, voltando à história e concluindo: se você quiser gravar imagens num formato padrão, pode implementar isso por conta própria (sugiro escolher um formato muito simples e bem documentado, como BMP), ou fazer esta conversão no PC. Esta última opção é viável quando a aplicação Java ME é apenas um front-end para outra aplicação em plataforma Java SE ou EE.
Um encoder, neste contexto, implementa um algoritmo que converte uma imagem num arquivo (ou stream de dados) codificado segundo determinado formato binário, incluindo características como compressão. Faz o oposto de um decoder.
O runtime Java ME costuma ocupar uma pequena parte da ROM, a parte do leão sendo ao sistema operacional e serviços e aplicações do dispositivo – das essenciais (como comunicação GSM) às secundárias (como jukeboxes MP3). Infelizmente não encontrei nenhum aparelho sem essas bobagens, com o hardware dedicado só ao Java!
Saiu na DevMedia!
- Buscas semânticas com Elasticsearch:
Elasticsearch é uma ferramenta de busca de alta performance. Com ela podemos armazenar grandes volumes de dados, analisá-los e processá-los, obtendo resultados em tempo real.
Saiba mais sobre Java ;)
- Guias de Java:
Encontre aqui os Guias de estudo que vão ajudar você a aprofundar seu conhecimento na linguagem Java. Desde o básico ao mais avançado. Escolha o seu!
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Artigo