Подтвердить что ты не робот

Android AlarmManager setExact() не является точным

Мне нужно планировать запланированную задачу каждые 10 минут.

Как и в Lollipop, и более высокая версия setRepeating() неточна, я использую setExact() и (при срабатывании тревоги) я установил новый точный сигнал тревоги за 10 минут.

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 рассчитывается SystemClock.elapsedRealtime() + 600_000;

Когда срабатывает будильник, сначала я планирую новый, только после этого запускаю свою запланированную задачу.

setAlarm();
mySheduledTask;

У меня есть разрешение WAKE_LOCK в моем манифесте.

Когда я тестирую это на Android 4 - он отлично работает (отклонение может быть 12-15 миллисекунд).

Но когда я запускаю приложение на Xiaomi Redmi Note 3 Pro (5.1.1) - отклонение может быть до 15 секунд!

Например, я вижу в своем файле журнала: первый запуск был в 1467119934477 (времени RTC), второй - на 1467120541683. Разница 607_206 миллисекунды, а не 600_000, как и планировалось!

Что мне не хватает?. Каким образом можно моделировать поведение системной тревоги (это самая близкая утилита, которая может описать мой подход)?

PS. Я использую IntentService для PendingIntent = PendingIntent.getService(context, 0, myIntent, 0);

4b9b3361

Ответ 1

os выбирает, как будут срабатывать тревоги, с учетом указанного вами времени. Из-за этого, когда телефон переходит в режим "полуспяти", он не обязательно будет использовать ресурс в тот момент, когда вы этого хотите. В принципе, он ждет "окон", которые откроет os, и только тогда будет запускаться тревога, которую вы хотите запустить, поэтому вы испытываете временные промежутки.

Это было введено в Marshmellow OS и будет продолжено и на операционной системе Nugat, как часть Google, пытающегося улучшить аккумулятор устройства.

Здесь, у вас есть 2 варианта:

  • Примите временные задержки (но, возможно, подумайте об использовании JobSchedular, который более рекомендуется и сэкономит вам батарею).
  • Используйте setExactAndAllowWhileIdle, что может вызвать проблемы с батареей (используйте это осторожно, слишком много аварийных сигналов будет плохо для вашей батареи). Этот метод не повторяется, поэтому вам нужно объявить следующее задание, которое будет запущено на службе, которую открывает pendingIntent.

Если вы выберете вариант 2, вот начало:

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.LOLLIPOP)
    am.setExact(ALARM_TYPE, calendar.getTimeInMillis(), pendingIntent);
else
    am.set(ALARM_TYPE, calendar.getTimeInMillis(), pendingIntent);

Ответ 2

Возможно, возможное обходное решение может быть примерно таким: вы планируете будильник примерно за 1 минуту до ожидаемого времени, чем вы используете Handler.postDelayed для покрытия оставшегося времени.

Здесь вы можете найти пример такой реализации. Операция просто настроила первый аварийный сигнал:

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

}

чем приемник продолжает цикл:

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

    }

}

Я надеюсь, что это помогло.

Ответ 3

Из документации android от AlarmManager

Начиная с доставки сообщений API 19 (KITKAT) неточно: ОС сбрасывает аварийные сигналы, чтобы свести к минимуму пробуждения и использование батареи. Существуют новые API-интерфейсы для поддержки приложений, требующих строгих гарантий доставки; см. setWindow (int, long, long, PendingIntent) и setExact (int, long, PendingIntent). Приложения, чья targetSdkVersion ранее, чем API 19, будут продолжать видеть предыдущее поведение, при котором все сигналы тревоги отправляются точно по запросу.

Также при использовании setExact():

Тревога будет доставлена ​​ как можно ближе к запрошенному времени запуска.

Таким образом, он все еще не гарантирует, что setExact будет Exact.

Ответ 4

Вы можете попробовать AlarmManager.setAlarmClock, возможно, это может вам помочь.

Еще одна вещь, которую вам нужно проверить, какой тип BroadcastReceiver вы используете, будет лучше использовать WakefulBroadcastReceiver

Btw вам нужно изменить логику работы с Alarm Manager для поддержки Android M, вы можете сделать что-то вроде этого:

if(Build.VERSION.SDK_INT < 23){
    if(Build.VERSION.SDK_INT >= 19) {
        setExact(...);
    } else {
        set(...);
    }
} else {
    setExactAndAllowWhileIdle(...);
}

Ответ 5

Чтобы ответить на вопрос о системной тревоге...

Приложение Android Clock Clock/Desk Clock использует комбинацию setAlarmClock и setExactAndAllowWhileIdle.

Следующий код используется для обновления уведомлений:

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

alarmManager.setAlarmClock(info, operation);


В то же время для расчета фактического сигнала тревоги используется следующий код:

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


Ожидающее намерение, установленное в setExactAndAllowWhileIdle, вызывает тревогу, тогда как setAlarmClock намерение просто игнорируется.


Android Googlesource

Ответ 6

Вы можете вызвать метод из support.v4:

AlarmManagerCompat.setExact(...);

Внутренняя реализация содержит проверки по версии sdk.