A Service and a Handler for long tasks

Say your app's got a job that could become quite time-consuming. Downloads, data processing... how about syncing data with a remote source?

You wouldn't want this work to be canceled, even if the app goes to the background, so that rules out using AsyncTask. And let's say you'd like to notify the user when the long-running work is done.

I'm using the technique described here for a login procedure that syncs data from a remote database. This process can take 5 seconds or more, and I can't possibly expect app users to stay focused on the login screen for that long. I want this multi-step process to finish even if the Activity is backgrounded and garbage-collected.

So, I'll use the Activity to start a sequence of login steps on a dedicated thread. This thread can communicate back to the launching Activity when login and sync are finally complete.

Activity, Service, Handler

Here's how it goes. An Activity starts up a Service. The Activity needs a way of communicating with the Service, so it passes a ResultReceiver object to the Service class. This ResultReceiver provides the communication link between the Service's behavior and the Activity.

The Service stores a reference to the ResultReceiver before it does anything else. The Service then starts up a sequence of related tasks using a Handler, and the Service itself is bound to the Handler by passing itself as an instance of a callback object.

Through this last step, the Handler can signal to its callback object -- the Service -- that the Handler's work is done, and the Service can tell its reference to ResultReceiver to pass a message back to the Activity.

The ResultReceiver in the Activity can interpret the message based on the integer result code. If the Activity is still visible, then the ResultReceiver can show a confirmation (or an error message) and we can see a confirmation that the work was finished.

Pick a thread and run with it

You'll use the Service as the manager of a new thread. The Handler that belongs to this Service needs to know which thread to perform its work on.

From the Android docs, Communicating on the UI Thread says, "When you instantiate a Handler based on a particular Looper instance, the Handler runs on the same thread as the Looper." You can exploit this feature to tell your Handler to run on the thread belonging to the Service.

That was a lot of words. Here's a picture.

  1. The Service is started from the Activity. The Intent that starts the Service carries a reference to the Activity's ResultReceiver as a bundled object.

  2. The LoginService does several things in order. It starts up, holds a reference to the ResultReceiver, starts a new thread, and sets up a LoginHandlerManager. The LoginService passes itself to the LoginHandlerManager class as a callback object.

  3. The Handler belonging to the LoginHandlerManager starts performing work on the LoginService's thread. When all the background tasks are finished, it calls its callback LoginService's completion method.

  4. From the callback completion method, a message is sent back to the ResultReceiver in the launching Activity.

Binding the service as a callback to the handler is just a nice way of organizing your code. The service could have a callback object as a member variable. Making the Service instance itself into the callback object gives you one less object to manage.

A working example

The Activity where this work starts is responsible for constructing a ResultReceiver and starting the service. Services are started through an Intent, so you can't pass them any arguments except those that can be bundled. Happily, though, ResultReceiver implements Parcelable! So it's no trouble to pass it as an extra on the service Intent.

LoginActivity

// somewhere in LoginActivity.java
Intent loginIntent = new Intent(this, LoginService.class);

// In case the Handler is going to need any other data from the Activity, bundle up those values here.
Bundle optionalArguments = new Bundle();
// ... assign whatever you need to otherArguments ...
loginIntent.putExtras(optionalArguments);

// and now, the ResultReceiver 
loginIntent.putExtra(LoginService.BUNDLED_RECEIVER_KEY, new ResultReceiver(new Handler()) {
    @Override
    protected void onReceiveResult(int resultCode, Bundle resultData) {
        if (resultCode == Activity.RESULT_OK) {
            // ... show UI for successful login ...
        } else {
            // ... show UI for unsuccessful login ...
        }
    }
});

startService(loginIntent);

LoginService

There are two things to watch out for with this service. First, it's not an IntentService, it's an instance of the Service base class. I wanted more control over firing the ResultReceiver than IntentService affords. Inheriting from Service means I have to manage the service's working thread more directly.

Second, I've defined an interface called LoginCallback that the service's Handler will use to send its messages. The service itself is an instance of this callback, so I need to implement the LoginCallback methods here. Those LoginCallback methods will send messages via the ResultReceiver.

// LoginService.java
public class LoginService extends Service implements LoginHandler.LoginCallback { 
    public static final String TAG = "LoginService";
    public static final String BUNDLED_RECEIVER_KEY = "receiver";
    // got any more Bundled arguments that need String keys? 
    // more labels can go here...

    private HandlerThread thread;
    private ResultReceiver receiver;

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

        thread = new HandlerThread(TAG, android.os.Process.THREAD_PRIORITY_BACKGROUND);
        thread.start();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Bundle bundle = intent.getExtras();
        if (bundle == null) {
            throw new IllegalArgumentException("LoginService must be provided with bundled arguments.");
        }

        if (!bundle.containsKey(BUNDLED_RECEIVER_KEY) || bundle.getParcelable(BUNDLED_RECEIVER_KEY) == null) {
            throw new IllegalArgumentException("LoginService must be provided with a ResultReceiver.");
        }

        receiver = bundle.getParcelable(BUNDLED_RECEIVER_KEY);

        // If you want to pass any more arguments to the Handler, pass them in the start() method.
        // For now, pass only this Service, in its capacity as a LoginCallback.
        new LoginHandlerManager(getApplicationContext(), thread.getLooper()).start(this);

        return Service.START_NOT_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        // Not bound.
        return null;
    }

    @Override
    public void onLoginSuccess(Intent intent) {
        receiver.send(Activity.RESULT_OK, intent.getExtras());
        stopSelf();
    }

    @Override
    public void onLoginFailure() {
        receiver.send(Activity.RESULT_CANCELED, null);
        stopSelf();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        thread.quit();
    }
}

Notice that the service must maintain its own Thread (here, a HandlerThread) and remember to shut down the thread with thread.quit() when the service is terminated.

Also, notice that the service is started with the START_NOT_STICKY flag. Because this service is supposed to start only at the user's request, you don't want the service to automatically restart if the app is killed and restarted.

LoginHandlerManager

// Now I've come to defining the sequence for this service's behavior. I chose a Handler to help organize the parts of the process into a human-readable sequence. If the Service had a simpler task that were appropriate for just one function, I would've just defined its behavior as a LoginService method. Using the Handler approach, you can organize as many subtasks as it takes to finish.

// LoginHandlerManager.java
/**
 * This class uses a Handler to run a sequence of tasks in order.
 * A Looper from a non-UI thread should be supplied to the constructor.
 *
 * Execution is kicked off by a call to Handler.post with the LOOPER_STARTED message after this handler is defined.
 *
 * The messages (other than LOGIN_FAILED) are meant to be sent in order,
 * and only once in the thread's lifetime.
 *
 * You can exit early by sending a LOGIN_FAILED message to the handler.
 */
public class LoginHandler {
    private Context context;
    private Looper looper;
    private static final int LOGIN_FAILED = -1;
    private static final int LOOPER_STARTED = 0;
    private static final int LOGIN_FINISHED = 1;

    private static final String TAG = "LoginHandlerManager";

    public LoginHandlerManager(Context context, Looper looper) {
        this.context = context;
        this.looper = looper;
    }

    // Here's the interface that helps LoginService 
    // behave as a callback object
    public interface LoginCallback {
        public void onLoginSuccess(Intent intent);
        public void onLoginFailure();
    }

    public void start(final LoginCallback callback) {
        // And here's that Handler.
        final Handler handler = new Handler(looper) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case LOGIN_FAILED:
                        callback.onLoginFailure();
                        break;
                    case LOOPER_STARTED:
                        //Time to start the first step.
                        // When it's done, 
                        // either synchronously or
                        // asynchronously, send
                        // the Handler a message
                        // to run the next (or final) step.

                        // whatever code you like, then...

                        Message message = Message.obtain(h, LOGIN_FINISHED);
                        // If your next step requires 
                        // any inputs from this step, 
                        // you can assign data to a Bundle
                        // and send it with your message
                        Bundle optionalData = new Bundle();
                        optionalData.putString("MESSAGE_KEY", "Done!");
                        message.setData(optionalData);

                        // Now send it.
                        sendMessage(message);

                    case LOGIN_FINISHED:
                        // Do we need any data 
                        // from the last message 
                        // to continue our work?
                        // If so, grab it here.
                        Bundle msgData = msg.getData();
                        Log.i(TAG, msgData.getString("MESSAGE_KEY"))

                        final Intent intent = new Intent();
                        // You could bundle some result data with the Intent here, if you like!

                        callback.onLoginSuccess(intent);
                        break;
                }
            }
        };

        /*
         * Nothing will happen until 
         * you post a message to start the Handler.
         */
        handler.post(new Runnable() {
            @Override
            public void run() {
                handler.sendEmptyMessage(LOOPER_STARTED);
            }
        });
    }
}

Notice that when the handler receives its LOGIN_FINISHED message, it calls the callback that you provided. But your callback is the service itself, so the service's callback methods fire.

AndroidManifest.xml

One more thing. If you try this out, don't forget to register the Service in the manifest.

<service android:name="com.a.b.c.LoginService" android:exported="false"/>

That's it!