Monday

Android - Reliable Alarms to Services

I love programming for Android. The APIs just make sense. You can tell that fluent Java developers designed this thing. Despite the fact that everyone's favorite criticism is that Android is "fragmented" I haven't had any significant cross-hardware issues. Follow the guidelines (What?!? I have to follow rules if I want my app to work? So much for freedom...) and 95% of apps should just work on any platform without much trouble. I'd say you're even a step ahead of iOS development because the platform has extremely simple ways to support multiple screen sizes, so your app can easily look good on any device. That said, there are occasional bugs, and this post is about one of them.

This may not even be a bug, but rather a case of poor documentation. The problem arrises when you use the AlarmManager to schedule a repeating alarm that is supposed to wake the phone and start a service. e.g.,

PendingIntent pi = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, timeToStart, 15 * 60 * 1000, pi);

It will probably work great during most of your testing as the phone will be awake. However, once you stick your phone in your pocket and forget about it for a while, you'll realize, "Hey, wasn't an alarm supposed to go off?" You'll pull your phone out, turn it on, and suddenly it will go off. The first few times you'll think it's odd, until you realize that the alarms aren't going off when the phone has been asleep for awhile, but they are delivered as soon as it wakes.

Hopefully you find this post or something like it and don't have to spend days Googling to figure out that only a BroadcastReceiver can reliably receive a wakeup alarm and then spend another day learning that the Context that onReceive gets can't start services. The solution is pretty simple, but you'd never figure it out from the documentation.

EDIT: I may be totally wrong about the context being a problem as you can see in the comments. Try it without the using getContext first and if you don't have any problems getting the service to start, avoid the Cargo Cult code I'm using below. If you do have problems and the context trick helps, please let me know in the comments.

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;


public class AlarmProxy extends BroadcastReceiver {

    Context getContext(Context ctx) {
        try {
            return ctx.createPackageContext("com.yourpackage.ofyourapp", 0);
        } catch (NameNotFoundException ignore) {}
    }
    
    
    @Override
    public void onReceive(Context ctx, Intent intent) {
        Context context = getContext(ctx);
        Intent newIntent = new Intent(context, YourService.class);
        newIntent.setData(intent.getData());
        newIntent.putExtras(intent);
        context.startService(newIntent);
    }
    
}

The context your receiver gets is probably a ReceiverRestrictedContext which isn't connected to your application and can't start services, so you may need to use createPackageContext to get your app context and start your service. You'd then set alarms by sending them to your receiver:

Intent intent = new Intent(this,AlarmProxy.class);
// ...
PendingIntent pi = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, timeToStart, 15 * 60 * 1000, pi);

Happy alarming!

EDIT: Don't forget to register the receiver in your Android manifest:

<receiver android:name=".AlarmProxy"> </receiver>

11 comments:

Mark Murphy said...

"The context your receiver gets is probably a ReceiverRestrictedContext which isn't connected to your application and can't start services,"

No, the context your receiver gets can start services just fine, via startService().

noah said...

@Mark you can try calling startService, but it wont actually start anything if your service is private to your application because it's not your application's context.

Mark Murphy said...

"you can try calling startService, but it wont actually start anything if your service is private to your application because it's not your application's context."

Yes, it will. See http://github.com/commonsguy/cw-advandroid/tree/master/SystemServices/Alarm/ for an example.

noah said...

@Mark Post all the code you want, but in practice, on a real device, I've watched (via the debugger and LogCat) that startService call do nothing.

This thread shows someone else with the same problem and the same solution. I'm not a big time book writer like yourself, but I know that createPackageContext fixed my problem. Sorry if you think it should work, but it wasn't working in my app and I'm trying to help anyone else who has the same problem.

Mark Murphy said...

"Post all the code you want, but in practice, on a real device, I've watched (via the debugger and LogCat) that startService call do nothing."

BroadcastReceivers can start services just fine using the Context passed into onReceive(). For example, many app widgets use an IntentService for the long-running portions of their update work, and AppWidgetProvider is a BroadcastReceiver. See http://github.com/commonsguy/cw-advandroid/tree/master/AppWidget/Microblog/ and http://developer.android.com/resources/samples/Wiktionary/src/com/example/android/wiktionary/WordWidget.html for examples.

You are welcome to post a project demonstrating how startService() from a BroadcastReceiver never works.

noah said...

"You are welcome to post a project demonstrating how startService() from a BroadcastReceiver never works."

I never said it never works. But it often doesn't. Maybe things work differently for app widgets, or in the context of an RTC_WAKEUP when the phone has been in deep sleep. I don't know, I don't need to run your example code to know that I had a problem and this fixed it.

I posted a thread showing other people have the same problem, the and the same solution worked for them. Please stop spamming my comments with links to your toy example code just because you've never experienced the same problem.

Divided Mind said...

http://developer.android.com/reference/android/app/AlarmManager.html says:

The Alarm Manager holds a CPU wake lock as long as the alarm receiver's onReceive() method is executing. This guarantees that the phone will not sleep until you have finished handling the broadcast. Once onReceive() returns, the Alarm Manager releases this wake lock. This means that the phone will in some cases sleep as soon as your onReceive() method completes. If your alarm receiver called Context.startService(), it is possible that the phone will sleep before the requested service is launched. To prevent this, your BroadcastReceiver and Service will need to implement a separate wake lock policy to ensure that the phone continues running until the service becomes available.

Anonymous said...

Thanks! I was working on this wake-up problem for quite a while and this article helped me solve it.

For getting the right context in the broadcast receiver I also found this possibility:

Intent newIntent = new Intent(context, MyServiceClass.class);
newIntent.setData(intent.getData());
newIntent.putExtras(intent);
context.startService(newIntent);

Eberhard

ewmksoft.de

Anonymous said...

... I wanted to say it works even without the getContext() call (at least within the same application)

glen said...

Thanks for the code. Eclipse wouldn't accept context.startService() in my Alarm Manager BroadcastReceiver without createPackageContext.

Kamran said...

I want to ask what is better to use broadcast receiver or service with alarms?