9781449390501
Android_System_Services.html

Chapter 13. System Services

If you're new to the Android mobile operating system, Learning Android is the perfect way to master the fundamentals. This gentle introduction shows you how to use Android's basic building blocks to develop user interfaces, store data, and more. Buy the print book or ebook.

Like many modern operating systems, Android comes with a number of system services that are always on, always running, and readily available to developers to tap into. These system services include things like Location service, Sensor service, WiFi service, Alarm service, Telephony service, Bluetooth service, and so on. System services are started at boot time and are guaranteed to be running by the time your application launches.

In this chapter, we’ll see how we can use some of the system services to further expand the Yamba application. First we’ll take a look at the Sensor service in a small example in order to demonstrate some of the concepts that are common to most of the system services. Then we’ll add support for location information to our status updates via the Location service.

Additionally, we’re going to refactor the Yamba application to take advantage of Intent Service support. This will demonstrate how to use Alarm service and will make our Updater slightly more efficient and simpler.

Compass Demo

To start with system services, we are going to look at a simple, self-contained example of a compass application. This application uses the Sensor service to get updates from the orientation sensor and use its information to rotate a Rose, our custom UI component. The Sensor service is very typical of system services and a relatively easy one to understand.

To build this example, we’ll create an activity that will get the Sensor service and register for updates from a particular sensor. Next, we’ll build the Rose that will rotate on the screen based on the sensor readings.

Common Steps in Using System Services

To get any system service, issue the getSystemService() call. This returns a Manager object representing that system service, which you then use to access the service. Most system services work on some sort of publish/subscribe mechanism. In other words, you generally register your app for notifications from that service and provide your own callbacks methods that the service will invoke when an event happens. To do this in Java, create a Listener that implements an interface so that the service can call the callback methods.

Keep in mind that requesting notifications from a system service can be costly in terms of the battery usage. For example, getting a GPS signal or processing sensor updates takes a lot of energy from the device. To preserve the battery, we typically want to be doing the work of processing updates only when the user is looking at the activity itself. In terms of the Activity lifecycle (see the section called “Activity Lifecycle”), this means we want to get the notifications only while in the Running state (see the section called “Running State”).

To ensure that you request service updates only while in the Running state, register for updates in onResume() and unregister in onPause(). This is because all roads into the Running state go via onResume() and all roads out of it go via onPause(). In certain other situations, you may want to cast the net wider and have the activity be registered between onStart() and onStop(), or even between onCreate() and onDestroy(). In our case, we don’t want to register in onCreate() because it would waste a lot of battery and processing time by making us listen and process sensor updates even when our activity is not in the foreground. You can now see how understanding the Activity lifecycle plays an important role in optimizing usage of system services for the battery consumption.

Getting Updates From the Compass

To code our Compass demo application, we get SensorManager, the class that represents the Sensor system service. We make our main activity implement SensorEventListener so that we can register it (i.e. this) to get updates for a specific sensor. We register and unregister the listener in onResume() and onPause() respectively. To implement the sensor listeners, our activity provides onAccuracyChanged() and onSensorChanged(). The former is a requirement, but we’ll leave it empty because the accuracy of the orientation sensor is not expected to change. The latter call is what’s really of interest to us.

When the orientation sensor changes, the Sensor service calls back our sensor listener via onSensorChanged() and reports the new sensor data. The data always comes back as an array of float values that represent degrees and therefore range from 0 to 359. In the case of orientation sensor, the elements represent the following dimensions, illustrated in Figure 13.1, “Axis”:

Index [0], the azimuth:: The amount of rotation around the Z axis from the vertical position around the back and then around the bottom toward the front. Index [1], the pitch:: The amount of rotation around the X axis from the front to the left and then the around the back toward the right. Index [2], the roll
The amount of rotation around the Y axis from the vertical position to the left and then the around the bottom toward the right.

For the Compass demo, we are interested only in the first element, i.e., the azimuth. The data returned by each sensor has a different meaning, and you should look up the particulars in the documentation at http://d.android.com/reference/android/hardware/SensorManager.html.

Figure 13.1. Axis

Axis

Compass Main Activity

The main Compass activity sets the Rose as its only widget on the screen. It also registers with SensorManager to listen to sensor events, and updates the Rose orientation accordingly.

Example 13.1. Compass.java

package com.marakana;

import android.app.Activity;
import android.content.res.Configuration;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;

// implement SensorListener
public class Compass extends Activity implements SensorEventListener { // 1
  SensorManager sensorManager; // 2
  Sensor sensor;
  Rose rose;

  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) { // 3
    super.onCreate(savedInstanceState);

    // Set full screen view 4
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
        WindowManager.LayoutParams.FLAG_FULLSCREEN);
    requestWindowFeature(Window.FEATURE_NO_TITLE);

    // Create new instance of custom Rose and set it on the screen
    rose = new Rose(this); // 5
    setContentView(rose); // 6

    // Get sensor and sensor manager
    sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); // 7
    sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); // 8

    Log.d("Compass", "onCreated");
  }

  // Register to listen to sensors
  @Override
  public void onResume() {
    super.onResume();
    sensorManager.registerListener(this, sensor,
        SensorManager.SENSOR_DELAY_NORMAL); // 9
  }

  // Unregister the sensor listener
  @Override
  public void onPause() {
    super.onPause();
    sensorManager.unregisterListener(this); // 10
  }

  // Ignore accuracy changes
  public void onAccuracyChanged(Sensor sensor, int accuracy) { // 11
  }

  // Listen to sensor and provide output
  public void onSensorChanged(SensorEvent event) { // 12
    int orientation = (int) event.values[0]; // 13
    Log.d("Compass", "Got sensor event: " + event.values[0]);
    rose.setDirection(orientation); // 14
  }

  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
  }

}

1

Since Compass listens to sensor events, it needs to implement the SensorEventListener interface.

2

We define local variable for the sensor, the sensor manager, and the Rose.

3

Because getting access to the sensor is a one-time activity, we do it when our app is created.

4

The window manager flags set the activity into full-screen mode.

5

We create a new instance of the Rose widget, our custom compass rose.

6

In this case, the activity content is the single Rose widget. This is unlike the usual reference to an XML layout resource.

7

We get the sensor manager from the system service.

8

From the sensor manager, we can obtain the actual sensor object that we are interested in.

9

We register to listen to sensor updates in activity’s onResume() method, as described earlier.

10

We unregister from sensor updates in onPause(), the counterpart to onResume().

11

onAccuracyChanged() is implemented because it is required by the SensorEventListener interface. but is left empty as explained earier.

12

onSensorChanged() is called whenever the sensor changes, indicating a rotation of the device in some direction. The particular information about the change is stored in SensorEvent.

13

We are interested in the first element of the array of new values reported.

14

Once we have the new orientation, we update our Rose widget to rotate accordingly.

Note

The way a device reports sensor data can be very erratic, coming at uneven intervals. There are ways to suggest to the system how frequently we’d like the sensor updates, but these are just suggestions and not a guarantee. Also, sensors are not supported by the emulator, so to really test your application, you’ll need a physical device with support for orientation sensor. Most Android phones have that support.

Custom Rose Widget

Rose is our custom UI widget showing the rose of a compass that can be rotated like a real compass. Every UI widget in Android needs to be a subclass of View. But since this is an image, we’ll start from a higher starting point, in this case the ImageView class, which is a View. By subclassing ImageView, our Rose inherits some useful methods to load an image and draw it on the screen.

With any custom UI widget, one of the most important methods is onDraw(), which draws the widget onto a Canvas that is provided to the method. In case of our Rose, we rotate this canvas around its middle point for the same number of degrees as reported by the orientation sensor. Next, we draw the image onto this rotated sensor as it would have normally be drawn by the super class. The result is a rotated compass rose representing the direction we are pointing to.

Example 13.2. Rose.java

package com.marakana;

import android.content.Context;
import android.graphics.Canvas;
import android.widget.ImageView;

public class Rose extends ImageView { // 1
  int direction = 0;

  public Rose(Context context) {
    super(context);

    this.setImageResource(R.drawable.compassrose); // 2
  }

  // Called when component is to be drawn
  @Override
  public void onDraw(Canvas canvas) { // 3
    int height = this.getHeight();  // 4
    int width = this.getWidth();

    canvas.rotate(direction, width / 2, height / 2); // 5
    super.onDraw(canvas); // 6
  }

  // Called by Compass to update the orientation
  public void setDirection(int direction) { // 7
    this.direction = direction;
    this.invalidate(); // request to be redrawn 8
  }

}

1

Our widget has to be a subclass of View, but since our widget is an image, we get more functionality by starting from ImageView.

2

ImageView already knows how to set an image as its content. We just specify to super which image resource to use. Note that compassrose.jpg is in our /res/drawable folder.

3

onDraw() is the method that the layout manager calls to have each widget draw itself. The layout manager passes the Canvas to this method. This method is where you typically do any custom drawing to the canvas.

4

Once we have the canvas, we can figure out its size.

5

We simply rotate the entire canvas for some amount (in degrees) around its mid point.

6

We tell super to draw the image on this rotated canvas. At this point we have our rose drawn at the proper angle.

7

setDirection() is called by the Compass activity to update the direction of the rose based on the values that sensor manager reported.

8

Calling invalidate() on a view marks it for redrawing, which happens later via a call to onDraw().

At this point, your compass application is working. The compass rose should be pointing north, more or less, when the device is held upright as usual. Keep in mind that you should run this application on a physical device as the emulator doesn’t support.

Location Service

Now that you have seen how the sensor manager works, we can look at the Location API, another system service provided by Android. Just like sensors, the Location API is supported via the Location manager. And just like sensors, we get the Location manager via a getSystemService() call.

Once we have access to the Location service, we need to register a Location listener with it so the service can call back when there’s a change in location. Again, we’ll do this by implementing a Location listener interface.

If you recall from the section called “Common Steps in Using System Services”, processing GPS and other location updates can be very taxing for the battery. To minimize the battery consumption, we want to listen to location updates only while in the Running state. To do that, we’ll register for the updates in onResume() and unregister in onPause(), taking the advantage of the Activity lifecycle.

Where Am I? Demo

This example illustrates how to use location-based services in Android. First, we use LocationManager to figure out our current location based on the resources in the environment available to the device, such as GPS or a wireless network. Secondly, we use Geocoder to convert this location to an address.

The Layout

The layout for this example is trivial. Our resource file provides a TextView widget for the title and another TextView widget for the output. Since the output could be longer than the screen size, we wrap the output in a ScrollView widget.

Example 13.3. res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_height="fill_parent" android:layout_width="fill_parent"
  android:background="#fff" android:orientation="vertical">
  <!-- 1 -->
  <TextView android:layout_width="wrap_content"
    android:layout_height="wrap_content" android:layout_gravity="center"
    android:textColor="#333" android:textSize="30dp" android:text="@string/title"></TextView>
  <!-- 2 -->
  <ScrollView android:layout_height="fill_parent"
    android:layout_width="fill_parent">
    <!-- 3 -->
    <TextView android:textColor="#333" android:layout_gravity="center"
      android:layout_height="fill_parent" android:layout_width="fill_parent"
      android:gravity="center" android:textSize="25dp" android:text="Waiting..."
      android:id="@+id/textOut"></TextView>
  </ScrollView>
</LinearLayout>

1

The title for our application.

2

A ScrollView to enable scrolling if the output grows beyond the size of the screen.

3

A TextView to represent the output. It will be programmatically set from the WhereAmI activity.

The Activity for our Location Listener

This is our main activity, which sets up the screen, connects to LocationManager, and uses the Geocoder to figure out our address. The LocationManager uses location providers, such as GPS or Network, to figure out our current location. The location is expressed as latitude and longitude values. The Geocoder searches an online database for known addresses in the vicinity of the location provided. It may come up with multiple results, some more specific than others.

Example 13.4. WhereAmI.java

package com.marakana;

import java.io.IOException;
import java.util.List;

import android.app.Activity;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

public class WhereAmI extends Activity implements LocationListener { // 1
  LocationManager locationManager; // 2
  Geocoder geocoder; // 3
  TextView textOut; // 4

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    textOut = (TextView) findViewById(R.id.textOut);

    locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); // 5
    geocoder = new Geocoder(this); // 6

    // Initialize with the last known location
    Location lastLocation = locationManager
        .getLastKnownLocation(LocationManager.GPS_PROVIDER); // 7
    if (lastLocation != null)
      onLocationChanged(lastLocation);
  }

  @Override
  protected void onResume() { // 8
    super.onRestart();
    locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000,
        10, this);
  }

  @Override
  protected void onPause() { // 9
    super.onPause();
    locationManager.removeUpdates(this);
  }

  // Called when location has changed
  public void onLocationChanged(Location location) { // 10
    String text = String.format(
        "Lat:\t %f\nLong:\t %f\nAlt:\t %f\nBearing:\t %f", location
            .getLatitude(), location.getLongitude(), location.getAltitude(),
        location.getBearing()); // 11
    textOut.setText(text);

    // Perform geocoding for this location
    try {
      List<Address> addresses = geocoder.getFromLocation(
          location.getLatitude(), location.getLongitude(), 10); // 12
      for (Address address : addresses) {
        textOut.append("\n" + address.getAddressLine(0)); // 13
      }
    } catch (IOException e) {
      Log.e("WhereAmI", "Couldn't get Geocoder data", e);
    }
  }

  // Methods required by LocationListener 14
  public void onProviderDisabled(String provider) {
  }

  public void onProviderEnabled(String provider) {
  }

  public void onStatusChanged(String provider, int status, Bundle extras) {
  }

}

1

Notice that WhereAmI implements LocationListener. This is the interface that LocationManager uses to notify us of changes to location.

2

Local reference to LocationManager.

3

Local reference to Geocoder.

4

textOut is the text view to which we print our output to for the user to see.

5

We get the local reference to LocationManager by asking the context to get the location manager system service. For a reference on context, see the section called “Application Context”.

6

We create a new instance of Geocoder and pass the current context to it.

7

The Location manager memorizes its last known location. This is useful since it may take a while until we get the location lock via either a network or a GPS provider.

8

As usual, we register in onResume(), since that is the method that is called en route to Running state. We use location manager’s requestLocationUpdates() method to register for updates.

9

We unregister in onPause(), which will be called just before the activity goes into the Stopped state.

10

onLocationChanged() is the callback method called by the location manager when it detects that the location has changed.

11

We get the Location object, which contains a lot of useful information about the current location. We create a human-readable string with this info.

12

Once we have the location, we can try to "geocode" the location, a process of converting latitude and longitude to a known address.

13

If we do find known addresses for this location, we print them out.

14

Some other callback methods are required to implement the LocationListener interface. We don’t use them for this example.

Manifest file

The manifest file for this app is fairly standard. Notice that in order to be able to register as a location listener, we have to hold the appropriate permissions. Keep in mind that although we have GPS and Network as two most commonly used Location Providers, Android is built with extensibility in mind. In the future, we may have other types of providers as well. For that reason, Android breaks down the location permissions into abstract fine location and coarse location permissions.

Example 13.5. AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.marakana" android:versionCode="1" android:versionName="1.0">
  <application android:icon="@drawable/icon" android:label="@string/app_name">
    <activity android:name=".WhereAmI" android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
  <uses-sdk android:minSdkVersion="4" />
  <!-- 1 -->
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

</manifest>

1

Declares that this app uses location providers. The location permissions could be android.permission.ACCESS_FINE_LOCATION for a GPS provider or android.permission.ACCESS_COARSE_LOCATION for a wireless network provider.

At this point, your WhereAmI application is complete. It illustrates how to use LocationManager to get the actual location via a specific location provider and how to convert that location into known addresses via Geocoder. An example of the result is shown in Figure 13.2, “WhereAmI”.

Figure 13.2. WhereAmI

WhereAmI

Updating Yamba to Use the Location Service

The WhereAmI application was a small standalone test to make sure we can get location information. Now we’ll incorporate location information into our larger Yamba app.

Updating Our Preferences

First, the user may not want to broadcast her location to the world, so we should ask. A good place to ask would be the Preferences. And to do that, this time around, we’ll use a ListPreference property. This is somewhat different from the EditTextPreferences we’ve seen before in Chapter 7, Preferences, File System, Options Menu, and Intents, in that it requires a list of items. In fact, it requires two lists: one to display and one to use for actual values.

So we’ll add couple of strings to our strings.xml file and create two new string resources: one to represent names of our location providers in a form friendly to human readers, and the other to represent their values. To do that, we’ll add the following to our strings.xml file:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  ...
  <string-array name="providers">
    <item>None, please</item>
    <item>GPS via satellites!</item>
    <item>Mobile Network will do</item>
  </string-array>

  <string-array name="providerValues">
    <item>NONE</item>
    <item>gps</item>
    <item>network</item>
  </string-array>
</resources>

Notice that both string arrays have the same number of elements. They basically represent name-value pairs and match each other.

Now that we have the names and values for our location providers, we can update prefs.xml with that information.

Example 13.6. Updated res/xml/prefs.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
  <EditTextPreference android:title="@string/titleUsername"
    android:summary="@string/summaryUsername" android:key="username"></EditTextPreference>
  <EditTextPreference android:title="@string/titlePassword"
    android:password="true" android:summary="@string/summaryPassword"
    android:key="password"></EditTextPreference>
  <EditTextPreference android:title="@string/titleApiRoot"
    android:summary="@string/summaryApiRoot" android:key="apiRoot"></EditTextPreference>
  <ListPreference android:title="@string/titleProvider"
    android:summary="@string/summaryProvider" android:key="provider"
    android:entryValues="@array/providerValues" android:entries="@array/providers" />
  <!-- 1 -->
  <ListPreference android:entryValues="@array/intervalValues"
    android:summary="@string/summaryUpdaterInterval" android:title="@string/titleUpdaterInterval"
    android:entries="@array/interval" android:key="interval"></ListPreference>
</PreferenceScreen>

1

The new ListPreference displaying the names and values of various location providers that we support: GPS, network, and none at all.

Updating the Yamba Application

Now that we have the preferences supporting settings for a specific location provider the user wishes to use, we have to expose those preferences via our YambaApplication to rest of the app, namely StatusActivity.

To do that, simply add a getter method to YambaApplication.java:

Example 13.7. YambaApplication.java

public class YambaApplication extends Application implements
    OnSharedPreferenceChangeListener {
  ...
  public static final String LOCATION_PROVIDER_NONE = "NONE";
  ...
  public String getProvider() {
    return prefs.getString("provider", LOCATION_PROVIDER_NONE);
  }
}

Now that we have support for providers in the preferences and in the Yamba app object, we’re ready to update the Status activity.

Updating the Status Activity

The Status activity is the main place where we use the location information. Just as in the WhereAmI demo, we’re going to get the Location manger by calling getSystemService() and register for location updates. We’re also going to implement the LocationListener interface, which means adding a number of new callback methods to this activity. When the location does change, we’ll update the location object, and next time around when we update our status online, we’ll have proper location information.

Example 13.8. StatusActivity.java

package com.marakana.yamba8;

import winterwell.jtwitter.Twitter;
import android.graphics.Color;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

public class StatusActivity extends BaseActivity implements OnClickListener,
    TextWatcher, LocationListener { // 1
  private static final String TAG = "StatusActivity";
  private static final long LOCATION_MIN_TIME = 3600000; // One hour
  private static final float LOCATION_MIN_DISTANCE = 1000; // One kilometer
  EditText editText;
  Button updateButton;
  TextView textCount;
  LocationManager locationManager; // 2
  Location location;
  String provider;

  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.status);

    // Find views
    editText = (EditText) findViewById(R.id.editText);
    updateButton = (Button) findViewById(R.id.buttonUpdate);
    updateButton.setOnClickListener(this);

    textCount = (TextView) findViewById(R.id.textCount);
    textCount.setText(Integer.toString(140));
    textCount.setTextColor(Color.GREEN);
    editText.addTextChangedListener(this);
  }

  @Override
  protected void onResume() {
    super.onResume();

    // Setup location information
    provider = yamba.getProvider(); // 3
    if (!YambaApplication.LOCATION_PROVIDER_NONE.equals(provider)) { // 4
      locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); // 5
    }
    if (locationManager != null) {
      location = locationManager.getLastKnownLocation(provider); // 6
      locationManager.requestLocationUpdates(provider, LOCATION_MIN_TIME,
          LOCATION_MIN_DISTANCE, this); // 7
    }

  }

  @Override
  protected void onPause() {
    super.onPause();

    if (locationManager != null) {
      locationManager.removeUpdates(this);  // 8
    }
  }

  // Called when button is clicked
  public void onClick(View v) {
    String status = editText.getText().toString();
    new PostToTwitter().execute(status);
    Log.d(TAG, "onClicked");
  }

  // Asynchronously posts to twitter
  class PostToTwitter extends AsyncTask<String, Integer, String> {
    // Called to initiate the background activity
    @Override
    protected String doInBackground(String... statuses) {
      try {
        // Check if we have the location
        if (location != null) { // 9
          double latlong[] = {location.getLatitude(), location.getLongitude()};
          yamba.getTwitter().setMyLocation(latlong);
        }
        Twitter.Status status = yamba.getTwitter().updateStatus(statuses[0]);
        return status.text;
      } catch (RuntimeException e) {
        Log.e(TAG, "Failed to connect to twitter service", e);
        return "Failed to post";
      }
    }

    // Called once the background activity has completed
    @Override
    protected void onPostExecute(String result) {
      Toast.makeText(StatusActivity.this, result, Toast.LENGTH_LONG).show();
    }
  }

  // TextWatcher methods
  public void afterTextChanged(Editable statusText) {
    int count = 140 - statusText.length();
    textCount.setText(Integer.toString(count));
    textCount.setTextColor(Color.GREEN);
    if (count < 10)
      textCount.setTextColor(Color.YELLOW);
    if (count < 0)
      textCount.setTextColor(Color.RED);
  }

  public void beforeTextChanged(CharSequence s, int start, int count, int after) {
  }

  public void onTextChanged(CharSequence s, int start, int before, int count) {
  }

  // LocationListener methods
  public void onLocationChanged(Location location) { // 10
    this.location = location;
  }

  public void onProviderDisabled(String provider) { // 11
    if (this.provider.equals(provider))
      locationManager.removeUpdates(this);
  }

  public void onProviderEnabled(String provider) { // 12
    if (this.provider.equals(provider))
      locationManager.requestLocationUpdates(this.provider, LOCATION_MIN_TIME,
          LOCATION_MIN_DISTANCE, this);
  }

  public void onStatusChanged(String provider, int status, Bundle extras) { // 13
  }

}

1

StatusActivity now implements LocationListener, the interface for callbacks from the Location manager.

2

Here we define local variables for the Location manager, Location, and our provider.

3

We get the provider from the Yamba application object, as we explained earlier. And ultimately, the user chooses the provider in the preferences.

4

We check whether the user wants us to provide her location information at all.

5

If we pass that test, we get the location information via getSystemService(). This call is relatively inexpensive even if it happens every time the method runs, because we’re just getting a reference to an already running system service.

6

Get the cached location, if the Location manager has it.

7

Register with the Location manager to receive location updates. Here, we get to specify how often we’d like to get notifications and for what kind of change in location. In our example, we care only about the general vicinity at a city level, we set these values to 1,000 meters (one kilometer) and 3,600,000 milliseconds (one hour). Note that this is just a hint to the system.

8

When this activity is no longer visible, we unregister from the Location manager and no longer receive any updates to save battery.

9

Once the user is about to update her status, we check whether we have a location. If we do, we pack it into required double array and pass it on to setMyLocation() in the Twitter object that Yamba has.

10

Now we implement the methods that Location manager calls. onLocationChanged() is called whenever there’s a change in location and provides us with the actual new Location object.

11

This method is called when the provider is no longer available. We can simply remove any updates so that we don’t waste battery.

12

When the provider we care about is made available, we can request location updates again.

13

This method is called when there’s a change with the provider in general. In this case, we ignore it.

At this point our Yamba application supports location updates. User can set preferences to indicate what location provider to use, if any.

Next we’re going to see another system service: this time the Alarm service, which we’ll use to trigger an Intent service.

Intent Service

Now that we understand how system services work, we can use another service concept to substantially simplify our Updater service. If you recall, our Updater service is an always-on, always-running service that periodically goes to the cloud and pulls down the latest timeline updates. Since by default a service runs in the same thread as the User Interface (i.e., runs on the UI thread), we had to create a separate thread called Updater within the Updater service that is responsible for the actual network connection. We then started this thread in the service’s onCreate() and onStartCommand() methods. We ran it forever until onDestroy() got called. However, our Updater thread would sleep between the updates for some amount of time. All this worked well and we explained it all in Chapter 8, Services. But there’s a simpler way to accomplish this task.

An IntentService is a subclass of Service and is also activated by a startService() intent. It differs from a regular service by running on its own worker thread, so it doesn’t block our precious UI thread. Also, once it’s done, it’s done. This means it runs only once, but we will use an Alarm later to run it periodically. Any call to the intent’s startService() will recreate it.

Unlike a regular service, we don’t override onCreate(), onStartCommand(), onDestroy(), and onBind(), but rather a new onHandleIntent() method. This method is where we want to put our code that goes online and handles the network updates. Also, unlike a regular Service, an IntentService has a default constructor that needs to be provided.

In short, instead of creating a separate thread and delaying network updates in regular Service, we can simplify our code by using an IntentService to run status updates on its worker thread. Now we just need something to periodically wake up our IntentService so it knows it needs to handle the updating job. For that, we’ll use the Alarm manager, another system service.

The key to Intent Services is the onHandleIntent() method, a block of code that will run on a separate thread.

Example 13.9. UpdaterService.java based on IntentService

package com.marakana.yamba8;

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

public class UpdaterService1 extends IntentService { // 1
  private static final String TAG = "UpdaterService";

  public static final String NEW_STATUS_INTENT = "com.marakana.yamba.NEW_STATUS";
  public static final String NEW_STATUS_EXTRA_COUNT = "NEW_STATUS_EXTRA_COUNT";
  public static final String RECEIVE_TIMELINE_NOTIFICATIONS = "com.marakana.yamba.RECEIVE_TIMELINE_NOTIFICATIONS";

  public UpdaterService1() { // 2
    super(TAG);

    Log.d(TAG, "UpdaterService constructed");
  }

  @Override
  protected void onHandleIntent(Intent inIntent) { // 3
    Intent intent;
    Log.d(TAG, "onHandleIntent'ing");
    YambaApplication yamba = (YambaApplication) getApplication();
    int newUpdates = yamba.fetchStatusUpdates();
    if (newUpdates > 0) { // 4
      Log.d(TAG, "We have a new status");
      intent = new Intent(NEW_STATUS_INTENT);
      intent.putExtra(NEW_STATUS_EXTRA_COUNT, newUpdates);
      sendBroadcast(intent, RECEIVE_TIMELINE_NOTIFICATIONS);
    }
  }
}

1

We now subclass IntentService instead of its parent Service.

2

A default constructor is needed. This a good place to give your service a name, which can be relevant for example in TraceView, to be able to identify various threads.

3

This is the key method. The work inside of it takes place on a separate worker thread and doesn’t interfere with the main UI thread.

4

The rest of the code in this section broadcasts the change as described in the section called “Broadcasting Intents”.

At this point, our service is updated. An easy way to test it would be to change the Start/Stop Service menu item to a Refresh button. To do that, update your menu.xml file to have this new item, and change its handling in our BaseActivity class.

Example 13.10. res/xml/menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
  ...
  <item android:title="@string/titleRefresh" android:id="@+id/itemRefresh"
    android:icon="@android:drawable/ic_menu_rotate"></item>
</menu>

I’ve replaced itemToggle with itemRefresh so that the names make more sense. We must also add the appropriate string to the strings.xml file.

Now, we need to update our BaseActivity.java file to handle this new Refresh button. To do that, we change the appropriate case statement in onOptionsItemSelected(). Additionally, we can now remove onMenuOpened() altogether, since we no longer need to change the state of that toggle button—it doesn’t exist any more.

Example 13.11. BaseActivity.java with support for Refresh button

public class BaseActivity extends Activity {
  ...
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {

    switch (item.getItemId()) {
    ...
    case R.id.itemRefresh:
      startService(new Intent(this, UpdaterService.class)); // 1
      break;
    ...
    }
    return true;
  }
  ...
}

1

We simply fire off an intent to start our Updater service.

So our options menu now has a Refresh button that will start a service and have it update the status data in the background. We can use this button to test whether this feature works well.

Another way to accomplish the same same functionality would have been to use an AsyncTask. In fact, AsyncTask would probably have been slightly more appropriate in this case from a design point of view, to keep all the functionality at the UI level, but we’ve already discussed it in the section called “Threading in Android”. In this case, we wanted to demonstrate quickly how an IntentService is started, and as you can see, it works just like any other service.

Next we want to have our Updater service triggered periodically. To do that, we’ll use the Alarm manager.

Alarms

The previous reincarnation of our Updater service had a regular service that was always running in a loop, where it pulled network updates, then slept for some amount of time and and looped again. With IntentService, we turned the process around. Our Updater service now runs only once when fired up by the startService() intent. So, now we need a way to have something fire these intents every so often.

Android comes with yet another system service just for that. The Alarm service, represented by the AlarmManager class, lets you schedule certain things to happen at certain times. The time can be recurring, so that makes it easy to have our service be started every so often. And the event that happens is an intent, or more precisely a PendingIntent.

Pending Intents

A PendingIntent is a combination of an intent with an action to be executed on it. This is typically used for future intents that you are passing to someone else. Create a Pending Intent via one of the static methods in the PendingIntent class. Since there are only handful of ways to send an intent, there are only a handful of static methods to create Pending Intents along with their actions. If you recall, you typically use an intent to start an activity via startActivity(), start a service via startService(), or send a broadcast via sendBroadcast(). So to create a Pending intent that will execute startService() with our intent in the future, we call the getService() static method.

Now that we know how to leave an intent for someone to execute later and how to tell an Alarm service to do that over and over periodically, we need to choose where to implement this feature. One good place is our existing Boot Receiver. But before we do that, we’ll add another preference option to our preferences

Adding Interval to Preferences

Example 13.12. strings.xml with arrays for interval options

<?xml version="1.0" encoding="utf-8"?>
<resources>
  ...

  <!-- 1 -->
  <string-array name="interval">
    <item>Never</item>
    <item>Fifteen minutes</item>
    <item>Half hour</item>
    <item>An hour</item>
    <item>Half day</item>
    <item>Day</item>
  </string-array>

  <!-- 2 -->
  <string-array name="intervalValues">
    <item>0</item>
    <item>900000</item>
    <item>1800000</item>
    <item>3600000</item>
    <item>43200000</item>
    <item>86400000</item>
  </string-array>
</resources>

1

These will be the names of options that show up in the list.

2

These will be their corresponding values.

Now that we have these arrays, we can update prefs.xml to add our list of intervals.

Example 13.13. prefs.xml with support for interval preference setting

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
  ...
  <!-- 1 -->
  <ListPreference android:entryValues="@array/intervalValues"
    android:summary="@string/summaryUpdaterInterval"
    android:title="@string/titleUpdaterInterval"
    android:entries="@array/interval" android:key="interval" />
</PreferenceScreen>

1

This is the list preference. It shows as a list of entities as represented by android:entities. The value associated with it comes from android:entityValues.

Now we are ready to update the Boot receiver and add the Alarm service alarms.

Updating Boot Receiver

If you recall from the section called “BootReceiver”, a Boot Receiver wakes up every time the device is booted up. So far, our Boot Receiver just starts our Updater service. That was fine when the Updater service was always on and running. But now, it would cause only a one-time execution of the Updater.

What we can do instead is use the Alarm service to periodically fire intents to start our Updater service. To do that, we’ll get the reference to the Alarm manager, create a Pending intent to be started each time, and set up the interval at which to start the updates. As our Pending intent is to start a service, we’ll use PendingIntent.getService() call, as described in the section called “Pending Intents”.

Example 13.14. BootReceiver.java updated with Alarm service calls to periodically start Updater service

package com.marakana.yamba8;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class BootReceiver extends BroadcastReceiver {

  @Override
  public void onReceive(Context context, Intent callingIntent) {

    // Check if we should do anything at boot at all
    long interval = ((YambaApplication) context.getApplicationContext())
        .getInterval(); // 1
    if (interval == YambaApplication.INTERVAL_NEVER)  // 2
      return;

    // Create the pending intent
    Intent intent = new Intent(context, UpdaterService.class);  // 3
    PendingIntent pendingIntent = PendingIntent.getService(context, -1, intent,
        PendingIntent.FLAG_UPDATE_CURRENT); // 4

    // Setup alarm service to wake up and start service periodically
    AlarmManager alarmManager = (AlarmManager) context
        .getSystemService(Context.ALARM_SERVICE); // 5
    alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, System
        .currentTimeMillis(), interval, pendingIntent); // 6

    Log.d("BootReceiver", "onReceived");
  }

}

1

Our Yamba application has a simple getter method to return the value of the interval preference.

2

We check user’s preference to set how often to check for network updates. A value of INTERVAL_NEVER (zero) means not to check for updates at all.

3

This is the intent that will run to start our Updater service.

4

Here we wrap that intent with the action to start a service and get a new Pending intent. The value of -1 is for a request code that is currently not being used. The flag in the final argument indicates that whether this intent already exists. We need just to update it and not recreate it.

5

We get the reference to AlarmManager via the usual getSystemService() call.

6

setInexactRepeating() specifies that we’d like this pending intent to be sent repeatedly, but we’re not concerned with being exactly on time. The ELAPSED_REALTIME flag will keep the alarm from waking up the phone just to run the updates. The other parameters are the current time as the start time for this alarm, our desired interval, and the actual Pending intent to execute when the alarm runs.

You can now install our application on a device (and thus install the updated Boot Receiver), then reboot the device. Once the device starts, the logcat should indicate that the Boot receiver ran and started the Updater service by posting a Pending intent to the Alarm service.

Sending Notifications

Here’s an opportunity to introduce yet another system service—this time the Notification service. We worked hard to have our Updater service run in the background and get the latest status updates, but what’s the point of all this work if the user is not made aware that there’s something new to look at? A standard Android UI approach to this would be to post a notification to the notification bar up at the top of the screen. To do that, we use Notification system service.

We’re going to make the Updater service responsible for posting the notifications, since it is the part of the app that knows of new statuses in the first place. To do that, we’ll get the reference to the system Notification service, create a new Notification object, and update it with the latest information. The notification itself will contain a Pending Intent so that when the user clicks on it, it takes the user to Timeline Activity to view the latest status updates.

Example 13.15. UpdaterService.java with Notifications

package com.marakana.yamba8;

import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.util.Log;

public class UpdaterService extends IntentService {
  private static final String TAG = "UpdaterService";

  public static final String NEW_STATUS_INTENT = "com.marakana.yamba.NEW_STATUS";
  public static final String NEW_STATUS_EXTRA_COUNT = "NEW_STATUS_EXTRA_COUNT";
  public static final String RECEIVE_TIMELINE_NOTIFICATIONS = "com.marakana.yamba.RECEIVE_TIMELINE_NOTIFICATIONS";

  private NotificationManager notificationManager; // 1
  private Notification notification; // 2

  public UpdaterService() {
    super(TAG);

    Log.d(TAG, "UpdaterService constructed");
  }

  @Override
  protected void onHandleIntent(Intent inIntent) {
    Intent intent;
    this.notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); // 3
    this.notification = new Notification(android.R.drawable.stat_notify_chat,
        "", 0); // 4

    Log.d(TAG, "onHandleIntent'ing");
    YambaApplication yamba = (YambaApplication) getApplication();
    int newUpdates = yamba.fetchStatusUpdates();
    if (newUpdates > 0) {
      Log.d(TAG, "We have a new status");
      intent = new Intent(NEW_STATUS_INTENT);
      intent.putExtra(NEW_STATUS_EXTRA_COUNT, newUpdates);
      sendBroadcast(intent, RECEIVE_TIMELINE_NOTIFICATIONS);
      sendTimelineNotification(newUpdates); // 5
    }
  }

  /**
   * Creates a notification in the notification bar telling user there are new
   * messages
   *
   * @param timelineUpdateCount
   *          Number of new statuses
   */
  private void sendTimelineNotification(int timelineUpdateCount) {
    Log.d(TAG, "sendTimelineNotification'ing");
    PendingIntent pendingIntent = PendingIntent.getActivity(this, -1,
        new Intent(this, TimelineActivity.class),
        PendingIntent.FLAG_UPDATE_CURRENT); // 6
    this.notification.when = System.currentTimeMillis(); // 7
    this.notification.flags |= Notification.FLAG_AUTO_CANCEL; // 8
    CharSequence notificationTitle = this
        .getText(R.string.msgNotificationTitle); // 9
    CharSequence notificationSummary = this.getString(
        R.string.msgNotificationMessage, timelineUpdateCount);
    this.notification.setLatestEventInfo(this, notificationTitle,
        notificationSummary, pendingIntent); // 10
    this.notificationManager.notify(0, this.notification);
    Log.d(TAG, "sendTimelineNotificationed");
  }

}

1

This is just our local reference to the NotificationManager class, which is our access to the Notification system service.

2

We create a class-global Notification object and update it each time there’s something new to notify our listeners about.

3

We obtain the reference to the Notification service via the usual getSystemService() call.

4

We create the notification object that we’ll reuse later. For now, we just specify the standard icon to use with our notification, and leave the text and timestamp to be updated later when we are about to post this notification.

5

We call our private sendTimelineNotification() method once we know there are new statuses to notify the user of.

6

This Pending intent will be kicked off when user checks the notification in the notification bar and clicks on the actual notification item. In this case, we want to take the user to the Timeline activity, so we create an intent for that.

7

We’re now updating the data for the most recent notification. This is the timestamp when it happened.

8

This flag tells the Notification manager to cancel this notification as soon as the user clicks on it. The notification will be removed from the notification bar at that point.

9

Here, we get the title and summary to go into the notification from our strings.xml file. Notice that the summary has parameters, so that we can use String.format() to update the actual number of new statuses.

10

Finally, we tell the Notification manager to post this notification. In this case, we do not need the ID, so we specify zero. An ID can be used to refer to a notification later, usually in order to cancel it.

At this point our application is complete, yet again. We now have a way to notify the user of any new status updates so the user can stay on top of what it going on in the world.

Summary

At this point you have seen a few system services: Sensor, Location, Alarm, and Notification. There are a few more services provided by Android. You may have noticed that most of them have a lot of similarities, an hopefully you have started extrapolating certain patterns. We have also used this chapter to somewhat simplify our Updater service and introduce IntentServices and PendingIntents.

Site last updated on: April 8, 2011 at 12:51:47 PM PDT
Cover for Learning Android

View 1 comment

  1. Frank Maker – Posted Jan. 4, 2011

    You don't need an API key for the geocoder?

Add a comment

View 1 comment

  1. Bill Schrickel – Posted Jan. 17, 2011

    It would be nice to include a screen shot of what the final application should look like.

Add a comment

View 1 comment

  1. Isaac Liu – Posted Sept. 6, 2011

    In this implementation if the user turns on location and then decides the turn it off, wouldn't a stale location still be sent because the location variable isn't cleared?

    Also, in this code there is a bug. If the user chooses a network provider first, then goes back to preferences to change it to none, the program will crash because in onResume() the locationManager will not be null (set from the first preference change), but the provider will be NONE (set from the second change), so an exception will be thrown on requestLocationUpdates for invalid arguments (can't pass in NONE).

    both issues can be fixed by the following code: if (!YambaApplication.LOCATION_PROVIDER_NONE.equals(provider)) { // locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); // } else { locationManager = null; location = null; }

    Edited on September 6, 2011, 6:38 p.m. PDT

Add a comment

View 1 comment

  1. Bill Schrickel – Posted Jan. 17, 2011

    "handle the updating job"

Add a comment

View 1 comment

  1. Isaac Liu – Posted Sept. 6, 2011

    There is major bug in this code:

    alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, System .currentTimeMillis(), interval, pendingIntent);

    The alarm type should be AlarmManager.RTC instead of ELAPSED_REALTIME, because RTC tells the alarmManager to trigger at ms since boot, which is what System.currentTimeMillis() returns. ELAPSED_REALTIME tells it the start triggering at realtime, in which case SystemClock.elapsedRealtime() should be used. The current code will not trigger an alarm

Add a comment

View 1 comment

  1. Bill Schrickel – Posted Jan. 17, 2011

    Again, a screen shot of the notification would be nice.

Add a comment