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.
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 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.
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.
3.145.107.66