Chapter 36

Basic Service Patterns

Now that you have seen the pieces that make up services and their clients, let’s examine a few scenarios that employ services and how those scenarios might be implemented.

The Downloader

If you elect to download something from the Android Market, after the download begins, you are free to exit the Market application entirely. This does not cancel the download—the download and installation run to completion, despite no Android Market activity being shown onscreen.

You may have a similar scenario in your application. Perhaps you want to enable users to download a purchased e-book, download a map for a game, download a file from a “drop box” file-sharing service, or download some other type of material, and you want to allow them to exit the application while the download is taking place in the background.

Android 2.3 introduced the DownloadManager (covered in Chapter 34), which could handle that functionality for you. However, you might need that sort of capability on older versions of Android, at least through 2011. Therefore, this section introduces a downloader that you can incorporate into your application to support earlier versions of Android. The sample project reviewed in this section is Services/Downloader.

The Design

This sort of situation is a perfect use for the command pattern and an IntentService. The IntentService has a background thread, so downloads can take as long as needed. An IntentService will automatically shut down when the work is done, so the service will not linger and you do not need to worry about shutting it down yourself. Your activity can simply send a command via startService() to the IntentService to tell it to go do the work.

Admittedly, things get a bit trickier when you want to have the activity find out when the download is complete. This example will show how to use Messenger for this purpose.

The Service Implementation

Here is the implementation of this IntentService, named Downloader:

package com.commonsware.android.downloader;

import android.app.Activity;
import android.app.IntentService;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.os.Message;
import android.os.Messenger;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;

public class Downloader extends IntentService {
  public static final String
EXTRA_MESSENGER="com.commonsware.android.downloader.EXTRA_MESSENGER";
  private HttpClient client=null;

  public Downloader() {
    super("Downloader");
  }

  @Override
  public void onCreate() {
    super.onCreate();

    client=new DefaultHttpClient();
  }

  @Override
  public void onDestroy() {
    super.onDestroy();

    client.getConnectionManager().shutdown();
  }

  @Override  
  public void onHandleIntent(Intent i) {
    HttpGet getMethod=new HttpGet(i.getData().toString());
    int result=Activity.RESULT_CANCELED;

    try {
      ResponseHandler<byte[]> responseHandler=new ByteArrayResponseHandler();
      byte[] responseBody=client.execute(getMethod, responseHandler);
      File output=new File(Environment.getExternalStorageDirectory(),
                         i.getData().getLastPathSegment());

      if (output.exists()) {
        output.delete();
      }

      FileOutputStream fos=new FileOutputStream(output.getPath());

      fos.write(responseBody);
      fos.close();
      result=Activity.RESULT_OK;
    }
    catch (IOException e2) {
      Log.e(getClass().getName(), "Exception in download", e2);
    }

    Bundle extras=i.getExtras();

    if (extras!=null) {
      Messenger messenger=(Messenger)extras.get(EXTRA_MESSENGER);
      Message msg=Message.obtain();

      msg.arg1=result;

      try {
        messenger.send(msg);
      }
      catch (android.os.RemoteException e1) {
        Log.w(getClass().getName(), "Exception sending message", e1);
      }
    }
  }
}

In onCreate(), we obtain a DefaultHttpClient object, as was described in Chapter 34. In onDestroy(), we shut down the client. This way, if several download requests are invoked in sequence, we can use a single DefaultHttpClient object. The IntentService will shut down only after all enqueued work has been completed.

The bulk of the work is accomplished in onHandleIntent(), which is called on the IntentService, on a background thread, every time startService() is called. For the Intent, we obtain the URL of the file to download via a call to getData() on the supplied Intent. Actually downloading the file uses the DefaultHttpClient object, along with an HttpGet object. However, since the file might be binary (e.g., MP3) instead of text, we cannot use a BasicResponseHandler. Instead, we use a ByteArrayResponseHandler, which is a custom ResponseHandler cloned from the source for BasicResponseHandler, but one that returns a byte[] instead of a String:

package com.commonsware.android.downloader;

import java.io.IOException;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.HttpResponseException;
import org.apache.http.util.EntityUtils;

public class ByteArrayResponseHandler implements ResponseHandler<byte[]> {
  public byte[] handleResponse(final HttpResponse response)
                  throws IOException, HttpResponseException {
    StatusLine statusLine=response.getStatusLine();

    if (statusLine.getStatusCode()>=300) {
      throw new HttpResponseException(statusLine.getStatusCode(),
                                       statusLine.getReasonPhrase());
    }

    HttpEntity entity=response.getEntity();

    if (entity==null) {
      return(null);
    }

    return(EntityUtils.toByteArray(entity));
  }
}

Once the file is downloaded to external storage, we need to alert the activity that the work is completed. If the activity is interested in this sort of message, it will have attached a Messenger object as EXTRA_MESSENGER to the Intent. Downloader gets the Messenger, creates an empty Message object, and puts a result code in the arg1 field of the Message. It then sends the Message to the activity. If the activity was destroyed before this point, the request to send the message will fail with a RemoteObjectException.

Since this is an IntentService, it will automatically shut down when onHandleIntent() completes, if there is no more work queued to be completed.

Using the Service

The activity demonstrating the use of Downloader has a trivial UI, consisting of one large button:

<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/button"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:text="Do the Download"
  android:onClick="doTheDownload"
/>

That UI is initialized in onCreate(), as usual:

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

  b=(Button)findViewById(R.id.button);
}

When the user clicks the button, doTheDownload() is called to disable the button (to prevent accidental duplicate downloads) and call startService():

public void doTheDownload(View v) {
  b.setEnabled(false);

  Intent i=new Intent(this, Downloader.class);

  i.setData(Uri.parse("http://commonsware.com/Android/excerpt.pdf"));
  i.putExtra(Downloader.EXTRA_MESSENGER, new Messenger(handler));

startService(i);
}

Here, the Intent we pass over has the URL of the file to download (in this case, a URL pointing to a PDF), plus a Messenger in the EXTRA_MESSENGER extra. That Messenger is created with an attachment to the activity’s Handler:

private Handler handler=new Handler() {
  @Override
  public void handleMessage(Message msg) {
    b.setEnabled(true);

    Toast
      .makeText(DownloaderDemo.this, "Download complete!",
                Toast.LENGTH_LONG)
      .show();
  }
};

If the activity is still around when the download is complete, the Handler enables the button and displays a Toast to let the user know that the download is complete. Note that the activity ignores the result code supplied by the service, though in principle it could do something different in both the success and failure cases.

The Music Player

Most audio player applications in Android—for music, audiobooks, or whatever—do not require the user to remain in the player application itself to keep it running. Rather, the user can go on and do other things with their device, with the audio playing in the background. This is similar in many respects to the download scenario from the previous section. However, in this case, the user is the one who controls when the work (playing audio) ends.

The sample project reviewed in this section is Services/FakePlayer.

The Design

Once again, we will use startService(), since we want the service to run even after the activity that started it has been destroyed. However, this time we will use a regular Service rather than an IntentService. An IntentService is designed to do work and stop itself, whereas in this case we want the user to be able to stop the music playback.

Since music playback is outside the scope of this book, the service will simply stub out those particular operations.

The Service Implementation

Here is the implementation of this Service, named PlayerService:

package com.commonsware.android.fakeplayer;

import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

public class PlayerService extends Service {
  public static final String EXTRA_PLAYLIST="EXTRA_PLAYLIST";
  public static final String EXTRA_SHUFFLE="EXTRA_SHUFFLE";
  private boolean isPlaying=false;

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    String playlist=intent.getStringExtra(EXTRA_PLAYLIST);
    boolean useShuffle=intent.getBooleanExtra(EXTRA_SHUFFLE, false);

    play(playlist, useShuffle);      

    return(START_NOT_STICKY);
  }

  @Override
  public void onDestroy() {
    stop();
  }

  @Override
  public IBinder onBind(Intent intent) {
    return(null);
  }

  private void play(String playlist, boolean useShuffle) {
    if (!isPlaying) {
      Log.w(getClass().getName(), "Got to play()!");
      isPlaying=true;
    }
  }

  private void stop() {
    if (isPlaying) {
      Log.w(getClass().getName(), "Got to stop()!");
      isPlaying=false;
    }
  }
}

In this case, we really do not need anything for onCreate(), so we skip that lifecycle method. On the other hand, we have to implement onBind(), because that is a required method of Service subclasses. IntentService implements onBind() for us, which is why that was not needed for the Downloader sample.

When the client calls startService(), onStartCommand() is called in PlayerService. Here, we get the Intent and pick out some extras to tell us what to play back (EXTRA_PLAYLIST) and other configuration details (e.g., EXTRA_SHUFFLE). onStartCommand() calls play(), which simply flags that our player is playing and logs a message to LogCat—a real music player would use MediaPlayer to start playing the first song in the playlist. onStartCommand() returns START_NOT_STICKY, indicating that if Android has to kill off this service (e.g., due to low memory), it should not restart it after conditions improve.

onDestroy() stops the music from playing (theoretically, anyway) by calling a stop() method. Once again, this just logs a message to LogCat and updates our internal are-we-playing flag.

In Chapter 37, which discusses notifications, we will revisit this sample and discuss the use of startForeground(), which makes it easier for the user to get back to the music player and lets Android know that the service is delivering part of the foreground experience and therefore should not be shut down.

Using the Service

The UI of the FakePlayer activity demonstrating the use of PlayerService is more complex than the UI in the previous sample, consisting of two large buttons:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
  <Button
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
      android:layout_weight="1"
      android:text="Start the Player"
      android:onClick="startPlayer"
      />
  <Button
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
      android:layout_weight="1"
      android:text="Stop the Player"
      android:onClick="stopPlayer"
      />
</LinearLayout>

The activity itself is not much more complex:

package com.commonsware.android.fakeplayer;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class FakePlayer extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
  }

  public void startPlayer(View v) {
    Intent i=new Intent(this, PlayerService.class);

    i.putExtra(PlayerService.EXTRA_PLAYLIST, "main");
    i.putExtra(PlayerService.EXTRA_SHUFFLE, true);

    startService(i);
  }

  public void stopPlayer(View v) {
    stopService(new Intent(this, PlayerService.class));
  }
}

The onCreate() method merely loads the UI. The startPlayer() method constructs an Intent with fake values for EXTRA_PLAYLIST and EXTRA_SHUFFLE, and then calls startService(). After you click the top button, you will see the corresponding message in LogCat. Similarly, stopPlayer() calls stopService(), triggering the second LogCat message. Notably, you do not need to keep the activity running in between those button clicks—you can exit the activity by pressing BACK and come back later to stop the service.

The Web Service Interface

If you are going to consume a REST-style web service, you may wish to create a Java client-side API for that service. This allows you to isolate details about the web service (URLs, authorization credentials, etc.) in one place, enabling the rest of your application to use only the published API. If the client-side API might involve state, such as a session ID or cached results, you may wish to use a service to implement the client-side API. In this case, the most natural form of service would be one that publishes a Binder, so clients can call a “real” API, that the service translates into HTTP requests.

In this case, we want to create a client-side Java API for the National Weather Service’s forecast web service, so we can get a weather forecast (timestamps, projected temperatures, and projected precipitation) for a given latitude and longitude (for geographic locations in the United States). As you may recall, we examined this web service in Chapter 34.

The sample project reviewed in this section is Services/WeatherAPI.

The Design

To use the binding pattern, we will need to expose an API from a “binder” object. Since the weather forecast arrives in a singularly awful XML structure, we will make the binder be responsible for parsing the XML. Hence, the binder will have a getForecast() method to get us an ArrayList of Forecast objects, each Forecast representing one timestamp/temperature/precipitation triple.

Once again, to supply the latitude and longitude of the forecast roster to retrieve, we will use a Location object, which will be obtained from GPS. This part of the sample will be described in greater detail in Chapter 39.

Since the web service call may take a while, it is unsafe to do this on the main application thread. In this sample, we will have the service use an AsyncTask to call our weather API, so the activity largely can be ignorant of threading issues.

The Rotation Challenge

Chapter 20 noted the issues involved with orientation changes (or other configuration changes) and background threads in activities. The solution given was to use onRetainNonConfigurationInstance() with a static inner class AsyncTask implementation and manually associate it with the new, post-configuration-change activity.

That same problem crops up with the binding pattern as well, which is one of the reasons binding is difficult to use. If we bind to a service from an activity, that binding will not magically pass to the new activity instance after an orientation change. Instead, we need to do two things:

  • Bind to the service not by using the activity as the Context, but rather by using getApplicationContext(), as that Context is one that will live for the lifetime of our process
  • Pass the ServiceConnection representing this binding from the old activity instance to the new one as part of the configuration change

To accomplish the second feat, we will need to use the same onRetainNonConfigurationInstance() trick that we used with threads in Chapter 20.

The Service Implementation

Our service-side logic is broken into three classes, Forecast, WeatherBinder, and WeatherService, plus one interface, WeatherListener.

The Forecast

The Forecast class merely encapsulates the three pieces of the forecast data triple—the timestamp, the temperature, and the icon indicating the expected precipitation (if any):

package com.commonsware.android.weather;

class Forecast {
  String time="";
  Integer temp=null;
  String iconUrl="";

  String getTime() {
    return(time);
  }

  void setTime(String time) {
    this.time=time.substring(0,16).replace('T', ' '),
  }

  Integer getTemp() {
    return(temp);
  }

  void setTemp(Integer temp) {
    this.temp=temp;
  }

  String getIcon() {
    return(iconUrl);
  }

  void setIcon(String iconUrl) {
    this.iconUrl=iconUrl;
  }
}
The Interface

Because we are going to fetch the actual weather forecast on a background thread in the service, we have a slight API challenge—calls on our binder are synchronous. Hence, we cannot have a getForecast() method that returns our forecast. Rather, we need to provide some way for the service to get the forecast back to our activity. In this case, we will pass in a listener object (WeatherListener) that the service will use when a forecast is ready:

package com.commonsware.android.weather;

import java.util.ArrayList;

public interface WeatherListener {
  void updateForecast(ArrayList<Forecast> forecast);
  void handleError(Exception e);
}
The Binder

The WeatherBinder extends Binder, a requirement for the local binding pattern. Other than that, the API is up to us.

Hence, we expose three methods:

  • onCreate(): To be called when the WeatherBinder is set up, so we can get a DefaultHttpClient object to use with the web service
  • onDestroy(): To be called when the WeatherBinder is no longer needed, so we can shut down that DefaultHttpClient object
  • getForecast(): The main public API for use by our activity, to kick off the background work to create our ArrayList of Forecast objects when given a Location
package com.commonsware.android.weather;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Bundle;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

public class WeatherBinder extends Binder {
  private String forecast=null;
  private HttpClient client=null;
  private String format=null;

  void onCreate(Context ctxt) {
    client=new DefaultHttpClient();
    format=ctxt.getString(R.string.url);
  }

  void onDestroy() {
    client.getConnectionManager().shutdown();
  }

  void getForecast(Location loc, WeatherListener listener) {
    new FetchForecastTask(listener).execute(loc);
  }

  private ArrayList<Forecast> buildForecasts(String raw) throws Exception {
    ArrayList<Forecast> forecasts=new ArrayList<Forecast>();
    DocumentBuilder builder=DocumentBuilderFactory
                             .newInstance()
                             .newDocumentBuilder();
    Document doc=builder.parse(new InputSource(new StringReader(raw)));
    NodeList times=doc.getElementsByTagName("start-valid-time");

    for (int i=0;i<times.getLength();i++) {
      Element time=(Element)times.item(i);
      Forecast forecast=new Forecast();

      forecasts.add(forecast);
      forecast.setTime(time.getFirstChild().getNodeValue());
    }

    NodeList temps=doc.getElementsByTagName("value");

    for (int i=0;i<temps.getLength();i++) {
      Element temp=(Element)temps.item(i);
      Forecast forecast=forecasts.get(i);

      forecast.setTemp(new Integer(temp.getFirstChild().getNodeValue()));
    }

    NodeList icons=doc.getElementsByTagName("icon-link");

    for (int i=0;i<icons.getLength();i++) {
      Element icon=(Element)icons.item(i);
      Forecast forecast=forecasts.get(i);

      forecast.setIcon(icon.getFirstChild().getNodeValue());
    }

    return(forecasts);
  }

  class FetchForecastTask extends AsyncTask<Location, Void, ArrayList<Forecast>> {
    Exception e=null;
    WeatherListener listener=null;

    FetchForecastTask(WeatherListener listener) {
      this.listener=listener;
    }

    @Override
    protected ArrayList<Forecast>doInBackground(Location... locs) {
      ArrayList<Forecast> result=null;

      try {
        Location loc=locs[0];
        String url=String.format(format, loc.getLatitude(),
                                 loc.getLongitude());
        HttpGet getMethod=new HttpGet(url);
        ResponseHandler<String> responseHandler=new BasicResponseHandler();
        String responseBody=client.execute(getMethod, responseHandler);

        result=buildForecasts(responseBody);
      }
      catch (Exception e) {
        this.e=e;
      }

      return(result);
    }

    @Override
    protected void onPostExecute(ArrayList<Forecast> forecast) {
      if (listener!=null) {
        if (forecast!=null) {
          listener.updateForecast(forecast);
        }

        if (e!=null) {
          listener.handleError(e);
        }
      }
    }
  }
}

Most of this code is merely doing the web service request using DefaultHttpClient and an HttpGet object, plus using the DOM parser to convert the XML into the Forecast objects. However, this is wrapped in a FetchForecastTask, an AsyncTask that will do the HTTP operation and parsing on a background thread. In onPostExecute(), the task invokes our WeatherListener, either to supply the forecast (updateForecast()) or to hand over an Exception that was raised (handleError()).

The Service

The WeatherService is fairly short, with the business logic delegated to WeatherBinder:

package com.commonsware.android.weather;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import java.util.ArrayList;

public class WeatherService extends Service {
  private final WeatherBinder binder=new WeatherBinder();

  @Override
  public void onCreate() {
    super.onCreate();

    binder.onCreate(this);
  }

  @Override
  public IBinder onBind(Intent intent) {
    return(binder);
  }

  @Override
  public void onDestroy() {
    super.onDestroy();

    binder.onDestroy();    
  }
}

Our onCreate() and onDestroy() methods delegate to the WeatherBinder, and onBind() returns the WeatherBinder itself.

Using the Service

On the surface, the WeatherDemo activity should be simple:

  • Bind to the service in onCreate()
  • Arrange to get GPS fixes, in the form of Location objects
  • When a fix comes in, use the WeatherBinder to get a forecast, convert it to HTML, and display it in a WebView
  • Unbind from the service in onDestroy()

However, our decision to use the binding pattern and to have the activity deal with the background thread means there is more work involved than those bullet points.

First, here is the full WeatherDemo implementation:

package com.commonsware.android.weather;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.DeadObjectException;
import android.os.RemoteException;
import android.os.IBinder;
import android.util.Log;
import android.webkit.WebView;
import java.util.ArrayList;

public class WeatherDemo extends Activity {
  private WebView browser;
  private LocationManager mgr=null;
  private State state=null;
  private boolean isConfigurationChanging=false;

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

    browser=(WebView)findViewById(R.id.webkit);
    state=(State)getLastNonConfigurationInstance();

    if (state==null) {
      state=new State();
      getApplicationContext()
        .bindService(new Intent(this, WeatherService.class),
                     state.svcConn, BIND_AUTO_CREATE);
    }
    else if (state.lastForecast!=null) {
      showForecast();
    }

    state.attach(this);

    mgr=(LocationManager)getSystemService(LOCATION_SERVICE);
    mgr.requestLocationUpdates(LocationManager.GPS_PROVIDER,
                               3600000, 1000, onLocationChange);
  }

  @Override
  public void onDestroy() {
    super.onDestroy();

    if (mgr!=null) {
      mgr.removeUpdates(onLocationChange);
    }

    if (!isConfigurationChanging) {
      getApplicationContext().unbindService(state.svcConn);
    }
  }

  @Override
  public Object onRetainNonConfigurationInstance() {
    isConfigurationChanging=true;

    return(state);
  }

  private void goBlooey(Throwable t) {
    AlertDialog.Builder builder=new AlertDialog.Builder(this);

    builder
      .setTitle("Exception!")
      .setMessage(t.toString())
      .setPositiveButton("OK", null)
      .show();
  }

  static String generatePage(ArrayList<Forecast> forecasts) {
    StringBuilder bufResult=new StringBuilder("<html><body><table>");

    bufResult.append("<tr><th width="50%">Time</th>"+
                     "<th>Temperature</th><th>Forecast</th></tr>");

    for (Forecast forecast : forecasts) {
      bufResult.append("<tr><td align="center">");
      bufResult.append(forecast.getTime());
      bufResult.append("</td><td align="center">");
      bufResult.append(forecast.getTemp());
      bufResult.append("</td><td><img src="");
      bufResult.append(forecast.getIcon());
      bufResult.append(""></td></tr>");
    }

    bufResult.append("</table></body></html>");

    return(bufResult.toString());
  }

  void showForecast() {
    browser.loadDataWithBaseURL(null, state.lastForecast,
                                 "text/html", "UTF-8", null);
  }

  LocationListener onLocationChange=new LocationListener() {
    public void onLocationChanged(Location location) {
      if (state.weather!=null) {
        state.weather.getForecast(location, state);
      }
      else {
        Log.w(getClass().getName(), "Unable to fetch forecast – no WeatherBinder");
      }
    }

    public void onProviderDisabled(String provider) {
      // required for interface, not used
    }

    public void onProviderEnabled(String provider) {
      // required for interface, not used
    }

    public void onStatusChanged(String provider, int status,
                                 Bundle extras) {
      // required for interface, not used
    }
  };

  static class State implements WeatherListener {
    WeatherBinder weather=null;
    WeatherDemo activity=null;
    String lastForecast=null;

    void attach(WeatherDemo activity) {
      this.activity=activity;
    }

    public void updateForecast(ArrayList<Forecast> forecast) {
      lastForecast=generatePage(forecast);
      activity.showForecast();
    }

    public void handleError(Exception e) {
      activity.goBlooey(e);
    }

    ServiceConnection svcConn=new ServiceConnection() {
      public void onServiceConnected(ComponentName className,
                                   IBinder rawBinder) {
        weather=(WeatherBinder)rawBinder;
      }

      public void onServiceDisconnected(ComponentName className) {
        weather=null;
      }
    };
  }
}

Now, let’s look at the highlights of the service connection and the background thread.

Managing the State

We need to ensure that our ServiceConnection can be passed between activity instances on a configuration change. Hence, we have a State static inner class to hold that, plus two other bits of information: the Activity the state is associated with, and a String showing the last forecast we retrieved:

static class State implements WeatherListener {
  WeatherBinder weather=null;
  WeatherDemo activity=null;
  String lastForecast=null;

  void attach(WeatherDemo activity) {
    this.activity=activity;
  }

  public void updateForecast(ArrayList<Forecast> forecast) {
    lastForecast=generatePage(forecast);
    activity.showForecast();
  }

  public void handleError(Exception e) {
    activity.goBlooey(e);
  }

  ServiceConnection svcConn=new ServiceConnection() {
    public void onServiceConnected(ComponentName className,
                                 IBinder rawBinder) {
      weather=(WeatherBinder)rawBinder;
    }

    public void onServiceDisconnected(ComponentName className) {
      weather=null;
    }
  };
}

The lastForecastString allows us to redisplay the generated HTML after a configuration change. Otherwise, when the user rotates the screen, we would lose our forecast (held only in the old instance’s WebView) and would have to either retrieve a fresh one or wait for a GPS fix.

We return this State object from onRetainNonConfigurationInstance():

@Override
public Object onRetainNonConfigurationInstance() {
  isConfigurationChanging=true;

  return(state);
}

In onCreate(), if there is no nonconfiguration instance, we create a fresh State and bind to the service, since we do not have a service connection at present. On the other hand, if onCreate() gets a State from getLastNonConfigurationInstance(), it simply holds onto that state and reloads our forecast in the WebView. In either case, onCreate() indicates to the State that the new activity instance is the current one:

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

  browser=(WebView)findViewById(R.id.webkit);
  state=(State)getLastNonConfigurationInstance();

  if (state==null) {
    state=new State();
getApplicationContext()
      .bindService(new Intent(this, WeatherService.class),
                    state.svcConn, BIND_AUTO_CREATE);
  }
  else if (state.lastForecast!=null) {
    showForecast();
  }

  state.attach(this);

  mgr=(LocationManager)getSystemService(LOCATION_SERVICE);
  mgr.requestLocationUpdates(LocationManager.GPS_PROVIDER,
                             3600000, 1000, onLocationChange);
}

Depending on the subjective importance of your weather forecasting, you might decide that your application and its service aren’t necessarily the most important thing running on the device. Recall that the previous chapter highlighted the BIND_ALLOW_OOM_MANAGEMENT parameter, new to Android 4.0, for bindService(). Weather forecasting is probably not as important as maintaining a phone call, so we can opt to modify our bindService() call in the preceding onCreate() method to volunteer for OOM memory reclaim (and process destruction) in the event of low memory:

bindService(new Intent(this, WeatherService.class),
                    state.svcConn, BIND_AUTO_CREATE |
                    BIND_ALLOW_OOM_MANAGEMENT );

This does not affect the rest of our logic.

Time to Unbind

We bind to the service when onCreate() is called, if it did not receive a State via getLastNonConfigurationInstance() (in which case, we are already bound). This begs the question: when do we unbind from the service?

We want to unbind when the activity is being destroyed, but not if the activity is being destroyed because of a configuration change.

Unfortunately, there is no built-in way to make that determination from onDestroy(). There is an isFinishing() method we can call on an Activity, which will return true if the activity is going away for good or false otherwise. This does return false for a configuration change, but it will also return false if the activity is being destroyed to free up RAM and the user might be able to return to it via the Back button.

This is why onRetainNonConfigurationInstance() flips an isConfigurationChanging flag in WeatherDemo to true. That flag is initially false. We then check that flag to see whether or not we should unbind from the service:

@Override
public void onDestroy() {
  super.onDestroy();

  if (mgr!=null) {
    mgr.removeUpdates(onLocationChange);
  }

  if (!isConfigurationChanging) {
    getApplicationContext().unbindService(state.svcConn);
  }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.209.184