2011年5月18日水曜日

AndroidStudyMemo=Services

Determining When to Use Services

A service within the Android Software Development Kit (SDK) can mean one of two

things. First, a service can mean a background process, performing some useful operation

at regular intervals. Second, a service can be an interface for a remote object, called from

within your application. In both cases, the service object extends the Service class from

the Android SDK, and it can be a stand-alone component or part of an application with a

complete user interface.

 

Certainly, not all applications require or use services. However, you might want to

consider a service if your application meets certain criteria, such as the following:

The application performs lengthy or resource-intensive processing that does not require

input from the user.

The application must perform certain functions routinely, or at regular intervals,

such as uploading or downloading fresh content or logging the current location.

The application performs a lengthy operation that, if cancelled because the application

exits,would be wasteful to restart.An example of this is downloading large files.

The application needs to expose and provide data or information services (think

web services) to other Android applications without the need of a user interface.

 

 

Understanding the Service Lifecycle

Before we get into the details of how to create a service, let's look at how services interact

with the Android operating system. First, it should be noted that a service implementation

must be registered in that application's manifest file using the <service> tag.The service

implementation might also define and enforce any permissions needed for starting, stopping,

and binding to the service, as well as make specific service calls.

 

After it's been implemented, an Android service can be started using the

Context.startService() method. If the service was already running when the

startService() method was called, these subsequent calls don't start further instances of

the service.The service continues to run until either the Context.stopService() method

is called, or the service completes its tasks and stops itself using the stopSelf() method.

 

To connect to a service, interested applications use the Context.bindService()

method to obtain a connection. If that service is not running, it is created at that time.After

the connection is established, the interested applications can begin making requests of

that service, if the applications have the appropriate permissions. The application can then

disconnect from the service when finished using the Context.unbindService() method.

 

 

Creating a Service

Creating an Android service involves extending the Service class and adding a service

block to the AndroidManifest.xml permissions file.

 

Defining the service name enables other applications to start the service that runs in the

background and stop it. Both the onStart() and onStartCommand() methods are essentially

the same, with the exception that onStart() is deprecated in API Levels 5 and

above. (The default implementation of the onStartCommand() on API Level 5 or greater

is to call onStart() and returns an appropriate value such that behavior will be compatible

to previous versions.) In the following example, both methods are implemented.

 

For this example,we implement a simple service that listens for GPS changes, displays

notifications at regular intervals, and then provides access to the most recent location data

via a remote interface.The following code gives a simple definition to the Service class

called GPXService:

              public class GPXService extends Service {

                            public static final String GPX_SERVICE =

                                          "com.androidbook.GPXService.SERVICE";

                            private LocationManager location = null;

                            private NotificationManager notifier = null;

                           

                            @Override

                            public void onCreate() {

                                          super.onCreate();

                            }

                           

                            @Override

                            public void onStart(Intent intent, int startId) {

                                          super.onStart(intent, startId);

                            }

                           

                            @Override

                            public void onStartCommand(Intent intent, int flags, int startId) {

                                          super.onStart(intent, startId);

                            }

                           

                            @Override

                            public void onDestroy() {

                                          super.onDestroy();

                            }

              }

 

You need to understand the lifecycle of a service because it's different from that of an

activity. If a service is started by the system with a call to the Context.StartService()

method, the onCreate() method is called just before the onStart() or

onStartCommand() methods. However, if the service is bound to with a call to the

Context.bindService() method, the onCreate() method is called just before the

onBind() method.The onStart() or onStartCommand() methods are not called in this

case. Finally, when the service is finished?that is, it is stopped and no other

process is bound to it?the onDestroy() method is called. Everything for the service

 must be cleaned up in this method.

 

With this in mind, here is the full implementation of the onCreate() method

              public void onCreate() {

                            super.onCreate();

                            location = (LocationManager)

                            getSystemService(Context.LOCATION_SERVICE);

                            notifier = (NotificationManager)

                            getSystemService(Context.NOTIFICATION_SERVICE);

              }

 

Because the object doesn't yet know if the next call is to either of the start methods or

the onBind() method,we make a couple of quick initialization calls, but no background

processing is started. Even this might be too much if neither of these objects were used by

the interface provided by the binder.

 

Because we can't always predict what version of Android our code is running on,we

can simple implement both the onStart() and onStartCommand() methods and have

them call a third method that provides a common implementation.This enables us to customize

behavior on later Android versions while being compatible with earlier versions.

To do this, the project needs to be built for an SDK of Level 5 or higher, while having a

minSdkValue of whatever earlier versions are supported. Of course,we highly recommend

testing on multiple platform versions to verify that the behavior is as you expect. Here are

sample implementations of the onStartCommand() and onStart() methods:

              @Override

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

                            Log.v(DEBUG_TAG, "onStartCommand() called, must be on L5 or later");

                            if (flags != 0) {

                                          Log.w(DEBUG_TAG, "Redelivered or retrying service start: "+flags);

                            }

                            doServiceStart(intent, startId);

                            return Service.START_REDELIVER_INTENT;

              }

             

              @Override

              public void onStart(Intent intent, int startId) {

                            super.onStart(intent, startId);

                            Log.v(DEBUG_TAG, "onStart() called, must be on L3 or L4");

                            doServiceStart(intent,startId);

              }

 

Next, let's look at the implementation of the doStartService() method in greater

detail:

              @Override

              public void doServiceStart(Intent intent, int startId) {

                            super.onStart(intent, startId);

                            updateRate = intent.getIntExtra(EXTRA_UPDATE_RATE, -1);

                            if (updateRate == -1) {

                                          updateRate = 60000;

                            }

             

                            Criteria criteria = new Criteria();

                            criteria.setAccuracy(Criteria.NO_REQUIREMENT);

                            criteria.setPowerRequirement(Criteria.POWER_LOW);

             

                            location = (LocationManager)

                                          getSystemService(Context.LOCATION_SERVICE);

                            String best = location.getBestProvider(criteria, true);

                            location.requestLocationUpdates(best,

                                          updateRate, 0, trackListener);

                            Notification notify = new

                                          Notification(android.R.drawable.stat_notify_more,

                                                        "GPS Tracking", System.currentTimeMillis());

                            notify.flags |= Notification.FLAG_AUTO_CANCEL;

                            Intent toLaunch = new Intent(getApplicationContext(),

                                          ServiceControl.class);

                           

                            PendingIntent intentBack =

                                          PendingIntent.getActivity(getApplicationContext(),

                                                        0, toLaunch, 0);

                            notify.setLatestEventInfo(getApplicationContext(),

                                                        "GPS Tracking", "Tracking start at " +

                            updateRate+"ms intervals with [" + best +

                                          "] as the provider.", intentBack);

                            notifier.notify(GPS_NOTIFY, notify);

              }

 

The background processing starts within the two start methods. In this example, though,

the background processing is actually just registering for an update from another service.

 

The Intent extras object retrieves data passed in by the process requesting the service.

Here,we retrieve one value, EXTRA_UPDATE_RATE, for determining the length of time between

updates.The string for this, update-rate,must be published externally, either in

developer documentation or in a publicly available class file so that users of this service

know about it.

 

There are two common methods to communicate data to the user.The first is to use

Notifications.This is the least-intrusive method and can be used to drive users to the

application for more information. It also means the users don't need to be actively using

their phone at the time of the notification because it is queued. For instance, a weather

application might use notifications to provide weather updates every hour.

 

The other method is to use Toast messages. From some services, this might work well,

especially if the user expects frequent updates and those updates work well overlaid briefly

on the screen, regardless of what the user is currently doing. For instance, a background

music player could briefly overlay the current song title when the song changes.

 

The onDestroy() method is called when no clients are bound to the service and a request

for the service to be stopped has been made via a call to the Context.stopService() method,

or a call has been made to the stopSelf() method from within the service. At this point,

everything should be gracefully cleaned up because the service ceases to exist.

Here is an example of the onDestroy() method:

              @Override

              public void onDestroy() {

                            if (location != null) {

                                          location.removeUpdates(trackListener);

                                          location = null;

                            }

                            Notification notify = new

                                          Notification(android.R.drawable.stat_notify_more,

                                                        "GPS Tracking", System.currentTimeMillis());

                           

                            notify.flags |= Notification.FLAG_AUTO_CANCEL;

                            Intent toLaunch = new Intent(getApplicationContext(),

                                          ServiceControl.class);

                            PendingIntent intentBack =

                                          PendingIntent.getActivity(getApplicationContext(),

                                                        0, toLaunch, 0);

                           

                            notify.setLatestEventInfo(getApplicationContext(),

                                          "GPS Tracking", "Tracking stopped", intentBack);

                            notifier.notify(GPS_NOTIFY, notify);

                           

                            super.onDestroy();

              }

 

Here,we stop updates to the LocationListener object.This stops all our background

processing.Then,we notify the user that the service is terminating. Only a single call to the

onDestroy() method happens, regardless of how many times the start methods are called.

 

The system does not know about a service unless it is defined within the

AndroidManifest.xml permissions file using the <service> tag. Here is the <service>

tag we must add to the Android Manifest file:

              <service

                            android:enabled="true"

                            android:name="GPXService">

                                          <intent-filter>

                                                        <action android:name=

                                                        "com.androidbook.GPXService.SERVICE" />

                                          </intent-filter>

              </service>

 

This block of XML defines the service name, GPXService, and that the service is enabled.

Then, using an Intent filter,we use the same string that we defined within the class.

 

This is the string that is used later on when controlling the service.With this block of

XML inside the application section of the manifest, the system now knows that the

service exists and it can be used by other applications.

 

 

Controlling a Service

At this point, the example code has a complete implementation of a Service.Now we

write code to control the service we previously defined.

              Intent service = new Intent("com.androidbook.GPXService.SERVICE");

              service.putExtra("update-rate", 5000);

              startService(service);

 

Starting a service is as straightforward as creating an Intent with the service name and

calling the startService() method. In this example,we also set the update-rate

Intent extra parameter to 5 seconds.That rate is quite frequent but works well for testing.

For practical use,we probably want this set to 60 seconds or more.This code triggers

a call to the onCreate() method, if the Service isn't bound to or running already. It also

triggers a call to the onStart() or onStartCommand() methods, even if the service is already

running.

 

Later, when we finish with the service, it needs to be stopped using the following code:

              Intent service = new Intent("com.androidbook.GPXService.SERVICE");

              stopService(service);

 

This code is essentially the same as starting the service but with a call to the

stopService() method.This calls the onDestroy() method if there are no bindings to it.

 

However, if there are bindings, onDestroy() is not called until those are also terminated.

This means background processing might continue despite a call to the stopService()

method. If there is a need to control the background processing separate from these system

calls, a remote interface is required.

 

 

Implementing a Remote Interface

Sometimes it is useful to have more control over a service than just system calls to start

and stop its activities. However, before a client application can bind to a service for making

other method calls, you need to define the interface.The Android SDK includes a useful

tool and file format for remote interfaces for this purpose.

 

To define a remote interface, you must declare the interface in an AIDL file, implement

the interface, and then return an instance of the interface when the onBind()

method is called.

 

Using the example GPXService service we already built,we now create a remote interface for it.

This remote interface has a method, which can be called especially for returning the last

location logged.You can use only primitive types and objects that implement the Parcelable

protocol with remote service calls.This is because these calls cross process boundaries

where memory can't be shared.The AIDL compiler handles the details of crossing these

boundaries when the rules are followed.The Location object implements the Parcelable

interface so it can be used.

 

Here is the AIDL file for this interface, IRemoteInterface:

              package com.androidbook.services;

              interface IRemoteInterface {

                            Location getLastLocation();

              }

             

When using Eclipse, you can add this AIDL file, IRemoteInterface.aidl, to the project

under the appropriate package and the Android SDK plug-in does the rest. Now we

must implement the code for the interface. Here is an example implementation of this

interface:

              private final IRemoteInterface.Stub

                            mRemoteInterfaceBinder = new IRemoteInterface.Stub() {

                                          public Location getLastLocation() {

                                                        Log.v("interface", "getLastLocation() called");

                                                        return lastLocation;

                                          }

                            };

 

The service code already stored off the last location received as a member variable, so we

can simply return that value.With the interface implemented, it needs to be returned from

the onBind() method of the service:

              @Override

              public IBinder onBind(Intent intent) {

                            // we only have one, so no need to check the intent

                            return mRemoteInterfaceBinder;

              }

 

If multiple interfaces are implemented, the Intent passed in can be checked within the

onBind() method to determine what action is to be taken and which interface should be

returned. In this example, though,we have only one interface and don't expect any other

information within the Intent, so we simply return the interface.

 

We also add the class name of the binder interface to the list of actions supported by

the intent filter for the service within the AndroidManifest.xml file. Doing this isn't required

but is a useful convention to follow and allows the class name to be used.The following

block is added to the service tag definition:

              <action android:name =

              "com.androidbook.services.IRemoteInterface" />

 

The service can now be used through this interface.This is done by implementing a

ServiceConnection object and calling the bindService() method.When finished, the

unbindService() method must be called so the system knows that the application is

done using the service.The connection remains even if the reference to the interface is

gone.

 

Here is an implementation of a ServiceConnection object's two main methods,

onServiceConnected() and onServiceDisconnected():

              public void onServiceConnected(ComponentName name,

                            IBinder service) {

                                          mRemoteInterface =

                                          IRemoteInterface.Stub.asInterface(service);

                                          Log.v("ServiceControl", "Interface bound.");

              }

              public void onServiceDisconnected(ComponentName name) {

                                          mRemoteInterface = null;

                                          Log.v("ServiceControl",

                                                        "Remote interface no longer bound");

              }

             

When the onServiceConnected() method is called, an IRemoteInterface instance that

can be used to make calls to the interface we previously defined is retrieved.A call to the

remote interface looks like any call to an interface now:

              Location loc = mRemoteInterface.getLastLocation();

 

To use this interface from another application, you should place the AIDL file within the

project and appropriate package.The call to onBind() triggers a call to the

onServiceConnected() after the call to the service's onCreate() method. Remember,

the onStart() or onStartCommand() methods are not called in this case.

 

              bindService(new Intent(IRemoteInterface.class.getName()),

                            this, Context.BIND_AUTO_CREATE);

 

In this case, the Activity we call from also implements the ServiceConnection interface.

This code also demonstrates why it is a useful convention to use the class name as an intent

filter. Because we have both intent filters and we don't check the action on the call to

the onBind() method,we can also use the other intent filter, but the code here is clearer.

When done with the interface, a call to unbindService() disconnects the interface.

 

However, a callback to the onServiceDisconnected() method does not mean that the

service is no longer bound; the binding is still active at that point, just not the connection.

 

Implementing a Parcelable Class

In the example so far,we have been lucky in that the Location class implements the

Parcelable interface.What if a new object needs to be passed through a remote interface?

Let's take the following class, GPXPoint, as an example:

              public final class GPXPoint {

                            public int latitude;

                            public int longitude;

                            public Date timestamp;

                            public double elevation;

 

                            public GPXPoint() {

                            }

              }

 

The GPXPoint class defines a location point that is similar to a GeoPoint but also includes

the time the location was recorded and the elevation.This data is commonly found in the

popular GPX file format. On its own, this is not a basic format that the system recognizes

to pass through a remote interface.However, if the class implements the Parcelable

interface and we then create an AIDL file from it, the object can be used in a remote

interface.

 

To fully support the Parcelable type,we need to implement a few methods and a

Parcelable.Creator<GPXPoint>.The following is the same class now modified to be a

Parcelable class:

              public final class GPXPoint implements Parcelable {

                            public int latitude;

                            public int longitude;

                            public Date timestamp;

                            public double elevation;

                           

                            public static final Parcelable.Creator<GPXPoint>

                            CREATOR = new Parcelable.Creator<GPXPoint>() {

                                          public GPXPoint createFromParcel(Parcel src) {

                                                        return new GPXPoint(src);

                                          }

 

                                          public GPXPoint[] newArray(int size) {

                                                        return new GPXPoint[size];

                                          }

                            };

 

                            public GPXPoint() {

                            }

             

                            private GPXPoint(Parcel src) {

                                          readFromParcel(src);

                            }

             

                            public void writeToParcel(Parcel dest, int flags) {

                                          dest.writeInt(latitude);

                                          dest.writeInt(longitude);

                                          dest.writeDouble(elevation);

                                          dest.writeLong(timestamp.getTime());

                            }

 

                            public void readFromParcel(Parcel src) {

                                          latitude = src.readInt();

                                          longitude = src.readInt();

                                          elevation = src.readDouble();

                                          timestamp = new Date(src.readLong());

                            }

 

                            public int describeContents() {

                                          return 0;

                            }

              }

 

The writeToParcel() method is required and flattens the object in a particular order

using supported primitive types within a Parcel.When the class is created from a

Parcel, the Creator is called, which, in turn, calls the private constructor. For readability,

we also created a readFromParcel() method that reverses the flattening, reading the

primitives in the same order that they were written and creating a new Date object.

Now you must create the AIDL file for this class.You should place it in the same directory

as the Java file and name it GPXPoint.aidl to match.You should make the contents

look like the following:

              package com.androidbook.services;

              parcelable GPXPoint;

 

Now the GPXPoint class can be used in remote interfaces.This is done in the same way as

any other native type or Parcelable object.You can modify the

IRemoteInterface.aidl file to look like the following:

              package com.androidbook.services;

              import com.androidbook.services.GPXPoint;

              interface IRemoteInterface {

                            Location getLastLocation();

                            GPXPoint getGPXPoint();

              }

 

Additionally,we can provide an implementation for this method within the interface, as

follows:

                            public GPXPoint getGPXPoint() {

                                          if (lastLocation == null) {

                                                        return null;

                                          } else {

                                                        Log.v("interface", "getGPXPoint() called");

                                                        GPXPoint point = new GPXPoint();

 

                                                        point.elevation = lastLocation.getAltitude();

                                                        point.latitude =

                                                                      (int) (lastLocation.getLatitude() * 1E6);

                                                        point.longitude =

                                                                      (int) (lastLocation.getLongitude() * 1E6);

                                                        point.timestamp =

                                                                      new Date(lastLocation.getTime());

                                                        return point;

                                          }

                            }

As can be seen, nothing particularly special needs to happen. Just by making the object

Parcelable, it can now be used for this purpose.

 

0 件のコメント:

コメントを投稿