Communicating with Services

In all of our dealings with Services so far, we have initiated work by invoking startService with an Intent, but that isn't our only option. If our Service is designed to only be used locally from within our own application process, we can take significant shortcuts and work with Service just as we do with any other Java object.

Direct communication with local Services

To create a Service that we can interact with directly, we must implement the onBind method that we previously ignored and from which we returned null. This time, we'll return an implementation of IBinder that provides direct access to the Service it binds. We'll always return the same IBinder as shown in the following code:

public class LocalPrimesService extends Service {
  public class Access extends Binder {
    public LocalPrimesService getService() {
      return LocalPrimesService.this;
    }
  };
  private final Access binder = new Access();

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

An Activity or Fragment that wants to directly interact with this Service needs to bind to it using the bindService method and supply a ServiceConnection to handle the connect/disconnect callbacks.

Services such as LocalPrimesService that are started by a client binding to them are known as "bound" Services, and stop themselves automatically when all clients have unbound. By contrast, our ConcurrentIntentService is a "started" Service. A Service can be both "bound" and "started," but a Service that is explicitly started must also be explicitly stopped.

The ServiceConnection implementation simply casts the IBinder it receives to the concrete class defined by the Service, obtains a reference to the Service, and records it in a field of the Activity.

public class LocalPrimesActivity extends Activity {
  private LocalPrimesService service;
  private ServiceConnection connection;

  private class Connection implements ServiceConnection {
    @Override
    public void onServiceConnected(
      ComponentName name, IBinder binder) {
      LocalPrimesService.Access access =
        ((LocalPrimesService.Access)binder);
      service = access.getService();
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
      service = null;
    }
  }
}

We can make the Activity bind and unbind during its onResume and onPause lifecycle methods:

@Override
protected void onResume() {
  super.onResume();
  bindService(
    new Intent(this, LocalPrimesService.class),
    connection = new Connection(),
    Context.BIND_AUTO_CREATE);
}

@Override
protected void onPause() {
  super.onPause();
  unbindService(connection);
}

This is great—once the binding is made, we have a direct reference to the Service instance and can call its methods! However, we didn't implement any methods in our Service yet, so it's currently useless. Let's add a method to LocalPrimesService that calculates the nth prime number in the background using AsyncTask:

public void calculateNthPrime(final int n) {
  new AsyncTask<Void,Void,BigInteger>(){
    @Override
    protected BigInteger doInBackground(Void... params) {
      BigInteger prime = new BigInteger("2");
      for (int i=0; i<n; i++) {
        prime = prime.nextProbablePrime();
      }
      return prime;
    }
    @Override
    protected void onPostExecute(BigInteger result) {
      // todo—communicate result to user
    }
  }.execute();
}

Since we now have a direct object reference to LocalPrimesService in our LocalPrimesActivity, we can go ahead and invoke its calculateNthPrime method directly—taking care to check that the Service is actually bound first, of course.

if (service != null) {
  service.calculateNthPrime(500);
}

This is a very convenient and efficient way of submitting work to a Service—there's no need to package up a request in an Intent, so there's no excess object creation or communication overhead.

Since calculateNthPrime is asynchronous, we can't return a result directly from the method invocation, and LocalPrimesService itself has no user interface, so how can we present results to our user?

One possibility is to pass a callback to LocalPrimesService so that we can invoke methods of our Activity when the background work completes. In LocalPrimesService, we define a callback interface for the Activity to implement:

public interface Callback {
  public void onResult(BigInteger result);
}

There is a serious risk that by passing an Activity into the Service, we'll expose ourselves to memory leaks. The lifecycles of Service and Activity do not coincide, so strong references to an Activity from a Service can prevent it from being garbage collected in a timely fashion.

The simplest way to prevent such memory leaks is to make sure that LocalPrimesService only keeps a weak reference to the calling Activity so that when its lifecycle is complete, the Activity can be garbage collected, even if there is an ongoing calculation in the Service. The modified calculateNthPrime method of LocalPrimesService is shown in the following code:

public void calculateNthPrime(final int n, Callback activity) {
  final WeakReference<Callback> maybeCallback =
    new WeakReference<Callback>(activity);
  new AsyncTask<Void,Void,BigInteger>(){
    @Override
    protected BigInteger doInBackground(Void... params) {
      BigInteger prime = new BigInteger("2");
      for (int i=0; i<n; i++) {
        prime = prime.nextProbablePrime();
      }
      return prime;
    }

    @Override
    protected void onPostExecute(BigInteger result) {
      Callback callback = maybeCallback.get();
      if (callback != null)
        callback.onResult(result);
    }
  }.execute();
}

We invoke the callback on the main thread using onPostExecute, so that PrimesActivity can interact with the user interface directly in the callback method. We can implement the callback as a method of PrimesActivity:

public class PrimesActivity extends Activity
implements LocalPrimesService.Callback {

  @Override
  public void onResult(BigInteger result) {
        // … display the result
  }

  // other methods elided for brevity…
}

Now we can directly invoke methods on LocalPrimesService and return results via a callback method of PrimesActivity by passing the Activity itself as the callback:

if (service != null) {
  service.calculateNthPrime(500, this);
}

This direct communication between PrimesActivity and LocalPrimesService is very efficient and easy to work with. However, there is a downside: if the Activity restarts because of a configuration change, such as a device rotation, the WeakReference to the callback will be garbage collected and LocalPrimesService cannot send the result.

If we want to retain the ability to respond, even across Activity restarts, we'll need a communication channel that can be reattached to our restarted Activity instance.

Earlier, we saw that we can use a Messenger to communicate results from a Service to the originating Activity, even across Activity restarts. When dealing with bound local Services, we can skip Messenger and directly use a Handler:

public void calculateNthPrime(final int n, Handler handler);

This is a very efficient option when we only care about collecting the result in the originating Activity. If we want to allow other parts of the application to also receive the results, we need a different mechanism—broadcasts.

Broadcasting results with Intents

Broadcasting an Intent is a way of sending results to anyone who registers to receive them. This can even include other applications in separate processes if we choose, but if the Activity and Service are a part of the same process, broadcasting is best done using a local broadcast, as this is more efficient and secure.

We can update LocalPrimesService to broadcast its results with just a few extra lines of code. First, let's define two constants to make it easy to register a receiver for the broadcast and extract the result from the broadcast Intent object:

public static final String PRIMES_BROADCAST =   
  "com.packt.PRIMES_BROADCAST";
public static final String RESULT = "nth_prime";

Now we can implement the method that does most of the work using the LocalBroadcastManager to send an Intent object containing the calculated result. We're using the support library class LocalBroadcastManager here for efficiency and security—broadcasts sent locally don't incur the overhead of interprocess communication and cannot be leaked outside of our application.

private void broadcastResult(String result) {
  Intent intent = new Intent(PRIMES_BROADCAST);
  intent.putExtra(RESULT, result);
  LocalBroadcastManager.getInstance(this).
    sendBroadcast(intent);
}

The sendBroadcast method is asynchronous and will return immediately without waiting for the message to be broadcast and handled by receivers. Finally, we invoke our new broadcastResult method from calculateNthPrime:

@Override
public void calculateNthPrime(final int n) {
  new AsyncTask<Void,Void,Void>(){
    @Override
    protected Void doInBackground(Void... params) {
      BigInteger prime = new BigInteger("2");
      for (int i=0; i<n; i++) 
        prime = prime.nextProbablePrime();
        broadcastResult(prime.toString());
        return null;
        }
  }.execute();
}

Great! We're broadcasting the result of our background calculation. Now we need to register a receiver in PrimesActivity to handle the result. Here's how we might define our BroadcastReceiver subclass:

private static class NthPrimeReceiver extends BroadcastReceiver {
  private TextView view;

  @Override
  public void onReceive(Context context, Intent intent) {
    if (view != null) {
      String result = intent.getStringExtra(
        LocalPrimesService.RESULT);
      view.setText(result);
    } else {
      Log.i(TAG, " ignoring - we're detached");
    }
  }

  public void attach(TextView view) {
    this.view = view;
  }
  public void detach() {
    this.view = null;
  }
}

This BroadcastReceiver implementation is quite simple—all it does is extract and display the result from the Intent it receives—basically fulfilling the role of the Handler we used in the previous section.

We only want this BroadcastReceiver to listen for results while our Activity is at the top of the stack and visible in the application, so we'll register and unregister it in the onStart and onStop lifecycle methods. As with the Handler that we used previously, we'll also apply the attach/detach pattern to make sure we don't leak View objects:

private NthPrimeReceiver receiver = new NthPrimeReceiver();

@Override
protected void onStart() {
  super.onStart();
  bindService(
    new Intent(this, LocalPrimesService.class),
    connection = new Connection(),
    Context.BIND_AUTO_CREATE);
  receiver.attach((TextView)
    findViewById(R.id.result));
  IntentFilter filter = new IntentFilter(
    LocalPrimesService.PRIMES_BROADCAST);
  LocalBroadcastManager.getInstance(this).
    registerReceiver(receiver, filter);
}

@Override
protected void onStop() {
  super.onStop();
  unbindService(connection);
  LocalBroadcastManager.getInstance(this).
    unregisterReceiver(receiver);
  receiver.detach();
}

Of course, if the user moves to another part of the application that doesn't register a BroadcastReceiver, or if we exit the application altogether, they won't see the result of the calculation.

If our Service could detect unhandled broadcasts, we could modify it to alert the user with a system notification instead. We'll see how to do that in the next section.

Detecting unhandled broadcasts

In the previous chapter, we used system notifications to post results to the notification drawer—a nice solution when the user has navigated away from our app before the background work has completed. However, we don't want to annoy the user by posting notifications when our app is still in the foreground and can display the results directly.

Ideally, we'll display the results in the app if it is still in the foreground and send a notification otherwise. If we're broadcasting results, the Service will need to know if anyone handled the broadcast and if not, send a notification.

One way to do this is to use the sendBroadcastSync synchronous broadcast method and take advantage of the fact that the Intent object we're broadcasting is mutable (any receiver can modify it). To begin with, we'll add one more constant to LocalIntentService:

public static final String HANDLED = "intent_handled";

Next, modify broadcastResult to use the synchronous broadcast method and return the value of a boolean extra property HANDLED from the Intent:

private boolean broadcastResult(String result) {
  Intent intent = new Intent(PRIMES_BROADCAST);
  intent.putExtra(RESULT, result);
  LocalBroadcastManager.getInstance(this).
    sendBroadcastSync(intent);
  
return intent.getBooleanExtra(HANDLED, false);
}

Because sendBroadcastSync is synchronous, all registered BroadcastReceivers will have handled the broadcast by the time sendBroadcastSync returns. This means that if any receiver sets the Boolean "extra" property HANDLED to true, broadcastResult will return true.

In our BroadcastReceiver, we'll update the Intent object by adding a boolean property to indicate that we've handled it:

@Override
public void onReceive(Context context, Intent intent) {
  if (view != null) {
    String result = intent.getStringExtra(
      PrimesServiceWithBroadcast.RESULT);
    intent.putExtra(LocalPrimesService.HANDLED, true);
    view.setText(result);
  } else {
    Log.i(TAG, " ignoring - we're detached");
  }
}

Now if PrimesActivity is still running, its BroadcastReceiver is registered and receives the Intent object and will put the extra boolean property HANDLED with the value true.

However, if PrimesActivity has finished, the BroadcastReceiver will no longer be registered and LocalPrimesService will return false from its broadcastResult method. We can use this to decide whether we should post a notification:

if (!broadcastResult(prime.toString()))
  notifyUser(n, prime.toString());

There's one final complication: unlike sendBroadcast, which always invokes BroadcastReceivers on the main thread, sendBroadcastSync uses the thread that it is called with. Our BroadcastReceiver interacts directly with the user interface, so we must call it on the main thread. This is simple enough to achieve from our AsyncTask—we'll move the broadcast from doInBackground to onPostExecute:

public void calculateNthPrime(final int n) {
  new AsyncTask<Void,Void,BigInteger>(){
    @Override
    protected BigInteger doInBackground(Void... params) {
      BigInteger prime = new BigInteger("2");
      for (int i=0; i<n; i++)
        prime = prime.nextProbablePrime(); 
      return prime;
    }
    @Override
    protected void onPostExecute(BigInteger result) {
      if (!broadcastResult(result.toString()))
        notifyUser(n, result.toString());
    }
  }.execute();
}

This does just what we want—if our BroadcastReceiver handles the message, we don't post a notification; otherwise, we will do so to make sure the user gets their result.

Having developed a good understanding of how to use Services to conduct long-running background work, let's consider some real-world applications and use cases.

..................Content has been hidden....................

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