Services

Intro

AsyncTasks are great, but is it always advisable to use an AsyncTask to push the application's work to another thread?  That depends on your app.  An AsyncTask is created by creating a new thread from a running Activity, which means that you must first launch that Activity and its associated user interface (UI).  But what happens when the Activity is destroyed?  The AsyncTask (being that it's on its own thread) will continue to run until it's done doing its work, at which time, presumably, there will be some code in the AsyncTask's onPostExecute() method that updates the Activity UI...which does not exist!  To avoid this, you'll need to add the proper code in the Activity's onDestroy method to gracefully cancel the AsyncTask.  

Read this article for a good explanation of how to properly cancel an AsyncTask:  http://www.shanekirk.com/2012/04/asynctask-missteps/

 

Say you have an app that polls a network service on a regular basis, or determines the device's geographic location continuously, or plays music in the background.  In each of these examples, you would definitely want to do that work in another thread (I'll let you think about why that is).  Additionally, none of those tasks requires a user interface.  Using a Service would be a logical choice in all of these scenarios.  For long-running tasks that need to be performed asynchronously, and/or tasks that do not require a user interface, an ideal design choice is to use a Service.  A Service is a component of an application that runs in the device's background, usually performing some long-running operation, without any user interaction.  Services can run even while the user is using another application.  It can be interacted with by other components of the application, or by other applications on the device.

Creating a Service

The best way to understand a Service is by seeing one in action.  To create a Service you must extend the Service base class:

package edu.usna.mobileos.servicesexample;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.widget.Toast;

public class PepinService 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) {

		Toast.makeText(this, "PepinService Started", 
                    Toast.LENGTH_LONG).show();
               
	        return START_STICKY;
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		Toast.makeText(this, "PepinService Destroyed",
                    Toast.LENGTH_LONG).show();
	}

}

When you extend the Service class, at a minimum your Service must implement the onBind method.  This method is used to bind an Activity to this service, giving the Activity access to the service's class members (i.e. all of the class's fields and methods)

The onStartCommand is ran whenever an Activity executes the startService method.  For example, from an Activity in the same application as the Service you would do this:

Intent intent = new Intent(getBaseContext(), PepinService.class);
startService(intent);

and from an Activity in another application:

Intent intent = new Intent("edu.usna.mobileos.PepinService");
startService(intent);

The startService method triggers the onStartCommand() method of the Service.  Once a Service is started, it will run indefinitely, even after the component that started it is destroyed.  Presumably, this is not the behavior you typically want.

There are two options to stop a service:

  1. call stopService(Intent) from another component 
  2. call stopSelf() from with the service
stopService(new Intent("edu.usna.cs.PepinService"));

When either stopService or stopSelf is called, the service's onDestroy method is triggered.

Just like your application's Activities, you must declare the Service in the applications' manifest file inside the application element:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="edu.usna.mobileos.servicesexample"
    android:versionCode="1"
    android:versionName="1.0" >

<application
   android:allowBackup="true"
   android:icon="@drawable/ic_launcher"
   android:label="@string/app_name"
   android:theme="@style/AppTheme" >
   <activity
     android:name="edu.usna.mobileos.servicesexample.MainActivity"
     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=".PepinService" />
 
 </application>
</manifest>

 In order to allow other applications on the device to call your service, you must also include an intent-filter:

<service android:name=".PepinService" >
  <intent-filter>
    <action android:name="edu.usna.mobileos.PepinService" />
  </intent-filter>
</service>

TASK 1: Create a new Android Studio Project and implement a Service referencing the sample code above.  In your Activity's layout file, include two buttons which, when clicked, trigger startService and stopService respectively.

Adding threads

When a Service runs, it does so on the application's main thread.  This means that if there is a task in the Service that takes a long time your app will be blocked from executing other tasks until the long-running task is complete.  To demonstrate this, in the sample app you just created, modify the Service's onStartCommand method by adding a simulated long-running task:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
	Toast.makeText(this, "PepinService Started", Toast.LENGTH_LONG).show();
	try {
                // this is our long-running task simulation
		Thread.sleep(10000);
	} catch (InterruptedException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
	return START_STICKY;
}

Run the app again and click the button to start the Service.  What happened?  First, you'll notice that you saw the Toast.  Secondly, your entire app froze until out "task" finished.  These two clues verify that the Service is running on the UI thread.  Had it been running on a background thread, the app would have crashed when we attempted to show the Toast.  So what's the solution?  That's right...we need a Thread!  Even in a Service, another thread should be used for background work.  To create the thread, you can use the exact same techniques you saw in the Background Tasks lessons, including AsyncTasks!

Task 2: Modify your app to execute the long-running task in another thread.

IntentService

The AsyncTask is a helper class that simplifies the running of background tasks by simplifying the use of threads and handlers.  In a Service, because the Service runs on the application's main thread, you still need to run background tasks in another thread to prevent blocking behavior.  Fortunately for us, the geniuses at Google have recognized this common problem and provided us another helper class: IntentService.

An IntentService is a subclass of Service.  When created, it does whatever work it needs to do in a worker thread (not the application's main thread).  Unlike a normal service, an IntentService stops itself once all its work is complete. To create an IntentService, you must extend the IntentService class:

package edu.usna.mobileos.servicesexample;

import android.app.IntentService;
import android.content.Intent;
import android.util.Log;

public class PepinIntentService extends IntentService {

    public PepinIntentService() {
	super("PepinIntentServiceName");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.i("PEPIN", "PepinIntentService started working");
	DoMuchWork();
        Log.i("PEPIN", "PepinIntentService done working");
    }

    private void DoMuchWork() {
        try {
	    // do some long-running task
	    Thread.sleep(5000);
	} catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

When the IntentService is started, the onHandleIntent method is triggered by the onStartCommand of the super class.  You start the IntentService the same way you started a normal Service:

startService(new Intent(getBaseContext(), PepinIntentService.class));

When onHandleIntent is finished, the IntentService stops itself.

Task 3: Referencing the above code, modify your app so that it uses an IntentService instead of a Service

Communication with Calling Activity

Many times a Service (including an IntentService) needs to send a notification to the Activity that called it, usually when some type of work is complete.  In order to make this happen that, we need two objects: a BroadcastReceiver and an IntentFilter.  The Service creates an Intent, sets an action and sends a broadcast:

@Override
protected void onHandleIntent(Intent intent) {
	Log.i("PEPIN", "PepinIntentService started working");
	DoMuchWork();
	Log.i("PEPIN", "PepinIntentService done working");
	
	Intent broadcastIntent = new Intent();
        broadcastIntent.setAction("WORK_COMPLETE_ACTION");
        getBaseContext().sendBroadcast(broadcastIntent);
}

In the Activity that needs to receive the broadcast you must define an IntentFilter.  The "action" for which the IntentFilter filters must match the action that will be broadcast by the Service.  

IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("WORK_COMPLETE_ACTION");

After you've created the filter, you must create a broadcast receiver and register it with the device.  Once that's done, your app will be notified whenever a broadcast is sent from any Service with an action that matches your filter. 

To create the receiver:

private BroadcastReceiver intentReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(getBaseContext(), "Work Complete!",
                Toast.LENGTH_LONG).show();
    }
};

To register the receiver:

registerReceiver(intentReceiver, intentFilter);

To unregister the receiver when you no longer need it (i.e. when your activity is not active):

unregisterReceiver(intentReceiver);

Putting It All Together

MainActivity.java

package edu.usna.cs.services;

import android.os.Bundle;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends Activity {
	
    IntentFilter intentFilter;
	
    private BroadcastReceiver intentReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(getBaseContext(), "Work Complete!",
                    Toast.LENGTH_LONG).show();
        }
    };
	
    @Override
    public void onResume() {
    	super.onResume();

        // create a new intent filter 
        intentFilter = new IntentFilter();
        intentFilter.addAction("WORK_COMPLETE_ACTION");

        // register the broadcast receiver to listen using 
        // the intent filter defined above
        registerReceiver(intentReceiver, intentFilter);
    }
    
    @Override
    public void onPause() {
    	super.onPause();
    	
    	// unregister the broadcast receiver
    	unregisterReceiver(intentReceiver);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);
    }
	
    public void startService(View view) {
        // start an IntentService
        startService(new Intent(getBaseContext(), PepinIntentService.class)); 	
    }

}

PepinIntentService.java

package edu.usna.cs.services;

import android.app.IntentService;
import android.content.Intent;
import android.util.Log;

public class PepinIntentService extends IntentService {

    public PepinIntentService() {
	super("PepinIntentServiceName");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
	Log.i("PEPIN", "PepinIntentService started working");
	DoMuchWork();
	Log.i("PEPIN", "PepinIntentService done working");
		
	Intent broadcastIntent = new Intent();
        broadcastIntent.setAction("WORK_COMPLETE_ACTION");
        getBaseContext().sendBroadcast(broadcastIntent);
    }

    private void DoMuchWork() {
        try {
	    // do some long-running task
	    Thread.sleep(5000);
	} catch (InterruptedException e) {
            // TODO Auto-generated catch block
	    e.printStackTrace();
	}
    }
}

Notice that the above code creates the intent filter and registers the receiver in the onResume method, and unregistered the receiver in onPause.  I'll leave that as an exercise for you to think about!   

More Reading

To learn more about using Services and IntentServices, read here: Services Developer Guide  

Think you're finished?  Then you should be able to answer these bonus questions: