Tarea Larga dentro de un Service en Android

Buenas tardes, en esta entrada vamos a ver como podemos usar un Service para tareas largas en nuestra aplicación.

Para ello, vamos a basarnos en nuestro anterior proyecto, el cual podemos encontrar aquí. Igualmente, crearemos de nuevo todo para no tener que ir cambiando de páginas.


Vamos a ponernos manos a la obra:

  • Creación del Proyecto

    Para comenzar, vamos a ir a File -> New -> Android Application Project. SI por un casual no encontramos esta opción, pinchamos en “Other”, donde una nueva ventana nos aparecerá con todos los tipos de archivos que se pueden crear, buscaremos “Android Application Project”, y si queremos podemos introducir “Android” en la caja de texto que se nos muestra para filtrar los resultados y nos sea mas fácil.

    Una vez nos aparezca la primera ventana del asistente, vamos a dar nombre a nuestro proyecto, en mi caso lo he llamado EjemploService2, pero realmente podéis dar el nombre que queráis. El resto de opciones podéis dejarlas como estan.

    A medida que avancemos por el asistente encontraremos una ventana que nos preguntará como queremos llamar a nuestra primera Activity, en este caso le pondré de nombre ServiceActivity, y su archivo layout entonces se llamará activity_service.xml.

    Cuando terminemos el asistente, podremos comenzar a construir nuestra aplicación.

  • ServiceActivity

    Una vez creado nuestro proyecto tendremos nuestra activity principal llamada “ServiceActivity.java”, en la carpeta src/package_name/ de nuestro proyecto. Por otro lado, su layout estará en res/layout, y se llamará activity_service.xml, el cual modificaremos para que su código quede como 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 podemos ver, solo tenemos 2 botones, uno dará comienzo al Service, y otro lo detendrá.

    A continuación vamos a ver el código java de nuestra clase “ServiceActivity.java” encontrado en src/package_name/:

    		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 ver cada método por separado:

    • protected void onCreate(Bundle savedInstanceState)

      Vemos su código:

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

      Como vemos, lo dejamos tal como está, ya que no le vamos a dar funcionalidad alguna.

    • public void onClickComenzar(View v)

      Es el método que se ejecutamos cuando pulsamos el primer botón:

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

      Su función es invocar al método startService, al cuál pasa como argumentos un nuevo objeto de la clase Intent con el Context de nuestra aplicación, y el nombre de la clase del Service, el cual aún no hemos creado.

    • public void onClickDetener(View v)

      Es el método que se ejecutará cuando pulsemos el botón de detener el servicio:

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

      Su función es detener el servicio que está en ejecución. Para ello se invoca el método stopService, el cual como argumento tiene el Context de nuestra aplicación y el nombre de la clase del Service, el cuál aún no hemos creado.

  • MyService

    Para crear nuestro Service, vamos a crear una nueva clase en nuestro paquete, y le pondremos de nombre “MyService”, o el que queráis. Esta clase heredará de la superclase Service de Android. Una vez creada la clase, vamos a editar su código para que quede como el siguiente:

    		package sekth.droid.services;
    
    		import java.net.MalformedURLException;
    		import java.net.URL;
    		
    		import android.app.Service;
    		import android.content.Intent;
    		import android.os.IBinder;
    		import android.widget.Toast;
    		
    		public class MyService extends Service {
    		
    			@Override
    			public IBinder onBind(Intent arg0) {
    				// TODO Auto-generated method stub
    				return null;
    			}
    		
    			@Override
    			public int onStartCommand(Intent intent, int flags, int startId) {
    				// TODO Auto-generated method stub
    		
    				try {
    					int result = DownloadFile(new URL(
    							"http://www.google.com/imagen.jpg"));
    					Toast.makeText(getBaseContext(), "Descargados " + result + " kb",
    							Toast.LENGTH_SHORT).show();
    				} catch (MalformedURLException ex) {
    					ex.printStackTrace();
    				}
    				return START_STICKY;
    			}
    		
    			@Override
    			public void onDestroy() {
    				// TODO Auto-generated method stub
    				super.onDestroy();
    				Toast.makeText(getBaseContext(), "Servicio Detenido",
    						Toast.LENGTH_SHORT).show();
    			}
    		
    			private int DownloadFile(URL url) {
    				try {
    					// Simulamos un tiempo de descarga
    					Thread.sleep(5000);
    				} catch (InterruptedException ex) {
    					ex.printStackTrace();
    				}
    				return 100;
    			}
    		
    		}
    		

    Vamos a ir analizando cada una de las cosas que hemos hecho:

    • public IBinder onBind(Intent arg0)

      Este método viene por defecto al heredar de Service:

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

      Es el encargado de enlazar, por así decirlo, un Service a una Activity. En nuestro ejemplo no lo usaremos, así que lo dejaremos tal como está.

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

      Este método se ejecutará al iniciar el Service. Vemos su código:

      				@Override
      				public int onStartCommand(Intent intent, int flags, int startId) {
      					// TODO Auto-generated method stub
      			
      					try {
      						int result = DownloadFile(new URL(
      								"http://www.google.com/imagen.jpg"));
      						Toast.makeText(getBaseContext(), "Descargados " + result + " kb",
      								Toast.LENGTH_SHORT).show();
      					} catch (MalformedURLException ex) {
      						ex.printStackTrace();
      					}
      					return START_STICKY;
      				}
      				

      En este método se cuece toda la funcionalidad del Service. Vamos a simular una descarga de un archivo de internet, aunque no descargaremos nada.

      Como vamos a usar un objeto de la clase URL, tenemos que encerrar nuestro código en un bloque try/catch, ya que esta operación puede lanzar una excepción del tipo MalformedURLException.

      Lo primero que hacemos es declarar un tipo de dato int, el cual almacenará el “tamaño” de un archivo, el cual va a dar nosotros ya que es una simulación. Este entero tendrá el valor que devuelva el método DownloadFile, el cual acepta como argumento un objeto de la clase URL:

      				int result = DownloadFile(new URL("http://www.google.com/imagen.jpg"));
      				

      A continuación, cuando la operación se lleve a cabo vamos a mostrar un mensaje mediante la clase Toast en el cuál mostraremos al usuario lo que hemos descargado:

      				Toast.makeText(getBaseContext(), "Descargados " + result + " kb",
      					Toast.LENGTH_SHORT).show();
      				

      Por último, al final del método retornamos el valor de la constante START_STICKY, ya que el Service será detenido explícitamente por nosotros.

    • public void onDestroy()

      Este método se ejecutará cuando detengamos el Service. Su código:

      				@Override
      				public void onDestroy() {
      					// TODO Auto-generated method stub
      					super.onDestroy();
      					Toast.makeText(getBaseContext(), "Servicio Detenido",
      							Toast.LENGTH_SHORT).show();
      				}
      				

      Lo único que haremos es mostrar un mensaje Toast para informar de que el servicio ha sido detenido.

    • private int DownloadFile(URL url)

      Este método lo vamos a usar para simular una descarga desde una URL. Su código:

      				private int DownloadFile(URL url) {
      					try {
      						// Simulamos un tiempo de descarga
      						Thread.sleep(5000);
      					} catch (InterruptedException ex) {
      						ex.printStackTrace();
      					}
      					return 100;
      				}
      				

      Vamos a usar el método Thread.sleep() para simular una tarea larga, y tardará 5000 milisegundos en llevarla a cabo. Ya que usamos la clase Thread, tenemos que encerrar nuestro código en un bloque try/catch ya que el uso de esta clase puede lanzar una excepción del tipo InterruptedException.

      Por último, retornamos un valor creado que podría ser el tamaño del archivo descargado.

  • AndroidManifest.xml

    Debido a que hemos hecho un Service, estos deben ser declarados en nuestro AndroidManifest para que puedan ser ejecutados:

    		<?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" />
    		

    El cual declara que tenemos un Service, y que su clase se llama “MyService”.

Nuestra aplicación es ya funcional, y podemos ejecutarla. Sin embargo vamos a darnos cuenta de una cosa, la cual es de suma importancia.

Lo importante que tenemos que ver en este ejemplo, además de que no hace nada útil, es que al ejecutar el Service, la aplicación se congela durante 5 segundos, que es el tiempo que se toma nuestro método mediante el Thread.Sleep(). Mientras, nuestra actividad no responde a nada de lo que hagamos, mostrándonos un punto interesante:

El Service se ejecuta en el mismo hilo que nuestra Activity. En este caso, si el Service se suspende durante 5 segundos, nuestra Activity también lo hará.

A continuación vemos unas capturas de pantalla sobre la aplicación:

Principal Service Descargado Service Detenido

Un vídeo de la aplicación siendo ejecutada. Tomemos especial nota que cuando el servicio se ejecuta, no responde la aplicación:



Con esta entrada hemos visto como realizar una tarea larga en nuestro Service, y que además nuestra aplicación se queda “congelada”, pues ambos se ejecutan en el mismo hilo. Para solucionar esto, en la siguiente entrada sobre Service veremos como podemos evitar esto.

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

Sin más, cualquier aporte o corrección es bienvenido.

Saludos!!!