«

»

mar 04 2012

Aplicación auto-actualizable sin Market

A veces se da el caso de que no queramos distribuir nuestra aplicación en el Market, bien porque no tengamos cuenta de desarrollador, porque estemos distribuyendo una beta privada, porque sea para un entorno cerrado de gente, etc. En estos casos tenemos un problema importante, la falta de actualizaciones automáticas. Como todos sabemos es una función muy útil e indispensable de cualquier tienda de aplicaciones que se precie pero, ¿qué hacemos sin ella? Hoy vamos a ver cómo solucionarlo por nuestra cuenta.

 

 

Vamos a hacerlo realmente sencillo y al alcance de todo el mundo. Lo único que necesitaremos será Dropbox (podéis usar otro parecido si queréis, el único requisito es tener archivos públicos que se puedan modificar manteniendo el enlace).  Os cuento en qué se basa la idea: metemos en la carpeta pública de Dropbox un archivo de texto que contendrá la información de la última versión disponible, y el APK de ésta. Desde nuestra aplicación comprobamos el contenido del archivo y si la última versión es más nueva que la instalada descargamos el APK. Cuando queramos sacar una actualización publicamos el APK y actualizamos el archivo de texto con los datos de la nueva versión y el enlace al APK. El resultado es algo como lo siguiente.



¡Dame el código!

Veamos cómo hacerlo. Obviamente voy a contar el mecanismo, no cómo implementarlo. El resultado final debe ser cosa vuestra, mediante comprobaciones manuales, automáticas o lo que veáis mejor y (menos intrusivo). La aplicación de ejemplo se compondrá de una actividad y una clase auxiliar, que es la importante. Ésta última es la siguiente:

 

package com.sloy.androcode.autoupdate;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.util.Log;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class VersionChecker {

	/**
	 * El enlace al archivo público de información de la versión. Puede ser de
	 * Dropbox, un hosting propio o cualquier otro servicio similar.
	 */
	public static final String INFO_FILE = "http://dl.dropbox.com/u/1587994/Androcode/autoupdate_info.txt";

	/**
	 * El código de versión establecido en el AndroidManifest.xml de la versión
	 * instalada de la aplicación. Es el valor numérico que usa Android para
	 * diferenciar las versiones.
	 */
	private int currentVersionCode;
	/**
	 * El nombre de versión establecido en el AndroidManifest.xml de la versión
	 * instalada. Es la cadena de texto que se usa para identificar al versión
	 * de cara al usuario.
	 */
	private String currentVersionName;

	/**
	 * El código de versión establecido en el AndroidManifest.xml de la última
	 * versión disponible de la aplicación.
	 */
	private int latestVersionCode;
	/**
	 * El nombre de versión establecido en el AndroidManifest.xml de la última
	 * versión disponible.
	 */
	private String latestVersionName;

	/**
	 * Enlace de descarga directa de la última versión disponible.
	 */
	private String downloadURL;

	/**
	 * Método para inicializar el objeto. Se debe llamar antes que a cualquie
	 * otro, y en un hilo propio (o un AsyncTask) para no bloquear al interfaz
	 * ya que hace uso de Internet.
	 *
	 * @param context
	 *            El contexto de la aplicación, para obtener la información de
	 *            la versión actual.
	 */
	public void getData(Context context) {
		try{
			// Datos locales
			PackageInfo pckginfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
			currentVersionCode = pckginfo.versionCode;
			currentVersionName = pckginfo.versionName;

			// Datos remotos
			String data = downloadHttp(new URL(INFO_FILE));
			JSONObject json = new JSONObject(data);
			latestVersionCode = json.getInt("versionCode");
			latestVersionName = json.getString("versionName");
			downloadURL = json.getString("downloadURL");
			Log.d("AutoUpdate", "Datos obtenidos con éxito");
		}catch(JSONException e){
			Log.e("AutoUpdate", "Ha habido un error con el JSON", e);
		}catch(NameNotFoundException e){
			Log.e("AutoUpdate", "Ha habido un error con el packete :S", e);
		}catch(IOException e){
			Log.e("AutoUpdate", "Ha habido un error con la descarga", e);
		}
	}

	/**
	 * Método para comparar la versión actual con la última .
	 *
	 * @return true si hay una versión más nueva disponible que la actual.
	 */
	public boolean isNewVersionAvailable() {
		return getLatestVersionCode() > getCurrentVersionCode();
	}

	/**
	 * Devuelve el código de versión actual.
	 *
	 * @return
	 */
	public int getCurrentVersionCode() {
		return currentVersionCode;
	}

	/**
	 * Devuelve el nombre de versión actual.
	 *
	 * @return
	 */
	public String getCurrentVersionName() {
		return currentVersionName;
	}

	/**
	 * Devuelve el código de la última versión disponible.
	 *
	 * @return
	 */
	public int getLatestVersionCode() {
		return latestVersionCode;
	}

	/**
	 * Devuelve el nombre de la última versión disponible.
	 *
	 * @return
	 */
	public String getLatestVersionName() {
		return latestVersionName;
	}

	/**
	 * Devuelve el enlace de descarga de la última versión disponible
	 *
	 * @return
	 */
	public String getDownloadURL() {
		return downloadURL;
	}

	/**
	 * Método auxiliar usado por getData() para leer el archivo de información.
	 * Encargado de conectarse a la red, descargar el archivo y convertirlo a
	 * String.
	 *
	 * @param url
	 *            La URL del archivo que se quiere descargar.
	 * @return Cadena de texto con el contenido del archivo
	 * @throws IOException
	 *             Si hay algún problema en la conexión
	 */
	private static String downloadHttp(URL url) throws IOException {
		HttpURLConnection c = (HttpURLConnection)url.openConnection();
		c.setRequestMethod("GET");
		c.setReadTimeout(15 * 1000);
		c.setUseCaches(false);
		c.connect();
		BufferedReader reader = new BufferedReader(new InputStreamReader(c.getInputStream()));
		StringBuilder stringBuilder = new StringBuilder();
		String line = null;
		while((line = reader.readLine()) != null){
			stringBuilder.append(line + "\n");
		}
		return stringBuilder.toString();
	}
}

 

Important!

La clase no es lo mejor que podría ser. Se puede hacer de muchas formas, pero para lo que quiero explicar vale. Si se os ocurren mejoras y las queréis compartir los comentarios están abiertos a ello.

 

La otra parte importante es el archivo que contiene la información de la última versión, el que colocaremos en la carpeta pública de dropbox junto al apk. Lo he llamado autoupdate_info.txt, está escrito en formato JSON por su facilidad de uso aprovechando que Android incluye las APIs necesarias para procesarlo. Podemos hacerlo tan completo como queramos, pero con esto nos vale:

 

{
    "versionCode":2,
    "versionName":"0.2",
    "downloadURL":"http://dl.dropbox.com/u/1587994/Androcode/AutoUpdate.apk"
}

 

Una vez tenemos esta clase lista procedemos con la implementación. Voy a mostrar un ejemplo funcional, será simplemente una actividad que leerá la información de la última versión, la comparará con la instalada y si es más nueva activará un botón para descargarla.

Desde Android +3.0 existe una restricción que impide hacer operaciones de red desde el hilo principal para evitar bloquear la interfaz, provocando una NetworkOnMainThreadException. Aunque puede ser deshabilitado en casos concretos, vamos a hacer las cosas bien y vamos a usar un hilo de trabajo para eso. Podríamos perfectamente usar un AsynTask, pero me he decidido por el hilo porque lo tengo más fresco. Que cada uno elija su sabor ;)

 
La actividad sería tal que así:
 

package com.sloy.androcode.autoupdate;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class AutoUpdateActivity extends Activity {

	// Handler para la actualización de la interfaz desde el hilo en segundo
	// plano
	private Handler handler = new Handler();

	// Runnable que se ejecutará desde el hilo principal cuando acabe la
	// obtención de datos en segundo plano. Equivalente al onPostExecute() de AsyncTask
	private Runnable finishBackgroundDownload = new Runnable() {
		@Override
		public void run() {
			mProgressBar.setVisibility(View.GONE);
			if(mVC.isNewVersionAvailable()){
				// Hay una nueva versión disponible
				mActualizacion.setVisibility(View.VISIBLE);
				mActualizada.setVisibility(View.GONE);
				// Mostramos los datos
				mVersionActual.setText("Versión actual: " + mVC.getCurrentVersionName());
				mVersionNueva.setText("Versión disponible: " + mVC.getLatestVersionName());
			}else{
				// La aplicación está actualizada
				mActualizacion.setVisibility(View.GONE);
				mActualizada.setVisibility(View.VISIBLE);
			}
		}
	};

	// Runnable encargado de descargar los datos en un hilo en segundo plano.
	// Equivalente al doInBackground() de AsyncTask
	private Runnable backgroundDownload = new Runnable() {
		@Override
		public void run() {
			mVC.getData(AutoUpdateActivity.this);
			// Cuando acabe la descarga actualiza la interfaz
			handler.post(finishBackgroundDownload);
		}
	};
	// Objeto de nuestra clase de utilidad
	private VersionChecker mVC = new VersionChecker();

	// Elementos de la interfaz
	private View mActualizada, mActualizacion, mProgressBar;
	private TextView mVersionActual, mVersionNueva;
	private Button mObtenerDatos, mDescargar;

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

		// Inicializa la interfaz
		setContentView(R.layout.main);
		mActualizada = findViewById(R.id.actualizada);
		mActualizacion = findViewById(R.id.actualizacion);
		mProgressBar = findViewById(R.id.progress_datos);
		mDescargar = (Button)findViewById(R.id.bt_descarga);
		mObtenerDatos = (Button)findViewById(R.id.bt_obtener_datos);
		mVersionActual = (TextView)findViewById(R.id.actualizacion_version_actual);
		mVersionNueva = (TextView)findViewById(R.id.actualizacion_version_nueva);

		// Coloca listeners en los dos botones
		mObtenerDatos.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				// Crea un nuevo hilo para descargar los datos
				mProgressBar.setVisibility(View.VISIBLE);
				Thread downloadThread = new Thread(backgroundDownload, "VersionChecker");
				downloadThread.start();;
			}
		});
		mDescargar.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				// Lanza un Intent con el enlace de la descarga, Android se
				// encargará del resto
				startActivity(new Intent("android.intent.action.VIEW", Uri.parse(mVC.getDownloadURL())));
			}
		});

	}
}

 

No tiene mucha complicación. Cuando se pulsa el botón de obtener datos se lanza un hilo para actualizar el objeto VersionChecker. Cuando acaba, dependiendo de si la aplicación está o no actualizada muestra una interfaz u otra. De no estarlo, habilita un botón para descargar directamente el APK. El usuario no tendrá más que pulsar sobre la notificación una vez terminada la descarga y se instalará.

El resto del código podéis encontrarlo en este repositorio donde está el proyecto completo. Siento el estropicio con los directorios, pero no soy muy amigo de SVN ;)
 

Una prueba

Por si queréis probarlo os dejo para descargar el apk con la versión 0.1 para actualizarlo a la 0.2, así veis cómo funciona el sistema: AutoUpdate_v01.apk (el ejemplo aloja los archivos en Google Code para depender de mi cuenta de Dropbox, pero como dije al principio el funcionamiento es el mismo).

 
Espero que os sea útil este truco y podáis hacerlo sin problemas en vuestras aplicaciones. Si no me he explicado bien en algún punto podéis tirarme piedras o preguntarme en los comentarios. ¡Buen provecho!
 
 

Acerca del autor

Rafa Vázquez

Estudio Ingeniería Informática del Software en Sevilla. Me considero geek sin dinero, amante y desarrollador novato de Android. He creado algunas aplicaciones como SeviBus, TicTacDroide, Kill Bieber y Traductor Hoygan, si es que se puede llamar aplicaciones a estas dos últimas ;) Ganas de aprender más y más no me faltan, e intentaré compartir mis experiencias con vosotros en la medida de lo posible.

12 comentarios

Un ping

Dejar un comentario

  1. Isuriv

    Buenas solución para aquellas app de prueba o independientes del market. Una solución que debería haberse puesto a muchas apps del sistema con independencia del market. Como por ejemplo Navegador Web. Para que se actualizará los errores de seguridad y las mejoras y no esperar a versiones de Android para que aparezcan.

    Aunque me parezca una buena solución.. Yo utilizo un repositorio privado basado en el Apktor para la distribución y actualización de varias apps.. Solo comento el sistema que uso.. No que sea la mejor.

  2. Erick

    me gustaría desarrollar un apps donde habría información actualisable semanalmente previo a una validación con usuario y password, estarías en condiciones de desarrollar algo así, cuanto tiempo te demorarías y cual seria el precio

    1. Javier Jonathan

      Lo desarrollaria en 2 sem aprox

  3. Felipeska

    Mi pregunta es: Cuando se sabe que se ha publicado una actualización… esta es la gran virtud del market de Android… que genera un push para indicar que hay una actualización pendiente o se realiza automáticamente.

    1. Sloy

      Podrías usar el propio sistema Push de Google para notificar de actualizaciones, aunque si implementas este sistema de actualización para no depender del Play Store, Google Cloud Messaging no es la mejor opción ya que requiere tener instalada la aplicación del Play Store en el dispositivo.

      Otra opción es comprobarlo en la propia aplicación cuando el usuario la abra. Dependiendo de cuánto pretendas actualizarla y cómo de importante es que el usuario esté al día, puedes hacer que se compruebe un máximo de 1 vez al día, a la semana, al mes… O incluso manualmente por el usuario. Sólo habría que ejecutar el código del ejemplo que comprueba la última versión disponible. Descargar n JSON tan pequeño no supone un gran gasto de datos ni batería.

  4. Javier J

    Cuando quiero actualizar me sale el prompt de instalar o es automatico?

    1. Sloy

      El procedimiento descrito aquí es muy simple. Sencillamente solicita al sistema abrir la URL del APK, y el navegador al detectar que es un archivo binario lo descargará. Al usuario le aparecerá la descarga en el menú de notificaciones, y pulsando sobre ella se abrirá la pantalla de instalación normal y corriente. La aplicación en este proceso no hace nada, se encarga de todo el propio sistema.

      Si quieres hacerlo más personalizado deberás tratar la descarga con otros métodos, pero el objetivo del tutorial es hacerlo lo más sencillo posible. De todas formas, si no recuerdo mal no puedes instalar la aplicación automáticamente sin solicitar autorización del usuario, ese privilegio sólo lo tienen las aplicaciones del sistema. En caso contrario sería un enorme agujero de seguridad, si cualquier aplicación pudiera instalar otras sin que el usuario se de cuenta.

  5. Javier J

    si sale el prompt , como haria para que no saltara el prompt y lo isntalra tal cual lo hace el market

  6. Israel

    Tengo una duda. Estoy intentando hacer algo similar pero con un servidor web propio.
    Existe algún certificado o algo especial que necesite la apk para que sea valida?

    Sucede que desde mi servidor web tengo la Apk, puedo acceder verificando la URL en el navegador del movil y descarga perfectamente.

    Pero cuando intento acceder desde un boton en el móvil que me descargue la apk y me levante la aplicacion me sale un mensaje diciendo: “Se ha producido un problema al analizar el paquete.”

    Si subo una aplicacion diferente a dropbox e intento realizar el mismo procedimiento funciona perfectamente. Lo que me hace dudar ¿sera algun certificado o firma especial que me falta al generar la apk o será que tengo que tener en el servidor alguna configuracion de algun parametro?

    PD:
    La apk la estoy generando desde eclipse como “Export Signed Application Package”.

  7. Alexander Valencia

    es muy interesante su articulo, deseo probarlo ya que necesito esto para un proyecto en la empresa, lo voy a tratar de implementar, si tengo dudas y/o inconvenientes con codigo donde lo podría localizar para solicitar su ayuda y asesoría.

  8. Delko

    Hola, he intentado mil veces que intente actualizar una app de prueba y siempre me sale que no hay ninguna versión disponible… a qué se puede deber?

  9. Diego

    Hola, muy bueno y me anduvo bien. Alguno sabe como hacer para que se instale la app directamente (osea que no me quede en descargas y luego instalarla manualmente), Muy bueno el post ! Saludos

Deja un comentario