1

I have a fairly standard Service which I wish to trigger using an alarm. Here's the initiation section of the service:

class MyService extends Service {
    private Context context;
    private AlarmManager  alarmManager = null;

    private final String startReason = "com.stuff.myreason";
    private final int REASON_NO_INTENT = 0;
    private final int REASON_ALARM     = 1;
    private final int REASON_X         = 2; // and so on.

    @Override
    void onCreate() {
        super.onCreate();
        context = getApplicationContext();
        alarmManager = (AlarmManager)getSystemService(ALARM_SERVICE);
        // do onCreate stuff
    }

    @Override
    int onStartCommand (Intent intent, int flags, int startId) {
        int reason = REASON_NO_INTENT;
        if (intent != null) {
            reason = intent.getExtra(startReason, REASON_NO_INTENT);
        }

        switch(reason) {
            // handle the different reasons we may have been "started"
        }

        return START_STICKY;
    }
}

When I trigger it using context.startService from an activity, it starts absolutely normally. In particular, if it is already running it doesn't (re)start from scratch but simply enters the existing instantiation via onStartCommand(). This is the expected behaviour. However, when I trigger it using the AlarmManager:

Intent intent = new Intent(context, MyService.class);
intent.putExtra(purposeOfStartCode, REASON_ALARM);

PendingIntent pi = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

alarmManager.set(AlarmManager.RTC_WAKEUP, /* A time in the future */, pi);

When the alarm is due it seems to restart the service from scratch: it starts a new instantiation, calls onCreate() and then onStartCommand() rather than just calling onStartCommand() in the already running instantiation.

I have already tried changing the PendingIntent flag to FLAG_ONE_SHOT and replacing context with MyService.this with no improvement.

I am rather bemused by this - can anyone explain this behaviour and suggest ways to get it to behave as expected?

EDIT - The collection of actions that resulted in a solution are in my answer below.

Neil Townsend
  • 5,784
  • 5
  • 32
  • 50

3 Answers3

4

After some investigation and work, I've discovered a number of things. Having done all of them, this problem looks like it's disappeared:

  1. If you override onStart and onStartCommand in a service (to allow for older devices) and you put super.onStartCommand in the latter, it will call onStart, meaning you get every intent coming in twice!

  2. As per one of the other answers (and comments on it), the AlarmManager is designed and specified to deliver Broadcast intents, not other types. However, in practice, it isn't picky and seems to honour other forms. I think that this was one of the keys in resolving the issue.

  3. If the service is in the same process as other activites etc, the service sometimes seems to "just get restarted". This may be the actual cause of the issue noted in this question. See Android service onCreate is called multiple times without calling onDestroy.

  4. Things seem to be more stable when solely using intents to communicate with the Service rather than binding and using a Messenger or binding and accessing methods. Whilst both of these are correct, they are quite complex to manage (although you could use this approach: What is the preferred way to call an Android Activity back from a Service thread and Using the Android Application class to persist data). Whilst I fully appreciate that the android docs disagree with me, in my observation moving to broadcast intent only communication seemed key. If you go for the separate process approach you'll have to do this anyway.

  5. It pays to be consistent in how you declare and address your classes. It's a bit messy, but, because it sometimes seems to pay to use full names ("com.company.superapp.CleverService") rather than short ("CleverService" or ".CleverService"). So, it's probably better to always use full names.

  6. The rule of thumb floating around out there about contexts ("use getApplicationContext") isn't really the right way to do it. See When to call activity context OR application context?; in essence use this, unless you really need to use a broader context, and manage your variables well.

  7. It's possible for the garbage collector to clear up something still in use if it was created in an Activity, Service, Thread, AsyncTask, etc that is no longer around. If the application is based around a service, it may be wise to make a copy of classes coming in so that they don't get cleared up later.

  8. A neater way to start a service than is often suggested is to give the service an intentFilter with it's full name as the action. You can then create the intent to start it with just the class name as a string. This means you don't have to worry about context. See Issue in Calling Service.

Community
  • 1
  • 1
Neil Townsend
  • 5,784
  • 5
  • 32
  • 50
1

Well, I'm actually surprised that it runs your Service at all! The PendingIntent that you pass to the AlarmManager needs to be a broadcast Intent. So you need to rearchitect your code a bit. The AlarmManager will trigger a BroadcastReceiver and the BroadcastReciever can then call startService().

See the description of AlarmManager.set()

David Wasser
  • 85,616
  • 15
  • 182
  • 239
  • 1
    Fascinating - the approach I've used is often recommended on various tutorial websites, eg http://www.vogella.com/articles/AndroidServices/article.html#scheduleservice_scheduling and http://android-er.blogspot.co.uk/2010/10/simple-example-of-alarm-service-using.html . Is it really that fundamentally flawed? – Neil Townsend Mar 15 '13 at 14:54
  • Thanks for the links. Actually you've made me more curious. I'll need to examine the Android source code to be sure. In any case, I've never tried to start a service directly from the AlarmManager, I've always done it using a `BroadcastReceiver`, the way I've described. However, it is also possible that I am wrong. – David Wasser Mar 15 '13 at 15:25
  • Please do let me know what you find - it would be good to put a definitive answer up somewhere because if there is a clear reason to do it one way or the other it should be easier to find it out! – Neil Townsend Mar 15 '13 at 15:36
  • Please let me know if your problem is solved by rearchitecting and using the `BroadcastReceiver` to start the `Service`. – David Wasser Mar 15 '13 at 16:08
  • Will do, it will take a bit of time because being clear about symptoms and sequence can take some time to emerge (see also http://stackoverflow.com/questions/15409796/log-not-working-in-service) for one reason why it has been hard to nail it this far!! – Neil Townsend Mar 15 '13 at 16:42
  • Yeah, I saw that question too. My guess is that you've got something very bad going on that is causing these problems. Post your manifest, maybe that will give a clue. – David Wasser Mar 15 '13 at 16:55
  • Have done - I will now start on rearchitecting (is that really a word?) – Neil Townsend Mar 15 '13 at 18:01
  • I had a look at the source code for `AlarmManager`. Reading the comments in the code it appears that it was intended to only send broadcast Intents. However, it doesn't actually check anywhere that the Intent it is given is a broadcast intent. Therefore, in theory, you should be able to give it any Intent you want (broadcast, activity, service) and it will fire it when the alarm triggers. However, I don't think it was intended to be used this way. – David Wasser Mar 18 '13 at 09:23
  • Are your problems occuring on emulator, device, or both? If device, what device are you testing on? Have you tried a different device? – David Wasser Mar 18 '13 at 09:25
  • Thanks for reading up - interesting. I'm testing on a 4.0.3 device. The app this is part of really batters the emulator (5-10 minutes app start on the emulator as opposed to <10 seconds on the device), so my plan for this morning is to build an "app" with only a service and an activity and a regular tick to try and explore both this issue and the Logging issue. I will then run it on the device and a few emulators ... – Neil Townsend Mar 18 '13 at 10:43
  • OK, I've built a really simple app with an activity with two ways of sending intents to the service and the service sending timed intents to itself via both methods (direct start and via Broadcast). Everything is working exactly correctly, even the logging. So, I have obviously done something Very Bad in the other application which I will need to seek out. Whatever I learn (if of use to others) I'll put up somewhere ... – Neil Townsend Mar 18 '13 at 12:18
0

I got this to work by using the following code:

AlarmManager almgr = (AlarmManager)MyContext.getSystemService(Context.ALARM_SERVICE);
Intent timerIntent = new Intent(MyUniqueLabel);
timerIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
PendingIntent pendingOffLoadIntent = PendingIntent.getBroadcast(MyContext, 1, timerIntent, 0);

you MUST do these things for it to work.
1.) Call addFlags and the intent and pass it in FLAG_RECEIVER_FORGROUND
2.) Use a non-zero request code in PendingIntent.getBroadcast

If you leave any of those steps out it will not work.