29

I need to plan sheduled task every 10 minutes.

As in Lollipop and higher version setRepeating() is inexact, I use setExact() and (on alarm firing) I set new exact alarm in 10 minutes.

private void setAlarm(long triggerTime, PendingIntent pendingIntent) {
        int ALARM_TYPE = AlarmManager.ELAPSED_REALTIME_WAKEUP;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            alarmManager.setExact(ALARM_TYPE, triggerTime, pendingIntent);
        } else {
            alarmManager.set(ALARM_TYPE, triggerTime, pendingIntent);
        }
    }

triggerTime is calculated SystemClock.elapsedRealtime() + 600_000;

When alarm fires, firstly I plan new one, only after that I run my sheduled task.

setAlarm();
mySheduledTask;

I do have WAKE_LOCK permission in my manifest.

When I test this on Android 4 - it works perfect (deviation might be 12-15 milliseconds).

But when I run app on Xiaomi Redmi Note 3 Pro (5.1.1) - deviation can be up to 15 seconds!

For example, I see in my log file: first run was at 1467119934477 (of RTC time), second - at 1467120541683. Difference is 607_206 milliseconds, not 600_000, as it was planned!

What am I missing? What is a way to simulate behaviour of system alarm (it's the most close usecase that can describe my tack)?

PS. I use IntentService for PendingIntent = PendingIntent.getService(context, 0, myIntent, 0);

Goltsev Eugene
  • 3,325
  • 6
  • 25
  • 48

6 Answers6

41

The OS chooses how the alarms will work, with consideration of the time you've specified. Because of that, when the phone gets into a 'semi-sleep' mode, it won't necessary use the resource at the time you wish it to. Basically, it waits for 'windows' that the OS opens for it, and only then the alarm you want to run will run, that's why you're experiencing time gaps.

This was introduced on Marshmallow OS and will continue on Nougat OS as well, as part of Google trying to improve the device's battery.

Here's the thing, you have 2 options:

  1. Accept the time delays (but maybe consider using JobScheduler which is more recommended and will save you battery).
  2. Use setExactAndAllowWhileIdle which might cause you battery issues (use this carefully, too many alarms will be bad for your battery). This method isn't repeating, so you have to declare the next job to be run at the service which the pendingIntent opens.

If you choose option 2, here's the start:

AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
int ALARM_TYPE = AlarmManager.RTC_WAKEUP;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
    am.setExactAndAllowWhileIdle(ALARM_TYPE, calendar.getTimeInMillis(), pendingIntent);
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
    am.setExact(ALARM_TYPE, calendar.getTimeInMillis(), pendingIntent);
else
    am.set(ALARM_TYPE, calendar.getTimeInMillis(), pendingIntent);
Dus
  • 4,052
  • 5
  • 30
  • 49
  • 2
    Good point, thanks. But the problem is on Lollipop, not on Marshmellow. Also, how system alarm is working (it works good, at the time it was planned)? – Goltsev Eugene Jul 12 '16 at 07:22
  • @Dus the third case is not exact because apps having targetSdkVersion 19 or later will treat set as inexact on devices having 18 and earlier. So, how to get exact alarm on pre-Kitkat devices with targetSdkVersion 25? – dira Jan 26 '17 at 13:25
  • 2
    In second `if` Lollipop should be substituted with Kitkat. – azizbekian Dec 03 '17 at 08:11
  • 3
    Oh, I need to show sort of message to the user: "Sorry you haven't heard an alarm and did not get up on time, because exact alarm triggering will cause battery issues". – Oleksandr Albul Apr 29 '19 at 07:11
  • Recommending JobScheduler for an alarm clock app? Why? – Mitulát báti Nov 23 '19 at 15:01
  • This solution does not work for Lolipop (which the question was about). Lollipop is version API version 22, setExactAndAllowWhileIdle is API 23 – Code Wiget Dec 12 '19 at 15:18
  • I'm using `setExactAndAllowWhileIdle` (on API level 28) but I do get inexact times. Strangely enough, most often these seem to be rounded to minutes: I set 5 min intervals, but I get 6, 10 or even 30, while very rarely "random" values. Anyone knows why this strange behaviour? – Franco Jul 27 '20 at 19:21
  • 1
    . Mr google, when I say exact I really mean EXACT, what's really the point of calling setExact... if it's not exact. Damn it. – Alex Sep 06 '21 at 05:44
11

You can call the method from support.v4:

AlarmManagerCompat.setExact(...);

The internal implementation contains checks by sdk version.

northerngirl
  • 225
  • 3
  • 8
8

Probably a possible workaround could be something like this: you schedule the Alarm about 1 minute before the expected time, than you use a Handler.postDelayed to cover the remaining time.

Here you can find an example of this kind of implementation. The activity just set-up the first alarm:

public class MainActivity extends AppCompatActivity {

    private static int WAIT_TIME = 60*1000; //1 minute
    public static int DELAY_TIME = 10*60*1000; // delay between iterations: 10min
    public static String UPDATE_TIME_KEY = "update_time_key";


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setAlarm(this,(new Date().getTime())+DELAY_TIME);
    }

    public static void setAlarm(Context context, long delay) {

        long fireDelay = delay-WAIT_TIME;
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
        sharedPreferences.edit().putLong(UPDATE_TIME_KEY,delay).apply();

        Intent startIntent = new Intent(context, UpdateReceiver.class);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 1, startIntent,PendingIntent.FLAG_UPDATE_CURRENT );
        AlarmManager alarmManager = (AlarmManager) context.getApplicationContext().getSystemService(Context.ALARM_SERVICE);
        int ALARM_TYPE = AlarmManager.RTC;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            alarmManager.setExact(ALARM_TYPE, fireDelay, pendingIntent);
        } else {
            alarmManager.set(ALARM_TYPE, fireDelay, pendingIntent);
        }
    }

}

than the receiver continues the loop:

public class UpdateReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(final Context context, Intent intent) {
        Log.e("RECEIVED","RECEIVED");
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
        long fireTime = sharedPreferences.getLong(MainActivity.UPDATE_TIME_KEY, (new Date()).getTime());

        long fireDelay  =(fireTime-(new Date().getTime())>0)?fireTime-(new Date().getTime()):0;

        (new Handler()).postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.e("RECEIVED","PERFORMED");
                MainActivity.setAlarm(context,(new Date()).getTime()+MainActivity.DELAY_TIME);
            }
        },fireDelay);

    }

}

I hope it helped.

7

To answer the question on the system alarm...

Android's stock Alarm Clock/Desk Clock app uses a combination of setAlarmClock and setExactAndAllowWhileIdle.

The following code is used to update notifications:

final PendingIntent operation = PendingIntent.getBroadcast(context, 0, 
    AlarmStateManager.createIndicatorIntent(context), flags);
final AlarmClockInfo info = new AlarmClockInfo(alarmTime, viewIntent);

alarmManager.setAlarmClock(info, operation);


While at the same time the following code is used to schedule the actual alarm:

if (Utils.isMOrLater()) {
    // Ensure the alarm fires even if the device is dozing.
    alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
} else {
    alarmManager.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent)
}


The Pending intent set in setExactAndAllowWhileIdle triggers the alarm while setAlarmClock's intent is then simply ignored.


Android Googlesource

Maksim Ivanov
  • 3,991
  • 31
  • 25
  • 13
    Welcome to the world of Android, the world where even Google developers have to hack their way through. – Maksim Ivanov Feb 16 '18 at 22:58
  • I don't see the first part. Where is it exactly? – android developer Feb 06 '20 at 08:08
  • @androiddeveloper https://android.googlesource.com/platform/packages/apps/DeskClock/+/master/src/com/android/deskclock/alarms/AlarmStateManager.java#235 – Maksim Ivanov Feb 06 '20 at 22:09
  • But how do you see that the second is called after the other? They are in different functions, and I don't see them being called on after another. – android developer Feb 09 '20 at 09:09
  • @androiddeveloper the code isn't easy to follow, but it all starts with `registerInstance()` which sets the alarm state (which eventually calls the second bit of code) and updates notifications (the first bit of code). – Maksim Ivanov Feb 10 '20 at 14:29
  • Did it work for you though? I've noticed that in some cases, either it doesn't get triggered, or that I don't wait enough to see it. Please check this: https://stackoverflow.com/q/60079472/878126 – android developer Feb 11 '20 at 09:40
  • @androiddeveloper it did work at the time I posted this answer, but I haven't tested it with the latest Android version. – Maksim Ivanov Feb 12 '20 at 10:23
  • Can you please try now? You can do it on emulator. It also has the same issue – android developer Feb 12 '20 at 14:28
2

From android documentation of AlarmManager

Beginning with API 19 (KITKAT) alarm delivery is inexact: the OS will shift alarms in order to minimize wakeups and battery use. There are new APIs to support applications which need strict delivery guarantees; see setWindow(int, long, long, PendingIntent) and setExact(int, long, PendingIntent). Applications whose targetSdkVersion is earlier than API 19 will continue to see the previous behavior in which all alarms are delivered exactly when requested.

Also while using setExact() :

The alarm will be delivered as nearly as possible to the requested trigger time.

So its still not guaranteed that setExact will be Exact.

Sujay
  • 3,417
  • 1
  • 23
  • 25
2

You can try use AlarmManager.setAlarmClock maybe it can help you.

Another thing you need to check which type of BroadcastReceiver you are using, it will be better to use WakefulBroadcastReceiver

Btw you need to change logic for work with Alarm Manager for support Android M, you can you something like this:

if(Build.VERSION.SDK_INT < 23){
    if(Build.VERSION.SDK_INT >= 19) {
        setExact(...);
    } else {
        set(...);
    }
} else {
    setExactAndAllowWhileIdle(...);
}
Scott Key
  • 119
  • 5
  • Its written that setAlarmClock is like setExact, so I don't think, that switching will help much. About WakefulBroadcastReceiver - it's interesting, will try it, for now I'm using service. – Goltsev Eugene Jul 12 '16 at 07:40
  • WakefulBroadcastReceiver is deprecated now. – Alex Sep 06 '21 at 05:45