5

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.

Akshay
  • 806
  • 11
  • 26
  • I know that posting url is not recommended since the websites can be gone. But here's a website example I have been referring to create GeoFence if anyone is curious. https://www.mytrendin.com/android-geofences-google-api/ – Akshay Oct 10 '17 at 20:19
  • 4
    Use a BroadcastReceiver as a pending intent for Geofence API. Then schedule a job in this BroadcastReceive once it's triggered by Geofence API. – andrei_zaitcev Oct 10 '17 at 20:27
  • I thought, Android O doesn't recommend usage of broadcast receiver – Akshay Oct 10 '17 at 20:40
  • @andrei_zaitcev in that case what intent filters would you recommend for the broadcast? Especially keeping Android Oreo in mind? – Akshay Oct 10 '17 at 22:14
  • 1
    Android Oreo doesn't have any background limits for broadcast receivers. You can keep it without any intent filters. You just can't run a background service from this receiver if your app is in a so called background mode. – andrei_zaitcev Oct 12 '17 at 10:38
  • @Akshay what the docs say about broadcast receivers is for implicit intents. Anyway you can register them programmatically, and omit the declaration in the manifest completely – Jose_GD Nov 20 '18 at 21:12
  • @Jose_GD care to elaborate? I have so far no problem doing this way, and to my knowledge I didn't see any google doc mentioning that this is a bad practice. Alternately, if you know any other way to get intent from JobIntentService without creating broadcast, I am curious to know. – Akshay Nov 21 '18 at 01:17
  • @Akshay here: https://developer.android.com/about/versions/oreo/background. *Broadcast Limitations:* With limited exceptions, apps cannot use their manifest to register for implicit broadcasts. They can still register for these broadcasts at runtime, and they can use the manifest to register for explicit broadcasts targeted specifically at their app. – Jose_GD Nov 22 '18 at 12:43
  • @Akshay I was answering only to your comment: "Android O doesn't recommend usage of broadcast receiver". Answering your 2nd question, I don't know another way, it seems your solution is fine – Jose_GD Nov 22 '18 at 12:47
  • @Jose_GD Note the exception of explicit broadcast. This is an explicit broadcast – Akshay Nov 23 '18 at 16:46

2 Answers2

10

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);
Community
  • 1
  • 1
Cord Rehn
  • 1,119
  • 14
  • 22
  • To your point `This can be repetitive and error prone when migrating multiple services.` this is subjective. The solution you provided may work but is not the best imho, or at least at the time I got the responses while looking google docs, was that to enqueue the job. – Akshay Jun 07 '19 at 17:06
-1

As @andrei_zaitcev suggested, I implemented my custom BroadCastReceiver and call enqueueWork() of the Service, which works perfectly.

Akshay
  • 806
  • 11
  • 26
  • 3
    Would you happen to have an example of your implementation? Been searching for a similar solution as I upgrade an app to Oreo. – Kyle Dec 21 '17 at 03:42
  • You can see this approach in their geofence sample, but I could not get that to work and astonishingly, it's deprecated! How can code that was updated for O already be deprecated?? Jesus Google gets an F for this dung pile of idiocy. – Rob Jul 30 '18 at 20:05