AsyncTask en Service en Android

Buenas tardes, en esta entrada vamos a realizar tareas Asíncronas en nuestro Service. Seguiremos con el anterior proyecto que usamos en la primera entrada sobre Services, que podemos encontrar aquí.

Aún así explicaré todo desde el principio para no tener que estar cambiando entre páginas.


Vamos a comenzar:

  • Creación del Proyecto

    Para comenzar vamos a ir a File -> New -> Android Application Project. Si por algún caso no vemos esta opción disponible, nos vamos a dirigir a “Other”, donde al pulsar nos aparecerá una ventana para buscar en una lista. En el apartado de Android podremos encontrar “Android Application Project”. Sin embargo, podemos introducir la palabra “Android” en la caja de texto para filtrar los resultados y que nos sea mas fácil encontrarlo.

    Una vez encontremos la opción y pulsemos sobre ella, nos aparecerá el asistente de creación de un proyecto Android, en el cual vamos a darle un nombre, en mi caso lo he llamado “EjemploService3”, pero podeis poner el nombre que deseéis. El resto de las opciones podéis dejarlas tal como están.

    Según vamos avanzando por el asistente, nos encontraremos con una ventana en la que podremos configurar el nombre de nuestra Activity, en mi caso la he llamado “ServiceActivity”, aunque podéis poner también el que queráis. Su archivo layout, se llamará “activity_service.xml”.

    Una vez acabemos el asistente, podremos comenzar con la construcción de nuestra aplicación.

  • ServiceActivity

    Una vez creado el proyecto, tendremos nuestra clase “ServiceActivity.java” bajo la carpeta src/package_name/, que será la Activity que habremos creado en el asistente del proyecto, y al igual que se nos ha creado la Activity, se nos habrá creado su layout, el cual podemos encontrar en la carpeta res/layout/, y se llama “activity_service.xml”, el cual vamos a modificar para que quede como el código que muestro a continuación:

    		<LinearLayout 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"
    		    android:orientation="vertical"
    		    tools:context=".ServiceActivity" >
    		
    		    <Button
    		        android:id="@+id/btnComenzar"
    		        android:layout_width="fill_parent"
    		        android:layout_height="wrap_content"
    		        android:onClick="onClickComenzar"
    		        android:text="Comenzar Servicio" />
    		
    		    <Button
    		        android:id="@+id/btnDetener"
    		        android:layout_width="fill_parent"
    		        android:layout_height="wrap_content"
    		        android:onClick="onClickDetener"
    		        android:text="Detener Servicio" />
    		
    		</LinearLayout>
    		

    Como vemos, lo único que tenemos son 2 elementos Button, los cuales serán los encargados de dar comienzo al Service o de detenerlo.

    Lo siguiente que vamos a ver es el código de nuestra Activity, llamada ServiceActivity.java:

    		package sekth.droid.Services;
    
    		import android.app.Activity;
    		import android.content.Intent;
    		import android.os.Bundle;
    		import android.view.View;
    		
    		public class ServiceActivity extends Activity {
    		
    			@Override
    			protected void onCreate(Bundle savedInstanceState) {
    				super.onCreate(savedInstanceState);
    				setContentView(R.layout.activity_service);
    			}
    		
    			public void onClickComenzar(View v) {
    				startService(new Intent(getBaseContext(), MyService.class));
    			}
    		
    			public void onClickDetener(View v) {
    				stopService(new Intent(getBaseContext(), MyService.class));
    			}
    		
    		}
    		

    Vamos a inspeccionar sus métodos un poco, aunque no sean gran cosa:

    • protected void onCreate(Bundle savedInstanceState)

      Vemos su código a continuación:

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

      Este método es el primero que es ejecutado cuando nuestra Activity se crea, y pertenece a su ciclo de vida. En este caso no hace nada excepto establecer su layout.

    • public void onClickComenzar(View v)

      Vemos su código:

      				public void onClickComenzar(View v) {
      					startService(new Intent(getBaseContext(), MyService.class));
      				}
      				

      Como podemos apreciar, hay una llamada al método startService, el cuál tiene como argumentos un nuevo objeto de la clase Intent el cual es construido con el Context de la Activity, y el nombre de la clase del Service, el cuál aún no hemos creado.

    • public void onClickDetener(View v)

      Vemos su código a continuación:

      				public void onClickDetener(View v) {
      					stopService(new Intent(getBaseContext(), MyService.class));
      				}
      				

      Este método hace algo parecido a lo anterior, solo que cambia el método que usa, en este caso es stopService, cuyos argumentos son los mismos que en el anterior punto. Este método se encargará de parar un Service que se esté ejecutando de manera explícita.

  • MyService

    Una vez hemos analizado nuestra Activity, podemos ver que aún no tenemos todo realizado, sino que falta la parte mas importante de la aplicación, la creación del Service.

    Para ello vamos a crear una nueva clase Java en nuestro package, la cual voy a llamar “MyService” y heredará de la clase Service de Android.

    Cuando la tengamos creada, vamos a modificar su interior para que quede como a continuación:

    		package sekth.droid.Services;
    
    		import java.net.MalformedURLException;
    		import java.net.URL;
    		
    		import android.app.Service;
    		import android.content.Intent;
    		import android.os.AsyncTask;
    		import android.os.IBinder;
    		import android.util.Log;
    		import android.widget.Toast;
    		
    		public class MyService extends Service {
    		
    			@Override
    			public IBinder onBind(Intent intent) {
    				// TODO Auto-generated method stub
    				return null;
    			}
    		
    			@Override
    			public int onStartCommand(Intent intent, int flags, int startId) {
    				// TODO Auto-generated method stub
    				try {
    					new DoBackgroundTask().execute(new URL(
    							"http://www.google.com/imagen1"), new URL(
    							"http://www.google.com/imagen2"), new URL(
    							"http://www.google.com/imagen3"), new URL(
    							"http://www.google.com/imagen4"));
    				} catch (MalformedURLException ex) {
    					ex.printStackTrace();
    				}
    		
    				return START_STICKY;
    			}
    		
    			private int DownloadFile(URL url) {
    				try {
    					// Simulamos la descarga de un fichero
    					Thread.sleep(5000);
    				} catch (InterruptedException ex) {
    					ex.printStackTrace();
    				}
    				// Retornamos un n�mero que puede ser el tama�o del archivo
    				return 100;
    			}
    		
    			private class DoBackgroundTask extends AsyncTask<URL, Integer, Long> {
    		
    				@Override
    				protected Long doInBackground(URL... urls) {
    					// TODO Auto-generated method stub
    					int count = urls.length;
    					long totalKBytesDownloaded = 0;
    					for (int i = 0; i < count; i++) {
    						totalKBytesDownloaded += DownloadFile(urls[i]);
    						// Calculamos el porcentaje bajado y actualizamos el progreso
    						publishProgress((int) (((i + 1) / (float) count) * 100));
    					}
    					return totalKBytesDownloaded;
    				}
    		
    				@Override
    				protected void onProgressUpdate(Integer... values) {
    					// TODO Auto-generated method stub
    					super.onProgressUpdate(values);
    					Log.d("Descargando Ficheros", String.valueOf(values[0])
    							+ "% descargado");
    					Toast.makeText(getBaseContext(), values[0] + "% descargado",
    							Toast.LENGTH_SHORT).show();
    				}
    		
    				@Override
    				protected void onPostExecute(Long result) {
    					// TODO Auto-generated method stub
    					super.onPostExecute(result);
    					Toast.makeText(getBaseContext(),
    							"Descargados " + result + " Kbytes", Toast.LENGTH_SHORT)
    							.show();
    					stopSelf();
    				}
    		
    			}
    		
    			@Override
    			public void onDestroy() {
    				// TODO Auto-generated method stub
    				super.onDestroy();
    				Toast.makeText(getBaseContext(), "Servicio Detenido",
    						Toast.LENGTH_SHORT).show();
    			}
    		
    		}
    		

    Vamos a analizar cada método por separado:

    • public IBinder onBind(Intent intent)

      Vemos su código a continuación:

      				@Override
      				public IBinder onBind(Intent intent) {
      					// TODO Auto-generated method stub
      					return null;
      				}
      				

      Este método será el encargado de “enlazar” un Service a una Activity, pero en este caso no vamos a implementarlo.

    • public int onStartCommand(Intent intent, int flags, int startId)

      Vamos a ver su código:

      				@Override
      				public int onStartCommand(Intent intent, int flags, int startId) {
      					// TODO Auto-generated method stub
      					try {
      						new DoBackgroundTask().execute(new URL(
      								"http://www.google.com/imagen1"), new URL(
      								"http://www.google.com/imagen2"), new URL(
      								"http://www.google.com/imagen3"), new URL(
      								"http://www.google.com/imagen4"));
      					} catch (MalformedURLException ex) {
      						ex.printStackTrace();
      					}
      			
      					return START_STICKY;
      				}
      				

      Este método se ejecuta cuando comenzamos el Service. En él lo que haremos será instanciar una clase que vamos a explicar a continuación:

      para tareas asíncronas hemos introducido una clase interna que hereda de la clase AsyncTask, lo cual nos ofrece un método de realizar tareas en segundo plano sin la necesidad de tener que manejar los hilos manualmente. Aunque ya explicamos en una entrada anterior (Cargar ListView con AsyncTask) como funciona una AsyncTask:

      • AsyncTask

        En la AsyncTask hemos definido 3 tipos, URL, Integer y Long. Estos 3 tipos de dato especifican el tipo de dato usado para los 3 métodos que implementaremos en la clase AsyncTask:

        • doInBackground
          								@Override
          								protected Long doInBackground(URL... urls) {
          									// TODO Auto-generated method stub
          									int count = urls.length;
          									long totalKBytesDownloaded = 0;
          									for (int i = 0; i < count; i++) {
          										totalKBytesDownloaded += DownloadFile(urls[i]);
          										// Calculamos el porcentaje bajado y actualizamos el progreso
          										publishProgress((int) (((i + 1) / (float) count) * 100));
          									}
          									return totalKBytesDownloaded;
          								}
          								

          Este método acepta un array del primer tipo genérico especificado anteriormente. En este caso, el tipo es URL. Este método es ejecutado en un hilo secundario y es ahí donde ponemos nuestro código que se va a ejecutar. Para mostrar el proceso de la tarea, llamamos al método publishProgress(), el cual invoca el siguiente método, onProgressUpdate(), el cual implementamos de la clase AsyncTask. Por último, el tipo de retorno de este método es el tercer tipo genérico que especificamos con anterioridad, el cual es un Long en este caso.

          En nuestro caso, vamos a llamar al método que simula la descarga de un fichero, para cada una de las url que hemos recibido.

        • onProgressUpdate
          								@Override
          								protected void onProgressUpdate(Integer... values) {
          									// TODO Auto-generated method stub
          									super.onProgressUpdate(values);
          									Log.d("Descargando Ficheros", String.valueOf(values[0])
          											+ "% descargado");
          									Toast.makeText(getBaseContext(), values[0] + "% descargado",
          											Toast.LENGTH_SHORT).show();
          								}
          								

          Este método es invocado en el Thread principal y es llamado cuando usamos el método publishProgress(). Acepta un array del segundo tipo genérico especificado anteriormente. En este caso, el tipo es Integer. Usaremos este método para mostrar el proceso de la tarea en el hilo secundario al usuario.

        • onPostExecute
          								@Override
          								protected void onPostExecute(Long result) {
          									// TODO Auto-generated method stub
          									super.onPostExecute(result);
          									Toast.makeText(getBaseContext(),
          											"Descargados " + result + " Kbytes", Toast.LENGTH_SHORT)
          											.show();
          									stopSelf();
          								}
          								

          Este método es invocado en el hilo principal y es llamado cuando el método doInBackground() ha finalizado su ejecución. Este método acepta un argumento del tercer tipo genérico especificado anteriormente, en este caso es un Long.

          En nuestro ejemplo, cuando la tarea se ha llevado a cabo completamente, hemos usado el método stopSelf(), el cuál es un método perteneciente al Service y lo detiene automáticamente una vez se haya completado, evitando que se nos pueda olvidar cerrarlo manualmente.

      Cabe destacar que nuestra AsyncTask está también encerrada en un bloque try/catch, debido a que usamos la clase URL, la cual puede lanzar una excepción del tipo MalformedURLException.

  • AndroidManifest.xml

    No podemos terminar nuestra aplicación sin declarar que nuestra aplicación usa un Service, por lo que para ello lo modificaremos agregando una línea:

    		<?xml version="1.0" encoding="utf-8"?>
    		<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    		    package="sekth.droid.Services"
    		    android:versionCode="1"
    		    android:versionName="1.0" >
    		
    		    <uses-sdk
    		        android:minSdkVersion="8"
    		        android:targetSdkVersion="17" />
    		
    		    <application
    		        android:allowBackup="true"
    		        android:icon="@drawable/ic_launcher"
    		        android:label="@string/app_name"
    		        android:theme="@style/AppTheme" >
    		        <activity
    		            android:name="sekth.droid.Services.ServiceActivity"
    		            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=".MyService" />
    		    </application>
    		
    		</manifest>
    		

    Hemos agregado la línea:

    		<service android:name=".MyService" />
    		

    Ahora sí podemos dar por terminado nuestro ejemplo y podemos ejecutar la aplicación.

Ahora veremos como nuestro service “descarga” archivos en segundo plano, y reporta el porcentaje de los archivos descargados. Lo mas importante, la actividad seguirá respondiendo al usuario mientras los archivos son descargados en segundo plano, en un hilo separado.

A continuación podemos ver unas capturas de pantalla sobre nuestra aplicación en funcionamiento:

Principal Descargado 25 Descarga 50 Descargado 75 Descargado 100 Descargado total

Un vídeo para ver nuestra aplicación siendo ejecutada en tiempo real:



Con esta entrada hemos podido ver como se puede también crear una clase que herede de AsyncTask en nuestro Service, y por ello ejecutarlo sin que bloquee el hilo principal, evitando que la aplicación no responda mientras el Service esté en ejecución.

El código de ejemplo podéis descargarlo de aquí.

Sin mas, cualquier aporte o corrección es bienvenido.

Saludos!!!