sexta-feira, 23 de dezembro de 2011

Agradecimento

Olá povo,

Esse será o último post de 2011. Um ano fantástico para mim, Graças a Deus! Consegui atingir vários objetivos, e por isso resolvi agradecer a todos que acreditaram em mim e no meu trabalho.

Primeiramente ao CESAR, onde trabalho desde 2006, e me dá a oportunidade de aprender muito todos os dias. Mexer com diversas tecnologias e em um ambiente diferenciado, são coisas que me faz gostar e querer continuar nesse time fantástico.

Um agradecimento Especial a Especializa :) onde dei os primeiros paços como instrutor. Foi uma grande escola, e continua sendo uma satisfação ministrar cursos em uma instituição que tem como meta, formar especialistas com cursos de qualidade.


A LinuxFi me permitiu conhecer a bela capital da Paraíba. E tive a satisfação de ministrar o primeiro curso de Android de João Pessoa. Depois tive a oportunidade de voltar, e espero voltar mais vezes :)


Após terminar o mestrado no CESAR.edu, tive a felicidade de iniciar minha carreira como professor universitário na própria instituição. Dando aulas de Java ME e Android na pós-graduação em Tecnologias para Desenvolvimento de Aplicações Móveis, aprendi muito com meus alunos, e isso gerou muitos posts aqui do blog :)


Esse é o meu mais novo desafio. Esse ano eu comecei a ministrar a disciplina de Programação Móvel no curso de Análise e Desenvolvimento de Software (ADS) na Unibratec. Totalmente diferente de cursos isolados e de pós-graduação, está sendo bem gratificante e desafiador.

Venho escrevendo matérias para as revistas Java Magazine e Mobile Magazine da DevMedia. Essa parceria tem me ajudado a escrever melhor (inclusive aqui pro blog) e falar melhor também durante as aulas. Além é claro de me forçar a estudar para enriquecer as aulas.

Para finalizar, agradeço a todos vocês. Colegas de trabalho, Alunos, Leitores do Blog e/ou dos meus artigos. Espero contar com todos vocês em 2012.

8045 F35745!

4br4ç05,
nglauber

segunda-feira, 12 de dezembro de 2011

Google Maps: Traçando Rotas

[EDITADO EM 03/04/2013: atualizado para Google Api v2]

Olá povo,

A API do Google Maps disponível para Android é muito bacana, e incorporar esse recurso na sua aplicação pode ser um bom atrativo. Em um dos projetos que trabalhei, precisava mostrar a rota entre dois pontos no mapa, e a Google disponibilizou em 27/07/12 uma API Web que retorna a rota entre duas coordenadas geográficas no formato JSON (ou XML se preferir).

Nesse arquivo, temos uma lista com as coordenadas geográficas que devem ser percorridas desde o ponto de partida até o destino. Com essas coordenadas lidas do JSON, criamos uma lista de LatLng que devem ser desenhadas no objeto GoogleMap através de uma PolyLine.

Não vou detalhar muito a configuração do Google Maps v2 no Android pois já falei nesse post aqui. Também não vou entrar em detalhes sobre leitura de JSON pois falei nesse post aqui. E se você tiver dúvidas sobre AsyncTask, dê uma olhada nesse post.

Vou começar pelo código da Activity mostrado abaixo. No onCreate pegamos a instância de GoogleMap e definimos a posição inicial no mapa e um zoom padrão. Em seguida iniciamos a AsyncTask passando a GoogleMap no construtor e informando a latitude e longitude de origem e destino.
public class TesteRotaMapaActivity 
  extends FragmentActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
  
    SupportMapFragment fragment = 
      (SupportMapFragment)
        getSupportFragmentManager()
          .findFragmentById(R.id.map);

    GoogleMap map = fragment.getMap();

    LatLng latLng = new LatLng(-8.102739,-34.89917);
  
    map.animateCamera(
      CameraUpdateFactory.newLatLngZoom(latLng, 12.0f));
  
    new RotaAsyncTask(this, map).execute(
      // Latitude, Logintude de Origem
      latLng.latitude, latLng.longitude,    
      // Latitude, Longitude de Destino
      -23.604262,-46.676513);  
    }
}

A classe RotaAsyncTask é quem faz o trabalho de acessar o site do Google Maps, baixar o JSON com a informação da rota.
public class RotaAsyncTask extends 
  AsyncTask<Double, Void, Void>{

  private ProgressDialog dialog;
  private GoogleMap mapView;
  private Context context;
  private Route rota;

  public RotaAsyncTask(Context ctx, GoogleMap mapa) {
    mapView = mapa;
    context = ctx;
  }
 
  @Override
  protected void onPreExecute() {
    super.onPreExecute();
    dialog = ProgressDialog.show(
      mapView.getContext(), "Aguarde", 
      "Calculando rota");
  }
 
  @Override
  protected Void doInBackground(Double... params) {

    rota = directions(
      new LatLng(params[0], params[1]), 
      new LatLng(params[2], params[3]));
    return null;
  }
 
  @Override
  protected void onPostExecute(Void result) {
    super.onPostExecute(result);
    PolylineOptions options = new PolylineOptions()
      .width(5)
      .color(Color.RED)
      .visible(true);

    for (LatLng latlng : rota.getPoints()) {
      options.add(latlng);
    }

    mapView.addPolyline(options);
    dialog.dismiss();
  }
 
  private Route directions(
    final LatLng start, final LatLng dest) {
  
    // Formatando a URL com a latitude e longitude  
    // de origem e destino.  
    String urlRota = String.format(Locale.US,
      "http://maps.googleapis.com/maps/api/"+
      "directions/json?origin=%f,%f&"+
      "destination=%f,%f&" +
      "sensor=true&mode=driving",   
      start.latitude, 
      start.longitude, 
      dest.latitude, 
      dest.longitude); 
  
    GoogleParser parser;
    parser = new GoogleParser(urlRota);
    return parser.parse();
  }
}

Quem viu o post anterior pode notar que houveram algumas modificações. Delegamos o parse do JSON para a classe GoogleParser e exibimos as rotas utilizando a classe PoluLineOptions (da API v2) com ajuda da classe Route. Essas novas classes foram extraídas desse post aqui. Graças ao comentário do Hadson Gomes.
Eu peguei essas classes e deixei apenas o necessário. Vejamos elas abaixo.
public class Route {
  private final List<LatLng> points;
  private String polyline;

  public Route() {
    points = new ArrayList<LatLng>();
  }

  public void addPoints(final List<LatLng> points) {
    this.points.addAll(points);
  }

  public List<LatLng> getPoints() {
    return points;
  }

  public void setPolyline(String polyline) {
    this.polyline = polyline;
  }

  public String getPolyline() {
    return polyline;
  }
}
Note que a classe acima tem uma propriedade chamada polyline. Nesse JSON, a rota retorna algumas coordenadas "macro" digamos assim. Por isso os comentários abaixo informando que eram traçadas linhas retas no caminho. A rota com as coordenadas mas precisas estão codificadas em base64 no atributo polyline. A classe GoogleParser faz essa conversão de base64 para GeoPoint conforme abaixo.
public class GoogleParser {

  protected URL feedUrl;

  public GoogleParser(String feedUrl) {
    try {
      this.feedUrl = new URL(feedUrl);
    } catch (MalformedURLException e) {
    }
  }
 
  public Route parse() {
    // Cria uma rota vazia
    final Route route = new Route();
    try {
      // Obtém a String do JSON
      final String result = 
        convertStreamToString(
          feedUrl.openConnection()
            .getInputStream());

      // Transforma a string em JSON
      JSONObject json = new JSONObject(result);
      // Pega a primeira rota retornada
      JSONObject jsonRoute = 
        json.getJSONArray("routes")
          .getJSONObject(0);
      JSONObject leg = jsonRoute
        .getJSONArray("legs").getJSONObject(0);

      // Obtém os passos do caminho
      JSONArray steps = leg.getJSONArray("steps");

      final int numSteps = steps.length();
      /*
       * Itera através dos passos, decodificando 
       * a polyline e adicionando à rota.
       */
      JSONObject step;
      for (int i = 0; i < numSteps; i++) {
        // Obtém o passo corrente
        step = steps.getJSONObject(i);
        // Decodifica a polyline e adiciona à rota
        route.addPoints(
          decodePolyLine(
            step.getJSONObject("polyline")
              .getString("points")));
      }
    } catch (Exception e) {
    }
    return route;
  }

  private String convertStreamToString(
    final InputStream input) {

    final BufferedReader reader = 
      new BufferedReader(
        new InputStreamReader(input));
    final StringBuilder sBuf = new StringBuilder();

    String line = null;
    try {
      while ((line = reader.readLine()) != null) {
        sBuf.append(line);
      }
    } catch (IOException e) {
    } finally {
      try {
        input.close();
      } catch (IOException e) {
      }
    }
    return sBuf.toString();
  }

  private List<LatLng> decodePolyLine(final String poly) {
    int len = poly.length();
    int index = 0;
    List<LatLng> decoded = 
      new ArrayList<LatLng>();

    int lat = 0;
    int lng = 0;

    while (index < len) {
      int b;
      int shift = 0;
      int result = 0;
      do {
        b = poly.charAt(index++) - 63;
        result |= (b & 0x1f) << shift;
        shift += 5;
      } while (b >= 0x20);
      int dlat = 
        ((result & 1) != 0 ? 
          ~(result >> 1) : 
          (result >> 1));
      lat += dlat;

      shift = 0;
      result = 0;
      do {
        b = poly.charAt(index++) - 63;
        result |= (b & 0x1f) << shift;
        shift += 5;
      } while (b >= 0x20);

      int dlng = ((result & 1) != 0 ? 
        ~(result >> 1) : 
        (result >> 1));
      lng += dlng;

      decoded.add(
        new LatLng(
          (float)(lat / 1E5),
          (float)(lng / 1E5)));
    }
    return decoded;
  }
}
Quem viu o post anterior deve lembrar que a classe RouteOverlay não existe mais. Quem faz seu papel é a classe PolyLine da nova versão da Google Maps Api.
A imagem abaixo mostra nossa aplicação em execução.



Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 5 de dezembro de 2011

Resoluções de Tela

Olá povo,

Frequentemente nas aulas de Android o pessoal tem dificuldade de lembrar as diversas resoluções de tela disponíveis para os smartphones e tablets atuais. Hoje meu colega de trabalho André mandou uma imagem com os tamanhos e siglas que as representam.

No Android, a grande maioria dos dispositivos utilizam as resoluções: QVGA, HVGA, WVGA e WXGA.

4br4ç05,
nglauber

Fonte: http://samuelvarela.wordpress.com/2009/03/20/resolucao-de-tela-vga-qvga-etc/

segunda-feira, 28 de novembro de 2011

Lendo RSS no Android

Olá povo,

A tecnologia RSS tornou-se uma forma padrão que os sites encontraram para compartilhar suas informações. Com ele, os usuários podem visualizar seu conteúdo de forma padronizada, uma vez que utiliza o formato XML. Sendo assim, vou mostrar nesse post como ler um RSS com Android. Mas o código utilizado aqui pode ser utilizado para qualquer XML.

Vou começar criando a classe que vai representar uma notícia do RSS. Ela tem basicamente um título, um resumo da notícia e um link para notícia completa.

class Noticia {
  String titulo;
  String descricao;
  String link;
 
  public Noticia(String t, String d, String l) {
    titulo = t;
    descricao = d;
    link = l;
  }
 
  @Override
  public String toString() {
    return titulo;
  }
}

Simplifiquei ao máximo a classe aqui, não coloquei nem os GETs nem os SETs. Implementei apenas o método toString() pois o mesmo é utilizado pelo Adapter (que veremos em seguida) para exibir a notícia.

Agora vamos ver o código da Activity.
public class LeitorRSSActivity extends ListActivity {

  List<Noticia> news;
  ArrayAdapter<Noticia> adapter;
 
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    news = new ArrayList<Noticia>();
    adapter = new ArrayAdapter<Noticia>(
    this, android.R.layout.simple_list_item_1, news);
    setListAdapter(adapter);

    // Criando AsyncTask que buscará o RSS da globo
    new RssAsyncTask().execute(
      "http://g1.globo.com/dynamo/rss2.xml");
  }

  // Método que lê o XML do RSS
  private List<Noticia> readXML(InputStream is){
    List<Noticia> noticias =
      new ArrayList<Noticia>();

    try {
      // Criando os objetos que representam o XML
      DocumentBuilderFactory factory =
        DocumentBuilderFactory.newInstance();

      DocumentBuilder builder = 
        factory.newDocumentBuilder();
      Document xmlDocument = builder.parse(is);
   
      // Cada notícia é representada pela tag <item>
      // Aqui obtemos a lista de nós com essa tag
      NodeList posts = 
        xmlDocument.getElementsByTagName("item");

      // Vamos iterar sobre a lista de itens
      String titulo = null, descricao = null, 
        link = null;

      for (int i = 0; i < posts.getLength(); i++) {
        Node post = posts.item(i);

        // Cada nó ITEM tem os filhos:
        // TITLE, DESCRIPTION e LINK
        NodeList postInfo = post.getChildNodes();
        for (int j = 0; j < postInfo.getLength(); j++){
          Node info = postInfo.item(j);

          if ("title".equals(info.getNodeName())){
            titulo = info.getTextContent();

          } else if ("link".equals(
            info.getNodeName())){
            link = info.getTextContent();
      
          } else if ("description".equals(
            info.getNodeName())){
            descricao = info.getTextContent();
          }
        }
        // Com as informações das tags, criamos o
        // objeto notícia e adicionamos na lista
        noticias.add(
          new Noticia(titulo, descricao, link));
      }
    } catch (Throwable e) {
      e.printStackTrace();
    }
    return noticias;
  }

  // A AsyncTask realiza a comunicação em background
  class RssAsyncTask extends
    AsyncTask<String, Void, List<Noticia>>{

    ProgressDialog dialog;
 
    @Override
    protected void onPreExecute() {
      super.onPreExecute();
      // Antes de baixaro XML, mostra o dialog
      dialog = ProgressDialog.show(
        LeitorRSSActivity.this, 
        "Aguarde", "Baixando RSS");
    }
 
    @Override
    protected List<Noticia> doInBackground(
      String... params) {

      List<Noticia> lista = null;
      HttpURLConnection conexao = null;
      InputStream is = null;
   
      try {
        URL url = new URL(params[0]);
        conexao = (HttpURLConnection)
          url.openConnection();
        conexao.connect();
    
        is = conexao.getInputStream();
        lista = readXML(is);

      } catch (Throwable t){
        t.printStackTrace();
      } finally {
        try {
          if (is != null) is.close();
          if (conexao != null) conexao.disconnect();
        } catch (Throwable t){
        }
      }
      return lista;
    }
  
    @Override
    protected void onPostExecute(List<Noticia> result){
      super.onPostExecute(result);
      dialog.dismiss();
      news.addAll(result);
      adapter.notifyDataSetChanged();
    }
  }
}


Quem tiver dúvida sobre AsyncTask, dá uma olhada aqui, ou deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 21 de novembro de 2011

Google Maps e GPS

Olá povo,

Nesse post de 2009 mostrei como fazer um "Hello World" com a API do Google Maps. Agora vamos incrementar um pouco esse exemplo colocando uma imagem para indicar o seu local baseado na posição GPS.

Pré-requisitos
A configuração inicial para obter a chave e exibir o mapa na tela é idêntica a apresentada no post de dois anos atrás, mas caso não consiga gerar a chave, dê uma olhada nesse post aqui (Dica 5). Outra ressalva é que, caso você esteja usando o emulador para testar sua aplicação, certifique-se de que ele esteja usando a "Google APIs" no "Android AVD Manager" conforme a figura abaixo:



O código do Overlay
Uma coordenada geográfica é representada pela classe GeoPoint. Ela tem a latitude e longitude de um ponto no mapa. Entretanto essa classe não é visual, ou seja, ela não serve para enxergarmos o ponto no mapa. Quando queremos adicionar no mapa alguma indicação para uma coordenada geográfica, devemos utilizar a classe Overlay, que é uma subclasse de View. O código abaixo cria um Overlay que desenhará uma imagem em um dado ponto no mapa (representado por um GeoPoint).
public class MeuOverlay extends Overlay {

private Bitmap imagem;
private GeoPoint geopoint;

public MeuOverlay(Bitmap img ) {
imagem = img;
}

public void setGeopoint(GeoPoint geopoint) {
this.geopoint = geopoint;
}

@Override
public void draw(Canvas canvas, MapView mapView,
boolean shadow) {
super.draw(canvas, mapView, shadow);

if (geopoint != null){
Point pontoNaTela = mapView.getProjection()
.toPixels(geopoint, null);

canvas.drawBitmap(imagem,
pontoNaTela.x - (imagem.getWidth() / 2),
pontoNaTela.y - (imagem.getHeight()), null);
}
}
}

Essa classe tem dois atributos: um Bitmap e um GeoPoint. O Bitmap será a imagem que desenharemos para representar a o ponto no mapa; já o GeoPoint, como o próprio nome diz, é um ponto geográfico, que obteremos a partir da posição do GPS.
No método draw() é desenhado o ponto no mapa. Inicialmente checamos se o GeoPoint foi setado, em caso positivo, convertemos essa coordenada geográfica em um ponto na tela, que é representado pela classe Point. Quem faz essa conversão é a classe Projection obtida através do método getProjection() do MapView recebido como parâmetro do método. Depois, é só desenhar a imagem na tela. O cálculo feito na chamada do método é para que a imagem fique imediatamente acima do ponto geográfico.

O arquivo de layout
O arquivo de layout não tem nada de mais, apenas a declaração da MapView com a API key (veja no post de 2009 como obter a sua API key).
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">

<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Exemplo GPS e MapActivity" />

<com.google.android.maps.MapView
android:id="@+id/mapa"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="true"
android:apiKey="SUA_API_KEY" />
</LinearLayout>

A Activity
O código abaixo representa a Activity da aplicação e está todo comentado e só vou fazer alguns comentários no final. A nossa classe herda de MapActivity e implementa a interface LocationListener, que é utilizada para receber as notificações do GPS.
public class Aula09Activity extends MapActivity
// Interface que tem os métodos para tratar
// os eventos do GPS
implements LocationListener {

private MapView mapa;
private MapController controller;
private MeuOverlay overlay;
private LocationManager locationManager;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// O LocationManager vai registrar e desregistrar
// a classe para ouvir eventos do GPS
locationManager = (LocationManager)
getSystemService(LOCATION_SERVICE);

// Configurando a MapView
mapa = (MapView)findViewById(R.id.mapa);
// Habilita os botões de Zoom
mapa.setBuiltInZoomControls(true);
// Mostra em modo satélite
mapa.setSatellite(true);

// O MapController 'controla' o mapa :)
controller = mapa.getController();
controller.setZoom(17);

// Cria a imagem que vai representar o Overlay
Bitmap marcador = BitmapFactory.decodeResource(
getResources(), R.drawable.ponto);

// Cria o Overlay e adiciona ao mapView
overlay = new MeuOverlay(marcador);
mapa.getOverlays().add(overlay);

// Determinando um ponto inicial
int latitude = (int)(-8.058698 * 1E6);
int longitude = (int)(-34.872129 * 1E6);

GeoPoint geopoint = new GeoPoint(latitude, longitude);
controller.setCenter(geopoint);
overlay.setGeopoint(geopoint);
}

@Override
protected void onResume() {
super.onResume();
// Registrando a Activity para receber notificações
// de mudança na posição GPS
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 0, 0, this);

locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER, 0, 0, this);
}

@Override
protected void onPause() {
super.onPause();
// Desregistrando a Activity para receber
// notificações de mudança na posição GPS
locationManager.removeUpdates(this);
}

@Override
protected boolean isRouteDisplayed() {
return false;
}

@Override
public void onLocationChanged(Location location) {
// Método chamado quando a posição GPS muda
int latitude = (int)(location.getLatitude() * 1E6);
int longitude = (int)(location.getLongitude() * 1E6);

GeoPoint geopoint = new GeoPoint(latitude, longitude);
overlay.setGeopoint(geopoint);
controller.animateTo(geopoint);
}

@Override
public void onProviderDisabled(String provider) {
// Método chamado quando o GPS é desabilitado
}

@Override
public void onProviderEnabled(String provider) {
// Método chamado quando o GPS é habilitado
}

@Override
public void onStatusChanged(String provider,
int status, Bundle extras) {
// Método chamado quando o status do GPS muda.
// Pode ser: OUT_OF_SERVICE,
// TEMPORARILY_UNAVAILABLE e AVAILABLE
}
}

No código acima fazemos algumas inicializações no método onCreate. A classe LocationManager é responsável por registrar uma classe para "ouvir" os eventos de GPS (definidos na interface LocationListener que nossa Activity está implementando). Para criar um GeoPoint devemos passar a latitude e longitude em microdegrees, para tal devemos multiplicar os valores por 1.000.000 ou em notação científica 1E6 (10 elevado a 6).
No método onResume registramos nossa classe para ouvir os eventos do GPS. O método requestLocationUpdates recebe como parâmetros:
- o provedor de informações de posição GPS: aqui podemos utilizar o GPS do telefone, que demora mais a obter os dados, porém eles são mais precisos; e o da rede de dados, que obtém a posição baseado no esquema de triangulação de antenas da operadora de telefonia;
- a distância mínima em metros na posição GPS;
- o intervalo de tempo em milisegundos para receber a atualização;
- o objeto de uma classe que implemente a interface LocationListener (no nosso caso, a própria Activity).
No método onPause, desregistramos nossa classe para não ouvir mais os eventos de GPS. Isso evitará que ao sair da aplicação o Android tente ficar enviando as coordenadas mesmo com a aplicação inativa.
O método onLocationChanged é o mais importante, pois a cada vez que a posição GPS muda, esse método é chamado pelo Android. A partir do objeto Location que vem como parâmetro, obtemos a latitude e longitude para atualizarmos a posição do Overlay.
AndroidManifest.xml
No manifest, adicionamos as permissões INTERNET e ACCESS_FINE_LOCATION para podermos utilizar o Google Maps e obter a posição GPS respectivamente. Outra informação relevante é a tag <uses-library> que adicionamos para usar a API do Google Maps, uma vez que ela não é "padrão" da plataforma Android.
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="br.edu.cesar.aula09"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk android:minSdkVersion="10" />

<uses-permission
android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION" />

<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:label="@string/app_name"
android:name=".Aula09Activity" >
<intent-filter >
<action
android:name="android.intent.action.MAIN" />
<category
android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<uses-library
android:name="com.google.android.maps"/>
</application>
</manifest>

Para testar o código da aplicação, você deve habilitar as opções de GPS no dispositivo através da opção Settings > Location & Security > My Location. Notem que temos duas opções nessa seção: GPS satélites e redes móveis. A primeira é mais precisa, porém demora mais; já a segunda obtém a posição baseada triangulação entre as distâncias das redes de telefonia ou wireless.

Mas se você estiver usando o emulador, use a aba "Emulator control" da perspectiva DDMS do Eclipse (para exibir, acesse Window > Open perspective > DDMS).

Digite uma coordena válida e clique em "Send". Para ober uma posição, vá no Google Maps, clique com o botão direito e selecione "O que há aqui? | What's here?". Na barra de busca, ficará a latitude e a longitude, aí é só copiar e testar.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

terça-feira, 8 de novembro de 2011

AutoCompleteTextView personalizado

Olá povo,

Estou com um monte de posts legais pra publicar, mas infelizmente (pra variar) estou sem tempo. São coisas que precisei fazer nos projetos que trabalhei, e que gosto de compartilhar com quem lê o blog. Além do mais, me serve como fonte de consulta para uma uso posterior. Afinal de contas, é pra isso que serve o blog (pelo menos pra mim).

Hoje vou mostrar como criar uma consulta personalizada para o componente AutoCompleteTextView. Esse componente é utilizado quando você tem uma lista de valores, e ao invés de selecioná-lo em uma lista, você digita parte do texto e os resultados vão sendo exibidos em uma lista (estilo drop-down) abaixo do componente. Este componente já conta com um mecanismo de busca padrão, entretanto a necessidade de personalizá-lo surgiu quando precisei realizar a busca por nomes de cidades. O cliente solicitou que a acentuação fosse ignorada, ou seja, quando eu digitasse "sao" era para aparecer "São Paulo" nos resultados.

Vamos à implementação! Primeiro vou mostrar o método que substitui os caracteres acentuados pelos mesmos não acentuados.

public static String[] REPLACES = 
{ "a", "e", "i", "o", "u", "c" };

public static Pattern[] PATTERNS = null;

public static void compilePatterns() {
PATTERNS = new Pattern[REPLACES.length];
PATTERNS[0] = Pattern.compile(
"[âãáàä]", Pattern.CASE_INSENSITIVE);
PATTERNS[1] = Pattern.compile(
"[éèêë]", Pattern.CASE_INSENSITIVE);
PATTERNS[2] = Pattern.compile(
"[íìîï]", Pattern.CASE_INSENSITIVE);
PATTERNS[3] = Pattern.compile(
"[óòôõö]", Pattern.CASE_INSENSITIVE);
PATTERNS[4] = Pattern.compile(
"[úùûü]", Pattern.CASE_INSENSITIVE);
PATTERNS[5] = Pattern.compile(
"[ç]", Pattern.CASE_INSENSITIVE);
}

public static String removeAcentos(String text) {
if (PATTERNS == null) {
compilePatterns();
}

String result = text;
for (int i = 0; i < PATTERNS.length; i++) {
Matcher matcher = PATTERNS[i].matcher(result);
result = matcher.replaceAll(REPLACES[i]);
}
return result.toUpperCase();
}

O código acima, utiliza as classes Pattern e Matcher para checar a presença de caracteres acentuados e substitui-los pelos caracteres correspondentes sem acentuação. Observe que só são sendo tratadas as vogais e o cedilha, para textos em outros idiomas você deve fazer os ajustes necessários (como o 'ñ' do espanhol).

Para que um AutoCompleteTextView faça a busca, é necessário definir um Adapter com os dados que ele irá filtrar para exibir. Para fazer nossa busca personalizada (ignorando acentuação) devemos criar nosso Adapter e sobrescrever o método getFilter(). Esse método deve retornar um objeto da classe android.widget.Filter que representa o resultado da busca.

public class MeuAutoCompleteAdapter
extends ArrayAdapter<String>
implements Filterable {

private List<String> listaCompleta;
private List<String> resultados;
private Filter meuFiltro;

public MeuAutoCompleteAdapter(
Context ctx, int layout,
List<String> textos) {

super(ctx, layout, textos);
this.listaCompleta = textos;
this.resultados = listaCompleta;
this.meuFiltro = new MeuFiltro();
}

@Override
public int getCount() {
return resultados.size();
}

@Override
public String getItem(int position) {
if (resultados != null
&& resultados.size() > 0
&& position < resultados.size()){
return resultados.get(position);
} else {
return null;
}
}

@Override
public Filter getFilter() {
return meuFiltro;
}

private class MeuFiltro extends Filter {
@Override
protected FilterResults performFiltering(
CharSequence constraint) {

FilterResults filterResults =
new FilterResults();

ArrayList<String> temp =
new ArrayList<String>();

if (constraint != null) {
String term = removeAcentos(
constraint.toString().trim().toLowerCase());

String placeStr;
for (String p : listaCompleta) {
placeStr = removeAcentos(p.toLowerCase());

if ( placeStr.indexOf(term) > -1){
temp.add(p);
}
}
}
filterResults.values = temp;
filterResults.count = temp.size();
return filterResults;
}

@SuppressWarnings("unchecked")
@Override
protected void publishResults(
CharSequence contraint,
FilterResults filterResults) {

resultados = (ArrayList<String>)
filterResults.values;

notifyDataSetChanged();
}
}
}

A classe acima tem duas listas de strings: a original e a que serve como resultado da busca. Note que os métodos getCount e getItem trabalha em cima da lista dos resultados. O outro atributo da classe é da classe MeuFiltro, e esse atributo é retornado no método getFilter.
A classe interna MeuFiltro faz a nossa busca personalizada. O filtro é realizado no método performFiltering, que retorna um objeto da classe android.widget.Filter.FilterResults que contém o resultado da busca e o total de registros encontrados. Esse resultado é passado para o método publishResults que seta o resultado da busca no atributo resultados, e logo após atualiza o adapter através do método notifyDatasetChanged.

Agora vamos ver como usar esse nosso adapter.


List<String> cidade = new ArrayList<String>();
cidade.add("Recife");
cidade.add("São Paulo");
cidade.add("Santos");
cidade.add("Santa Cruz");

MeuAutoCompleteAdapter adapter =
new MeuAutoCompleteAdapter(
contexto,
android.R.layout.simple_dropdown_item_1line,
cidade);

AutoCompleteTextView actv = (AutoCompleteTextView)
findViewById(R.id.autoCompleteTextView1);
actv.setAdapter(adapter);


O resultado é exibido na figura abaixo:


Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

sexta-feira, 4 de novembro de 2011

Baixe arquivos com o DownloadManager

Olá povo,

Frequentemente alunos e colegas me perguntam qual é um bom livro sobre Android. E normalmente eu costumo indicar o livro do Ricardo Lecheta, Google Android (Editora Novatec), ou o Professional Android 2, do Reto Meyer. Vou adicionar nessa lista o Pro Android 2 da Editora Apress. Estava folheando esse livro quando achei um exemplo bem bacana: utilizar a classe DownloadManager para baixar arquivos. A utilização dela é bem simples, como podemos observar no exemplo abaixo:

public class ExDownldMngrActivity
extends Activity {

private DownloadManager dm;
private MeuReceiver receiver;
private ImageView imgView;
private long downloadId;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.ex_dwnldmngr);

imgView = (ImageView)findViewById(
R.id.imageView1);

dm = (DownloadManager)
getSystemService(DOWNLOAD_SERVICE);

receiver = new MeuReceiver();
}

@Override
protected void onResume() {
super.onResume();

IntentFilter filter = new IntentFilter(
DownloadManager.
ACTION_DOWNLOAD_COMPLETE);

registerReceiver(receiver, filter);
}

@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(receiver);
}

public void baixarArquivoClick(View v){
String url = "http://developer.android.com/assets/"+
"images/home/honeycomb-android.png";

DownloadManager.Request request =
new DownloadManager.Request(Uri.parse(url));
request.setTitle("Meu download");
request.setDescription("Logo do Android");
request.setAllowedNetworkTypes(Request.NETWORK_MOBILE);

downloadId = dm.enqueue(request);
}

private class MeuReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
long id = intent.getLongExtra(
DownloadManager.EXTRA_DOWNLOAD_ID, -1);

if (id == downloadId) {
try {
FileDescriptor fd = dm.openDownloadedFile(
downloadId).getFileDescriptor();

Bitmap image =
BitmapFactory.decodeFileDescriptor(fd);

imgView.setImageBitmap(image);

} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
}
}

Na classe acima, temos quatro atributos. O DownloadManager que realizará a solicitação de download. O atributo MeuReceiver receberá a notificação quando o download for concluído. No nosso exemplo, baixaremos uma imagem, e ela será exibida no ImageView declarado em seguida. O último atributo é o ID do download que a nossa aplicação iniciará. Esse atributo é importante para obtermos o arquivo que foi baixado.
No onCreate fazemos as inicializações necessárias, inclusive, a do DownloadManager, através do método getSystemService. No método onResume, registramos o nosso BroadcastReceiver para ser chamado quando a ação ACTION_DOWNLOAD_COMPLETE for disparada. Essa ação é disparada quando o download do arquivo é concluído. E no onDestroy, desregistamos o nosso receiver.
O método baixarArquivoClick é chamado quando o botão (que declarei no arquivo de layout) é clicado. Nesse método criamos um objeto da classe DownloadManager.Request passando a URL do arquivo. Em seguida, setamos o título e a descrição do download. Além disso, podemos restringir o tipo de rede usada para download, no nosso caso, informamos que podemos fazer com a rede de dados da operadora, mas poderíamos informar que o download só poderia ser feito via Wi-Fi.
Finalmente, enviamos a requisição de download e armazenamos o id do download no atributo downloadId.
Para tratar o fim do download, temos a classe MeuReceiver. Quando o download é concluído o método onReceive é chamado. Nosso broadcast será chamado mesmo se não for o nosso download que for concluído, então temos que checar se o id que foi concluído é igual ao atributo downloadId. Se for, obtemos o FileDescriptor do arquivo baixado, criamos a imagem e setamos no ImageView.

Abaixo, temos a imagem da nossa aplicação em execução.


4br4ços,
nglauber

quarta-feira, 26 de outubro de 2011

Android: Tratando mudança de orientação

Olá povo,

Chegaram 4 novos estagiários aqui no projeto e coube "a minha pessoa" orientá-los. Durante a aula, o assunto enveredou para tratamento de mudança de orientação do aparelho (portrait e landscape | retrato e paisagem).
Por padrão, ao girar o telefone o método onCreate é chamado novamente e com isso, dados que foram carregados dinamicamente (como itens de um ArrayList que estão preenchendo um ListView) são perdidos.

Vejamos o exemplo abaixo:

Arquivo de layout.
<LinearLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent"
 android:orientation="vertical"
 android:background="#0000FF">

 <TextView
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"
   android:text="Digite o nome" />

 <EditText
   android:id="@+id/editText1"
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"/>

 <Button
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:onClick="meuBotaoClick"
   android:text="Adicionar" />

 <ListView
   android:id="@+id/listView1"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent" />
</LinearLayout>

Classe da Activity
public class TelaPrincipalActivity
 extends Activity {

 EditText edt;
 ArrayList<String> nomes;
 ArrayAdapter<String> adapter;

 @Override
 public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.main);

   edt = (EditText) findViewById(R.id.editText1);

   ListView listView = (ListView) 
     findViewById(R.id.listView1);
   adapter = new ArrayAdapter<String>(this,
     android.R.layout.simple_list_item_1, nomes);

   listView.setAdapter(adapter);
 }

 public void meuBotaoClick(View v) {
   nomes.add(edt.getText().toString());
   edt.setText("");
   adapter.notifyDataSetChanged();
 }
}

No código acima, ao clicar no botão, o texto digitado no EditText é inserido na lista, e em seguida o adapter é atualizado através do método notifyDataSetChanged.
O exemplo é bem simples, mas se você girarmos o aparelho os dados que estão sendo exibidos são perdidos, uma vez que a lista inicia vazia e (como já falei) o onCreate é chamado novamente.

Para isso não acontecer você tem 3 alternativas:

1)Forçar uma orientação
Na declaração da sua Activity, no AndroidManifest.xml, você pode usar a propriedade android:screenOrientation para manter sua aplicação em uma orientação específica (portrait/landscape).
<activity
 android:name="TelaPrincipalActivity"
 android:screenOrientation="portrait" />


2)Avisar o Android para não chamar o onCreate
Essa é a melhor opção quando você quer utilizar a tela nas duas orientações e impedir que o Android chame o onCreate cada vez que você girar o aparelho. Para isso basta dizer que ao mudar a configuração (configChanges) de orientação, ele não recrie a Activity.
<activity
 android:name="TelaPrincipalActivity"
 android:configChanges="orientation|keyboardHidden|screenSize"/>


3) Salvar o estado da Activity
A opção número dois é perfeita quando você quer usar o mesmo layout nas duas orientações. Mas, e se eu quiser usar layouts diferentes? Neste caso você dever permitir que a Activity seja recriada. Entretanto, devemos salvar seu estado com o método onSaveInstanceState e no onCreate recuperar esse estado.

No exemplo que fiz os "estags", criei a pasta "layout-land" com o mesmo layout definido acima, só mudando o background (apenas para notarmos os diferentes layouts). Depois implementamos o método onSaveInstanceState para salvar os itens da lista.
@Override
protected void onSaveInstanceState(Bundle outState) {
 super.onSaveInstanceState(outState);
 outState.putStringArrayList("nomes", nomes);
}

Se vocês notaram, o método onSaveInstanceState usa um objeto da classe Bundle para salvar o estado. Esse estado é passado para o método onCreate da Activity como parâmetro, que utilizaremos para restaurar o seu estado.
@Override
public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 // Código já apresentado
 if (savedInstanceState != null) {
   nomes = savedInstanceState.
    getStringArrayList("nomes");

 } else {
   nomes = new ArrayList<String>();
 }
}


Com isso podemos ter, para a mesma tela, um layout diferente para cada orientação, desde que tenhamos os mesmos componentes (e com os mesmos ids).

Ficou com dúvida? Deixe seu comentário.

4br4ç05,
nglauber

segunda-feira, 24 de outubro de 2011

Android: utilizando layout_weight

Olá povo,

Em todas as minhas turmas de Android, noto uma certa dificuldade em entender como funciona a propriedade layout_weight utilizada nos componentes inseridos dentro do LinearLayout. Por padrão, essa propriedade serve para definir como o espaço restante do layout deve ser distribuído pelos componentes. Mas esse comportamento pode variar de acordo com o valor definido na propriedade layout_width e layout_height. Observemos a figura abaixo:




O layout acima é um LinearLayout vertical (orientation=vertical) com vários LinearLayout horizontal iguais, com 3 botões cada, e com os respectivos textos: "Um", "exemplo" e "de peso".

Vamos ver como da linha está configurada:

Linha1 - Nessa linha não fizemos nenhuma modificação, ou seja, está sem peso e com a largura e altura (layout_width e layout_height) definidos como wrap_content.
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Button
android:text="Um"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:text="Exemplo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:text="de peso"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>

Linha 2 - Nesse linha, definimos pesos iguais para todos os botões. Notem que apesar dos pesos estarem iguais, os componentes não têm tamanhos iguais. Essa é a aplicação clássica do layout_weight. Como vimos na linha 1, sobrou um espaço entre o terceiro botão e a margem direita. Com os pesos iguais, o espaço restante é distribuído igualmente entre os componentes. Uma vez que o texto de um botão é maior que o outro, os botões ficam com larguras diferentes.
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Button
android:text="Um"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="Exemplo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="de peso"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>

Linha 3 - Essa linha é similar a anterior, a diferença fica por conta da propriedade layout_width definida como 0dp. Como assim? Se setarmos a largura para zero, o componente não deveria aparecer certo? A resposta é sim, mas como estamos usando o peso, ele vai distribuir o espaço restante da tela igualmente para os 3 botões. Como os 3 botões estão com tamanho zero, o espaço restante (e que será dividido entre os componentes) é toda a tela.
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Button
android:text="Um"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="Exemplo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="de peso"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>

Linha 4 - Igual a anterior, mas modificamos a proporção dos componentes. Os dois primeiros ocupam metade da tela, e o terceiro ocupa a outra metade.
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Button
android:text="Um"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="Exemplo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="de peso"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"/>
</LinearLayout>

Linha 5 - Essa linha usa uma outra utilidade do layout_weight, a de prioridade. No código abaixo os dois primeiros botões estão com a propriedade layout_width setada para match_parent (que é o mesmo que fill_parent), ou seja, devem ocupar toda a largura do seu pai. Se esses componentes não tivessem o peso setado, o primeiro botão ocuparia toda a tela, e os demais não apareceriam. Uma vez que definimos o peso, cada um deles divide a tela, mas e o terceiro? Como não foi definido peso para ele, ele tem peso zero, então, o componente com MENOR peso, tem MAIOR prioridade, então ele passa a aparecer. Note que o layout_width do terceiro botão está como wrap_content, se estivesse fill_parent, ele ocuparia toda a tela, uma vez que ele tem peso menor (zero).
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Button
android:text="Um"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="Exemplo"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="de peso"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>

Linha 6 - Conforme explicado acima, todos os botões estão com a propriedade layout_width definidos como fill_parent, entretanto, o primeiro está com peso 2, enquanto os demais tem peso 1. Logo os com peso menor tem prioridade.
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<Button
android:text="Um"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="2"/>
<Button
android:text="Exemplo"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="de peso"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>


Espero que agora fique mais claro a utilização do peso. Na dúvida, deixem seus comentários.

4br4ç05,
nglauber

quinta-feira, 20 de outubro de 2011

Android: Dicas 5

Olá povo,

Esse é mais um post da série de dicas de Android. Aproveitem e deixem seus comentários :)

Dica 1 - Alterando a fonte dos componentes
O Android, por padrão, tem apenas 3 fontes que podem ser utilizadas nos componentes visuais: sans, serif e monospace. Para utilizar cada uma delas, basta atribuir o valor desejado à propriedade android:typeface. Mas se quiser utilizar uma nova fonte, basta adicionar o arquivo *.ttf na pasta assets e carregá-la utilizando o código abaixo.
Typeface typeface =
Typeface.createFromAsset(getAssets(), "Aliens.ttf");

TextView txt = (TextView)findViewById(R.id.textView1);
txt.setTypeface(typeface);

Infelizmente, não achei uma forma de setar essa fonte no XML. Se alguém souber, deixe um comentário. Ah! A fonte que usei nesse exemplo foi baixada do site http://www.webpagepublicity.com/free-fonts.html.

Dica 2 - Converter DIP (Density Independent Pixel) para Pixel
Após ministrar duas aulas seguidas (em turmas diferentes) sobre Views personalizadas no Android, uma dúvida frequente foi como converter DIP para PX (pixel). Isso é especialmente útil para que a View rode corretamente em diferentes densidades de tela (LDPI, MDPI, HDPI e agora XHDPI). Para tal, pode-se utilizar o método applyDimension da classe TypeValue.
Resources r = getResources();
float valorEmDp = 14;
float valorEmPixels = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, valorEmDp,
r.getDisplayMetrics());


Dica 3 - Evitando acumular Toasts
Um "bug" clássico que os engenheiros de teste do projeto em que eu trabalho levantam é sobre a utilização do Toast. Se você chamar o método show() dessa classe várias vezes seguidas, essas mensagens são acumuladas e ficam sendo exibidas sequencialmente. Um recurso que utilizo é criar um método separado, que verificará se já existe um Toast aberto, em caso positivo, ele o cancela para exibir um novo. Para tal, você deve criar um atributo da classe Toast (que abaixo chamo de 'toast') e utilizar o seguinte código:
// Declare o atributo
private static Toast toast;

// Método
public static void showToast(Context ctx, int res){
if (toast != null){
toast.cancel();
toast.setText(res);
} else {
toast = Toast.makeText(ctx, res, Toast.LENGTH_LONG);
}
toast.show();
}


Dica 4 - Definindo onde instalar sua aplicação
A partir da versão 2.2 (Froyo) é possível especificar onde sua aplicação pode ser instalada: cartão de memória ou memória interna. Por padrão, a aplicação será instalada na memória interna, mas você pode utilizar a propriedade android:installLocation da tag <manifest> do AndroidManifest.xml para um dos valores abaixo:
internalOnly - A aplicação só poderá ser instalada na memória interna (valor padrão).
auto - A aplicação é instalada na memória interna, mas poderá ser movida para o cartão de memória posteriormente ou quando a memória interna estiver cheia.
preferExternal - A aplicação deve ser instalada no cartão de memória preferencialmente, mas isso não é garantido.

Exemplo:
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="preferExternal"
... >


Dica 5: Ativar/Acender a tela do aparelho
Neste post eu mostrei como criar notificações no Android. Um recurso que pode ser utilizado juntamente com as notificações é acender/ligar a tela do aparelho. Para fazer isso, deve-se utilizar a classe PowerManager.

// Obtém a instância do PowerManager
PowerManager pm = (PowerManager)c.getSystemService(
Context.POWER_SERVICE);

// Liga o display do aparelho
WakeLock wakeLock = pm.newWakeLock(
// Liga a tela
PowerManager.SCREEN_DIM_WAKE_LOCK |
// Após liberar a tela para apagar,
// mantém a tela ligada por um pouco
// mais de tempo
PowerManager.ON_AFTER_RELEASE,
// Tag para debug
"tag_para_debug");

// Liga a tela por 10 segundos
wakeLock.acquire(10000);


Para usar o código acima, deve-se adicionar a permissão WAKE_LOCK no AndroidManifest.xml.
<uses-permission name="android.permission.WAKE_LOCK"/>


Dica 6: debug.keystore expirou

Essa dica foi enviada pelo meu colega Ricardo Gilson.
Toda aplicação Android é assinada para ser instalada no dispositivo, por padrão, ela é assinada com uma assinatura de debug através do arquivo debug.keystore que fica na subpasta ".android" no diretório do usuário (C:\Users\usuario\.android ou /Users/usuario/.android). Depois de um certo tempo sem utilizar a máquina, a mensagem abaixo pode ser exibida ao tentar rodar uma aplicação:

Error generating final archive: Debug Certificate expired on 24/09/2011.

Para resolver o problema é só apagar o arquivo debug.keystore, dar um clean no projeto do Eclipse (menu Project > Clean) e tentar rodar novamente.

Dica 7: Habilite a aceleração de Hardware (por Fernado Fragoso)
Habilitar a aceleração de hardware quando necessitar de manipulação de imagens, adicionar na tag application do AndroidManifest.xml.

android:hardwareAccelerated="true"

Faz uma diferença gigantesca! Valeu Fernando!

Por hoje é só, qualquer dúvida ou sugestão, deixem seus comentários.

4br4ç05,
nglauber

terça-feira, 18 de outubro de 2011

Ice Cream Sandwich: O Android 4.0

Olá povo,

Fiquei acordado até as 02:00 para ver o lançamento do Android "Sanduíche de Sorvete". A apresentação mostrou as novidades da nova versão do S.O. no Galaxy Nexus da Samsung. Ao terminar a apresentação, foi anunciado que o SDK já estaria disponível. Então, não perdi tempo e já o atualizei juntamente com o plugin ADT (Android Development Tools).
Sendo assim, antes de dormir, vou deixar uns screenshots de algumas novidades que tirei com o emulador novo :)

Na tela de lock, é possível ir direto para câmera.


No Launcher, agora podemos adicionar Widgets. Inclusive os do Honeycomb.


Um Hello World pra ver se funciona mesmo :)



O SDK Manager ficou de cara nova. Bem melhor! Ah! O Assistente de novo projeto do Eclipse também melhorou.

Vou tentar atualizar esse post mais tarde.

4br4ç05,
nglauber

sexta-feira, 7 de outubro de 2011

20 dicas para programadores

Olá povo,

Conviver com pessoas mais experientes é uma coisa muito boa, pois você pode explorar o know-how dessas pessoas e crescer profissionalmente (e às vezes pessoalmente). Eu tenho a satisfação de ter trabalhado, tido aulas e ter sido orientando por Luiz Eugênio Fernandes Tenório (left), um excelente profissional de desenvolvimento de software (senão o melhor que conheço).
Em 21 de agosto de 2007, ele me mandou um email com um texto de Jonathan Danylko que a muito tempo queria postar aqui no blog. É um conjunto de boas práticas, que um desenvolvedor de software pode seguir para melhorar a produtividade e a qualidade do seu trabalho. Adotei uma boa parte dessas dicas, e notei bons resultados através de feedbacks de gestores e colegas de trabalho. E por causa disso, resolvi compartilhar com vocês.

O texto abaixo é uma tradução/adaptação minha (vixe!) do texto original em inglês disponível aqui.

  1. Defina quanto tempo você acha que deve gastar para resolver um problema. Já vi programadores sentarem na frente de um monitor por oito horas para resolver um problema e nada. Defina uma tabela de tempo para si mesmo de 1 hora, 30 minutos, ou até mesmo 15 minutos. Se você não conseguir descobrir uma solução para seu problema dentro de seu prazo, peça ajuda ou pesquise seu problema na Internet em vez de tentar ser um super-coder.
  2. As linguagens são similares. Com o tempo, você entenderá como uma linguagem funciona, então você vai notar semelhanças com outras linguagens. A linguagem que você escolher deve: te fornecer um nível de "conforto" adequado, a capacidade de produzir código eficiente (e limpo), e acima de tudo, se adequar ao projeto e vice-versa.
  3. Não exagere nos padrões de projeto. Pense duas vezes antes de usá-los, se houver uma solução mais simples (e que funcione), use-a. Um problema que necessita de um padrão de projeto como solução, será identificado naturalmente (obviamente depois de um certo tempo de experiência).
  4. Sempre faça backup do código. Quando eu era mais novo, por uma falha no disco rígido, perdi um monte de código, e me senti horrível por causa do que tinha acontecido. A única vez que você não fizer backup dos dados, pode ser o momento onde você tem um prazo restrito com um cliente, e eles precisam do que você estava fazendo para amanhã. Controle de código fonte/versão (CVS, SVN, GIT,...) também se aplica aqui.
  5. Você não é o melhor em programação. Aceite isso. Eu sempre pensei que eu sabia muito sobre programação, mas sempre há alguém lá fora (ou do seu lado), melhor que você. Sempre! Aprenda com eles.
  6. Aprenda a aprender mais. Como número cinco explicou, eu sempre tive à mão uma revista ou livro sobre computadores/programação  (pergunte aos meus amigos, eles vão confirmar). Verdade, há muita tecnologia lá fora, e manter-se com ela é um trabalho em tempo integral. E se você tem uma maneira inteligente de receber novidades, você vai aprender sobre novas tecnologias a cada dia.
  7. Mudanças são constantes. Seu conhecimento de tecnologia e/ou programação deve ser semelhante à forma como você trata investimentos: Diversifique. Não fique muito confortável com uma tecnologia particular. Se não houver suporte suficiente para aquela linguagem ou tecnologia, é bom você pode começar a atualizar o seu currículo imediatamente, e começar o seu período de treinamento. Minha regra geral: Conheça pelo menos duas ou três linguagens, assim, se uma morre, você tem outra para trabalhar enquanto você estuda uma nova para colocar no lugar.
  8. Ajude os novatos. Oriente os desenvolvedores iniciantes sobre as boas técnicas de programação. Você nunca sabe... mas você pode ganhar pontos com isso, e vai se sentir mais confiante de ter, pessoalmente, treinado e preparado uma pessoa para seu próximo trabalho.
  9. Simplifique o algoritmo. Codifique como um demônio, mas quando estiver tudo pronto, volte para otimizá-lo. Uma pequena melhoria no código aqui e ali, e você terá uma manutenção de código mais feliz no longo prazo.
  10. Documente seu código. Se a documentação é uma API de Web Service ou uma simples classe, documente do mesmo jeito. Eu tenho sido acusado de excesso de comentários no meu código, e isso é algo que eu me orgulho. Leva apenas um segundo para adicionar uma linha de comentário adicional para cada 3 linhas de código.
  11. Teste, teste e teste. Eu sou um fã de testes de caixa preta. Quando sua rotina é concluída, o seu período de "selo de aprovação" começa. Se você tem um departamento de Quality Assurance, você pode estar falando mais para eles do que para seu gerente de projeto sobre os erros em seu código. Se você não testar seu código completamente, você pode desenvolver mais que código, e possivelmente uma má reputação.
  12. Celebre todo sucesso. Se um programador feliz te pedir para vir ver sua obra extraordinária de código, mesmo que você já tenha visto um trecho de código como esse mais de 100 vezes em suas experiências, comemore o sucesso desse desenvolvedor.
  13. Faça revisão de código frequentes. Em projetos e pessoalmente. Na empresa, você sempre terá as revisões de código de quão bem você codificou alguma coisa. Não olhar para isso como pessoas crucificando o seu estilo de codificação. Pense nisso como uma crítica construtiva. Quando estiver na revisão, pergunte: "Como eu poderia ter feito melhor?" Isto irá acelerar o seu aprendizado e torná-lo um programador melhor.
  14. Relembre seu código antigo. Existem duas maneiras de olhar para código antigo: "Eu não posso acreditar que eu escrevi este código" e "Eu não posso acreditar que eu escrevi este código." A primeira afirmação é muitas vezes de nojo e perguntando como você pode melhorá-lo. Você ficaria surpreso como um código antigo pode ser ressuscitado em uma rotina possível e melhor, ou até mesmo um produto inteiro. A segunda declaração é de espanto e realização. Desenvolvedores têm suas conquistas de código, aquelas que eles concluíram e todos tomaram conhecimento.
  15. Humor é necessário. Em meus 20 anos de desenvolvimento, eu nunca conheci um programador que não tem um senso de humor decente. Na verdade, nesta indústria, é uma exigência.
  16. Cuidado com o programador sabe-tudo, programador possessivo e o programador inexperiente. O programador sabe-tudo tenta relegar você em vez de trabalhar como um membro da equipe. O programador possessivo cria o código e não quer partilhar com ninguém. E o programador inexperientes pede ajuda constantemente (a cada dez minutos) ao ponto que o código desenvolvido é seu, não dele.
  17. Jamais um projeto será simples. Se alguém precisa de um site de 3 páginas com o Microsoft Access no início, ele acaba se tornando um site de 15 páginas com o SQL Server, um fórum e um CMS personalizado (Content Management System).
  18. Nunca tome nada como garantido. Se você levar em um projeto simples, você pode pensar que uma determinada seção será fácil de concluir. Não pense, mesmo por um momento. Ao menos que você tenha uma classe, componente, ou um pedaço de código já codificados, testado exaustivamente e em produção. A partir de um projeto existente, não pense que será fácil.
  19. Software nunca está acabado. Um colega programador me disse uma vez que o software nunca está acabado, está "temporariamente concluído". Bons conselhos. Se o cliente ainda está usando um programa que você escreveu e tem resistido ao teste do tempo, existem boas chances de você ainda estar atualizando-o, o que não é uma coisa ruim. Mantém seu emprego.
  20. A paciência é definitivamente uma virtude. Quando os clientes, amigos ou membros da família usar um PC, ficam frustrados e partem para bater no computador. Eu continuo dizendo a todos: "você está controlando o computador, não o contrário". Você precisa ter um certo nível de paciência para programar computadores. Tão logo os programadores entendem o que fizeram de errado, eles dizem: "Ah, é por isso que estava acontecendo isso".


Qualquer sugestão, deixem seus comentários.

4br4ç05,
nglauber

quarta-feira, 28 de setembro de 2011

AlertDialog Personalizado no Android

Olá povo,

Ultimamente estou escrevendo post curtos, com soluções para problemas que as equipes onde eu trabalho/trabalhei têm/tiveram, e que ao meu ver poderiam ter sido feitas de outra forma. A última foi como personalizar o AlertDialog do Android, dependendo da versão do Android (e até do fabricante) o aspecto visual das mensagens que são exibidas para o usuário tem um visual diferente. Por causa disso, o pessoal de UX (User eXperience) resolveu padronizar isso. Só que o pessoal estava usando uma Activity com o tema de diálogo.

Eu não acho essa uma boa abordagem, uma vez que você mexe com o ciclo de vida da activity (métodos onCreate, onStart, onPause, ...) para exibir um simples dialog. Esse tutorial do site do Android Developers mostra como criar um dialog com o conteúdo personalizado.

Sim, mas e se eu quiser mudar o background do título e dos botões? Nesse post vou mostrar como fazer isso. O código abaixo mostra como personalizar as 3 áreas de um AlertDialog: a barra de título, a área de conteúdo e parte inferior onde ficam os botões.
// O parâmetro temTitulo deve ser informado,
// pois não há como saber se o Dialog tem
// título para mudar a imagem de background
// do topo e do meio.
public void mostraDialog(
AlertDialog dialog, boolean temTitulo) {

dialog.show();
Resources res = dialog.getContext().getResources();

// Pega a View do AlertDialog toda
ViewGroup contentView = (ViewGroup)
dialog.findViewById(android.R.id.content);

// Pega a ViewGroup com os filhos
// (title, textView, customView e barra dos botões)
ViewGroup contentChilds =
(ViewGroup)contentView.getChildAt(0);

// Barra de título
if (temTitulo)
contentChilds.getChildAt(0).setBackgroundDrawable(
res.getDrawable(R.drawable.box_top));

// Layout onde fica o texto (mensagem) do dialog
contentChilds.getChildAt(1).setBackgroundDrawable(
res.getDrawable(
temTitulo?
R.drawable.box_middle :
R.drawable.box_top));

// Layout onde fica a View personalizada (quando usada)
contentChilds.getChildAt(2).setBackgroundDrawable(
res.getDrawable(
temTitulo?
R.drawable.box_middle :
R.drawable.box_top));

// Layout dos botões
// Se não colocar wrap_content, o botão fica cortado
contentChilds.getChildAt(3).setLayoutParams(
new LinearLayout.LayoutParams(
LayoutParams.FILL_PARENT,
LayoutParams.WRAP_CONTENT));

contentChilds.getChildAt(3).setBackgroundDrawable(
res.getDrawable(R.drawable.box_bottom));

// Alterando botões
Button btn = (Button)dialog.findViewById(
android.R.id.button1);
if (btn != null){
btn.setTextColor(Color.WHITE);
btn.setBackgroundResource(
R.drawable.selector_botao_verde);
}

btn = (Button)dialog.findViewById(
android.R.id.button2);
if (btn != null){
btn.setTextColor(Color.WHITE);
btn.setBackgroundResource(
R.drawable.selector_botao_vermelho);
}
btn = (Button)dialog.findViewById(
android.R.id.button3);
if (btn != null){
btn.setTextColor(Color.WHITE);
btn.setBackgroundResource(
R.drawable.selector_botao_vermelho);
}
}

Como todos devem/deveriam saber tudo que é exibido no Android contém objetos da classe View. A classe ViewGroup é uma implementação do padrão de projeto Composite, e como tal, herda de View e é composto de várias Views. Dessa forma, após exibir o Alert, pegamos a ViewGroup que representa o AlertDialog e obtemos cada um dos seus "filhos". O Alert é composto de 4 filhos: o primeiro contém a view com o título e o ícone; a segunda tem o texto do Alert; o terceiro pode conter uma view personalizada associada ao Alert; e o último contém os botões (no máximo 3: positivo, negativo e neutro). Nesse exemplo, pegamos cada filho e alteramos o seu background.

As imagens de background utilizadas estão no formato 9.png que falei nesse post aqui. Mas um detalhe interessante, é que o background do botão é definido utilizando os drawable selector_botao_verde e selector_botao_vermelho. Esses dois arquivos, apesar de ficarem na pasta res/drawable, não são imagens. Na verdade eles são arquivos XML que descrevem as imagens que serão utilizadas pelos botões dependendo do seu estado. Abaixo temos o XML que descreve o botão verde.
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/btn_verde"
android:state_pressed="false"/>
<item
android:drawable="@drawable/btn_verde_pressionado"
android:state_pressed="true" />
</selector>

Notem que devemos ter (por uma questão de usabilidade) duas imagens: uma imagem para o botão não pressionado, e outra para ele pressionado. Isso dará a sensação que o usuário estará pressionando no botão. Essa prática pode ser adotada para todos os botões da sua aplicação (inclusive para o botão vermelho usado nesse post).

Para utilizar o AlertDialog personalizado, basta criá-lo normalmente e passá-lo para o método que criamos anteriormente.
AlertDialog.Builder builder =
new AlertDialog.Builder(this)
.setTitle("Titulo")
.setMessage("Mensagem")
.setPositiveButton("Sim", null)
.setNegativeButton("Não", null);
AlertDialog d = builder.create();
mostraDialog(d, true);

Abaixo temos um exemplo do nosso Alert em funcionamento.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

segunda-feira, 26 de setembro de 2011

Máscara no EditText do Android

Olá povo,

Certo dia, um colega me perguntou se existia um EditText com máscara no Android. Apesar de ser uma coisa bem comum em aplicações Desktop, eu não conheço um meio fácil de obter o mesmo comportamento em Android. Na época, dei uma pesquisada e não achei o queria.
Durante as aulas de Android na Unibratec um aluno me perguntou novamente, e fui pesquisar. Como não achei nada tão fácil, o jeito foi implementar um listener (observer) para quando o texto do EditText é alterado. A interface que faz isso no Android é a TextWatcher.
O exemplo abaixo, mostra como fazer uma máscara para um campo de CEP. Passei como exercício para a turma, estender esse exemplo para CPF e CNPJ. Eles fizeram. E vocês?


final EditText edt =
(EditText)findViewById(R.id.editText1);

edt.addTextChangedListener(new TextWatcher() {

boolean isUpdating;

@Override
public void beforeTextChanged(
CharSequence s, int start, int count, int after) {
}

@Override
public void onTextChanged(
CharSequence s, int start, int before, int after) {

// Quando o texto é alterado o onTextChange é chamado
// Essa flag evita a chamada infinita desse método
if (isUpdating){
isUpdating = false;
return;
}

// Ao apagar o texto, a máscara é removida,
// então o posicionamento do cursor precisa
// saber se o texto atual tinha ou não, máscara
boolean hasMask =
s.toString().indexOf('.') > -1 ||
s.toString().indexOf('-') > -1;

// Remove o '.' e '-' da String
String str = s.toString()
.replaceAll("[.]", "")
.replaceAll("[-]", "");

// as variáveis before e after dizem o tamanho
// anterior e atual da String, se after > before
// é pq está apagando
if (after > before) {
// Se tem mais de 5 caracteres (sem máscara)
// coloca o '.' e o '-'
if (str.length() > 5) {
str =
str.substring(0, 2) + '.' +
str.substring(2, 5) + '-' +
str.substring(5);

// Se tem mais de 2, coloca só o ponto
} else if (str.length() > 2) {
str =
str.substring(0, 2) + '.' +
str.substring(2);
}
// Seta a flag pra evitar chamada infinita
isUpdating = true;
// seta o novo texto
edt.setText(str);
// seta a posição do cursor
edt.setSelection(edt.getText().length());

} else {
isUpdating = true;
edt.setText(str);
// Se estiver apagando posiciona o cursor
// no local correto. Isso trata a deleção dos
// caracteres da máscara.
edt.setSelection(
Math.max(0, Math.min(
hasMask ? start - before : start,
str.length() ) ) );
}
}

@Override
public void afterTextChanged(Editable s) {
}
});


4br4ç05,
nglauber

quarta-feira, 21 de setembro de 2011

Android e o formato 9-Patch

Por: Manuella Nejaim @manux | Nelson Glauber @nglauber

Olá povo,

Nossa experiência em projetos Android nos mostrou que não são apenas os engenheiros de sistema (ou programadores) que precisam saber do que a plataforma é capaz. Os designers também precisam conhecer as facilidades que a plataforma da Google traz.
É bastante comum em uma interface, termos um botão que aparece em vários contextos com diferentes dimensões. Nestes casos, os designers criavam várias imagens semelhantes, diferindo apenas o tamanho. Isso fazia com que a aplicação ficasse com o tamanho maior (em bytes) e muitas vezes causando problemas de memória devido ao tamanho e quantidade de imagens utilizadas.

Uma técnica que foi amplamente utilizada em aplicações Java ME foi a 9-slice, devido à escassez de recursos dos aparelhos dessa plataforma. Essa abordagem divide uma imagem maior em 9 imagens menores, conforme abaixo:
Imagem cortada para o 9-slice

Essa forma é bem interessante, pois as imagens (exceto as das quatro quinas) são idealizadas de modo a serem esticadas sem perder a qualidade. Entretanto, tinha-se o inconveniente gerenciar, para cada imagem “de botão”, nove imagens.

O Android adota uma forma similar para aperfeiçoar a utilização do 9-slice chamada de 9-patch. Essa abordagem utiliza marcações feitas na imagem, indicando a área “esticável”, assim como as margens que devem ser preservadas.

Indicação das áreas de marcação de uma imagem 9-patch

Os benefícios da utilização deste recurso são claros: diminuição da quantidade de imagens, diminuição do peso da própria imagem, otimização para o porting, flexibilidade para a interface, dentre vários outros. Além disso, representa ganho de desempenho e qualidade no produto final, beneficiando engenheiros, designers e principalmente o usuário.

As imagens precisam ser produzidas de forma adequada para adotar esse recurso, então existe a necessidade de colaboração dos designers de interface com os engenheiros para fazer uso dessa solução. Gerar imagens para o 9patch requer um mínimo de esforço, e a inclusão de uns dois passos a mais no trabalho de layout.
Vale à pena salientar que esta solução se aplica elementos da interface como boxes, botões e inputs que de alguma forma apresentam um padrão gráfico que pode ser distorcido na horizontal e/ou vertical.
Para gerar a imagem “9patched”, é preciso primeiro criá-la na ferramenta gráfica (como o Photoshop) e observar dois pontos:

O primeiro ponto é que, a imagem gerada não pode ter degradês diagonais, apenas em 90 graus.

Degradê do botão com ângulo de 90 graus

O segundo ponto, é gerar a imagem do botão nas dimensões mínimas que conservem a sua aparência, conforme a imagem abaixo:

Aparência do botão normal

Imagem exportada do botão cortado para fazer o 9patch

Uma vez criada a imagem, utilizaremos o draw9patch, uma ferramenta que vem com o Android SDK, onde podemos realizar as marcações para tornar uma imagem comum em uma imagem “9-patched”. Ao baixar e instalar o SDK do Android, a ferramenta encontra-se no diretório tools. Dentro dessa pasta, basta executar o arquivo draw9patch.bat (no Windows, no Linux e Mac o nome é similar). Após executar a aplicação, basta arrastar a imagem original para dentro do programa (ou carrega-la a partir do menu File).
A imagem exportada é aberta no draw9patch e ao lado direito serão apresentadas 3 variações dela esticada na horizontal e na vertical.

Tela do draw9patch

A imagem ganha automaticamente uma margem externa de 1 pixel de cada lado. Nessa área é que fazemos a marcação na imagem “pintando-a” com o clique. Para remover a marcação de uma área, pressione a tecla Shift enquanto pinta.
A linha do topo refere-se à largura do botão, indicando os limites pra distorcer o botão horizontalmente. A margem lateral esquerda refere-se à altura do botão, onde ele esticará verticalmente. A margem inferior e direita serve para indicar o padding, ou seja, margem interna do botão, onde entra o conteúdo. E por fim, no lado direito temos o padding vertical da imagem.

Na parte inferior da tela do draw9patch encontramos as opções de zoom para a área de marcação e para o preview das distorções. Existem também botões Show lock para acionar o “travamento” da imagem em si (marcado por padrão), Show content para exibição da área de conteúdo definida pelo padding (desmarcada por padrão) e a opção Show patches que mostra os “cortes” que formam a imagem por completo (desmarcada por padrão).

Após realizar as alterações desejadas, o arquivo que será salvo terá a extensão *.9.png. e pode ser usado como imagem de background de componentes do Android como Button, EditText, etc.

4br4ç05,
manux | nglauber

segunda-feira, 19 de setembro de 2011

PageScroll

Olá povo,

Certo dia, assisti uma apresentação de Silvio Meira em que ele falou que nós, como desenvolvedores, tínhamos sempre que olhar para uma aplicação e pensar: "como esses caras [desenvolvedores] fizeram isso?". Às vezes, utilizamos a aplicação sem notarmos coisas simples que poderíamos incorporar aos nossos próprios aplicativos (cuidado com as patentes :p).

Pois bem. Eu achei interessante na aplicação do TweetDeck, a forma com que ele alternava entre a Timeline e as Mentions, apenas rolando da tela na horizontal. Similar ao que é feito na home screen do próprio Android para alternar entre os "desktops".

Achava que o Android já tivesse um componente para fazer isso, mas não achei. Tentei fazer com um Gallery, mas o resultado não ficou legal. Finalmente, achei um componente chamado HorizontalPager, disponível em http://code.google.com/p/deezapps-widgets/. Distribuído sobre licença Apache, ele realiza o comportamento que eu queria e pode ser incorporado ao seu projeto sem culpa :)

Disponibilizei um projeto que utiliza "esse carinha" neste link. E Abaixo temos um screenshot do mesmo.



EDITADO
Depois de publicar esse post, recebi a dica de @acvilarim que existe uma outra forma de obter esse mesmo comportamento com a API do próprio Android: utilizando as classes ViewPager e PagerAdapter. Entretanto essas classes estão em uma biblioteca separada que provê compatibilidade da versão 1.6 em diante com o Honeycomb (3.x). Para instalá-la, você pode seguir esse tutorial aqui.
Meu colega Marcelo Alves me indicou esse artigo para começar a mexer com o ViewPager e o PagerAdapter. De qualquer forma, postei aqui um exemplo que fiz idêntico ao anterior, mas usando essa API.

4br4ç05,
nglauber

quinta-feira, 8 de setembro de 2011

Twitter API no Android

Olá povo,

Nunca me interessei em testar APIs para redes sociais por dois motivos: não gosto muito dessa febre; e pensava que era bem trivial ao ponto de ter milhões de tutoriais por aí. Grande engano. Quando fui adicionar suporte ao Twitter em uma aplicação que estou trabalhando, notei que existem algumas etapas 'chatinhas' para realizar essa integração. Não sei se a forma que estou utilizando aqui é a melhor, inclusive, se vocês tiverem sugestões, deixem seus comentários :)

Sem mais "delongas", vamos começar! Nesse post, vou mostrar como realizar o processo de autenticação no Twitter e 'twitar' uma mensagem. Para começar, obviamente você deve ter uma conta comum no Twitter. Para ter acesso aos recursos da API, você (como desenvolvedor) deverá cadastrar sua aplicação no site https://dev.twitter.com. Esse cadastro servirá para a equipe do Twitter saber qual aplicação (e consequentemente o login) responsável pelo cliente que está acessando o Twitter. Isso é similar ao que é feito quando utilizamos a API de Mapas do Google, onde você deve usar sua conta do Google para obter uma chave, para então utilizar o serviço.



O cadastro é bem simples, basta informar: o nome da aplicação, descrição da mesma, URL para contato e uma URL de callback. Este último é mais importante para aplicações web, onde após efetuar o login, a chave de acesso será direcionada.
Uma vez cadastrada, serão geradas alguma informações que serão utilizadas para integração da nossa aplicação com o Twitter. Nós precisaremos da "Consumer Key" e "Consumer Secret".

Como funciona a autenticação no Twitter?
Até meados do segundo semestre de 2010, o acesso ao twitter funcionava basicamente passando o login e a senha em texto plano. Por motivos óbvios de segurança, essa forma foi descontinuada, e passou-se a utilizar a forma de autenticação OAuth. Esse processo é ilustrado na figura abaixo:

Não vou entrar em muitos detalhes sobre esse protocolo, mas basicamente o cliente envia um solicitação de acesso utilizando a consumerKey e consumerSecret para o servidor, o servidor então retorna uma URL para que o usuário se autentique. Nesse momento o usuário insere seu usuário e senha do Twitter. Uma vez passadas essas informações, o servidor valida esses dados e gera um token de acesso, que será usada para toda a comunicação com o Twitter. Esse tone deve ser salvo no cliente para que ele possa se comunicar com o servidor sem ter passar o login e a senha a cada transacção.

Mãos à obra!
De posse do conceito sobre a autenticação do Twitter, vamos implementar nosso exemplo. Ele constará de apenas uma tela, onde o usuário poderá fazer o login/logout no Twitter e atualizar o seu status a partir da mesma. A tela da aplicação é exibida abaixo:


Você pode implementar a comunicação com o Twitter do zero, utilizando JSON. Mas existem algumas APIs prontas que facilitam nosso trabalho. A que utilizarei aqui é a Twitter4J. Baixe a última versão estável, descompacte em algum local do seu HD. Crie um novo projeto Android, e copie o arquivo twitter4j-core-android-X.X.X.jar que está na pasta lib do Twitter4J para a pasta lib do seu projeto Android (essa pasta não é criada por padrão, então crie na raiz do seu projeto). Em seguida, adicione esse JAR no Build-Path do Eclipe clicando com o botão direito sobre o arquivo, e depois selecionando Build Path > Add to Build path. Seu projeto deve ficar como na figura abaixo.



Vamos ao código :) Vou começar pelo AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="ngvl.android.twitter"
android:versionCode="1"
android:versionName="1.0">

<uses-sdk android:minSdkVersion="10" />
 
<uses-permission android:name="android.permission.INTERNET"/>

<application
 android:icon="@drawable/icon"
 android:label="@string/app_name">
 <activity
   android:name=".ExemploTwitterActivity"
   android:label="@string/app_name"
   android:noHistory="true">
   <intent-filter>
     <action 
      android:name="android.intent.action.MAIN"/>
     <category 
       android:name="android.intent.category.LAUNCHER"/>
   </intent-filter>
   <intent-filter>
    <action 
      android:name="android.intent.action.VIEW"/>
     <category 
       android:name="android.intent.category.DEFAULT"/>
     <category 
       android:name="android.intent.category.BROWSABLE"/>
     <data android:scheme="nglauber-android"/>
   </intent-filter>
 </activity>
</application>
</manifest>

Aqui nesse arquivo temos algumas coisas muito importantes:
- A tag uses-permission para nossa aplicação poder acessar internet;
- Foi adicionada a propriedade andróide:noHistory à tag activity, para que quando o Browser seja aberto para o usuário fazer o login, ao voltar, ele não crie outra instância dessa activity.
- Essa activity tem dois intent-filter. O primeiro é pra execução normal da aplicação e o segundo é usado pela página do Twitter. Após a autenticação, ele redirecionará para o endereço nglauber-android://?oauthverifier=sua_chave. Estamos informando que essa atividade trata esse "protocolo", dessa forma nossa activity será re-executada.

Agora vamos ver o código da Activity.
package ngvl.android.twitter;

import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.auth.AccessToken;
import twitter4j.auth.RequestToken;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

public class ExemploTwitterActivity extends Activity {

  private final String 
    consumerKey = "SUA CONSUMER KEY";
  private final String 
    consumerSecret = "SUA CONSUMER SECRET";
  private final String 
    CALLBACKURL = "nglauber-android:///";

  private Twitter twitter;

No código acima, as constantes cosumerKey e consumerSecret devem ser preenchidas com as informações do cadastro da sua aplicação no site do Twitter. Já a constante CALLBACKURL deve estar igual a que foi declarada no AndroidManifest.xml.
O atributo twitter proverá acesso ao processo de login e de atualização do status.

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  
  twitter = new TwitterFactory().getInstance();
  twitter.setOAuthConsumer(
    consumerKey, consumerSecret);
}

No onCreate apenas inicializamos o layout da tela, obtemos a instância do Twitter através da classe TwitterFactory e setamos a consumer key e secret.
 public void clickLogin(View v) {
  try {
    AccessToken accessToken = loadAccessToken();
    if (accessToken == null) {
      twitter = new TwitterFactory().getInstance();
      twitter.setOAuthConsumer(
        consumerKey, consumerSecret);
    
      RequestToken requestToken = 
        twitter.getOAuthRequestToken(CALLBACKURL);
 
      String url = requestToken.getAuthenticationURL();
      Intent it = new Intent(
        Intent.ACTION_VIEW, Uri.parse(url));
      it.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
      startActivity(it);
    
      saveRequestData(requestToken.getToken(), 
        requestToken.getTokenSecret());
   
    } else {
      twitter.setOAuthAccessToken(accessToken);
    }
  } catch (Exception e) {
    e.printStackTrace();
    showToast(e.getMessage());
  }
} 

O método acima, iniciará o processo de login e será chamado ao clicar no botão de login. Através do método loadAccessToken (definido mais adiante), ele tenta obter um objeto AccessToken salvo anteriormente. Caso não exista, o processo de login inicia com a obtenção da url para fazermos o login no Twitter. Notem que é passada a constante CALLBACKURL como parâmetro, isso indica qual endereço será chamado quando o login for concluído. Essa é uma URL "fake" que será chamada quando o login for efetuado. Neste momento, o browser redireciona a chave para nglauber-android://?oauthverifier=sua_chave, e o "protocolo" nglauber-android está sendo tratado por nossa atividade, como definido no AndroidManifest.xml.
A flag FLAG_ACTIVITY_NO_HISTORY evita que, ao pressionar back após o login, o browser seja exibido novamente. Após chamar o browser para o login, o token e o token secret da requisição são salvos pelo método saveRequestData (veremos esse método mais adiante).

Ao clicar no botão de login, será exibida a tela de autenticação do Twitter. Aqui é solicitado autorização para que a aplicação que cadastramos no começo desse post tenha acesso a sua conta do Twitter. Digite seu login e senha e clique em "Sign In".


Após realizar o login, a página do Twitter envia a chave para nossa Activity. Tratamos o recebimento dessa chave no método onResume conforme abaixo:
 @Override
protected void onResume() {
  super.onResume();
  
  Uri uri = getIntent().getData();
  if (uri != null) {
    String oauthVerifier = 
      uri.getQueryParameter("oauth_verifier");
 
    try {
      RequestToken requestToken = loadRequestToken();
      AccessToken at = twitter.getOAuthAccessToken(
        requestToken, oauthVerifier);

      saveAccessToken(
        at.getToken(), at.getTokenSecret());

    } catch (TwitterException e) {
      e.printStackTrace();
      showToast(e.getMessage());
    }
  }
}

Quando a atividade é chamada pela página do twitter, uma chave de verificação da requisição de acesso é retornada. Com a requisição e a chave de verificação podemos criar um objeto AccessToken para acessar os recursos do Twitter. O token e o token secret do objeto AccessToken são salvos no método saveAccessToken (definido mais adiante) e, como vimos anteriormente podem ser recuperados quando clicamos no botão de login.

Abaixo temos o método que é chamado ao clicar no botão Tweet.

public void clickTweet(View v) {
  try {
    if (loadAccessToken() != null){
      EditText edt = 
        (EditText) findViewById(R.id.editText1);
      String tweet = edt.getText().toString();

      twitter.updateStatus(tweet);
      showToast("Status atualizado com sucesso!");

    } else {
      showToast("Faça o login antes de Twittar");
    }
  } catch (TwitterException e) {
      e.printStackTrace();
      showToast(e.getMessage());
  }
}

Nada de mais aqui. Apenas pega o conteúdo da caixa de texto e manda alterar o status do objeto Twitter.

Abaixo temos método auxiliares que utilizamos no nosso código. O primeiro exibe um Toast na tela.
private void showToast(String s){
  Toast.makeText(this, s, Toast.LENGTH_LONG).show();
}

Os métodos que salvam e recuperam as chaves de solicitação (RequestToken) e de acesso (AccessToken) estão definidos abaixo. Para ambos são utilizados SharedPreferences.
 
private RequestToken loadRequestToken(){
  SharedPreferences prefs = PreferenceManager.
    getDefaultSharedPreferences(this);
  String reqToken = 
    prefs.getString("request_token", null);
  String reqTokenSecret = 
    prefs.getString("request_tokensecret", null);
  
  return new RequestToken(reqToken, reqTokenSecret);
}
 
private void saveRequestData(
  String requestToken, String requestTokenSecret){

  SharedPreferences prefs = PreferenceManager.
    getDefaultSharedPreferences(this);
  SharedPreferences.Editor editor = prefs.edit();

  editor.putString(
    "request_token", requestToken);
  editor.putString(
    "request_tokensecret", requestTokenSecret);

  editor.commit();  
}
 
private AccessToken loadAccessToken() {
  SharedPreferences prefs = PreferenceManager.
    getDefaultSharedPreferences(this);
  String acToken = 
    prefs.getString("access_token", null);
  String acTokenSecret = 
    prefs.getString("access_tokensecret", null);

  if (acToken != null || acTokenSecret != null){
    return new AccessToken(acToken, acTokenSecret);
  }
  return null;
}
 
private void saveAccessToken(
  String accessToken, String accessTokenSecret) {

  SharedPreferences prefs =
    PreferenceManager.getDefaultSharedPreferences(this);

  SharedPreferences.Editor editor = prefs.edit();

  editor.putString(
    "access_token", accessToken);
  editor.putString(
    "access_tokensecret", accessTokenSecret);

  editor.commit();
}

Por fim, o método que faz o logout, limpa os valores da sharedpreference de AccessToken, o que vai forçar a realização de um novo login.
public void clickLogout(View v) {
  saveAccessToken(null, null);
}


É isso pessoal, qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber