Issue Moving from IntentService to JobIntentService for Android O

Akshay picture Akshay · Oct 10, 2017 · Viewed 7.9k times · Source

I am using Intent Service to monitor Geofence transition. For that I am using following call from a Sticky Service.

 LocationServices.GeofencingApi.addGeofences(
                    mGoogleApiClient,
                    getGeofencingRequest(),
                    getGeofencePendingIntent()
            )

and the Pending Intent calls Transition service (an IntentService) like below.

  private PendingIntent getGeofencePendingIntent() {
        Intent intent = new Intent(this, GeofenceTransitionsIntentService.class);
        // We use FLAG_UPDATE_CURRENT so that we get the 
          //same pending intent back when calling addgeoFences()
        return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    }

This worked fine Pre Oreo. However, I had to convert my sticky service to a JobScheduler and I need to convert GeofenceTransitionsIntentService which is an intentService to JobIntentService.

Having said that I am not sure how to return create a PendingIntent for JobIntentService, because I need to call enqueueWork for JobIntentService.

Any suggestions/pointer would be appreciated.

Answer

Cord Rehn picture Cord Rehn · Mar 15, 2018

Problem

I had the same issue when migrating from IntentService to JobIntentService on Android Oreo+ devices.

All the guides and snippets I've found are incomplete, they leave out the breaking change this migration has on the use of PendingIntent.getServce.

In particular, this migration breaks any Alarms scheduled to start a service with the AlarmManager and any Actions added to a Notification that start a service.


Solution

Replace PendingIntent.getService with PendingIntent.getBroadcast that starts a BroastcastReceiver.

This receiver then starts the JobIntentService using enqueueWork.


This can be repetitive and error prone when migrating multiple services.

To make this easier and service agnostic, I created a generic StartJobIntentServiceReceiver that takes a job ID and an Intent meant for a JobIntentService.

When the receiver is started, it will start the originally intended JobIntentService with a job ID and actually forwards the Intent's original contents through to the service behind the scenes.

/**
 * A receiver that acts as a pass-through for enqueueing work to a {@link android.support.v4.app.JobIntentService}.
 */
public class StartJobIntentServiceReceiver extends BroadcastReceiver {

    public static final String EXTRA_SERVICE_CLASS = "com.sg57.tesladashboard.extra_service_class";
    public static final String EXTRA_JOB_ID = "com.sg57.tesladashboard.extra_job_id";

    /**
     * @param intent an Intent meant for a {@link android.support.v4.app.JobIntentService}
     * @return a new Intent intended for use by this receiver based off the passed intent
     */
    public static Intent getIntent(Context context, Intent intent, int job_id) {
        ComponentName component = intent.getComponent();
        if (component == null)
            throw new RuntimeException("Missing intent component");

        Intent new_intent = new Intent(intent)
                .putExtra(EXTRA_SERVICE_CLASS, component.getClassName())
                .putExtra(EXTRA_JOB_ID, job_id);

        new_intent.setClass(context, StartJobIntentServiceReceiver.class);

        return new_intent;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        try {
            if (intent.getExtras() == null)
                throw new Exception("No extras found");


            // change intent's class to its intended service's class
            String service_class_name = intent.getStringExtra(EXTRA_SERVICE_CLASS);

            if (service_class_name == null)
                throw new Exception("No service class found in extras");

            Class service_class = Class.forName(service_class_name);

            if (!JobIntentService.class.isAssignableFrom(service_class))
                throw new Exception("Service class found is not a JobIntentService: " + service_class.getName());

            intent.setClass(context, service_class);


            // get job id
            if (!intent.getExtras().containsKey(EXTRA_JOB_ID))
                throw new Exception("No job ID found in extras");

            int job_id = intent.getIntExtra(EXTRA_JOB_ID, 0);


            // start the service
            JobIntentService.enqueueWork(context, service_class, job_id, intent);


        } catch (Exception e) {
            System.err.println("Error starting service from receiver: " + e.getMessage());
        }
    }

}

You will need to replace package names with your own, and register this BroadcastReceiver per usual in your AndroidManifest.xml:

<receiver android:name=".path.to.receiver.here.StartJobIntentServiceReceiver"/>

You are now safe to use Context.sendBroadcast or PendingIntent.getBroadcast anywhere, simply wrap the Intent you want delivered to your JobIntentService in the receiver's static method, StartJobIntentServiceReceiver.getIntent.


Examples

You can start the receiver, and by extension your JobIntentService, immediately by doing this:

Context.sendBroadcast(StartJobIntentServiceReceiver.getIntent(context, intent, job_id));

Anywhere you aren't starting the service immediately you must use a PendingIntent, such as when scheduling Alarms with AlarmManager or adding Actions to Notifications:

PendingIntent.getBroadcast(context.getApplicationContext(),
    request_code,
    StartJobIntentServiceReceiver.getIntent(context, intent, job_id),
    PendingIntent.FLAG_UPDATE_CURRENT);