Implementando Atualizações de Versão em Aplicativos Android via Servidor

Hoje, vamos explorar como implementar atualizações de versão diretamente dentro de um aplicativo Android utilizando um servidor.

A abordagem tradicional de atualização através das lojas de aplicativos apresenta uma limitação: o aplicativo não consegue obter informações de versão disponíveis nas lojas. Por essa razão, ao utilizar lojas para atualizações, é necessário configurar um servidor que forneça ao aplicativo as informações da versão mais recente.

Consequentemente, se o aplicativo pode obter informações de versão do servidor, também pode baixar diretamente a versão mais recente do aplicativo para atualização. Atualmente, a maioria dos aplciativos populares, como aplicativos de bancos e serviços de pagamento, adota essa metodologia. Vamos detalhar como implementar essa funcionalidade.

Fundamentos de Controle de Versão Os atributos de controle de versão incluem versionCode e versionName.

versionCode O versionCode é um atributo crucial. Trata-se de um valor do tipo Integer. Ao definir este valor, evite números excessivamente grandes, mantendo-se dentro do intervalo de valores do Integer (o que geralmente não é um problema). A prática comum é iniciar com 1 na primeira versão publicada e incrementar a cada atualização.

versionName O versionName é um atributo do tipo String, geralmente apresantado em conjunto com o versionCode. Enquanto o versionCode serve para fins de desenvolvimento e manutenção, o versionName é uma descrição visível ao usuário, seguindo frequentemente o formato major.minor.point (ex: 1.2.3).

Resumo do Controle de Versão O versionCode é utilizado para determinar se uma atualização é necessária. Comparando-se o versionCode do servidor com o do aplicativo local, se o valor do servidor for maior, uma atualização é recomendada.

O versionName, por outro lado, indica a magnitude das mudanças na versão. Por exemplo:

  • De 2.0.1 para 2.0.2: pequenas correções
  • De 2.0.1 para 2.1.0: novas funcionalidades adicionadas
  • De 2.0.1 para 3.0.0: mudanças significativas, como novas interfaces ou funcionalidades

Em resumo: o versionCode determina a necessidade de atualização, enquanto o versionName informa ao usuário sobre o escopo das mudanças.

Localização dos Atributos de Versão Em projetos Eclipse, os atributos são definidos no arquivo Manifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="3"
    android:versionName="1.2.1"
    package="com.exemplo.atualizacaoDemo">
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
 
    ...
 
    </application>
 
</manifest>

No Android Studio, embora os atributos possam ser visualizados no Manifest.xml, eles devem ser configurados no arquivo build.gradle:

Obtendo Informações de Versão Para obter a versão do aplicativo em execução, utilize o seguinte código:

/*
 * Obtém o nome da versão atual do aplicativo
 */
private String obterNomeVersao() throws Exception{
    // Obtém instância do PackageManager
    PackageManager gerenciadorPacotes = getPackageManager();
    // getPackageName() retorna o pacote da classe atual, 0 indica obtenção de informações de versão
    PackageInfo infoPacote = gerenciadorPacotes.getPackageInfo(getPackageName(), 0);
    Log.e("TAG","Código da versão: "+infoPacote.versionCode);
    Log.e("TAG","Nome da versão: "+infoPacote.versionName);
    return infoPacote.versionName;
}

/*
 * Obtém o código da versão atual do aplicativo
 */
private int obterCodigoVersao() throws Exception{
    // Obtém instância do PackageManager
    PackageManager gerenciadorPacotes = getPackageManager();
    // getPackageName() retorna o pacote da classe atual, 0 indica obtenção de informações de versão
    PackageInfo infoPacote = gerenciadorPacotes.getPackageInfo(getPackageName(), 0);
    Log.e("TAG","Código da versão: "+infoPacote.versionCode);
    Log.e("TAG","Nome da versão: "+infoPacote.versionName);
    return infoPacote.versionCode;
}

O processo de atualização básico compara os códigos de versão do servidor e do aplicativo local. Se o código do servidor for maior, uma requisição HTTP é feita para baixar o APK. Após o download, o aplicativo é instalado por substituição.

Vamos implementar um exemplo completo de atualização via servidor:

Este exemplo exibirá um diálogo de confirmação quando houver atualizações disponíveis. Ao confirmar, uma notificação com barra de progresso será exibida durante o download. Ao cancelar, o processo de atualização é interrompido.

  1. Crie o arquivo de layout notification_item.xml para exibir o progresso na barra de notificações:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="3dp" >
 
    <ImageView
        android:id="@+id/notificationIcone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@android:drawable/stat_sys_download" />
 
    <TextView
        android:id="@+id/notificationTitulo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_toRightOf="@id/notificationIcone"
        android:paddingLeft="6dp"
        android:textColor="#FF000000" />
 
    <TextView
        android:id="@+id/notificationPercentual"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/notificationIcone"
        android:paddingTop="2dp"
        android:textColor="#FF000000" />
 
    <ProgressBar
        android:id="@+id/notificationProgresso"
        style="@style/ProgressBarHorizontal_cor"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@id/notificationTitulo"
        android:layout_alignParentRight="true"
        android:layout_alignTop="@id/notificationPercentual"
        android:layout_below="@id/notificationTitulo"
        android:paddingLeft="6dp"
        android:paddingRight="3dp"
        android:paddingTop="2dp" />
 
</RelativeLayout>

  1. Crie a classe AppContext, estendendo Application:
package com.exemplo.aplicativo;
 
import android.app.Application;
import android.content.Context;
 
import com.exemplo.atualizacao.configuracoes.Configuracoes;
 
public class AppContext extends Application {
    private static AppContext instanciaApp;
    private Context contexto;
 
    public static AppContext obterInstancia() {
        return instanciaApp;
    }
 
    @Override
    public void onCreate() {
        super.onCreate();
        instanciaApp = this;
        contexto = this.getBaseContext();
        inicializarGlobal();
    }
 
    public void inicializarGlobal() {
        try {
            Configuracoes.versaoLocal = getPackageManager().getPackageInfo(
                    getPackageName(), 0).versionCode;
            Configuracoes.versaoServidor = 2;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

  1. Crie a classe de configuração Configuracoes.java:
package com.exemplo.atualizacao.configuracoes;
 
public class Configuracoes {
    // Informações de versão
    public static int versaoLocal = 0;
    public static int versaoServidor = 0;
    /* Caminho de instalação do download */  
    public static final String caminhoSalvo = "/sdcard/teste/";  
  
    public static final String nomeArquivoSalvo = caminhoSalvo + "teste.apk";  
}

  1. Implemente o serviço de atualizacao AtualizacaoServico.java:
package com.exemplo.atualizacao;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
 
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.widget.RemoteViews;
 
import com.exemplo.atualizacao.configuracoes.Configuracoes;
 
public class AtualizacaoServico extends Service {
    // Título da notificação
    private int idTitulo = 0;
 
    // Armazenamento do arquivo
    private File diretorioAtualizacao = null;
    private arquivoAtualizacao = null;
    // Estado do download
    private final static int DOWNLOAD_CONCLUIDO = 0;
    private final static int DOWNLOAD_FALHOU = 1;
    // Componentes da notificação
    private NotificationManager gerenciadorNotificacao = null;
    private Notification notificacao = null;
    // Intent para redirecionamento da notificação
    private Intent intentNotificacao = null;
    private PendingIntent pendingIntentNotificacao = null;
    
    int contadorDownload = 0;
    int tamanhoAtual = 0;
    long tamanhoTotal = 0;
    int tamanhoTotalAtualizacao = 0;
 
    @SuppressWarnings("deprecation")
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // Obtém parâmetros
        idTitulo = intent.getIntExtra("idTitulo", 0);
        
        // Cria diretório e arquivo
        if (android.os.Environment.MEDIA_MOUNTED.equals(android.os.Environment
                .getExternalStorageState())) {
            diretorioAtualizacao = new File(Environment.getExternalStorageDirectory(),
                    Configuracoes.nomeArquivoSalvo);
            arquivoAtualizacao = new File(diretorioAtualizacao.getPath(), getResources()
                    .getString(idTitulo) + ".apk");
        }
 
        this.gerenciadorNotificacao = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        this.notificacao = new Notification();
 
        // Configura redirecionamento ao clicar na notificação
        intentNotificacao = new Intent(this, AtualizacaoActivity.class);
        pendingIntentNotificacao = PendingIntent.getActivity(this, 0, intentNotificacao,
                0);
        
        // Configura conteúdo da notificação
        notificacao.icon = R.drawable.ic_launcher;
        notificacao.tickerText = "Iniciando download";
        notificacao.setLatestEventInfo(this, "Atualização", "0%",
                pendingIntentNotificacao);
        
        // Exibe notificação
        gerenciadorNotificacao.notify(0, notificacao);
 
        // Inicia nova thread para download
        new Thread(new RunnableDownload()).start();
 
        return super.onStartCommand(intent, flags, startId);
    }
 
    @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }
 
    private Handler handlerAtualizacao = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case DOWNLOAD_CONCLUIDO:
                // Intent para instalação
                Uri uri = Uri.fromFile(arquivoAtualizacao);
                Intent intentInstalacao = new Intent(Intent.ACTION_VIEW);
                intentInstalacao.setDataAndType(uri,
                        "application/vnd.android.package-archive");
 
                pendingIntentNotificacao = PendingIntent.getActivity(
                        AtualizacaoServico.this, 0, intentInstalacao, 0);
 
                notificacao.defaults = Notification.DEFAULT_SOUND;
                notificacao.setLatestEventInfo(AtualizacaoServico.this,
                        "Atualização", "Download concluído, clique para instalar.",
                        pendingIntentNotificacao);
                gerenciadorNotificacao.notify(0, notificacao);
 
                // Para o serviço
                stopService(intentNotificacao);
            case DOWNLOAD_FALHOU:
                // Download falhou
                notificacao.setLatestEventInfo(AtualizacaoServico.this,
                        "Atualização", "Falha no download.",
                        pendingIntentNotificacao);
                gerenciadorNotificacao.notify(0, notificacao);
            default:
                stopService(intentNotificacao);
            }
        }
    };
 
    public long baixarArquivoAtualizacao(String urlDownload, File arquivoSalvo)
            throws Exception {
 
        HttpURLConnection conexaoHttp = null;
        InputStream inputStream = null;
        FileOutputStream outputStream = null;
 
        try {
            URL url = new URL(urlDownload);
            conexaoHttp = (HttpURLConnection) url.openConnection();
            conexaoHttp
                    .setRequestProperty("User-Agent", "PacificHttpClient");
            if (tamanhoAtual > 0) {
                conexaoHttp.setRequestProperty("RANGE", "bytes="
                        + tamanhoAtual + "-");
            }
            conexaoHttp.setConnectTimeout(10000);
            conexaoHttp.setReadTimeout(20000);
            tamanhoTotalAtualizacao = conexaoHttp.getContentLength();
            if (conexaoHttp.getResponseCode() == 404) {
                throw new Exception("Falha no download!");
            }
            inputStream = conexaoHttp.getInputStream();
            outputStream = new FileOutputStream(arquivoSalvo, false);
            byte buffer[] = new byte[4096];
            int tamanhoLido = 0;
            while ((tamanhoLido = inputStream.read(buffer)) > 0) {
                outputStream.write(buffer, 0, tamanhoLido);
                tamanhoTotal += tamanhoLido;
                
                // Atualiza notificação a cada 10% para evitar sobrecarga
                if ((contadorDownload == 0)
                        || (int) (tamanhoTotal * 100 / tamanhoTotalAtualizacao) - 10 > contadorDownload) {
                    contadorDownload += 10;
 
                    notificacao.setLatestEventInfo(AtualizacaoServico.this,
                            "Baixando", (int) tamanhoTotal * 100 / tamanhoTotalAtualizacao
                                    + "%", pendingIntentNotificacao);
 
                    // Define layout customizado para a notificação
                    notificacao.contentView = new RemoteViews(
                            getPackageName(), R.layout.notification_item);
                    notificacao.contentView.setTextViewText(
                            R.id.notificationTitulo, "Baixando");
                    notificacao.contentView.setProgressBar(
                            R.id.notificationProgresso, 100, contadorDownload, false);
                    
                    gerenciadorNotificacao.notify(0, notificacao);
                }
            }
        } finally {
            if (conexaoHttp != null) {
                conexaoHttp.disconnect();
            }
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
        return tamanhoTotal;
    }
 
    class RunnableDownload implements Runnable {
        Message mensagem = handlerAtualizacao.obtainMessage();
 
        public void run() {
            mensagem.what = DOWNLOAD_CONCLUIDO;
            
            try {
                if (!diretorioAtualizacao.exists()) {
                    diretorioAtualizacao.mkdirs();
                }
                if (!arquivoAtualizacao.exists()) {
                    arquivoAtualizacao.createNewFile();
                }
                
                long tamanhoDownload = baixarArquivoAtualizacao(
                        "http://softfile.3g.qq.com:8080/msoft/179/1105/10753/MobileQQ1.0(Android)_Build0198.apk",
                        arquivoAtualizacao);
                
                if (tamanhoDownload > 0) {
                    handlerAtualizacao.sendMessage(mensagem);
                } 
            } catch (Exception ex) {
                ex.printStackTrace();
                mensagem.what = DOWNLOAD_FALHOU;
                handlerAtualizacao.sendMessage(mensagem);
            }
        }
    }
}

  1. Crie a atividade AtualizacaoActivity:
package com.exemplo.atualizacao;
 
import com.exemplo.atualizacao.configuracoes.Configuracoes;
 
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
 
public class AtualizacaoActivity extends Activity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        verificarVersao();
    }
 
    /**
     * Verifica se há atualizações disponíveis
     */
    public void verificarVersao() {
        if (Configuracoes.versaoLocal < Configuracoes.versaoServidor) {
            Log.i("AtualizacaoApp", "Nova versão disponível");
            
            // Exibe diálogo de atualização
            AlertDialog.Builder alerta = new AlertDialog.Builder(this);
            alerta.setTitle("Atualização Disponível")
                    .setMessage("Uma nova versão está disponível. Recomendamos atualizar.")
                    .setPositiveButton("Atualizar",
                            new DialogInterface.OnClickListener() {
                                public void onClick(DialogInterface dialog,
                                        int which) {
                                    // Inicia serviço de atualização
                                    Intent intentAtualizacao = new Intent(
                                            AtualizacaoActivity.this,
                                            AtualizacaoServico.class);
                                    intentAtualizacao.putExtra("idTitulo",
                                            R.string.app_name);
                                    startService(intentAtualizacao);
                                }
                            })
                    .setNegativeButton("Cancelar",
                            new DialogInterface.OnClickListener() {
                                public void onClick(DialogInterface dialog,
                                        int which) {
                                    dialog.dismiss();
                                }
                            });
            alerta.create().show();
        } else {
            // Nenhuma atualização necessária
        }
    }
}

  1. Adicione as permissões necessárias e registre o serviço no manifesto:
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />

Registro do serviço:

<service android:name="com.exemplo.atualizacao.AtualizacaoServico" >
        </service>

Arquivo AndroidManifest.xml completo:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.exemplo.atualizacao"
    android:versionCode="1"
    android:versionName="1.0" >
 
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="8" />
 
    <application
        android:name="com.exemplo.aplicativo.AppContext"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name="com.exemplo.atualizacao.AtualizacaoActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name="com.exemplo.atualizacao.AtualizacaoServico" >
        </service>
    </application>
 
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
 
</manifest>

Neste exemplo, ao iniciar o aplicativo, um diálogo de atualização é exibido se houver uma nova versão disponível. Ao confirmar, uma requisição HTTP é iniciada para download do APK, simultaneamente uma notificação com barra de progresso é exibida. O progresso é atualizado a cada 10% de conclusão. Após o download, a notificação permite clicar para instalar a nova versão.

Tags: android Atualizações de Aplicativos controle de versão Download de APK Notificações Android

Publicado em 6-25 02:14