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.
- 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>
- 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();
}
}
}
- 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";
}
- 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);
}
}
}
}
- 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
}
}
}
- 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.