A service is an application in Android that runs in the background without needing to interact with the user. For example, while using an application, you might want to play some background music at the same time. In this case, the code that is playing the background music has no need to interact with the user; therefore, it can be run as a service. Also, services are ideal for situations in which there is no need to present a user interface (UI) to the user. A good example of this scenario is an application that continually logs the geographical coordinates of the device. In this case, you can write a service to do that in the background. In this chapter, you find out how to create your own services and use them to perform background tasks asynchronously.
The best way to understand how a service works is by creating one. The following Try It Out shows you the steps to create a simple service. Subsequent sections add more functionality to this service. For now, you see how to start and stop a service.
Because the service you created in the previous section does not do anything useful, in this section you modify it so that it performs a task. In the following Try It Out, you simulate the service of downloading a file from the Internet.
That means for a long-running service, it is important that you put all long-running code into a separate thread so that it does not tie up the application that calls it. The following Try It Out shows you how.
In addition to performing long-running tasks in a service, you might also perform some repeated tasks in a service. For example, you could write an alarm clock service that runs persistently in the background. In this case, your service might need to periodically execute some code to check whether a prescheduled time has been reached so that an alarm can be sounded. To execute a block of code to be executed at a regular time interval, you can use the Timer class within your service. The following Try It Out shows you how.
Earlier in this chapter, you learned how to start a service using the startService() method and stop a service using the stopService() method. You have also seen how you should execute long-running tasks on a separate thread—not the same thread as the calling activities. It is important to note that once your service has finished executing a task, it should be stopped as soon as possible so that it does not unnecessarily hold up valuable resources. That's why you use the stopSelf() method to stop the service when a task has been completed. Unfortunately, a lot of developers often forget to terminate a service when it is done performing its task. To easily create a service that runs a task asynchronously and terminates itself when it is done, you can use the IntentService class.
The IntentService class is a base class for Service that handles asynchronous requests on demand. It is started just like a normal service; and it executes its task within a worker thread and terminates itself when the task is completed. The following Try It Out demonstrates how to use the IntentService class.
Often a service simply executes in its own thread, independently of the activity that calls it. This doesn't pose a problem if you simply want the service to perform some tasks periodically and the activity does not need to be notified about the service's status. For example, you might have a service that periodically logs the geographical location of the device to a database. In this case, there is no need for your service to interact with any activities, because its main purpose is to save the coordinates into a database. However, suppose you want to monitor for a particular location. When the service logs an address that is near the location you are monitoring, it might need to communicate that information to the activity. If so, you need to devise a way for the service to interact with the activity.
The following Try It Out demonstrates how a service can communicate with an activity using a BroadcastReceiver.
So far, you have seen how services are created, how they are called, and how they are terminated when they are done with their task. All the services that you have seen are simple—either they start with a counter and increment at regular intervals or they download a fixed set of files from the Internet. However, real-world services are usually much more sophisticated, requiring the passing of data so that they can do the job correctly for you.
Using the service demonstrated earlier that downloads a set of files, suppose you now want to let the calling activity determine what files to download, instead of hardcoding them in the service. Here is what you need to do.
Intent object, specifying the service name: public void startService(View view) {
Intent intent = new Intent(getBaseContext(), MyService.class);
}
URL objects and assign it to the Intent object through its putExtra() method.Intent object: public void startService(View view) {
Intent intent = new Intent(getBaseContext(), MyService.class);
try {
URL[] urls = new URL[] {
new URL("http://www.amazon.com/somefiles.pdf"),
new URL("http://www.wrox.com/somefiles.pdf"),
new URL("http://www.google.com/somefiles.pdf"),
new URL("http://www.learn2develop.net/somefiles.pdf")};
intent.putExtra("URLs", urls);
} catch (MalformedURLException e) {
e.printStackTrace();
}
startService(intent);
}
URL array is assigned to the Intent object as an Object array.Intent object in the onStartCommand() method: @Override
public int onStartCommand(Intent intent, int flags, int startId) {
// We want this service to continue running until it is explicitly
// stopped, so return sticky.
Toast.makeText(this, "Service Started", Toast.LENGTH_LONG).show();
Object[] objUrls = (Object[]) intent.getExtras().get("URLs");
URL[] urls = new URL[objUrls.length];
for (int i=0; i<objUrls.length-1; i++) {
urls[i] = (URL) objUrls[i];
}
new DoBackgroundTask().execute(urls);
return START_STICKY;
}
getExtras() method to return a Bundle object.get() method to extract the URL array as an Object array.execute() method.This is one way in which your activity can pass values to the service. As you can see, if you have relatively complex data to pass to the service, you must do some additional work to ensure that the data is passed correctly. A better way to pass data is to bind the activity directly to the service so that the activity can call any public members and methods on the service directly. The following Try It Out shows you how to bind an activity to a service.
MyService.java file (note that you are modifying the existing onStartCommand()): import android.app.Service;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Timer;
import java.util.TimerTask;
public class MyService extends Service {
int counter = 0;
URL[] urls;
static final int UPDATE_INTERVAL = 1000;
private Timer timer = new Timer();
private final IBinder binder = new MyBinder();
public class MyBinder extends Binder {
MyService getService() {
return MyService.this;
}
}
@Override
public IBinder onBind(Intent arg0) {
return binder;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// We want this service to continue running until it is explicitly
// stopped, so return sticky.
Toast.makeText(this, "Service Started", Toast.LENGTH_LONG).show();
new DoBackgroundTask().execute(urls);
return START_STICKY;
}
private void doSomethingRepeatedly() {…}
private int DownloadFile(URL url) {…}
private class DoBackgroundTask extends AsyncTask<URL, Integer, Long> {…}
protected void onProgressUpdate(Integer… progress) {…}
@Override
public void onDestroy() {…}
}
MainActivity.java file, add the following bolded statements (note the change to the existing startService() method):import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import java.net.MalformedURLException;
import java.net.URL;
public class MainActivity extends AppCompatActivity {
IntentFilter intentFilter;
MyService serviceBinder;
Intent i;
private ServiceConnection connection = new ServiceConnection() {
public void onServiceConnected(
ComponentName className, IBinder service) {
//—-called when the connection is made—-
serviceBinder = ((MyService.MyBinder)service).getService();
try {
URL[] urls = new URL[] {
new URL("http://www.amazon.com/somefiles.pdf"),
new URL("http://www.wrox.com/somefiles.pdf"),
new URL("http://www.google.com/somefiles.pdf"),
new URL("http://www.learn2develop.net/somefiles.pdf")};
//---assign the URLs to the service through the
// serviceBinder object---
serviceBinder.urls = urls;
} catch (MalformedURLException e) {
e.printStackTrace();
}
startService(i);
}
public void onServiceDisconnected(ComponentName className) {
//---called when the service disconnects---
serviceBinder = null;
}
};
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {…}
@Override
public void onResume() {…}
@Override
public void onPause() {…}
public void startService(View view) {
i = new Intent(MainActivity.this, MyService.class);
bindService(i, connection, Context.BIND_AUTO_CREATE);
}
public void stopService(View view) {…}
private BroadcastReceiver intentReceiver = new BroadcastReceiver() {…};
}
To bind activities to a service, you must first declare an inner class in your service that extends the Binder class:
public class MyBinder extends Binder {
MyService getService() {
return MyService.this;
}
}
Within this class you implement the getService() method, which returns an instance of the service.
You then create an instance of the MyBinder class:
private final IBinder binder = new MyBinder();
You also modify the onBind() method to return the MyBinder instance:
@Override
public IBinder onBind(Intent arg0) {
return binder;
}
In the onStartCommand() method, you then call the execute() method using the urls array, which you declare as a public member in your service:
public class MyService extends Service {
int counter = 0;
URL[] urls;
…
…
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// We want this service to continue running until it is explicitly
// stopped, so return sticky.
Toast.makeText(this, "Service Started", Toast.LENGTH_LONG).show();
new DoBackgroundTask().execute(urls);
return START_STICKY;
}
}
Next, this URL array must be set directly from your activity.
In the MainActivity.java file, you first declare an instance of your service and an Intent object:
MyService serviceBinder;
Intent i;
The serviceBinder object will be used as a reference to the service, which you access directly.
You then create an instance of the ServiceConnection class so that you can monitor the state of the service:
private ServiceConnection connection = new ServiceConnection() {
public void onServiceConnected(
ComponentName className, IBinder service) {
//---called when the connection is made---
serviceBinder = ((MyService.MyBinder)service).getService();
try {
URL[] urls = new URL[] {
new URL("http://www.amazon.com/somefiles.pdf"),
new URL("http://www.wrox.com/somefiles.pdf"),
new URL("http://www.google.com/somefiles.pdf"),
new URL("http://www.learn2develop.net/somefiles.pdf")};
//---assign the URLs to the service through the
// serviceBinder object---
serviceBinder.urls = urls;
} catch (MalformedURLException e) {
e.printStackTrace();
}
startService(i);
}
public void onServiceDisconnected(ComponentName className) {
//---called when the service disconnects---
serviceBinder = null;
}
};
You need to implement two methods: onServiceConnected() and onServiceDisconnected().
onServiceConnected() method is called when the activity is connected to the service.onServiceDisconnected()method is called when the service is disconnected from the activity.When the activity is connected to the service, you obtain an instance of the service from the onServiceConnected() method by using the getService() method of the service argument. You then assign it to the serviceBinder object. The serviceBinder object is a reference to the service, and all the members and methods in the service can be accessed through this object. Here, you create a URL array and then directly assign it to the public member in the service:
URL[] urls = new URL[] {
new URL("http://www.amazon.com/somefiles.pdf"),
new URL("http://www.wrox.com/somefiles.pdf"),
new URL("http://www.google.com/somefiles.pdf"),
new URL("http://www.learn2develop.net/somefiles.pdf")};
//---assign the URLs to the service through the
// serviceBinder object---
serviceBinder.urls = urls;
You then start the service using an Intent object:
startService(i);
Before you can start the service, you must bind the activity to the service. This is done in the startService() method of the Start Service button:
public void startService(View view) {
i = new Intent(MainActivity.this, MyService.class);
bindService(i, connection, Context.BIND_AUTO_CREATE);
}
The bindService() method enables your activity to be connected to the service. It takes three arguments:
Intent objectServiceConnection objectSo far, you have seen how services are created and why it is important to ensure that your long-running tasks are properly handled, especially when updating the UI thread. Earlier in this chapter (as well as in Chapter 9), you also saw how to use the AsyncTask class for executing long-running code in the background. This section briefly summarizes the various ways to handle long-running tasks correctly using a variety of methods available.
For this discussion, assume that you have an Android project named Threading. The main.xml file contains a Button and TextView:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hello"/>
<Button
android:id="@+id/btnStartCounter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start"
android:onClick="startCounter"/>
<TextView
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"/>
</LinearLayout>
Suppose you want to display a counter on the activity, from 0 to 1,000. In your ThreadingActivity class, you have the following code:
package net.learn2develop.Threading;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
public class ThreadingActivity extends Activity {
TextView txtView1;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
txtView1 = (TextView) findViewById(R.id.textView1);
}
public void startCounter(View view) {
for (int i=0; i<=1000; i++) {
txtView1.setText(String.valueOf(i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.d("Threading", e.getLocalizedMessage());
}
}
}
}
When you run the application and click the Start button, the application is briefly frozen.
The UI freezes because the application is continuously trying to display the value of the counter at the same time it is pausing for one second after it has been displayed. This ties up the UI, which is waiting for the display of the numbers to be completed. The result is a nonresponsive application that will frustrate your users.
To solve this problem, one option is to wrap the part of the code that contains the loop using a Thread and Runnable class, like this:
public void startCounter(View view) {
new Thread(new Runnable() {
public void run() {
for (int i=0; i<=1000; i++) {
txtView1.setText(String.valueOf(i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.d("Threading", e.getLocalizedMessage());
}
}
}
}).start();
}
In the preceding code, you first create a class that implements the Runnable interface. Within this class, you put your long-running code within the run() method. The Runnable block is then started using the Thread class.
A Runnable is a block of code that can be executed by a thread.
However, the preceding application will not work, and it will crash if you try to run it. This code that is placed inside the Runnable block is on a separate thread, and in the preceding example you are trying to update the UI from another thread, which is not a safe thing to do because Android UIs are not thread-safe. To resolve this, you need to use the post() method of a View to create another Runnable block to be added to the message queue. In short, the new Runnable block created will be executed in the UI thread, so it would now be safe to execute your application:
public void startCounter(View view) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i<=1000; i++) {
final int valueOfi = i;
//---update UI---
txtView1.post(new Runnable() {
public void run() {
//---UI thread for updating---
txtView1.setText(String.valueOf(valueOfi));
}
});
//---insert a delay
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.d("Threading", e.getLocalizedMessage());
}
}
}
}).start();
}
This application will now work correctly, but it is complicated and makes your code difficult to maintain.
A second option to update the UI from another thread is to use the Handler class. A Handler enables you to send and process messages, similar to using the post() method of a View. The following code snippet shows a Handler class called UIupdater that updates the UI using the message that it receives:
For the following code to work, you must import the android.os.Handler package as well as add the static modifier to txtView1.
//---used for updating the UI on the main activity---
static Handler UIupdater = new Handler() {
@Override
public void handleMessage(Message msg) {
byte[] buffer = (byte[]) msg.obj;
//---convert the entire byte array to string---
String strReceived = new String(buffer);
//---display the text received on the TextView---
txtView1.setText(strReceived);
Log.d("Threading", "running");
}
};
public void startCounter(View view) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i<=1000; i++) {
//---update the main activity UI---
ThreadingActivity.UIupdater.obtainMessage(
0, String.valueOf(i).getBytes() ).sendToTarget();
//---insert a delay
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.d("Threading", e.getLocalizedMessage());
}
}
}
}).start();
}
}
A detailed discussion of the Handler class is beyond the scope of this book. For more details, check out the documentation at https://developer.android.com/reference/android/os/Handler.html.
So far, the two methods just described enable you to update the UI from a separate thread. In Android, you could use the simpler AsyncTask class to do this. Using the AsyncTask, you could rewrite the preceding code as follows:
private class DoCountingTask extends AsyncTask<Void, Integer, Void> {
protected Void doInBackground(Void… params) {
for (int i = 0; i < 1000; i++) {
//---report its progress---
publishProgress(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.d("Threading", e.getLocalizedMessage());
}
}
return null;
}
protected void onProgressUpdate(Integer… progress) {
txtView1.setText(progress[0].toString());
Log.d("Threading", "updating…");
}
}
public void startCounter(View view) {
new DoCountingTask().execute();
}
The preceding code will update the UI safely from another thread. What about stopping the task? If you run the preceding application and then click the Start button, the counter will start to display from zero. However, if you press the back button on the emulator/device, the task will continue to run even though the activity has been destroyed. You can verify this through the LogCat window. If you want to stop the task, use the following code snippets:
public class ThreadingActivity extends Activity {
static TextView txtView1;
DoCountingTask task;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
txtView1 = (TextView) findViewById(R.id.textView1);
}
public void startCounter(View view) {
task = (DoCountingTask) new DoCountingTask().execute();
}
public void stopCounter(View view) {
task.cancel(true);
}
private class DoCountingTask extends AsyncTask<Void, Integer, Void> {
protected Void doInBackground(Void… params) {
for (int i = 0; i < 1000; i++) {
//---report its progress---
publishProgress(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.d("Threading", e.getLocalizedMessage());
}
if (isCancelled()) break;
}
return null;
}
protected void onProgressUpdate(Integer… progress) {
txtView1.setText(progress[0].toString());
Log.d("Threading", "updating…");
}
}
@Override
protected void onPause() {
super.onPause();
stopCounter(txtView1);
}
}
To stop the AsyncTask subclass, you need to get an instance of it first. To stop the task, call its cancel() method. Within the task, you call the isCancelled() method to check whether the task should be terminated.
In this chapter, you learned how to create a service in your Android project to execute long-running tasks. You have seen the many approaches you can use to ensure that the background task is executed in an asynchronous fashion, without tying up the main calling activity. You have also learned how an activity can pass data into a service, and how you can alternatively bind to an activity so that it can access a service more directly.
IntentService class?AsyncTask class.You can find answers to the exercises in the appendix.
| TOPIC | KEY CONCEPTS |
| Creating a service | Create a class and extend the Service class. |
| Implementing the methods in a service | Implement the following methods: onBind(), onStartCommand(), and onDestroy(). |
| Starting a service | Use the startService() method. |
| Stopping a service | Use the stopService() method. |
| Performing long-running tasks | Use the AsyncTask class and implement three methods: doInBackground(), onProgressUpdate(), and onPostExecute(). |
| Performing repeated tasks | Use the Timer class and call its scheduleAtFixedRate() method. |
| Executing tasks on a separate thread and auto-stopping a service | Use the IntentService class. |
| Enabling communication between an activity and a service | Use the Intent object to pass data into the service. For a service, broadcast an Intent to notify an activity. |
| Binding an activity to a service | Use the Binder class in your service and implement the ServiceConnection class in your calling activity. |
Updating the UI from a Runnable block |
Use the post() method of a view to update the UI. Alternatively, you can also use a Handler class. The recommended way is to use the AsyncTask class. |