Implementing an Android Started Service – A Worked Example
Previous | Table of Contents | Next |
An Overview of Android Started and Bound Services | Android Local Bound Services – A Worked Example |
<google>BUY_ANDROID</google>
The previous chapter covered a considerable amount of information relating to Android services and, at this point, the concept of services may seem somewhat overwhelming. In order to reinforce the information in the previous chapter, this chapter will work through a tutorial intended to introduce the concepts of started service implementation.
Within this chapter, a sample application will be created and used as the basis for implementing an Android service. In the first instance, the service will be created using the IntentService class. This example will subsequently be extended to demonstrate the use of the Service class. Finally, the steps involved in performing tasks within a separate thread when using the Service class will be implemented. Having covered started services in this chapter, the next chapter will focus on the implementation of bound services and client-service communication.
Creating the Example Project
Launch Eclipse and follow the usual steps to create a new Android project named ServiceExample containing a blank activity named ServiceExampleActivity and a layout file named activity_service_example.
Creating the Service Class
Before writing any code, the first step is to add a new class to the project to contain the service. The first type of service to be demonstrated in this tutorial is to be based on the IntentService class. As outlined in the previous chapter (An Overview of Android Started and Bound Services), the purpose of the IntentService class is to provide the developer with a convenient mechanism for creating services that perform tasks asynchronously within a separate thread from the calling application.
Add a new class to the project by right-clicking on the ServiceExample project name in the Package Explorer panel and selecting the New -> Class menu option. Within the resulting New Java Class dialog, click on the Browse… button located to the right of the Package field and select the project’s package name from the list. Next, name the new class MyIntentService and change the Superclass field to android.app.IntentService. Finally, click on the Finish button to create the new class.
Within the Package Explorer panel, locate and double click on the new MyIntentService.java file to load it into an editing panel where it should read as follows:
package com.ebookfrenzy.ServiceExample; package com.example.serviceexample; import android.app.IntentService; import android.content.Intent; public class MyIntentService extends IntentService { @Override protected void onHandleIntent(Intent arg0) { // TODO Auto-generated method stub } }
When subclassing the IntentService class, there are two rules that must be followed. Firstly, a constructor for the class must be implemented which calls the superclass constructor, passing through the class name of the service. Secondly, the class must override the onHandleIntent() method. Clearly, Eclipse has created a stub method for the latter, but has not provided a constructor. Modify the code, therefore, to add the constructor:
package com.example.serviceexample; import android.app.IntentService; import android.content.Intent; public class MyIntentService extends IntentService { @Override protected void onHandleIntent(Intent arg0) { // TODO Auto-generated method stub } public MyIntentService() { super("MyIntentService"); } }
All that remains at this point is to implement some code within the onHandleIntent() method so that the service actually does something when invoked. Ordinarily this would involve performing a task that takes some time to complete such as downloading a large file or playing audio. For the purposes of this example, however, the handler will simply output a message on the Eclipse LogCat panel:
package com.example.serviceexample; import android.app.IntentService; import android.content.Intent; import android.util.Log; public class MyIntentService extends IntentService { private static final String TAG = "com.example.ServiceExample"; @Override protected void onHandleIntent(Intent arg0) { // TODO Auto-generated method stub Log.i(TAG, "Intent Service started"); } public MyIntentService() { super("MyIntentService"); } }
Adding the Service to the Manifest File
Before a service can be invoked, it must first be added to the manifest file of the application to which it belongs. At a minimum, this involves adding a <service> element together with the class name of the service.
Double click on the AndroidManifest.xml file for the current project to load it into the Manifest Editor and select the file name tab located along the bottom edge of the editor panel to display the raw XML content of the file. Modify the XML to add the service element as shown in the following listing:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.serviceexample" 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="com.example.serviceexample.ServiceExampleActivity" 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=".MyIntentService" /> </application> </manifest>
Starting the Service
Now that the service has been implemented and declared in the manifest file, the next step is to add code to start the service when the application launches. As is typically the case, the ideal location for such code is the onCreate() callback method of the activity class (which, in this case, can be found in the ServiceExampleActivity.java file). Locate and load this file into the editor and modify the onCreate() method to add the code to start the service:
package com.example.serviceexample; import android.os.Bundle; import android.app.Activity; import android.content.Intent; import android.view.Menu; public class ServiceExampleActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_service_example); Intent intent = new Intent(this, MyIntentService.class); startService(intent); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_service_example, menu); return true; } }
All that the added code needs to do is to create a new Intent object primed with the class name of the service to start and then use it as an argument to the startService() method.
Testing the IntentService Example
The example IntentService based service is now complete and ready to be tested. Since the message displayed by the service will appear in the LogCat panel, it is important that this is enabled in the Eclipse environment. If the LogCat panel is not already visible, select the Window -> Show View -> Other… menu option and in the Show View dialog select LogCat and click on OK.
With the LogCat panel displayed, click on the green ‘+’ located to the right of the Save Filters title to display the filter settings dialog. Name the filter ServiceExample and in the by Log Tag field, enter the Tag value declared in MyIntentService.java (in the above code example this was com.example.ServiceExample). Select the newly created filter from the list and then run the application. Once the application is running, the “Service started” message should appear within the LogCat panel.
Had the service been tasked with a long-term activity, the service would have continued to run in the background in a separate thread until the task was completed, allowing the application to continue functioning and responding to the user. Since all our service did was log a message, it will have simply stopped upon completion.
Using the Service Class
Whilst the IntentService class allows a service to be implemented with minimal coding, there are situations where the flexibility and synchronous nature of the Service class will be required. As will become evident in this section, this involves some additional programming work to implement.
In order to avoid introducing too many concepts at once, and as a demonstration of the risks inherent in performing time-consuming service tasks in the same thread as the calling application, the example service created here will not run the service task within a new thread, instead relying on the main thread of the application. Creation and management of a new thread within a service will be covered in the next phase of the tutorial.
Creating the New Service
For the purposes of this example, a new class will be added to the project that will subclass from the Service class. Right-click, therefore, on the ServiceExample project in the Package Explorer panel and select the New -> Class menu option. Create a new class named MyService, subclassed from android.app.Service and allocated to the appropriate application package.
Within the Package Explorer window, navigate to and load the new MyService.java file, which should read as follows:
package com.example.serviceexample; import android.app.Service; import android.content.Intent; import android.os.IBinder; public class MyService extends Service { @Override public IBinder onBind(Intent arg0) { // TODO Auto-generated method stub return null; } }
As the source file currently stands, only a stub callback method has been implemented by Eclipse. Since this is not a bound service, the stub correctly returns a null value. The minimal requirement in order to create an operational service is to implement the onStartCommand() callback method which will be called when the service is starting up. For the purposes of this example, this method will loop three times performing a 10-second wait on each loop. For the sake of completeness, stub versions of the onCreate() and onDestroy() methods will also be implemented:
package com.example.serviceexample; import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.util.Log; public class MyService extends Service { private static final String TAG = "com.example.ServiceExample"; @Override public void onCreate() { Log.i(TAG, "Service onCreate"); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.i(TAG, "Service onStartCommand"); for (int i = 0; i < 3; i++) { long endTime = System.currentTimeMillis() + 10*1000; while (System.currentTimeMillis() < endTime) { synchronized (this) { try { wait(endTime - System.currentTimeMillis()); } catch (Exception e) { } } } Log.i(TAG, "Service running"); } return Service.START_STICKY; } @Override public IBinder onBind(Intent arg0) { // TODO Auto-generated method stub Log.i(TAG, "Service onBind"); return null; } @Override public void onDestroy() { Log.i(TAG, "Service onDestroy"); } }
With the service implemented, modify the AndroidManifest.xml file to add a corresponding <service> element, which can be placed either before or after the service element for the previous service and should read:
<service android:name=".MyService" />
Modifying the User Interface
It will become evident when the application runs that not creating a new thread for the service to perform tasks creates a serious usability problem. In order to be able to appreciate fully the magnitude of this issue, it is going to be necessary to add a Button view to the user interface of the ServiceExampleActivity activity and configure it to call a method when “clicked” by the user.
Locate and load the activity_service_example.xml file in the Package Explorer panel (res -> layout -> activity_service_example.xml). Right-click on the TextView and select the Change Widget Type menu option. In the resulting dialog, change the TextView to a Button view and click on OK. Right click on the new button, select Edit Text… and create a new string resource named button_text with a string that reads Start Service.
Finally, right click on the button again, select Other Properties -> All by Name -> OnClick and assign a method named buttonClick to the value.
On completion, the XML for the user interface layout should reads as follows:
<RelativeLayout 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:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".ServiceExampleActivity" > <Button android:id="@+id/Button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="buttonClick" android:text="@string/button_text" /> </RelativeLayout>
Next, edit the ServiceExampleActivity.java file to add the buttonClick() method and remove the code from the onCreate() method that started the MyIntentService service:
package com.example.serviceexample; import android.os.Bundle; import android.app.Activity; import android.content.Intent; import android.view.Menu; import android.view.View; public class ServiceExampleActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_service_example); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_service_example, menu); return true; } public void buttonClick(View view) { Intent intent = new Intent(this, MyService.class); startService(intent); } }
All that the buttonClick() method does is create an intent object for the new service and then start it running.
Running the Application
Run the application and, once loaded, touch the Start Service button. Within the LogCat window (using the ServiceExample filter created previously) the log messages will appear indicating that the onCreate() method was called and that the loop in the onStartCommand() method is executing.
Before the final loop message appears, attempt to touch the Start Service button a second time. Note that the button is unresponsive. After approximately 20 seconds, the system will display a warning dialog containing the message “Application ServiceExample is not responding”. The reason for this is that the main thread of the application is currently being held up by the service while it performs the looping task. Not only does this prevent the application from responding to the user, but also to the system, which eventually assumes that the application has locked up in some way.
Clearly, the code for the service needs to be modified to perform tasks in a new thread.
Creating a New Thread for Service Tasks
As outlined in A Basic Overview of Android Threads and Thread handlers, when an Android application is first started, the runtime system creates a single thread in which all application components will run by default. This thread is generally referred to as the main thread. The primary role of the main thread is to handle the user interface in terms of event handling and interaction with views in the user interface. Any additional components that are started within the application will, by default, also run on the main thread.
As demonstrated in the previous section, any component that undertakes a time consuming operation on the main thread will cause the application to become unresponsive until that task is complete. It is not surprising, therefore, that Android provides an API that allows applications to create and use additional threads. Any tasks performed in a separate thread from the main thread are essentially performed in the background. Such threads are typically referred to as background or worker threads.
A very simple solution to this problem involves performing the service task within a new thread. The following onStartCommand() method, for example, has been modified to launch the task within a new thread using the most basic of thread handling examples:
@Override public int onStartCommand(Intent intent, int flags, int startId) { Log.i(TAG, "Service onStartCommand " + startId); final int currentId = startId; Runnable r = new Runnable() { public void run() { for (int i = 0; i < 3; i++) { long endTime = System.currentTimeMillis() + 10*1000; while (System.currentTimeMillis() < endTime) { synchronized (this) { try { wait(endTime - System.currentTimeMillis()); } catch (Exception e) { } } } Log.i(TAG, "Service running " + currentId); } stopSelf(); } }; Thread t = new Thread(r); t.start(); return Service.START_STICKY; }
When the application is now run, it should be possible to touch the Start Service button multiple times. Each time a new thread will be created by the service to process the task. The LogCat output should also include a number referencing the startId of each service request.
With the service now handling requests outside of the main thread, the application remains responsive to both the user and the Android system.
Summary
This chapter has worked through an example implementation of an Android started service using the IntentService and Service classes. The example also demonstrated the use of threads within a service to avoid making the main thread of the application unresponsive.
<google>BUY_ANDROID</google>
Previous | Table of Contents | Next |
An Overview of Android Started and Bound Services | Android Local Bound Services – A Worked Example |