segunda-feira, 30 de junho de 2014

Google I/O 2014

Olá povo,

No último dia 21/06 viajei para San Francisco para o meu primeiro Google I/O, conferência anual do Google. O evento aconteceu nos dias 25 e 26/06, e nesse post vou contar um pouquinho de como foi a viagem, pois todas as palestras podem ser vistas aqui ;)
Peguei um vôo da American Airlines de Recife para Miami (6 horas), e de Miami para San Francisco (mais 6 horas). Chegando em SF, fui recepcionado pelos meus estimados colegas Neto Marin e José Papo, que me levaram para conhecer o Pier 39, um ponto turístico da cidade.
Almoçamos por lá e depois demos uma volta pela cidade de carro. Depois eles me deixaram na casa onde fiquei hospedado (ótima por sinal) que fica a 15 minutos do centro. Dividi a casa com mais dois colegas: Alexandre Tarifa e Paulo Fernandes.
Na segunda-feira (23/06), fomos conhecer o Google, e foi simplesmente fantástico com vários prédios bacanas pra caramba. Mas minha principal meta era: tirar fotos no prédio onde ficam as estátuas do Android. Missão cumprida! :)


Assisti a vitória da seleção brasileira por 4x1 sobre Camarões no refeitório do Google, onde tinham vários brasileiros torcendo pela nossa seleção. Foi massa! :) Em seguida conheci outros prédios do Google Plex.
Lá dentro tem uma loja que vende camisas, bonecos, casacos com estampas das coisas do Google. Aí lascou! Comprei um monte :)
Depois do passeio no Google, fui conhecer um pouquinho do centro de SF. Uma coisa curiosa (e boa ao meu ver) é que as 20:00 ainda está claro, só começa a escurecer entre 20:30 e 21:00 :)
Jantei na Cheesecake Factory (infelizmente Penny não trabalha lá) e além de a comida ser excelente (e não tão cara) a vista de lá é bem bacana.
Terça-feira foi o dia de fazer o checkin no local do evento o Moscone Center West que estava vestido para o Google I/O.

Fiz a confirmação da minha presença e ganhei aquele kit básico de evento (crachá, camisa e garrafinha). Em seguida, fui para uma festa exclusiva para os GDEs onde conheci outros GDEs do resto mundo. Depois de algumas horas, fui pra casa pra descansar e me preparar para o primeiro dia do evento.

Acordei cedo na quarta-feira preparado para enfrentar a maratona de palestras que estava por vir. Peguei uma fila que deu a volta no quarteirão e perdi os 20 primeiros minutos do keynote de abertura (fiquei puto, porque GDE não precisava pegar essa fila) mas tudo bem, assisti quando cheguei em casa.
No keynote de abertura, os principais tópicos do Android foram:
- Android L Developer Preview que ficará no lugar do KitKat;
- Android Wear, o Android para dispositivos vestíveis, em especial, relógios e o glass;
- Android Auto, distribuição da plataforma para carros;
- Android TV, mais uma tentativa do Google em emplacar nas smartTVs.
Como é de costume o Google dá um "presentinho" a todos os participantes do I/O, e esse ano o participantes poderiam escolher entre o LG G watch ou Samsung Gear Live. Eu escolhi o segundo, (aos assaltantes de plantão, digo logo que não vou usa-lo :) A promessa é que depois (não sei quando) cada participante receberá também o Moto 360 (ficarei na torcida).
Uma coisa que foi impressionante pra mim foi o telão e a quantidade de pessoas (eu ouvi algo em torno de 6 mil).

Assisti palestras a tarde toda, e no final do primeiro dia, teve uma festinha bem legal. E o que é mais bacana nesse tipo de evento são as pessoas que você conhece. Conversei com um ex-gerente do NetFlix, com um TL do Hotel Urbano, uma designer do LinkedIn...
O segundo dia foi mais uma maratona de palestras, mas o que mais se falava era no novo visual/tema do Android: o Material. Muito bonito e consistente, ele foca muito em animações e promete trazer várias APIs (além do framework de animação existente desde o Honeycomb) para facilitar esse trabalho.

Um outro assunto recorrente eram os wearables, mais especificamente os relógios. Penso que o Google está investindo pesado e eu acho que a moda dos smartwatches vai mesmo pegar. Eles funcionam muito bem integrados com o smartphone, e inclusive o SDK já está disponível para download. Eu baixei o Android Studio 0.8 e já comecei a brincar com ele (já fiz até um app pro meu relógio :)

Ia esquecendo que não resisti e dei uma "tietada" em dois caras que eu sou muito fã: o Reto Meier e o Romain Guy. Se você mexe com Android e não conhece esses caras, bem... deveria conhecer :)
Bem pessoal, é isso. Quis resumir nesse post a história do meu primeiro Google I/O. Se vocês tiverem dúvidas sobre a viagem ou sobre o que aconteceu no evento, deixem seus comentários que eu vou editando aqui.

4br4ç05,
nglauber

quarta-feira, 4 de junho de 2014

GDG Recife


Olá povo,

No próximo dia 11/06 estará sendo lançado o Google Developer Group de Recife. O evento acontecerá no Anfiteatro do Centro de Informática (CIn) da UFPE às 19h. O objetivo dos GDGs é promover a discussão e troca de informações entre os desenvolvedores sobre produtos e tecnologias Google.
Eu estarei lá falando sobre Android, e quero fazer um bate-papo que seja legal tanto para desenvolvedores iniciantes, quanto para aqueles que já trabalham com Android. Dessa forma, resolvi escolher o tema "Aplicativos Android: faça da maneira certa", onde vou dar dicas de como desenvolver aplicativos que seguem os guidelines e as boas práticas de desenvolvimento Android.

Vocês podem se inscrever no site www.gdgrecife.com.

Aguardo vocês lá.

Editado em 11/06/2014
Segue abaixo os slides da apresentação.

4br4ç05,
nglauber

segunda-feira, 19 de maio de 2014

Google Developer Experts

Olá povo,

Tenho a enorme satisfação de compartilhar com vocês que fui nomeado como primeiro Google Developer Experts (GDE) de Android da América Latina*. Esse é um programa mundial do Google que reconhece desenvolvedores especializados em tecnologias da empresa, e que compartilham seus conhecimentos com a comunidade. Mais informações sobre o programa podem ser obtidas em https://developers.google.com/experts/.

Estou muito feliz com esse reconhecimento do Google e quero continuar meu trabalho no C.E.S.A.R. e na Unibratec, bem como passar um pouco do meu conhecimento com os posts aqui do blog.


A lista com todos os GDEs pode ser visualizada em https://developers.google.com/experts/members/.

Aproveito esse post para dar outra boa notícia. Como vocês devem ter notado, faz um tempinho que não posto aqui no blog. O motivo é que estou escrevendo um livro sobre Android (estou divulgando aqui em primeira mão \o/ ) que será publicado pela editora Novatec. Ainda não tenho previsão concreta para o lançamento, mas assim que tiver novidades divulgarei aqui, no meu Twitter e no Google+.

4br4ç05,
nglauber

*Ninguém me disse isso, tomei essa conclusão olhando a lista dos GDEs :)

terça-feira, 11 de março de 2014

Dicas de Android 10

Olá povo,

Mais um post da série "Dicas de Android". Aproveitem!

Dica 1. Densidades de tela no Android e seus DPIs.
Atualmente temos dispositivos Android de diversos tamanhos e com qualidade de tela diferentes. É importante sabermos a tabela abaixo para usar imagens adequadamente para cada um deles.
DensidadeDots per inchProporção
LDPI120dpi0.75
MDPI160dpi1.00
HDPI240dpi1.50
XHDPI320dpi2.00
XXHDPI480dpi3.00
XXXHDPI640dpi4.00

Dica 2. Obter as polegadas de um aparelho. Dica de Maurício Taumaturgo.
public double getInch(Activity act) {
  DisplayMetrics displayMetrics = new DisplayMetrics();
  act.getWindowManager().getDefaultDisplay()
    .getMetrics(displayMetrics);

  double x = Math.pow(
    displayMetrics.widthPixels/displayMetrics.xdpi,2);
  double y = Math.pow(
    displayMetrics.heightPixels/displayMetrics.ydpi,2);
  double screenInch = Math.sqrt(x+y);
  return screenInch; 
}

Dica 3. HTML no TextView. Dica de Diego Nascimento (aluno do TECDAM)
O Android suporta algumas tags HTML dentro do componente TextView. Mas se quisermos carregar as imagens da tag <img /> usamos a classe ImageGetter.
TextView tvText = (TextView) findViewById(R.id.text);
final String textoEmHtml = 
  "<html><body>Html em "
  + "<b>Negrito</b> e <i>Itálico</i>"
  + "<img src='mario.png' />"
  + "Mais um texto qualquer"
  + "<img src='luigi.png' />"
  + " texto depois da imagem</body></html>";

ImageGetter imgGetter = new ImageGetter() {
  public Drawable getDrawable(String source) {
    BitmapDrawable drawable = null;
    try {
      Bitmap bmp = BitmapFactory.decodeStream(
        getAssets().open(source));

      drawable = new BitmapDrawable(
        getResources(), bmp);

      drawable.setBounds(
        0, 0, bmp.getWidth(), bmp.getHeight());

    } catch (IOException e) {
      e.printStackTrace();
    }
    return drawable;
  }
};

meuTextView.setText(
  Html.fromHtml(textoEmHtml, imgGetter, null));

Dica 4. Customizando ActionBar
A ActionBar é utilizada como padrão navegacional do Android. Entretanto, customiza-la para com as cores da sua aplicação requer um certo esforço. Para minizar esse trabalho, é possível gerar os arquivos de recurso para personalizar a ActionBar através do site Android ActionBar Style Generator.

Dica 5. EditText com imeOptions e inpuType
A propriedade inputType do EditText indica qual o formato do teclado virtual mais adequado para a caixa de texto, enquanto a propriedade imeOptions indica o botão de ação do teclado.
<EditText
  android:id="@+id/editText1"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:inputType="textEmailAddress"
  android:imeOptions="actionDone" />
final EditText edtEmail = 
  (EditText)findViewById(R.id.editText1);
edtEmail.setOnEditorActionListener(
  new OnEditorActionListener() {
    @Override
    public boolean onEditorAction(
      TextView v, int actionId, KeyEvent event) {

      if (v == edtEmail && 
        EditorInfo.IME_ACTION_DONE == actionId) {

        String email = edtEmail.getText().toString();
        if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()){
          edtEmail.setError("Email inválido!");
        }
        return true;
      }
      return false;
    }
});
Dica 6. Carregando HTML com CSS e JavaScript no WebView
Se você for carregar um arquivo HTML que está dentro do seu APK, e esse arquivo usa arquivos de CSS e JavaScript, aconselho coloca-los na pasta assets do seu projeto e carregá-los da seguinte forma:
mWebView = (WebView) v.findViewById(R.id.webView1);
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
mWebView.loadDataWithBaseURL(
  "file:///android_asset/", 
  "Meu Html que usa CSS e JS",
   "text/html", "UTF-8", null);

4br4ç05,
nglauber

domingo, 23 de fevereiro de 2014

Google Cloud Messaging

Olá povo,

Depois de um bom tempo sem postar, resolvi voltar com um tópico muito bacana: Google Cloud Messaging, ou simplesmente GCM. Esse serviço gratuito permite que sua aplicação web envie mensagens de notificação para sua aplicação Android. Nesse tutorial vou tentar mostrar como aplicar esse recurso na sua aplicação.

Cada aplicação deve ter um ID associado. Com esse ID, a aplicação solicita um token para o servidor do GCM, esse token é único por dispositivo+aplicação. De posse desse token, a aplicação Android deve enviar esse valor para a aplicação servidora. Esse token deve ser armazenado de alguma forma no servidor, pois quando ele precisar/desejar enviar uma mensagem, o fará através de uma requisição via POST para o servidor do GCM que se encarregará de despachá-las para os dispositivos Android.
Nada muito complexo não é? :) Agora vamos para a prática!

Gerando as chaves de acesso

Acesse o Google Developers Console e crie um novo projeto. Precisaremos do número do projeto para podermos registrar a app+device no servidor do GCM.
Uma vez criado o projeto, selecione a opção APIs dentro do menu APIs & auth. Agora, habilite o Google Cloud Messaging.

Agora vamos gerar o SHA1 que será usado para gerar a chave para acessarmos o serviço do GCM. Digite o comando abaixo na pasta bin do JDK (Java Development Kit) (ou em qualquer lugar se esse caminho estiver no PATH do seu sistema operacional).

keytool -list -v -keystore ~/.android/debug.keystore

Aqui, estou usando o debug.keystore que por padrão fica no diretório usuário/.android (no Windows seria C:\Users\usuario). O próximo passo é criar duas chaves, uma para ser usada pela app Android e outra para app Web. Para isso, selecione a opção Credentials no menu APIs & auth. Clique na opção Create new key e selecione Android key. Na janela que for exibida, digite o SHA1 gerado pelo comando keytool, seguido de ";" e o pacote do projeto.
Exemplo:
41:F7:05:0F:72:29:99:28:55:BA:4B:79:AB:27:BC:7A:62:03:85:2C;ngvl.android.exemplogcm

Clique novamente em Create new key e selecione Server key. Aqui podemos filtrar o uso dessa chave por IP. Na prática utilizaríamos o IP real do servidor, mas para esse exemplo, não usaremos. É só confirmar para criar a nova chave.

Aplicação Cliente

Para a aplicação cliente primeiro temos que adicionar a referência ao projeto do Google Play Services, disponível na pasta ANDROID_SDK/extras/google/google_play_services/libproject. Depois adicione a dependência ao seu projeto.
No AndroidManifest.xml do seu projeto você deve adicionar as seguintes informações:
<manifest
  package="ngvl.android.exemplogcm"
  ...>
  <uses-permission android:name=
    "com.google.android.c2dm.permission.RECEIVE"/>
  <uses-permission android:name=
    "android.permission.INTERNET"/>
  <uses-permission android:name=
    "android.permission.GET_ACCOUNTS"/>
  <uses-permission android:name=
    "android.permission.WAKE_LOCK"/>

  <permission android:name=
    "ngvl.android.exemplogcm.permission.C2D_MESSAGE"
    android:protectionLevel="signature"/>

  <uses-permission android:name=
    "ngvl.android.exemplogcm.permission.C2D_MESSAGE"/>

  <application
    ...>

    ...

    <receiver 
      android:name=
        "ngvl.android.exemplogcm.GcmBroadcastReceiver"
      android:permission=
        "com.google.android.c2dm.permission.SEND">
      <intent-filter>
        <action 
          android:name=
            "com.google.android.c2dm.intent.RECEIVE"/>
        <category 
          android:name="ngvl.android.exemplogcm"/>
      </intent-filter>
    </receiver>

    <service 
      android:name=
        "ngvl.android.exemplogcm.GcmIntentService"/>
        
    <meta-data 
      android:name="com.google.android.gms.version"      
      android:value="@integer/google_play_services_version"/>
  </application>
</manifest>
Basicamente, adicionamos algumas permissões:
- Para receber mensagens do GCM;
- Acessar internet;
- Ter acesso às contas cadastradas do aparelho (para usar o GCM, temos que ter uma conta Google criada no aparelho);
- Wake Lock para ligar a tela quando chegar uma notificação;
Além disso, declaramos um Broadcast Receiver que recebe as mensagens e delega o tratamento para o Intent Service que também adicionamos.

Abaixo temos o código do BroadcastReceiver. Ele é um WakefulBroadcastReceiver para evitar que o aparelho apague a tela enquanto ele está executando, que nesse caso é receber a notificação. Quando uma mensagem GCM é recebida esse componente é disparado, e então delega o tratamento para o GcmIntentService.
public class GcmBroadcastReceiver 
  extends WakefulBroadcastReceiver {

  @Override
  public void onReceive(Context context, 
    Intent intent) {

    ComponentName comp = new ComponentName(
      context.getPackageName(),
      GcmIntentService.class.getName());

    startWakefulService(
      context, (intent.setComponent(comp)));

    setResultCode(Activity.RESULT_OK);
  }
}
O IntentService é um serviço especial do Android onde cada chamada ao mesmo é empilhada e executada sequencialmente em uma Thread separada. A requisição é tratada no método onHandleIntent e nele checamos se a mensagem que estamos recebendo é uma mensagem de dados (o GCM pode enviar mensagens de erro). Se for uma mensagem de dados, disparamos uma notificação.
public class GcmIntentService extends IntentService {
  public static final int NOTIFICATION_ID = 1;
  private NotificationManager mNotificationManager;
  private NotificationCompat.Builder builder;

  public GcmIntentService() {
    super("GcmIntentService");
  }

  @Override
  protected void onHandleIntent(Intent intent) {
    Bundle extras = intent.getExtras();
    GoogleCloudMessaging gcm = 
      GoogleCloudMessaging.getInstance(this);

    String messageType = gcm.getMessageType(intent);

    if (!extras.isEmpty()) {

      if (GoogleCloudMessaging.
          MESSAGE_TYPE_MESSAGE.equals(messageType)) {

          sendNotification("Mensagem: " + 
            extras.toString());
      }
    }
    GcmBroadcastReceiver.completeWakefulIntent(intent);
  }

  private void sendNotification(String msg) {
    mNotificationManager = (NotificationManager)
      this.getSystemService(
        Context.NOTIFICATION_SERVICE);

    PendingIntent contentIntent = 
      PendingIntent.getActivity(
        this, 0, new Intent(), 0);

    NotificationCompat.Builder mBuilder =
        new NotificationCompat.Builder(this)
      .setSmallIcon(R.drawable.ic_launcher)
      .setContentTitle("GCM Notification")
      .setStyle(new NotificationCompat.BigTextStyle()
      .bigText(msg))
      .setContentText(msg);

    mBuilder.setContentIntent(contentIntent);
    mNotificationManager.notify(
      NOTIFICATION_ID, mBuilder.build());
  }
}
Logo abaixo, criei uma classe utilitária para realizar as operações com o GCM. O método checkPlayService verifica se o google play está instalado, atualizado e se existe uma conta google criada no aparelho. Já o método getRegistrationId obtém o chave de acesso que o GCM retornou e que no nosso exemplo, estamos armazenando em uma SharedPreference. Isso está sendo feito no método storeRegistrationId.
O método registerInBackground tenta obter a chave de acesso do GCM usando uma AsyncTask. Ou seja, em uma Thread separada. Quando obtermos essa chave, a enviamos para nossa aplicação web e salvamos na SharedPreference.
Nesse exemplo, estamos enviando essa chave via requisição GET para o servidor. Obviamente, isso deve ser melhorado em uma aplicação de produção.
public class GcmHelper {

  public static final String 
    EXTRA_MESSAGE = "message";
  public static final String 
    PROPERTY_REG_ID = "registration_id";
    
  private static final String 
    PROPERTY_APP_VERSION = "appVersion";
  private static final String 
    SENDER_ID = "ID_DA_SUA_APP";
  private static final String 
    IP = "IP_DO_SERVIDOR";
 
  public static boolean checkPlayServices(
    Activity activity, int requestCode) {

    int resultCode = GooglePlayServicesUtil
       .isGooglePlayServicesAvailable(activity);

    if (resultCode != ConnectionResult.SUCCESS) {
      if (GooglePlayServicesUtil
          .isUserRecoverableError(resultCode)) {

        GooglePlayServicesUtil.getErrorDialog(
          resultCode, activity, requestCode).show();

      } else {
        Toast.makeText(activity, 
            "Dispositivo não suportado.", 
            Toast.LENGTH_SHORT).show();
        activity.finish();
      }
      return false;
    }
    return true;
  }
 
  public static String getRegistrationId(
    Context context) {

    final SharedPreferences prefs = 
      getGCMPreferences(context);

    String registrationId = 
      prefs.getString(PROPERTY_REG_ID, "");

    if (registrationId.isEmpty()) {
      return "";
    }
    int registeredVersion = 
      prefs.getInt(PROPERTY_APP_VERSION, 
        Integer.MIN_VALUE);

    int currentVersion = getAppVersion(context);
    if (registeredVersion != currentVersion) {
      return "";
    }
    return registrationId;
  }
 
  private static void storeRegistrationId(
    Context context, String regId) {

    final SharedPreferences prefs = 
      getGCMPreferences(context);

    int appVersion = getAppVersion(context);

    SharedPreferences.Editor editor = prefs.edit();
    editor.putString(PROPERTY_REG_ID, regId);
    editor.putInt(PROPERTY_APP_VERSION, appVersion);
    editor.commit();
  }

  public static void registerInBackground(
    final Context ctx, 
    final GoogleCloudMessaging gcm,
    final RegisterListener listener) {
  
    new AsyncTask<Void, Void, String>() {
      @Override
      protected String doInBackground(
        Void... params) {

        String regid = null;
        try {
          regid = gcm.register(SENDER_ID);

          sendRegistrationIdToBackend(regid);

          storeRegistrationId(ctx, regid);
                 
        } catch (IOException ex) {
          ex.printStackTrace();
        }
        return regid;
      }

      @Override
      protected void onPostExecute(String msg) {
        listener.onRegisterComplete(msg);
      }
    }.execute();
  }
 
  private static void sendRegistrationIdToBackend(
    String key) throws IOException {

    URL url = new URL(
      "http://"+ IP +"/SeuServer/registrar?token="+ key);

    HttpURLConnection conexao = 
      (HttpURLConnection)url.openConnection();
    conexao.connect();
  }
 
  private static SharedPreferences 
    getGCMPreferences(Context context) {

    return context.getSharedPreferences(
      "GDE", Context.MODE_PRIVATE);
  }
 
  private static int getAppVersion(Context context) {
    try {
      PackageInfo packageInfo = 
        context.getPackageManager()
          .getPackageInfo(context.getPackageName(), 0);
      return packageInfo.versionCode;
         
    } catch (NameNotFoundException e) {
      throw new RuntimeException(e);
    }
  }

  public interface RegisterListener {
    void onRegisterComplete(String s);
  }
}
A Activity né bem simples. Ela checa se o device suporta o Google Play (e se está atualizado), tenta obter a chave de acesso do GCM e depois a envia para o servidor.
public class ClientActivity extends Activity 
  implements RegisterListener {

  private static final int REQUEST_CODE_GOOGLEPLAY = 0;
 
  GoogleCloudMessaging gcm;
 
  String regid;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_client);
  
    if (GcmHelper.checkPlayServices(
        this, REQUEST_CODE_GOOGLEPLAY)) {

      gcm = GoogleCloudMessaging.getInstance(this);
      regid = GcmHelper.getRegistrationId(this);

      if (regid.isEmpty()) {
        GcmHelper.registerInBackground(this, gcm, this);
      }
    } else {
      Log.i("NGVL", "Google Play não instalado.");
    }
  }
  
  @Override
  public void onRegisterComplete(String s) {
    Toast.makeText(this, "Registered: "+ s, 
      Toast.LENGTH_LONG).show();
  }
}

Aplicação Servidora

Você pode usar qualquer tecnologia do lado do servidor para usar o GCM. Aqui vou usar um Servlet Java para receber o token do cliente Android e enviar mensagens para o mesmo. Aqui só estou salvando um device. Você terá que fazer a sua lógica para salvar vários.

@WebServlet("/gcm")
public class GcmServlet extends HttpServlet {

  private static final long serialVersionUID = 1L;

  protected void doGet(
    HttpServletRequest request,
    HttpServletResponse response) 
      throws ServletException, IOException {

    PrintWriter writer = response.getWriter();
    writer.print("Current key:"+ retrieveToken());
  
    String key = request.getParameter("token");
    if (key != null && !key.isEmpty()){
      saveToken(key);
      response.getWriter().print("\nRegistrado:"+ key);
    }
  }

  protected void doPost(
    HttpServletRequest request,
    HttpServletResponse response) 
      throws ServletException, IOException {
  
    String msgJson = 
      "{ \"data\": { \"mensagem\":\""+ 
      request.getParameter("msg") +"\"}, "+
      "\"registration_ids\": [ \""+ retrieveToken() +
      "\" ] }";

    byte[] data = msgJson.getBytes("UTF-8");
  
    URL url = new URL(
      "https://android.googleapis.com/gcm/send");

    HttpsURLConnection conexao = 
      (HttpsURLConnection)url.openConnection();

    conexao.addRequestProperty("Authorization", 
      "key="+"sua_chave_web_aqui");
    conexao.addRequestProperty(
      "Content-Type", "application/json");
    conexao.setRequestMethod("POST");
    conexao.setDoOutput(true);
    conexao.setUseCaches(false);
    conexao.connect();
  
    OutputStream os = conexao.getOutputStream();
    os.write(data);
    os.close();
  
    PrintWriter writer = response.getWriter();
    writer.print("Trying to send: "+ msgJson);
  
    if (conexao.getResponseCode() == 
      HttpURLConnection.HTTP_OK){

      writer.print("\nDONE");
    } else {
      writer.print(
        "\nERRO:"+ conexao.getResponseCode() +" - "+ 
      conexao.getResponseMessage());
    }
  }

  private void saveToken(String key)
    throws IOException {

    String serverDir = 
      getServletContext().getRealPath("/");

    File f = new File(serverDir, "devices");
    if (!f.exists())
      f.createNewFile();

    FileOutputStream fos = new FileOutputStream(f);
    BufferedWriter out = new BufferedWriter(
      new FileWriter(f, true));
    out.write(key);

    out.close();
    fos.close();
  }
 
  private String retrieveToken() throws IOException{
    String serverDir = 
      getServletContext().getRealPath("/");

    File f = new File(serverDir, "devices");
    if (f.exists()){
      InputStream is = new FileInputStream(f);
      BufferedReader rd = new BufferedReader(
        new InputStreamReader(is));
   
      String line = rd.readLine();
      rd.close();
      is.close();
      return line;
    }
    return null;
  }
}
Para fins de exemplo, o método GET desse servidor está recebendo o token do cliente. Aqui só estou armazenando 1 única chave em um arquivo TXT, mas na prática, devemos armazenar isso em um banco de dados. Para enviar uma mensagem para o Android, basta abrir a página abaixo, digitar a mensagem e clicar em enviar.
<html>
<head>
<title>Exemplo GCM</title>
</head>
<body>
<form method="post" action="gcm">
  Mensagem:<input type="text" name="msg"><br>
  <input type="submit" value="OK">
</form>
</body>
</html>

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

sexta-feira, 29 de novembro de 2013

Comunicação HTTP eficiente no Android com Volley

Olá povo,

Conversando com meus novos mentores Neto Marin, Marcelo Quinta e Lúcio Maciel que conheci no DevBus Brasil, percebi uma unanimidade no que diz respeito a acesso HTTP no Android: a biblioteca Volley. Quando assisti o vídeo do Ficus Kirkpatrick no Google IO de 2013 não dei muita bola, mas os meus novos "orientadores" me mostraram o quanto essa biblioteca é poderosa e fácil de usar. Tudo isso aliado ao fato dela ser mantida pelo próprio Google.
Eu já falei em outros posts aqui do blog como fazer comunicação HTTP com AsyncTask (link 1 e link 2) e com AsyncTaskLoader (link), assim como carregar imagens da web em um Adapter usando o UniversalImageLoader (link). E também como ler um JSON de um WebService REST (link). Mas com o Volley podemos resolver todos os problemas citados nos posts anteriores e ainda remover todo aquele boiler plate que fazemos ao realizar essas requisições, simplificando o código. Além disso, ela tem as seguintes vantagens:
  • Comunicação paralela com possibilidade de priorização. Por exemplo, podemos criar uma fila de requisições JSON e outra de requisição de imagens e dizer que a primeira tem maior prioridade que a segunda;
  • Quando giramos a Activity, a mesma é destruída (mostrei como resolver isso aqui) e se nesse momento estiver havendo alguma requisição HTTP, o resultado é perdido. Podemos implementar algum tipo de cache em memória ou em banco, mas o Volley já faz esse serviço pra gente.
  • Um "ImageLoader" para carregar imagens da web, particularmente útil em adapters (falei de adapter aqui).
Depois dessa breve introdução, vamos por a mão na massa!

O Volley está em um repositório git no código-fonte do próprio Android. Então você terá que fazer um clone do mesmo via comando. Se você não tem o git instalado na sua máquina, siga esse tutorial aqui. Depois é só digitar no terminal.
git clone https://android.googlesource.com/platform/frameworks/volley

Feito isso, importe o projeto do Volley dentro do Eclipse e marque-o como biblioteca clicando com o botão direito sobre o projeto e selecionando Properties. Em seguida, selecione a opção Android do lado esquerdo e marque a checkbox "Is Library".

Em homenagem ao Ricardo Lecheta, vou fazer uma Activity que lê o JSON de carros disponível no site do seu livro de Android (que por sinal é muito bom e uso nas minhas aulas). E para ler o JSON e as imagens dos carros, vamos usar o Volley.

Vamos começar pela classe que vai manter a fila de execução de requisições do Volley bem como seu ImageLoader. O Google recomenda que ela seja um singleton, pois podemos gerenciar quantas filas de execução podemos ter e não termos que criar várias.
public class VolleySingleton {
  private static VolleySingleton mInstance = null;
  // Fila de execução
  private RequestQueue mRequestQueue;
  // Image Loader
  private ImageLoader mImageLoader;
 
  private VolleySingleton(Context context){
    mRequestQueue = Volley.newRequestQueue(context);

    mImageLoader = new ImageLoader(this.mRequestQueue, 
      new ImageLoader.ImageCache() {
        // Usando LRU (Last Recent Used) Cache
        private final LruCache<String, Bitmap> 
          mCache = new LruCache<String, Bitmap>(10);

        public void putBitmap(
          String url, Bitmap bitmap) {
          mCache.put(url, bitmap);
        }
        public Bitmap getBitmap(String url) {
          return mCache.get(url);
        }
      });
  }
 
  public static VolleySingleton getInstance(
    Context context){

    if(mInstance == null){
      mInstance = new VolleySingleton(context);
    }
    return mInstance;
  }
 
  public RequestQueue getRequestQueue(){
    return this.mRequestQueue;
  }
 
  public ImageLoader getImageLoader(){
    return this.mImageLoader;
  }
}
Vou definir um POJO simples que representa os objetos carro que iremos listar.
public  class Carro {
  String nome;
  String imageUrl;
 
  public Carro(String nome, String imageUrl) {
    this.nome = nome;
    this.imageUrl = imageUrl;
  }
}
Abaixo temos o arquivo de layout usado no adapter. Note que estamos usando NetworkImageView ao invés do ImageView tradicional.
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/itemRoot"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content" >

  <com.android.volley.toolbox.NetworkImageView
    android:id="@+id/img"
    android:layout_width="140dp"
    android:layout_height="70dp"
    android:src="@drawable/ic_launcher" />

  <TextView
    android:id="@+id/txtName"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical"
    android:textSize="20dp"
    android:text="@null"/>
</LinearLayout>
A classe Adapter listada abaixo herda de ArrayAdapter e está usando o NetworkImageView e o ImageLoader do Volley.
public class CarroAdapter extends ArrayAdapter<Carro>{

  static final int LAYOUT = R.layout.item_lista;
 
  public CarroAdapter(Context context, 
    List<Carro> objects) {

    super(context, LAYOUT, objects);
  }

  @Override
  public View getView(int position, 
    View convertView, ViewGroup parent) {

    Context ctx = parent.getContext();
    if (convertView == null){
      convertView = LayoutInflater.from(ctx)
        .inflate(R.layout.item_lista, null);
    }
    NetworkImageView img = (NetworkImageView)
      convertView.findViewById(R.id.img);
    TextView txt = (TextView)
      convertView.findViewById(R.id.txtName);
  
    Carro carro = getItem(position);
    txt.setText(carro.nome);
    img.setImageUrl(
      carro.imageUrl, 
      VolleySingleton.getInstance(
        getContext()).getImageLoader());
  
    return convertView;
  }
}
E finalmente a Activity... Ela implementa duas interfaces: Response.Listener e Response.ErrorListener. O método da primeira (onResponse) será chamada quando a requisição ocorrer sem problemas, e da segunda (onErrorResponse) caso contrário. No onCreate, obtemos a fila de execução a partir do nosso singleton, e depois criamos um JsonObjectRequest. No Volley, além desse tipo de Request, temos o ImageRequest e o StringRequest, o primeiro para imagens e o segundo para qualquer requisição que retorne uma String. Após criar a requisição, a adicionamos na fila para ser executada.
No método onResponse já recebemos o JSONObject, então é só fazer o parse do mesmo. Aqui poderíamos usar o Gson, mas eu não gosto dele :p
Após transformar o JSONObject em uma Lista de Carros, criamos e setamos o adapter da nossa ListActivity.
Se alguma coisa der errado, o método onErrorResponse será chamado, e daí estamos exibindo um Toast.
public class MainActivity extends ListActivity 
  implements 
    Response.Listener<JSONObject>, 
    Response.ErrorListener {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    String url="http://www.livroandroid.com.br/livro/"+
      "carros/carros_classicos.json";
  
    RequestQueue queue = Volley.newRequestQueue(this);

    JsonObjectRequest jsObjRequest = 
      new JsonObjectRequest(
        Request.Method.GET, // Requisição via HTTP_GET
        url,   // url da requisição
        null,  // JSONObject a ser enviado via POST
        this,  // Response.Listener
        this); // Response.ErrorListener
  
    queue.add(jsObjRequest);  
  }

  @Override
  public void onResponse(JSONObject response) {
    List<Carro> carros = new ArrayList<Carro>();
  
    try {
      // Não precisamos converter o 
      // InputStream em String \o/
      JSONObject jsonCarros = 
        response.getJSONObject("carros");
      JSONArray jsonCarro = 
        jsonCarros.getJSONArray("carro");
   
      for (int i = 0; i < jsonCarro.length(); i++) {
        JSONObject jsonCarroItem = 
          jsonCarro.getJSONObject(i);
        String nome = 
          jsonCarroItem.getString("nome");
        String thumbnail = 
          jsonCarroItem.getString("url_foto");
    
        Carro carro = new Carro(nome, thumbnail);
        carros.add(carro);
      }
    } catch (Exception e){
      e.printStackTrace();
    }
   
    setListAdapter(new CarroAdapter(this, carros));
  }
 
  @Override
  public void onErrorResponse(VolleyError error) {
    Toast.makeText(this, "Erro!", 
      Toast.LENGTH_SHORT).show();
  }
}
Ah! Como toda app Android que acessa a Web, adicione a permissão de Internet no seu AndroidManifest.xml.

É isso, a partir de agora podemos utilizar o Volley em nossas aplicações com essa biblioteca usada e mantida pelo próprio Google. Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

terça-feira, 26 de novembro de 2013

ContentProvider, CursorLoader e CursorAdapter

Olá povo,

Quem já assistiu minhas aulas ou alguma palestra minha sobre Android sabe que eu sempre falo que o Android tem 4 componentes básicos: Activities, Services, Broadcast Receivers e Content Providers.
Nesse post vou falar sobre ContentProvider e os benefícios de utilizá-lo junto com um CursorLoader e um CursorAdapter.

Um ContentProvider, como o próprio nome diz, é um provedor de conteúdo que nos permite compartilhar informações da nossa aplicação com outras aplicações. Essas informações normalmente são dados do nosso banco de dados SQLite (mas nada impede que venha de outra fonte). Ao meu ver, as grande vantagens de utilizar esse mecanismo são: 1) se quisermos alterar a forma de persistir os dados, teremos apenas que alterar o provider internamente; 2) podemos deixar o provider público ou privado; 3) os dados ficam sincronizados com o cursor adapter, ou seja, alterou no provider, a tela é automaticamente atualizada.

No nosso exemplo, faremos um pequeno aplicativo que salva textos no SQLite. Para tal, vamos criar uma classe que herda de SQLiteOpenHelper e que vai criar a tabela.
public class DBHelper extends SQLiteOpenHelper {

  public static final String TABLE_NAME = "messages";
  public static final String COLUMN_ID = "_id";
  public static final String COLUMN_MESSAGE="message";
 
  public static final String[] ALL_COLUMNS = { 
    COLUMN_ID, COLUMN_MESSAGE
  }; 
 
  private static final String NOME_BANCO="dbMessages";
  private static final int    VERSAO_BANCO = 1;
 
  public DBHelper(Context context) {
    super(context, NOME_BANCO, null, VERSAO_BANCO);
  }

  @Override
  public void onCreate(SQLiteDatabase db) {
    db.execSQL(
      "CREATE TABLE "+ TABLE_NAME +" ("+ 
      COLUMN_ID+" INTEGER PRIMARY KEY AUTOINCREMENT,"+
      COLUMN_MESSAGE +" TEXT )");
  }

  @Override
  public void onUpgrade(SQLiteDatabase db, 
    int oldVersion, int newVersion) {
    // Utilizar só na proxima versão :)
  }
}
Essa classe ficará responsável por criar o banco (e as suas tabelas) se não existir, ou atualizá-las caso já existam (em uma nova versão da app por exemplo).

Declare o Content Provider no AndroidManifest.xml.
<provider
  android:name="ngvl.android.excp.db.MessageProvider"
  android:authorities="ngvl.android.excp"
  android:exported="false" />
Agora, vamos implementar nosso Content Provider conforme abaixo.
public class MessageProvider extends ContentProvider {
  // Deve estar igual ao Manifest
  private static final String 
    AUTHORITY = "ngvl.android.excp";
  // Tipo de acesso que retorna todas as mensagens
  private static final int TYPE_ALL_MESSAGES = 1;
  // Tipo de acesso que retorna apenas uma mensagem
  // usando o id da mesma
  private static final int TYPE_SINGLE_MESSAGE = 2;

  private static final String BASE_PATH = "messages";
  // É através dessa URI que acessamos nosso provider
  public static final Uri CONTENT_URI = Uri.parse(
    "content://" + AUTHORITY + "/" + BASE_PATH);

  // Classe para checar se a Uri passada é valida 
  private static final UriMatcher sUriMatcher;

  static {
    sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    sUriMatcher.addURI(AUTHORITY, 
      BASE_PATH, TYPE_ALL_MESSAGES);
    sUriMatcher.addURI(AUTHORITY, 
      BASE_PATH + "/#", TYPE_SINGLE_MESSAGE);
  }

  private DBHelper mOpenHelper;

  @Override
  public boolean onCreate() {
    // Ao criar o Provider, inicializamos o helper
    mOpenHelper = new DBHelper(getContext());
    return true; // success
  }

  @Override
  public String getType(Uri uri) {
    return null;
  }

  @Override
  public Uri insert(Uri uri, ContentValues values) {
    int uriType = sUriMatcher.match(uri);
    SQLiteDatabase sqlDB = 
      mOpenHelper.getWritableDatabase();
    long id = 0;

    switch (uriType) {
    case TYPE_ALL_MESSAGES:
      id = sqlDB.insert(
        DBHelper.TABLE_NAME, 
        null, 
        values);
      break;
   
    default:
      throw new IllegalArgumentException(
        "Unknown URI: " + uri);
    }

    getContext().getContentResolver()
      .notifyChange(uri, null);

    return Uri.parse(BASE_PATH + "/" + id);
  }

  @Override
  public int update(Uri uri, ContentValues values, 
    String selection, String[] selectionArgs) {

    int uriType = sUriMatcher.match(uri);
    SQLiteDatabase sqlDB = 
      mOpenHelper.getWritableDatabase();

    int rowsUpdated = 0;

    switch (uriType) {
    case TYPE_ALL_MESSAGES:
      rowsUpdated = sqlDB.update(
        DBHelper.TABLE_NAME, 
        values, 
        selection,
        selectionArgs);
      break;

    case TYPE_SINGLE_MESSAGE:
      String id = uri.getLastPathSegment();
      if (TextUtils.isEmpty(selection)) {
        rowsUpdated = sqlDB.update(
          DBHelper.TABLE_NAME, 
          values,
          DBHelper.COLUMN_ID + "=" + id, 
          null);
      } else {
        rowsUpdated = sqlDB.update(
          DBHelper.TABLE_NAME, 
          values,
          DBHelper.COLUMN_ID +"="+ id +
            " and "+ selection,
          selectionArgs);
      }
      break;

    default:
      throw new IllegalArgumentException(
        "Unknown URI: " + uri);
    }

    getContext().getContentResolver()
      .notifyChange(uri, null);
    return rowsUpdated;
  }

  @Override
  public int delete(Uri uri, String selection, 
    String[] selectionArgs) {

    int uriType = sUriMatcher.match(uri);
    SQLiteDatabase sqlDB = 
      mOpenHelper.getWritableDatabase();

    int rowsDeleted = 0;
    switch (uriType) {
    case TYPE_ALL_MESSAGES:
      rowsDeleted = sqlDB.delete(
        DBHelper.TABLE_NAME, 
        selection,
        selectionArgs);
      break;

    case TYPE_SINGLE_MESSAGE:
      String id = uri.getLastPathSegment();
      if (TextUtils.isEmpty(selection)) {
        rowsDeleted = sqlDB.delete(
          DBHelper.TABLE_NAME,
          DBHelper.COLUMN_ID + "=" + id, 
          null);
      } else {
        rowsDeleted = sqlDB.delete(
          DBHelper.TABLE_NAME,
          DBHelper.COLUMN_ID +"="+ id + 
            " and " + selection,
          selectionArgs);
      }
      break;
    default:
      throw new IllegalArgumentException(
        "Unknown URI: " + uri);
    }

    getContext().getContentResolver()
      .notifyChange(uri, null);
    return rowsDeleted;
  }

  @Override
  public Cursor query(Uri uri, String[] projection, 
    String selection, String[] selectionArgs, 
    String sortOrder) {

    SQLiteQueryBuilder queryBuilder = 
      new SQLiteQueryBuilder();

    queryBuilder.setTables(DBHelper.TABLE_NAME);

    int uriType = sUriMatcher.match(uri);
    Cursor cursor = null;
    SQLiteDatabase db = 
      mOpenHelper.getWritableDatabase();
  
    switch (uriType) {
    case TYPE_ALL_MESSAGES:
      cursor = queryBuilder.query(
        db, 
        projection, 
        selection,
        selectionArgs, 
        null, 
        null, 
        sortOrder);   
      break;

    case TYPE_SINGLE_MESSAGE:
      queryBuilder.appendWhere(
        DBHelper.COLUMN_ID + "= ?");

      cursor = queryBuilder.query(
        db, 
        projection, 
        selection,
        new String[]{ uri.getLastPathSegment() },
        null,
        null,
        null);
        break;
   
    default:
      throw new IllegalArgumentException(
        "Unknown URI: " + uri);
    }

    cursor.setNotificationUri(
      getContext().getContentResolver(), uri);
  
    return cursor;
  }
}
Nossa classe herda de ContentProvider e declaramos uma constante para o authority da mesma. Esse atributo deve estar igual ao que definimos no AndroidManifest.xml pois ele faz parte do caminho da Uri que utilizaremos para acessar o provider.
Nosso provedor de conteúdo responde por dois tipos de Uri: content://ngvl.android.excp/messages e content://ngvl.android.excp/messages/X onde "X" é o id da mensagem que queremos acessar.
Por isso, instanciamos um objeto do tipo UriMatcher para checar se a Uri passada bate (match) com uma das Uris disponíveis.
No onCreate instanciamos nosso SQLiteHelper, e depois temos os quatro principais métodos: insert, update, delete e query.
Os quatro métodos são bem parecidos, eles iniciam checando que tipo de Uri foi passada e instanciando o SQLiteDatabase a partir do nosso helper. Depois efetuamos a operação correspondente no banco de dados. A parte mais importante é que em todos os quatro métodos notificamos os cursores que alguma operação foi efetuada através da chamada: getContext().getContentResolver().notifyChange(uri, null).
Com isso, o adapter que estiver observando esse ContentProvider será atualizado automaticamente.

Agora vamos criar o CursorAdapter que acessará nosso provider para exibir as mensagens na tela. Ele é um pouquinho diferente do BaseAdapter, pois contém o método newView para quando uma nova View precisa ser criada e o bindView para quando precisamos apenas preencher os componentes da View.
public class MessageCursorAdapter 
  extends SimpleCursorAdapter {

  private static final int LAYOUT = 
    R.layout.item_message;

  public MessageCursorAdapter(
    Context context, Cursor cursor) {

    super(context, LAYOUT, cursor, 
      DBHelper.ALL_COLUMNS, null, 0);
  }

  @Override
  public void bindView(View view, Context context, 
    Cursor cursor) {

    TextView txtMessage = (TextView) 
      view.findViewById(R.id.txtMessage);
    TextView txtId = (TextView) 
      view.findViewById(R.id.txtId);

    txtId.setText(
      cursor.getString(
        cursor.getColumnIndex(
          DBHelper.COLUMN_ID)));
    txtMessage.setText(
      cursor.getString(
        cursor.getColumnIndex(
          DBHelper.COLUMN_MESSAGE)));
  }

  @Override
  public View newView(Context contex, Cursor cursor, 
    ViewGroup viewGroup) {

    return LayoutInflater.from(contex).inflate(
      LAYOUT, null);
  }
}
O arquivo de layout usado pelo adapter é listado abaixo.
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="horizontal" >

  <TextView
    android:id="@+id/txtMessage"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:text="@null"
    android:textSize="20sp" />

  <TextView
    android:id="@+id/txtId"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@null"
    android:textSize="20sp"
    android:textColor="#FF0000" />
</LinearLayout>
Agora é só usar na Activity. No nosso exemplo temos um EditText, um Button e uma ListView. Ao clicar no botão, adicionamos o texto no banco de dados via ContentProvider. Ao clicar em um item da lista, o EditText é preenchido e se clicarmos em salvar o mesmo será atualizado. Se clicarmos no item, apagar o conteúdo do EditText e clicarmos em salvar, o mesmo será excluído. Assim, podemos testar todas as operações do nosso ContentProvider.
É na Activity que usamos o CursorLoader. Esse padrão é essencial no uso de banco de dados no Android, pois ele fará a consulta dos dados em segundo plano, evitando assim o bloqueio da Thread de UI, que faz com que a aplicação apareça estar travada se tivermos com uma grande quantidade de dados. A interface LoaderCallbacks notifica quando podemos criar o loader, quando a busca foi concluída e quando o cursor foi resetado por algum motivo.
public class MainActivity extends FragmentActivity 
  implements 
    LoaderManager.LoaderCallbacks<Cursor>, 
    OnClickListener, 
    OnItemClickListener {

  SimpleCursorAdapter mAdapter;
 
  EditText mEdtMessage;
 
  boolean isEditing;
  long currentMessageId;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  
    mEdtMessage = (EditText)
      findViewById(R.id.edtMessage);
    findViewById(R.id.btnAdd).setOnClickListener(this);
  
    mAdapter = new MessageCursorAdapter(this, null);
    getSupportLoaderManager().initLoader(0, null, this);
  
    ListView listView = (ListView)
      findViewById(R.id.listMessages);
    listView.setOnItemClickListener(this);
    listView.setAdapter(mAdapter);
  }

  @Override
  public Loader<Cursor> onCreateLoader(
    int id, Bundle args) {

    return new CursorLoader(
      this,
      MessageProvider.CONTENT_URI,
      DBHelper.ALL_COLUMNS,
      null, 
      null, 
      DBHelper.COLUMN_ID);
  }

  @Override
  public void onLoadFinished(
    Loader<Cursor> loader, Cursor cursor) {

    mAdapter.swapCursor(cursor);
  }

  @Override
  public void onLoaderReset(Loader<Cursor> loader) {
    mAdapter.swapCursor(null);
  }

  @Override
  public void onClick(View v) {
    String message = mEdtMessage.getText().toString();
  
    if (!isEditing && TextUtils.isEmpty(message)){
      Toast.makeText(this, "Preencha a mensagem", 
        Toast.LENGTH_SHORT).show();
      return;
    }
  
    mEdtMessage.getText().clear();
  
    ContentValues values = new ContentValues();
    values.put(DBHelper.COLUMN_MESSAGE, message);
  
    if (isEditing){
      String whereClause = DBHelper.COLUMN_ID +" = ?";
      String[] whereArgs = new String[]{ 
        String.valueOf(currentMessageId) };

      if (TextUtils.isEmpty(message)){
        getContentResolver().delete(
          MessageProvider.CONTENT_URI, 
          whereClause,
          whereArgs);

      } else {
          getContentResolver().update(
            MessageProvider.CONTENT_URI, 
            values, 
            whereClause,
            whereArgs);
      }

    } else {
      getContentResolver().insert(
        MessageProvider.CONTENT_URI, 
        values);
    }
    isEditing = false;
  }

  @Override
  public void onItemClick(AdapterView<?> adaptView, 
    View view, int position, long id) {

    Cursor cursor = mAdapter.getCursor();
    cursor.moveToPosition(position);
  
    long messageId = cursor.getLong(
      cursor.getColumnIndex(DBHelper.COLUMN_ID));

    String messageText = cursor.getString(
      cursor.getColumnIndex(DBHelper.COLUMN_MESSAGE));
  
    currentMessageId = messageId;
    mEdtMessage.setText(messageText);
    isEditing = true;
  }
}
Pra que ninguém reclame, e peça o código fonte, segue abaixo o arquivo de layout :)
<RelativeLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity" >

  <Button
    android:id="@+id/btnAdd"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentRight="true"
    android:layout_alignParentTop="true"
    android:text="Save" />

  <EditText
    android:id="@+id/edtMessage"
    android:hint="Digite a mensagem aqui"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/btnAdd"
    android:layout_alignBottom="@+id/btnAdd"
    android:layout_alignParentLeft="true"
    android:layout_toLeftOf="@+id/btnAdd"/>

  <ListView
    android:id="@+id/listMessages"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignLeft="@+id/edtMessage"
    android:layout_below="@+id/btnAdd" />

</RelativeLayout>

Bem povo, esse post ficou gigante. Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber