Technè

Tecnologia & Experiência do Usuário no C.E.S.A.R

Touch Menu em Java ME

Posted by Telmo Mota On dezembro - 10 - 2010

Prosseguindo com os detalhes de como implementar aplicações Java ME que façam uso de telas sensíveis ao toque, hoje vamos ver uma forma de usar essas telas como entrada de dados: ter uma área da tela responsável por disparar uma ação, como um menu.

Também vamos mostrar como um menu desses pode ser construído de forma a se adaptar a mudanças de orientação da tela.

Primeiro vamos representar cada entrada do menu com a classe abaixo. A responsabilidade dela é saber pintar cada item e guardar o Command que será disparado ao se tocar na tela sobre esse item:

public class TouchItem {

  Command command;

  public TouchItem(Command command) {
    this.command = command;
  }

  // para funcionar é preciso que o objeto
  // Graphics seja ajustado de forma que
  // a origem esteja no canto superior esquerdo
  // e a area disponivel para pintura seja
  // definida pelo clip
  public void paint (Graphics g) {
    g.drawString(command.getLabel(),
        g.getClipWidth() / 2, g.getClipHeight() / 2,
        Graphics.HCENTER | Graphics.BASELINE);
    g.drawRect(0, 0, g.getClipWidth(), g.getClipHeight());
  }
}

Depois implementamos uma classe responsável por pintar todos os itens do menu. Essas duas primeiras classes também podem ser usadas para implementar a interface de jogos de tabuleiro, como Xadrez, por exemplo.

public class TouchGrid {

  private final TouchItem items[][];
  private int itemWidth, itemHeight;
  private int x, y;
  private int nextLine, nextColumn;

  public TouchGrid(int x, int y, int width, int height, int columns, int lines) {
    this.items = new TouchItem[columns][lines];
    setRectangle(x, y, width, height);
  }

  public void setRectangle (int x, int y, int width, int height) {
    this.x = x;
    this.y = y;
    this.itemWidth = width / this.items.length;
    this.itemHeight = height / this.items[0].length;
  }

  // preenche as colunas de uma linha antes de
  // passar para a proxima linha. Quatro chamadas
  // consecutivas resultam na seguinte apresentacao
  // [ 1, 2 ]
  // [ 3, 4 ]
  public void add (TouchItem item) {
    this.items[nextColumn][nextLine] = item;
    nextColumn = (nextColumn + 1) % items.length;
    if (nextColumn == 0) {
      nextLine = (nextLine + 1) % items[0].length;
    }
  }

  public void paint (Graphics g) {
    Clip clip = new Clip(g);
    int itemX = x;
    for (int i = 0; i < items.length; i++) {
      int itemY = y;
      for (int j = 0; j < items[i].length; j++) {
        if (items[i][j] != null) {
          // define a area de pintura e
          // desloca a origem
          g.setClip(itemX, itemY, itemWidth, itemHeight);
          g.translate(itemX, itemY);
          items[i][j].paint(g);
          // restaura o estado inicial
          g.translate(-g.getTranslateX(), -g.getTranslateY());
          clip.applyTo(g);
        }
        itemY += itemHeight;
      }
      itemX += itemWidth;
    }
  }

  // aqui mapeamos o ponto pressionado na tela
  // em um item do menu
  public TouchItem getTouchItemAt (int x, int y) {
    if (x < this.x || y < this.y) {
      return null;
    }

    int i = (x - this.x) / this.itemWidth;
    int j = (y - this.y) / this.itemHeight;

    if (i >= items.length || j >= items[0].length) {
      return null;
    }

    return items[i][j];
  }
}

No código acima usamos a seguinte classe utilitária para guardar os valores da área de pintura e resetá-la:

public class Clip {

  private int x, y;
  private int width, height;

  public Clip (Graphics g) {
    x = g.getClipX();
    y = g.getClipY();
    width = g.getClipWidth();
    height = g.getClipHeight();
  }

  public void applyTo (Graphics g) {
    g.setClip(x, y, width, height);
  }
}

Precisamos de uma classe que herda de Canvas para pintar o grid. Ela também será usada para criar as opções de menu:

public class TouchMenuCanvas extends Canvas {

  private TouchGrid touchGrid;
  private CommandListener commandListener;

  public TouchMenuCanvas () {
    final int columns = 2;
    final int lines = 3;
    int margin = getWidth() / 25;
    // 2 = margens esquerda e direita
    int width = getWidth() - (2 * margin);
    // 2 = margens superior e inferior
    int height = getHeight() - (2 * margin);
    touchGrid = new TouchGrid(margin, margin, width, height, columns, lines);
    for (int i = 0; i < lines * columns; i++) {
      Command c = new Command("item " + (i+1), Command.SCREEN, 1);
      touchGrid.add(new TouchItem(c));
    }
  }

  // quando a orientação da tela muda precisamos
  // atualizar as informações do grid
  protected void sizeChanged(int width, int height) {
    int margin = width / 25;
    width = width - (2 * margin);
    height = height - (2 * margin);
    touchGrid.setRectangle(margin, margin, width, height);
  }

  // precisamos sobre-escrever esse metodo pois nao
  // existe um metodo getCommandListener na API
  public void setCommandListener(CommandListener listener) {
    super.setCommandListener(listener);
    commandListener = listener;
  }

  protected void paint(Graphics g) {
    g.setColor(0xffffff); // white
    g.fillRect(0, 0, getWidth(), getHeight());
    g.setColor(0); // black
    this.touchGrid.paint(g);
  }

  protected void pointerPressed(int x, int y) {
    TouchItem item = this.touchGrid.getTouchItemAt(x, y);
    if (item != null) {
      commandListener.commandAction(item.command, this);
    }
  }
}

Para finalizar o MIDlet para mostrar o canvas na tela:

public class TouchMenuMIDlet extends MIDlet implements CommandListener {

  TouchMenuCanvas menuCanvas;

  public TouchMenuMIDlet() {
    menuCanvas = new TouchMenuCanvas();
    menuCanvas.setCommandListener(this);
    menuCanvas.addCommand(new Command("Exit", Command.EXIT, 1));
  }

  public void commandAction(Command c, Displayable d) {
    if (c.getCommandType() == Command.EXIT) {
      this.notifyDestroyed();
    } else {
      System.out.println(c.getLabel());
    }
  }

  protected void destroyApp(boolean unconditional) {
  }

  protected void pauseApp() {
  }

  protected void startApp() throws MIDletStateChangeException {
    Display.getDisplay(this).setCurrent(menuCanvas);
  }

}

Abaixo telas da aplicação executando no emulador DefaultCldcPhone1 do Java Platform Micro Edition SDK 3.0 nas duas orientações.

Esperamos ter ajudado. Até a próxima.

Suporte a telas touch em Java ME

Posted by Telmo Mota On dezembro - 1 - 2010

Cada vez mais aparelhos celulares possuem telas sensíveis ao toque. E quando se ouve falar nesse tipo de tela logo vem a mente os aparelhos mais caros que existem. Desses com muitas polegadas e especificações dignas de computadores pessoais de alguns anos atrás.

Mas também existem aparelhos mais modestos com Touch Screens. E entre eles vários dão suporte a Java ME. Então como habilitar uma aplicação nessa tecnologia a receber um “toque” do usuário?

Desde MIDP 1.0 a classe Canvas já possuia os métodos de callback para tratar eventos de tela: pointerPressed, pointerDragged e pointerReleased. Além deles também temos os métodos informativos: hasPointerEvents e hasPointerMotionEvents.

DICA: O emulador do SDK 3.0 já tem suporte a touch automaticamente, mas caso esteja usando o WTK 2.5 vá em wtklib\devices\DefaultColorPhone\DefaultColorPhone.properties e mude o valor de touch_screen para true.

Para entender o funcionamento dos métodos pointer nada melhor que um exemplo simples. Na classe abaixo temos um Canvas para desenhar de forma livre. Ela se comporta como o pincel do MS Paint.

public class TouchCanvas extends Canvas {
  // ponto inicial da reta desenhada no metodo paint
  int x1 = -1, y1 = -1;
  // ponto final da reta
  int x2, y2;
  // flag indicando a necessidade de limpar a tela
  boolean clear = true;

  protected void pointerPressed(int x, int y) {
    x1 = x;
    y1 = y;
  }
  protected void pointerReleased(int x, int y) {
    x1 = y1 = -1;
  }
  protected void pointerDragged(int x, int y) {
    x2 = x;
    y2 = y;
    repaint();
  }

  protected void paint(Graphics g) {
    if (clear) {
      g.setColor(0xffffff); // white
      g.fillRect(0, 0, getWidth(), getHeight());
      g.setColor(0); // black
      clear = false;
    }

    if (x1 >= 0 && y1 >= 0) {
      g.drawLine(x1, y1, x2, y2);
      x1 = x2;
      y1 = y2;
    }
  }
}

Com o MIDlet abaixo exibimos o Canvas e damos a opção de limpar a tela.

public class TouchMIDlet extends MIDlet implements CommandListener {

  TouchCanvas touchCanvas = new TouchCanvas();

  public TouchMIDlet() {
    touchCanvas.addCommand(new Command("Clear", Command.OK, 1));
    touchCanvas.addCommand(new Command("Exit", Command.EXIT, 1));
    touchCanvas.setCommandListener(this);
  }

  protected void destroyApp(boolean unconditional) { }

  protected void pauseApp() { }

  protected void startApp() {
    Display.getDisplay(this).setCurrent(touchCanvas);
  }

  public void commandAction(Command c, Displayable d) {
    if (c.getCommandType() == Command.EXIT) {
      notifyDestroyed();
    } else {
      touchCanvas.clear = true;
      touchCanvas.repaint();
    }
  }
}

Para um exemplo mais complexo vamos alterar duas classes do nosso Gráfico de Barras em Java ME para adicionar a capacidade de arrastar o gráfico. Primeiro vamos adicionar o seguinte método na classe BarChart:

public int getBarWidth (Font font) {
  return font.stringWidth(widestName);
}

Agora vamos adicionar o seguinte código na classe BarChartCanvas:

private int lastX, lastY;
protected void pointerPressed(int x, int y) {
  lastX = x;
  lastY = y;
}
protected void pointerDragged(int x, int y) {
  int w = barChart.getBarWidth(Font.getDefaultFont());
  // arrastou uma distancia maior ou igual a
  // largura das barras
  if (lastX - x >= w || lastY - y >= w) {
    barChart.nextBar();
    repaint();
    lastX = x;
  } if (x - lastX >= w || y - lastY >= w) {
    barChart.previousBar();
    repaint();
    lastX = x;
  }
}

Quanto menor a largura das barras do gráfico mais suave é o movimento. Vale salientar que o código funciona tanto em modo portrait quanto em modo landscape (Emulador do SDK 3.0: View .. Orientation .. 270).

Uma outra ação comum é o toque long (long press) que, infelizmente, não possui um método de callback fácil de usar. Então é preciso contar o tempo desde que o usuário tocou a tela e não fez mais nada. Abaixo segue uma forma de atualizar nossa TouchCanvas:

private Timer timer;
protected void showNotify() {
  timer = new Timer();
  timer.scheduleAtFixedRate(new TimerTask() {
    // long press x e y
    int lpx = -1, lpy = -1;
    public void run() {
      if (x1 >= 0) {
        if (lpx == x1 && lpy == y1) {
          lpx = lpy = -1;
          System.out.println("long press");
        } else {
          lpx = x1;
          lpy = y1;
        }
      }
    }
  },
  // comecando agora e repetindo a cada segundo
  0, 1000);
}
protected void hideNotify() {
  timer.cancel();
  timer = null;
}

Esperamos ter ajudado. Até a próxima.

Gráfico de Barras em Java ME

Posted by Telmo Mota On novembro - 24 - 2010

Ao desenvolver uma aplicação móvel deve-se considerar a diversidade de tamanhos de tela. Se a Apple só tem dois tamanhos de tela para o iPhone até agora, ela é a exceção. Todos os outros fabricantes variam bastante o tamanho das telas de seus produtos.

Para facilitar a vida dos desenvolvedores cada tecnologia disponibiliza widgets e formas de combiná-los. Mas chega o momento em que não há widget disponível para montar a tela do jeito que você (ou o designer) quer. O que fazer? Pintar a tela ‘na unha’ com gráficos de baixo nível. 2D ou 3D, não importa.

Mesmo que só haja um tamanho de tela alvo para a sua aplicação é bom codificar usando proporções. Evitar valores absolutos de x, y, width e height. Vamos mostrar como fazer isso implementando um gráfico de barras em Java ME.

Vale salientar que já existem soluções prontas. O objetivo do código abaixo é mostrar boas práticas de programação visual de baixo nível em Java ME.

Primeiro precisamos representar os nossos dados.

class ChartEntry {
  String name;
  float value;

  public BarChartEntry(String name, float value) {
    this.name = name;
    this.value = value;
  }
}

Os valores serão pintados no topo das barras e os labels na base. Uma linha horizontal será pintada de um lado ao outro para indicar o ponto do valor zero.

public class BarChart {

  private Vector entries = new Vector();
  private float maxValue;
  private float minValue;
  private String widestName = "";
  private int firstBar;

  // Cada vez que uma entrada for adicionada verificamos
  // o maior e o menor valor e a maior largura de nome.
  // Guardar esses valores em atributos reduz o tempo de
  // execucao do metodo paint.
  public void addEntry(BarChartEntry entry) {
    entries.addElement(entry);
    if (maxValue < entry.value) {
      maxValue = entry.value;
    }
    if (minValue > entry.value) {
      minValue = entry.value;
    }
    if (widestName.length() < entry.name.length()) {
      widestName = entry.name;
    }
  }

  public void nextBar () {
    this.setFirstBar(this.firstBar +1);
  }
  public void previousBar () {
    this.setFirstBar(this.firstBar -1);
  }
  private void setFirstBar(int firstBar) {
    if (firstBar >= 0 && firstBar < this.entries.size())
      this.firstBar = firstBar;
  }

  // semelhante a Graphics.drawRect. x e y indicam
  // o ponto superior esquerdo de um retangulo com
  // w de largura e h de altura.
  public void draw(Graphics g, int x, int y, int w, int h) {
    Font font = g.getFont();
    // altura disponivel para pintar a barra mais alta
    int availableHeight = h - (2 * font.getHeight());
    int barWidth = font.stringWidth(widestName);
    // espaco entre as barras
    int barMargin = w / 25;
    int maxBars = (int) Math.floor(w / (barWidth + barMargin));
    float valuesRange = maxValue - minValue;
    int barOrigin = y + h - font.getHeight();

    if (minValue < 0) {
      // desloca a linha da origem para cima
      barOrigin = barOrigin
          + (int) ((availableHeight * minValue) / valuesRange);
    }

    g.drawLine(x, barOrigin, x + w, barOrigin);

    // draw chart entries
    BarChartEntry drawEntries [] = new BarChartEntry[this.entries.size()];
    int barX = x;
    int barCount = 0;
    this.entries.copyInto(drawEntries);
    for (int i = 0; i < maxBars && this.firstBar + i < drawEntries.length; i++) {
      BarChartEntry chartEntry = drawEntries[this.firstBar + i];
      // obtemos a altura em pixels da barra atual
      // atraves de uma regra de 3 simples.
      // availableHeight -> valuesRange
      // barHeight       -> chartEntry.value
      int barHeight = (int) ((availableHeight * chartEntry.value) / valuesRange);
      int textCenterX = barX + barMargin + (barWidth / 2);
      if (barHeight > 0) {
        g.drawString(chartEntry.name, textCenterX, barOrigin,
            Graphics.HCENTER | Graphics.TOP);
        g.fillRect(barX + barMargin, barOrigin - barHeight, barWidth,
            barHeight);
        g.drawString(String.valueOf(chartEntry.value), textCenterX,
            barOrigin - barHeight, Graphics.HCENTER
                | Graphics.BOTTOM);
      } else {
        g.drawString(chartEntry.name, textCenterX, barOrigin,
            Graphics.HCENTER | Graphics.BOTTOM);
        g.fillRect(barX + barMargin, barOrigin, barWidth, -barHeight);
        g.drawString(String.valueOf(chartEntry.value), textCenterX,
            barOrigin - barHeight, Graphics.HCENTER | Graphics.TOP);
      }
      barX = barX + barMargin + barWidth;
      barCount++;
    }
  }
}

Abaixo código fonte de um Canvas que mostra o gráfico de barras e muda a primeira barra exibida de acordo com as teclas pressionadas.

public class BarChartCanvas extends Canvas {

  BarChart barChart = new BarChart();

  public BarChartCanvas() {
    barChart.addEntry(new BarChartEntry("J", 8.0f));
    barChart.addEntry(new BarChartEntry("FE", 10.0f));
    barChart.addEntry(new BarChartEntry("MAR", 6.0f));
    barChart.addEntry(new BarChartEntry("ABRI", 9.0f));
    barChart.addEntry(new BarChartEntry("MAIO", -2.0f));
    barChart.addEntry(new BarChartEntry("JUNHO", -4.0f));
    barChart.addEntry(new BarChartEntry("JULHO", 6.0f));
    barChart.addEntry(new BarChartEntry("AGOSTO", 9.0f));
  }

  protected void paint(Graphics g) {
    g.setColor(0xffffff);
    g.fillRect(0, 0, getWidth(), getHeight());
    g.setColor(0);
    barChart.draw(g, 0, 0, getWidth(), getHeight());
  }

  protected void keyPressed(int keyCode) {
    switch (keyCode) {
      case Canvas.KEY_NUM2:
      case Canvas.KEY_NUM4:
        barChart.previousBar();
        break;
      case Canvas.KEY_NUM6:
      case Canvas.KEY_NUM8:
        barChart.nextBar();
        break;
      default:
        switch (super.getGameAction(keyCode)) {
          case Canvas.LEFT:
          case Canvas.UP:
            barChart.previousBar();
            break;
          case Canvas.RIGHT:
          case Canvas.DOWN:
            barChart.nextBar();
            break;
        }
        break;
    }
    this.repaint();
  }
}

E o MIDlet usado para mostrar o Canvas.

public class BarChartMIDlet extends MIDlet implements
    CommandListener {

  BarChartCanvas chartCanvas = new BarChartCanvas();

  public BarChartMIDlet() {
    chartCanvas.addCommand(new Command("Exit", Command.EXIT, 1));
    chartCanvas.setCommandListener(this);
  }

  protected void destroyApp(boolean unconditional) { }

  protected void pauseApp() { }

  protected void startApp() {
    Display.getDisplay(this).setCurrent(chartCanvas);
  }

  public void commandAction(Command c, Displayable d) {
    this.notifyDestroyed();
  }
}

Abaixo telas da aplicação executando no emulador DefaultCldcPhone1 do Java Platform Micro Edition SDK 3.0.

Como era de se esperar, no modo landscape é possível pintar uma barra a mais na tela. Essa rotação da tela do emulador pode ser feita usando a opção de menu View .. Orientation.

Esperamos ter ajudado. Até a próxima.

XML Data Binding em Java ME

Posted by Telmo Mota On novembro - 10 - 2010

Mapear documentos XML em objetos Java é uma tarefa trivial nas versões SE e EE. Pode-se usar, por exemplo, JAXB ou Castor. É possível conseguir uma forma semelhante mesmo com as limitações da versão ME? Sim, vamos aos detalhes…

O primeiro problema é o limitado número de funcionalidades de reflection disponíveis em CLDC. Não é possível usar construtores com parâmetros ou acessar métodos dinamicamente. Além disso é desaconselhável usar Class.forName, pois assim temos que adicionar exceções ao ofuscador.

Na proposta apresentada aqui usaremos duas classes para implementar o serviço de Data Binding Unmarshal. Uma para representar uma tag encontrada e outra para lidar com os detalhes do parsing do XML. O código da primeira classe é:

public class XMLTag {
  // caso a memória disponível seja muito pequena pode-se
  // iniciar esses atributos de forma lazy nos métodos
  private Hashtable attributes = new Hashtable();
  private Vector childs = new Vector();

  public void setAttributeValue(String attribute, String value) {
    if (attribute != null && value != null) {
      this.attributes.put(attribute, value);
    }
  }
  public String getAttributeValue (String attribute) {
    return (String) this.attributes.get(attribute);
  }

  public void addChild (XMLTag child) {
    this.childs.addElement(child);
  }
  public Enumeration getChilds () {
    return this.childs.elements();
  }
  public XMLTag getChildAt (int index) {
    return (XMLTag) this.childs.elementAt(index);
  }
}

O XML exemplo que vamos usar é RSS versão 2.0. Os dados ficam no seguinte formato:

<rss version="2.0">
  <channel>
    <item>
      <title>item title</title>
      <link>http://site.com</link>
    </item>
    <item>
      <title>item title 2</title>
      <link>http://site.com/2</link>
    </item>
  </channel>
</rss>

Vamos criar as classes necessárias para tratar essas tags como filhas da classe XMLTag. Então temos:

class RSS extends XMLTag {
  Channel channel;
  public void addChild(XMLTag child) {
    if (child instanceof Channel) {
      this.channel = (Channel) child;
    }
  }
}
class Channel extends XMLTag {
  public void addChild(XMLTag child) {
    if (child instanceof Item) {
      super.addChild(child);
    }
  }
}
class Item extends XMLTag {
}

Não criamos classes para as tags title e link pois elas são consideradas atributos de item. Seus valores serão guardados em XMLTag.attributes.

Para evitar o uso de Class.forName fazemos o mapeamento de tags em classes através de uma Hashtable. A chave é um string e o valor é uma instância de Class que herda de XMLTag. Esse mapa é passado por parâmetro para o construtor da classe responsável pela detalhes do processamenso do XML. Abaixo um exemplo de configuração:

Hashtable map = new Hashtable();
map.put("rss", RSS.class);
map.put("channel", Channel.class);
map.put("item", Item.class);

O parser usado pela segunda classe é o SAX. Esse parser notifica um Handler cada vez que encontra um trecho de XML. Ao achar o inicio de uma tag chama o método startElement, ao achar o término chama endElement e ao encontrar texto chama characters.

A implementação da segunda classe usa uma Stack para empilhar as tags encontradas. Dessa forma, quando a tag rss for encontrada, uma instância da classe RSS será empilhada. Abaixo uma figura com a evolução da pilha até encontrar a primeira tag item:

Como a tag title não possui uma classe associada ela é considerada como um atributo da classe que está no topo da pilha. O valor informado pelo método characters é acumulado num buffer até encontrar o fim da tag title. Quando o fim da tag title é encontrado o valor acumulado é registrado com XMLTag.setAttributeValue.

Ao encontrar o fim de uma tag que possui classe associada, a instância é desempilhada e adicionada como child da tag anterior.

Ao fim do processamento a pilha fica vazia e as instâncias se relacionam da seguinte forma:

O código da segunda classe é:

class XMLBinder extends org.xml.sax.helpers.DefaultHandler {

  private Hashtable map = new Hashtable();
  private Stack stack = new Stack();
  private XMLTag rootElement;

  private String attribute;
  private StringBuffer value = new StringBuffer();

  /**
   * @param map with String keys and XMLTag values
   */
  public XMLBinder(Hashtable map) {
    Enumeration e = map.keys();
    while (e.hasMoreElements()) {
      Object key = e.nextElement();
      Object tag = map.get(key);
      if (validateMapping(key, tag)) {
        this.map.put(key, tag);
      } else {
        throw new IllegalArgumentException("key " + key);
      }
    }
  }

  private boolean validateMapping (Object key, Object tag) {
    return key instanceof String
           && tag instanceof Class
           && XMLTag.class.isAssignableFrom((Class) tag);
  }

  public XMLTag unmarshall (InputStream in) throws IOException {
    try {
      SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
      parser.parse(in, this);
      return this.rootElement;
    } catch (Exception ex) {
      throw new IOException("caused by " + ex);
    }
  }

  public void startElement(String uri, String localName, String qName,
      Attributes attributes) throws SAXException {
    Class tag = (Class) this.map.get(qName);
    if (tag != null) {
      try {
        XMLTag newTag = (XMLTag) tag.newInstance();
        addAttributesToXMLTag(attributes, newTag);
        this.stack.push(newTag);
      } catch (Exception e) {
        throw new SAXException("caused by " + e);
      }
    } else {
      this.attribute = qName;
    }
  }

  private void addAttributesToXMLTag (Attributes attributes, XMLTag newTag) {
    if (attributes != null) {
      for (int i = attributes.getLength() - 1; i >= 0; i--) {
        String attrName = attributes.getQName(i);
        String attrValue = attributes.getValue(i);
        newTag.setAttributeValue(attrName, attrValue);
      }
    }
  }

  public void characters(char[] ch, int start, int length) {
    if (this.attribute != null) {
      this.value.append(ch, start, length);
    }
  }

  public void endElement(String uri, String localName, String qName)
      throws SAXException {
    if (this.stack.isEmpty()) {
      throw new SAXException("no mapping for " + qName);
    }
    if (this.attribute != null && this.attribute.equals(qName)) {
      XMLTag parent = (XMLTag) this.stack.peek();
      parent.setAttributeValue(this.attribute, this.value.toString());
      this.attribute = null;
      this.value.setLength(0);
    } else {
      XMLTag child = (XMLTag) this.stack.pop();
      if (this.stack.isEmpty() == false) {
        XMLTag parent = (XMLTag) this.stack.peek();
        parent.addChild(child);
      } else {
        this.rootElement = (XMLTag) child;
      }
    }
  }
}

Para carregar, por exemplo, as notícias aqui do Technè – Blog de Tecnologia do c.e.s.a.r – criamos o seguinte MIDlet:

public class TechneMidlet extends MIDlet implements CommandListener, Runnable {

  private List news = new List("Techne News", List.IMPLICIT);
  private Thread runner;
  private RSS rss;

  public TechneMidlet() {
    news.addCommand(new Command("Exit", Command.EXIT, 1));
    news.setCommandListener(this);
  }

  protected void destroyApp(boolean arg0) { }

  protected void pauseApp() { }

  protected void startApp() {
    if (runner == null) {
      news.setTicker(new Ticker("Loading news..."));
      runner = new Thread(this);
      runner.start();
    }
    Display.getDisplay(this).setCurrent(this.news);
  }

  public void run() {
    try {
      String url = "http://techne.cesar.org.br/feed/?lang=en";
      InputStream in = Connector.openInputStream(url);
      XMLBinder binder = newXMLBinder();

      rss = (RSS) binder.unmarshall(in);
      Enumeration e = rss.channel.getChilds();
      while (e.hasMoreElements()) {
        Item i = (Item) e.nextElement();
        news.append(i.getAttributeValue("title"), null);
      }
    } catch (Exception ex) {
      showError(ex);
    }

    news.setTicker(null);
    this.runner = null;
  }

  private XMLBinder newXMLBinder () {
    Hashtable map = new Hashtable();
    map.put("rss", RSS.class);
    map.put("channel", Channel.class);
    map.put("item", Item.class);
    return new XMLBinder(map);
  }

  private void showError (Exception ex) {
    Alert a = new Alert("Error", ex.toString(), null, AlertType.ERROR);
    a.setTimeout(Alert.FOREVER);
    Display.getDisplay(this).setCurrent(a, this.news);
  }

  public void commandAction(Command c, Displayable d) {
    if (c == List.SELECT_COMMAND) {
      int index = news.getSelectedIndex();
      Item item = (Item) rss.channel.getChildAt(index);
      String url = item.getAttributeValue("link");
      try {
        platformRequest(url);
      } catch (ConnectionNotFoundException ex) {
        showError(ex);
      }
    } else {
      this.notifyDestroyed();
    }
  }
}

Esperamos que a dica seja útil. Até a próxima.

Uma funcionalidade muito comum nas aplicações atuais é a verificação de versões mais novas de si mesma. No caso de aplicações móveis isso pode ser feito pela própria tecnologia/plataforma em que foi desenvolvida. Por exemplo, aparelhos com Android ou iOS verificam automaticamente novas versões das aplicações baixadas. Caso haja uma nova versão uma notificação é exibida.

Vamos mostrar como adicionar essa funcionalidade em aplicações cuja tecnologia/plataforma não oferece essa facilidade.

Primeiro é preciso ter um site oficial e nele uma página que possa ser lida pela aplicação. Por exemplo, http://meusite.com/minhaapp.txt
Nessa nossa proposta o conteúdo da página contém, na primeira linha, o número da versão mais atual (1.1, 2.0, etc) e uma URL de onde ela pode ser baixada. Nas linhas seguintes a descrição do que mudou. Ou seja:
“<versão> <url>
<descrição>”

Depois adicionamos um item na interface gráfica da aplicação do tipo “Check for updates”. Quando o usuário selecionar esse item a página é lida do site oficial e seu conteúdo é analisado. Para baixar o conteúdo em Java ME pode-se usar o código abaixo:

InputStream in = Connector.openInputStream("http://meusite.com/minhaapp.txt");
ByteArrayOutputStream out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) >= 0) {
  out.write(i);
}
String content = new String(out.toByteArray());

A versão atual da aplicação pode estar presente numa constante de código ou ser lida da plataforma, caso ela permita.
Em Java ME a aplicação pode saber dinamicamente sua versão chamando MIDlet.getAppProperty(“MIDlet-Version”).
Basta os valores das versões serem diferentes para sugerir que o usuário atualize a sua versão.

IMPORTANTE: A descrição deve ser usada para que o usuário entenda os benefícios da atualização.

Caso ele concorde MIDlet.platformRequest(url) abrirá o browser do aparelho para mostrar a url.

Já que o usuário pode nunca selecionar essa opção é interessante adicionar um verificador interno. Caso a aplicação já faça uso contínuo da rede isso pode ser feito junto com outras conexões. Caso a aplicação seja offline podemos verificar, a cada vez que ela é iniciada, se já se passou um certo tempo sem buscar por atualizações. Para implementar esse verificador em Java ME criamos os seguintes métodos utilitários:

private long byteArrayToLong(byte [] buf) throws IOException {
  ByteArrayInputStream in = new ByteArrayInputStream(buf);
  DataInputStream dis = new DataInputStream(in);
  return dis.readLong();
}
private byte[] longToByteArray(long value) throws IOException {
  ByteArrayOutputStream out = new ByteArrayOutputStream();
  DataOutputStream dos = new DataOutputStream(out);
  dos.writeLong(value);
  return out.toByteArray();
}

E usamos um RecordStore com a data, em formato long, da última vez que a verificação feita. Esse código deve ser chamado no método MIDlet.startApp().

RecordStore rs = RecordStore.openRecordStore("lastUpdateCheck", true);
long now = System.currentTimeMillis();
if (rs.getNumRecords() == 0) {
  // first application execution
  byte[] data = longToByteArray(now);
  rs.addRecord(data, 0, data.length);
} else {
  final long MONTH = 30*24*60*60*1000;
  long before = byteArrayToLong(rs.getRecord(1));
  if (now - before > MONTH) {
    // show "Check for Update" Alert
    // save current time
    byte[] data = longToByteArray(now);
    rs.setRecord(1, data, 0, data.length);
  }
}
rs.closeRecordStore();

Ao desenvolver a atualização é bom reusar os dados da versão anterior pois usuários não gostam de perder suas configurações.  Para que isso aconteça em Java ME é preciso que o MIDlet-Name e o MIDlet-Vendor sejam exatamente os mesmos.

Quando somadas ao Crash Report, atualizações contínuas dão credibilidade à aplicação e seus desenvolvedores.
Esperamos que essa dica tenha sido útil. Até a próxima.

Adicionando Crash Report em aplicações Java ME

Posted by Telmo Mota On outubro - 28 - 2010

Crash Report é um conjunto de informações que detalham um problema que fez uma aplicação parar de funcionar. Sistemas operacionais como Microsoft Windows, Mac OS e distros do Linux possuem essa funcionalidade. Navegadores como Mozilla Firefox e Google Chrome também possuem.

Desde a versão 2.2 Android também dá suporte a Crash Reports. Usando apenas o que apresentamos no post anterior sobre Twitter API para Java ME é possível adicionar a funcionalidade de Crash Report para uma aplicação.

É interessante que a aplicação Java ME tenha uma conta própria no Twitter. Assim todos da equipe de desenvolvimento podem seguir os reports da aplicação.

Quais informações são relevantes num Crash Report?

  1. nome e versão da aplicação: MIDlet-Name e Version
  2. os valores dos atributos/variáveis usados
  3. a exceção que causou o problema
  4. modelo do aparelho que está executando a aplicação: microedition.platform

Abaixo código de uma classe para encapsular essas informações:

public class CrashReport {
  private StringBuffer report = new StringBuffer();
  public CrashReport (MIDlet midlet, String details, Throwable e) {
    if (midlet != null) {
      report.append(midlet.getAppProperty("MIDlet-Name"));
      report.append(' ');
      report.append(midlet.getAppProperty("MIDlet-Version"));
      report.append(' ');
    }
    if (details != null) {
      report.append(details).append(' ');
    }
    if (e != null) {
      report.append(e).append(' ');
    }
    String platform = System.getProperty("microedition.platform");
    if (platform != null) {
      report.append(platform);
    }
  }
  // facilita ser guardado em RecordStore
  public byte [] toByteArray() {
    return report.toString().getBytes();
  }
}

Assim que o erro acontecer devemos apresentar uma tela informando ao usuário que aconteceu um erro inesperado e perguntando se ele quer mandar uma mensagem para o desenvolvedor. Se o usuário responder que não, a aplicação descarta a mensagem. Se ele responder que sim, a aplicação tenta enviar imediatamente.

Abaixo código de classe que envia um CrashReport pelo Twitter:

public class CrashReportTwitterSender {
  private TweetER tweetEr;
  public CrashReportTwitterSender () throws IOException {
    // para definição da variavel credential vide post anterior
    UserAccountManager accountManager = UserAccountManager.getInstance(credential);
    try {
      if (accountManager.verifyCredential()) {
        tweetEr = TweetER.getInstance(accountManager);
      } else {
        throw new IOException("Could not verify credential");
      }
    } catch (LimitExceededException ex) {
      throw new IOException("caused by " + ex.getClass().getName());
    }
  }
  public boolean sendReport (CrashReport report) {
    try {
      tweetEr.post(new Tweet(report.toString()));
      return true;
    } catch (Exception ex) {
      return false;
    }
  }
}

Como pode acontecer uma falha no envio, nesse caso é importante oferecer a opção de salvar para enviar depois. Se o usuário responder que não, a aplicação descarta a mensagem. Se ele responder que sim, a aplicação guardar essas informações para uso futuro.

Abaixo código de classe que guarda instâncias de CrashReport em RecordStore:

public class CrashReportStore {
  private RecordStore reports;
  public CrashReportStore () throws IOException {
    try {
      reports = RecordStore.openRecordStore("reports", true);
    } catch (RecordStoreException ex) {
      throw new IOException("caused by " + ex.getClass().getName());
    }
  }
  public void addReport (CrashReport report) throws IOException {
    byte [] data = report.toByteArray();
    try {
      reports.addRecord(data, 0, data.length);
    } catch (RecordStoreException ex) {
      throw new IOException("caused by " + ex);
    }
  }
  public boolean hasReports () {
    try {
      return this.reports.getNumRecords() > 0;
    } catch (RecordStoreNotOpenException ex) {
      return false;
    }
  }
  public void sendAllRreports (CrashReportTwitterSender sender) throws IOException {
    try {
      RecordEnumeration re = this.reports.enumerateRecords(null, null, false);
      while (re.hasNextElement()) {
        sendReportAndDeleteRecord(sender, re.nextRecordId());
      }
    } catch (Exception ex) {
      throw new IOException("caused by " + ex);
    }
  }
  private void sendReportAndDeleteRecord (CrashReportTwitterSender sender, int id)
    throws RecordStoreException
  {
    byte [] data = this.reports.getRecord(id);
    CrashReport report = CrashReport.fromByteArray(data);
    if (sender.sendReport(report)) {
      this.reports.deleteRecord(id);
    }
  }
}

Da próxima vez que a aplicação for iniciada ela deve verificar se CrashReportStore.hasReports(). Se não houver não precisa fazer nada e segue com a inicialização normal. Se houver apresenta para o usuário as opções de envio: Agora ou Depois.

Abaixo um exemplo de código para testar o que foi descrito aqui. Ele deve ser colado em uma classe que herda de MIDlet.

CrashReport report = new CrashReport(this, "crash report test", null);
CrashReportTwitterSender sender = new CrashReportTwitterSender();
if (sender.sendReport(report) == false) {
  CrashReportStore store = new CrashReportStore();
  store.addReport(report);
}

Esperamos que essa abordagem seja útil. Até a próxima.

Twitter API para Java ME

Posted by Telmo Mota On outubro - 22 - 2010

Twitter não é apenas um site, é um meio de comunicação com API (Application Programming Interface) pública e bem documentada.
Para facilitar a vida existem bibliotecas que viabilizam o seu uso por desenvolvedores de diferentes tecnologias.
Nesse post vamos mostrar como usar uma biblioteca para Java ME no NetBeans 6.9.1 com o plugin de Java ME instalado.

Crie um novo projeto em File .. New Project .. Java ME .. Mobile Application .. Next
Project Name: TwitterClient
Deixar marcado “Set as Main Project” e desmarcar “Create Hello MIDlet” .. Finish
Não ativar ofuscação de código.

Crie um MIDlet em File .. New File .. MIDP .. MIDlet.
MIDlet name: TwitterClientMidlet
MIDlet class name é automaticamente alterado.

Baixe o zip da biblioteca para Java ME e descompacte.
Dentro da pasta dist encontramos o jar que nos interessa.
Adicione twitter_api_me-1.4.jar ao classpath do projeto selecionando Resources .. Add Jar/Zip.

Para começar vamos usar uma função da API do Twitter que não precisa de validação: search.
Adicione no método startApp do MIDlet as seguintes linhas:

SearchDevice searchDevice = SearchDevice.getInstance();
Query query = QueryComposer.containAll("Twitter API");
Tweet[] tweets = searchDevice.searchTweets(query);
for (int i = 0; i < tweets.length; i++) {
  System.out.println(tweets[i]);
}

Todas as classes são encontradas no pacote com.twitterapime.search.

Execute o MIDlet em Run .. Run Main project.
Quando o emulador for exibido selecione Launch.
Como a biblioteca está acessando a rede vai aparecer uma pergunta “Is it OK to Use Airtime”. Selecione Yes.
Na janela Output vão aparecer várias linhas no formato “<chave>: <valor>” começando com “TWEET_”.
Nós podemos obter o valor de cada linha separadamente usando o método Tweet.getString, por exemplo:

tweets[i].getString("TWEET_CONTENT"); // texto do tweet
tweets[i].getString("TWEET_AUTHOR_NAME"); // usuario que postou o tweet
tweets[i].getString("TWEET_URI"); // link para o tweet

Para usar as funções que requerem validação, como home_timeline, é preciso estar logado no Twitter.
Caso não esteja logado é lançada uma SecurityException: User’s credential must be verified.

Primeiro crie uma aplicação no Twitter e copie Consumer key e Consumer secret.
Depois entre na página My Access Token e copie oauth_token e oauth_token_secret.

Nesse exemplo não é preciso ter uma interface pedindo login e senha pois já temos o token para validar o acesso.
Use todos esses valores nas linhas abaixo substituindo “seu_*” pelos valores que você copiou.

Token token = new Token("seu_oauth_token", "seu_oauth_token_secret");
Credential credential = new Credential("seu_login",
    "seu_consumer_key", "seu_consumer_secret", token);
UserAccountManager accountManager = UserAccountManager.getInstance(credential);
if (accountManager.verifyCredential()) { // tudo ok
  Timeline timeLine = Timeline.getInstance(accountManager);
  timeLine.startGetHomeTweets(null, new SearchDeviceListener() {
    public void tweetFound(Tweet tweet) {
      System.out.println("----------------------------------");
      System.out.println(tweet);
    }
    public void searchCompleted() { }
    public void searchFailed(Throwable t) { }
  });
}

As classes estão nos pacotes com.twitterapime.rest, com.twitterapime.search e com.twitterapime.xauth.

Caso aconteça algum erro é lançada uma IOException com o código http correspondente.

Se a sua aplicação precisa dar acesso a outros usuários do Twitter será necessário pedir permissão para usar xAuth enviando email para api@twitter.com.
Nesse caso não se usa a classe Token e se usa um construtor diferente da classe Credential.

Esperamos que essa introdução tenha sido útil.  Até a próxima.