Как настроить будильник на точное время после всех новейших ограничений на Android? - PullRequest
27 голосов
/ 05 февраля 2020

Примечание: я пробовал различные решения, о которых написано здесь, в StackOverflow (пример здесь ). Пожалуйста, не закрывайте это, не проверяя, работает ли ваше решение из того, что вы нашли, используя тест, который я написал ниже.

Фон

В приложении есть требование, чтобы пользователь устанавливает напоминание, которое должно быть запланировано на определенное время c, поэтому, когда приложение запускается в это время, оно делает что-то крошечное в фоновом режиме (просто некоторая операция запроса к БД) и показывает простое уведомление, чтобы сообщить о напоминании .

Раньше я использовал простой код, чтобы установить что-то для планирования на относительно конкретное c время:

            val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
            val pendingIntent = PendingIntent.getBroadcast(context, requestId, Intent(context, AlarmReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
            when {
                VERSION.SDK_INT >= VERSION_CODES.KITKAT -> alarmManager.setExact(AlarmManager.RTC_WAKEUP, timeToTrigger, pendingIntent)
                else -> alarmManager.set(AlarmManager.RTC_WAKEUP, timeToTrigger, pendingIntent)
            }
class AlarmReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        Log.d("AppLog", "AlarmReceiver onReceive")
        //do something in the real app
    }
}

Использование:

            val timeToTrigger = System.currentTimeMillis() + java.util.concurrent.TimeUnit.MINUTES.toMillis(1)
            setAlarm(this, timeToTrigger, 1)

Проблема

Я сейчас тестировал этот код на эмуляторах на новых Android версиях и на Pixel 4 с Android 10, и он, похоже, не срабатывает, а может и не срабатывает после очень долгого времени, с тех пор как я это предоставляю. Мне хорошо известно о ужасном поведении , которое некоторых OEM * добавлено для удаления приложений из недавних задач, но этот включен и эмуляторы, и устройство Pixel 4. (в наличии).

Я прочитал на документах о настройке будильника, что он ограничен для приложений, так что это не будет происходить слишком часто, но это не объясняет, как установить будильник в определенное время c, и не объясняет, почему приложению Google Clock это удается.

Не только это, но в соответствии с тем, что я понимаю, в нем говорится, что ограничения должны применяться, особенно в отношении состояния устройства с низким энергопотреблением, но в моем случае у меня не было этого состояния, как на устройстве, так и на эмуляторах. Я установил, что аварийные сигналы должны срабатывать примерно через минуту.

Видя, что многие приложения с будильником больше не работают, как раньше, я думаю, что в документах чего-то не хватает. Примером таких приложений является популярное приложение Timely , которое было куплено Google , но никогда не получало новых обновлений для обработки новых ограничений, и теперь пользователи хотят его вернуть., Однако некоторые популярные приложения работают нормально, например , это .

Что я пробовал

Чтобы проверить, действительно ли работает будильник, я выполняю эти тесты, когда Попытка включить будильник через минуту, после первой установки приложения, все время, пока устройство подключено к P C (для просмотра журналов):

  1. Проверка, когда приложение находится на переднем плане, видимое для пользователя. - заняло 1-2 минуты.
  2. Проверка, когда приложение было отправлено в фоновый режим (например, с помощью кнопки «Домой»), - прошло около 1 минуты
  3. Проверка, когда задача приложения была удалена из недавние задачи. - Я ждал более 20 минут и не видел срабатывания будильника, записи в журналы.
  4. Как # 3, но и выключил экран. Наверное, было бы хуже ...

Я пытался использовать следующие вещи, все не работают:

  1. alarmManager.setAlarmClock(AlarmManager.AlarmClockInfo(timeToTrigger, pendingIntent), pendingIntent)

  2. alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeToTrigger, pendingIntent)

  3. AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, timeToTrigger, pendingIntent)

  4. комбинация любого из вышеперечисленных, с:

    if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) alarmManager.setWindow(AlarmManager.RTC_WAKEUP, 0, 60 * 1000L, pendingIntent)

  5. Попытка использовать службу вместо BroadcastReceiver. Также попробовал другой процесс.

  6. Попытка заставить приложение игнорироваться из-за оптимизации батареи (не помогло), но так как другие приложения не нуждаются в этом, я не должен используйте его либо.

  7. Попробовал с помощью этого:

            if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP)
                alarmManager.setAlarmClock(AlarmManager.AlarmClockInfo(timeToTrigger, pendingIntent), pendingIntent)
            AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, timeToTrigger, pendingIntent)
Попытка иметь службу, которая будет иметь триггер onTaskRemoved , чтобы перепланировать тревогу там, но это также не помогло (хотя служба работала нормально).

Что касается приложения Google Clock, я не увидел в нем ничего особенного, за исключением того, что оно отображает уведомление до его запуска, а также я не вижу его в разделе «неоптимизировано» экрана настроек оптимизации батареи. .

Видя, что это похоже на ошибку, я сообщил об этом здесь , включая пример проекта и видео, чтобы показать проблему.

Я проверил на нескольких версиях эмулятора, и кажется, что это поведение началось с API 27 (Android 8.1 - Oreo). Смотря на документы , я не вижу упоминания AlarmManager, но вместо этого было написано о различных фоновых работах.

Вопросы

  1. Как мы устанавливаем что-то для запуска в относительно точное время в наше время?

  2. Почему вышеперечисленные решения больше не работают? Я что-то упустил? Разрешение? Может быть, я должен использовать вместо Worker? Но не значит ли это, что он вообще может не сработать вовремя?

  3. Как приложение Google "Часы" преодолевает все это и в любом случае срабатывает в точное время, всегда , даже если это сработало всего минуту go? Только потому, что это системное приложение? Что, если оно будет установлено как пользовательское приложение на устройстве, которое не имеет встроенного?

Если вы скажете, что это потому, что это системное приложение, я обнаружил, что другое приложение, которое может вызвать тревогу дважды за 2 минуты, здесь , хотя я думаю, что иногда оно может использовать службу переднего плана.

РЕДАКТИРОВАТЬ: сделал крошечный репозиторий Github, чтобы примерить идеи, здесь .


РЕДАКТИРОВАТЬ: наконец нашел образец , который является как открытым исходным кодом, так и не есть эта проблема. К сожалению, это очень сложно, и я все еще пытаюсь выяснить, что делает его настолько отличным (и какой минимальный код я должен добавить к своему PO C), что позволяет планировать его сигналы тревоги после удаления приложения из недавних задач

Ответы [ 6 ]

4 голосов
/ 20 февраля 2020

Найден странный обходной путь (пример здесь ), который, кажется, работает для всех версий, включая даже Android R:

  1. Получите разрешение Разрешение SAW, заявленное в манифесте:
      <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

На Android R вам также необходимо будет предоставить его. Прежде, не кажется, что это должно быть предоставлено, просто объявлено. Не уверен, почему это изменилось на R, но я могу сказать, что SAW может потребоваться в качестве возможного решения для запуска вещей в фоновом режиме, как написано здесь для Android 10.

Иметь службу, которая будет определять, когда задачи были удалены, а когда она это делает, открыть поддельное действие, которое все, что он делает, это само закрывается:
class OnTaskRemovedDetectorService : Service() {
    override fun onBind(intent: Intent?) = null

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = START_STICKY

    override fun onTaskRemoved(rootIntent: Intent?) {
        super.onTaskRemoved(rootIntent)
        Log.e("AppLog", "onTaskRemoved")
        applicationContext.startActivity(Intent(this, FakeActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
        stopSelf()
    }

}

FakeActivity. kt

class FakeActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d("AppLog", "FakeActivity")
        finish()
    }
}

Вы также можете сделать это действие практически невидимым для пользователя, используя эту тему:

    <style name="AppTheme.Translucent" parent="@style/Theme.AppCompat.NoActionBar">
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:colorBackgroundCacheHint">@null</item>
        <item name="android:windowIsTranslucent">true</item>
    </style>

К сожалению, это странный обходной путь. Я надеюсь найти более удачный обходной путь к этому.

Ограничение говорит о запуске Activity, поэтому моя текущая идея заключается в том, что, возможно, если я запущу службу переднего плана на долю секунды, это тоже поможет, и для этого Мне даже не нужно разрешение SAW.

РЕДАКТИРОВАТЬ: ОК, я попытался с услугой переднего плана (образец здесь ), и это не сработало. Понятия не имею, почему активность работает, а не сервис. Я даже попытался перепланировать тревогу там и попытался оставить службу на некоторое время, даже после перепланировки. Также пробовал обычный сервис, но, конечно, он сразу же закрылся, поскольку задача была удалена, и она не работала вообще (даже если я создал поток для запуска в фоновом режиме).

Другое возможное решение то, что я не пробовал, - это всегда иметь службу переднего плана или, по крайней мере, до тех пор, пока задача не будет удалена, но это немного странно, и я не вижу приложений, о которых я упоминал, использующих его.

РЕДАКТИРОВАТЬ: попытался запустить службу переднего плана перед удалением задачи приложения, и немного позже, и сигнал тревоги все еще работал. Также пытался сделать так, чтобы этот сервис отвечал за событие с удаленной задачей, и сразу же закрывался, когда это происходит, и он все еще работал (пример здесь ). Преимущество этого обходного пути заключается в том, что вам вообще не нужно иметь разрешение SAW. Недостатком является то, что у вас есть служба с уведомлением, а приложение уже видно пользователю. Интересно, возможно ли скрыть уведомление, когда приложение уже находится на переднем плане через Activity.


EDIT: кажется, что это ошибка в Android Studio (сообщается здесь В том числе видео сравнения версий). Когда вы запускаете приложение из проблемной c версии, которую я пробовал, это может привести к сбросу аварийных сигналов.

Если вы запустите приложение из модуля запуска, оно будет работать нормально.

Это текущий код для установки будильника:

        val timeToTrigger = System.currentTimeMillis() + 10 * 1000
        val pendingShowList = PendingIntent.getActivity(this, 1, Intent(this, SomeActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
        val pendingIntent = PendingIntent.getBroadcast(this, 1, Intent(this, AlarmReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
        manager.setAlarmClock(AlarmManager.AlarmClockInfo(timeToTrigger, pendingShowList), pendingIntent)

Мне даже не нужно использовать "pendingShowList". Использование нуля тоже нормально.

4 голосов
/ 19 февраля 2020

Нам нечего делать.

Если ваше приложение не занесено в белый список, оно всегда будет уничтожено после удаления из недавних приложений.

Потому что Производитель оригинального оборудования (OME) постоянно нарушает Android соответствие .

Так что, если ваше приложение не занесено в белый список устройства, оно не будет запускать фоновую работу даже тревоги - в случае, если ваше приложение удалено из недавних приложений.

Вы можете найти список устройств с таким поведением здесь ТАКЖЕ вы можете найти побочное решение, однако оно выиграло ' хорошо работает.

1 голос
/ 13 февраля 2020
  1. Убедитесь, что переданное вами намерение является явным и имеет флаг Intent.FLAG_RECEIVER_FOREGROUND.

https://developer.android.com/about/versions/oreo/background#broadcasts

Intent intent = new Intent(context, Receiver.class);
intent.setAction(action);
...
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);

PendingIntent operation = PendingIntent.getBroadcast(context, 0, intent, flags);
Используйте setExactAndAllowWhileIdle() при настройке API 23 +.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, operation);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    alarmManager.setExact(AlarmManager.RTC_WAKEUP, time, operation);
} else {
    alarmManager.set(AlarmManager.RTC_WAKEUP, time, operation);
}
Начните свою тревогу в качестве службы переднего плана:

https://developer.android.com/about/versions/oreo/background#migration

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    context.startForegroundService(intent);
} else {
    context.startService(intent);
}
И не забывайте разрешения:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
0 голосов
/ 01 марта 2020

Я автор проекта с открытым исходным кодом, который вы упомянули в своем вопросе ( простой будильник) .

Я удивлен, что использование AlarmManager.setAlarmClock не работает для вас, потому что мое приложение делает именно это. Код находится в файле AlarmSetter.kt. Вот фрагмент:

  val pendingAlarm = Intent(ACTION_FIRED)
                .apply {
                    setClass(mContext, AlarmsReceiver::class.java)
                    putExtra(EXTRA_ID, id)
                    putExtra(EXTRA_TYPE, typeName)
                }
                .let { PendingIntent.getBroadcast(mContext, pendingAlarmRequestCode, it, PendingIntent.FLAG_UPDATE_CURRENT) }

            val pendingShowList = PendingIntent.getActivity(
                    mContext,
                    100500,
                    Intent(mContext, AlarmsListActivity::class.java),
                    PendingIntent.FLAG_UPDATE_CURRENT
            )

            am.setAlarmClock(AlarmManager.AlarmClockInfo(calendar.timeInMillis, pendingShowList), pendingAlarm)

По сути, в этом нет ничего особенного, просто убедитесь, что у намерения есть действие и целевой класс, который в моем случае является широковещательным приемником.

0 голосов
/ 22 февраля 2020

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

Вот код для запроса:

PowerManager powerManager = (PowerManager) getApplicationContext().getSystemService(POWER_SERVICE);
            String packageName = "your Package name";
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                Intent i = new Intent();
                if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
                    i.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
                    i.setData(Uri.parse("package:" + packageName));
                    startActivity(i);
0 голосов
/ 22 февраля 2020

Я знаю, что это не эффективно, но может быть более согласованным с точностью до 60 секунд.

https://developer.android.com/reference/android/content/Intent#ACTION_TIME_TICK

, если этот вещательный приемник используется внутри Служба переднего плана, вы можете проверять время каждую минуту и ​​принимать решение о принятии меры.

...