Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android AlarmManager setExact() is not exact

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);

like image 935
Goltsev Eugene Avatar asked Jun 29 '16 08:06

Goltsev Eugene


People also ask

How do I set the exact alarm on my android?

"Alarms & reminders" special app access page in system settings, where users can allow your app to set exact alarms. If necessary, you can send users to the Alarms & reminders screen in system settings, as shown in Figure 1.

Is AlarmManager deprecated?

IntentService + WakefulBroadcastReceiver + AlarmManager are deprecated with API 26 (Android 8.0 Oreo).

How do I stop my android phone from repeating an alarm?

Open your phone's Clock app . At the bottom, tap Alarm. On the alarm you want, tap the Down arrow . Cancel: To cancel an alarm scheduled to go off in the next 2 hours, tap Dismiss.

How does an application get access to the AlarmManager?

How does an application get access to the AlarmManager? Use the AlarmManager() constructor to create an instance of the AlarmManager. Use the AlarmManager. newInstance() method to retrieve the singleton instance of the AlarmManager.


3 Answers

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);
like image 134
Dus Avatar answered Oct 17 '22 18:10

Dus


You can call the method from support.v4:

AlarmManagerCompat.setExact(...);

The internal implementation contains checks by sdk version.

like image 33
northerngirl Avatar answered Oct 17 '22 18:10

northerngirl


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.

like image 8
Federico De Gioannini Avatar answered Oct 17 '22 17:10

Federico De Gioannini