Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
457 views
in Technique[技术] by (71.8m points)

alarmmanager - How to set an alarm to be scheduled at an exact time after all the newest restrictions on Android?

Note: I tried various solutions that are written about here on StackOverflow (example here). Please do not close this without checking if your solution from what you've found works using the test I've written below.

Background

There is a requirement on the app, that the user sets a reminder to be scheduled at a specific time, so when the app gets triggered on this time, it does something tiny in the background (just some DB query operation), and shows a simple notification, to tell about the reminder.

In the past, I used a simple code to set something to be scheduled at a relatively specific time:

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

Usage:

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

The problem

I've now tested this code on emulators on new Android versions and on Pixel 4 with Android 10, and it doesn't seem to trigger, or maybe it triggers after a very long time since what I provide it. I'm well aware of the terrible behavior that some OEMs added to removing apps from the recent tasks, but this one is on both emulators and Pixel 4 device (stock).

I've read on the docs about setting an alarm, that it got restricted for apps so that it won't occur too often, but this doesn't explain how to set an alarm at a specific time, and it doesn't explain how come Google's Clock app succeeds doing it.

Not only that, but according to what I understand, it says the restrictions should be applied especially for low power state of the device, but in my case, I didn't have this state, on both the device and on the emulators. I've set the alarms to be triggered in about a minute from now.

Seeing that many alarm clock apps don't work anymore as they used to, I think there is something that is missing on the docs. Example of such apps is the popular Timely app that was bought by Google but never got new updates to handle the new restrictions, and now users want it back.. However, some popular apps do work fine, such as this one.

What I've tried

To test that indeed the alarm works, I perform these tests when trying to trigger the alarm in a minute from now, after installing the app for the first time, all while the device is connected to the PC (to see the logs) :

  1. Test when the app is in the foreground, visible to the user. - took 1-2 minutes.
  2. Test when the app was sent to the background (using the home button, for example) - took about 1 minute
  3. Test when app's task was removed from the recent tasks. - I waited more than 20 minutes and didn't see the alarm being triggered, writing to logs.
  4. Like #3, but also turn off the screen. It would probably be worse...

I tried to use the next things, all don't work:

  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. combination of any of the above, with :

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

  5. Tried to use a service instead of BroadcastReceiver. Also tried on a different process.

  6. Tried making the app be ignored from the battery optimization (didn't help), but since other apps don't need it, I shouldn't use it either.

  7. Tried using this:

            if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP)
                alarmManager.setAlarmClock(AlarmManager.AlarmClockInfo(timeToTrigger, pendingIntent), pendingIntent)
            AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, timeToTrigger, pendingIntent)
  1. Tried having a service that will have a trigger of onTaskRemoved , to re-schedule the alarm there, but this also didn't help (the service worked fine though).

As for Google's Clock app, I didn't see anything special about it except that it shows a notification before being triggered, and I also don't see it in the "not optimized" section of the battery-optimization settings screen.

Seeing that this seems like a bug, I reported about this here, including a sample project and video to show the issue.

I've checked on multiple versions of the emulator, and it seems that this behavior started from API 27 (Android 8.1 - Oreo). Looking at the docs, I don't see AlarmManager being mentioned, but instead, it was written about various background work.

The questions

  1. How do we set something to be triggered at a relatively exact time nowadays?

  2. How come the above solutions don't work anymore? Am I missing anything? Permission? Maybe I'm supposed to use a Worker instead? But then wouldn't it mean that it might not trigger on time at all?

  3. How does Google "Clock" app overcome all of this, and triggers anyway on the exact time, always, even if it was triggered just a minute ago? Is it only because it's a system app? What if it gets installed as a user app, on a device that doesn't have it built-in?

If you say that it's because it's a system app, I've found another app that can trigger an alarm twice in 2 minutes, here, though I think it might use a foreground service sometimes.

EDIT: made a tiny Github repository to try ideas on, here.


EDIT: finally found a sample that is both open-sourced and doesn't have this issue. Sadly it's very complex and I still try to figure out what makes it so different (and what's the minimal code that I should add to my POC) that lets its alarms stay scheduled after removing the app from the recent tasks

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Found a weird workaround (sample here) that seems to work for all versions, including even Android R:

  1. Have the permission SAW permission declared in the manifest:
      <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

On Android R you will have to also have it granted. On before, doesn't seem like it's needed to be granted, just declared. Not sure why this changed on R, but I can say that SAW could be required as a possible solution to start things in the background, as written here for Android 10.

  1. Have a service that will detect when the tasks was removed, and when it does, open a fake Activity that all it does is to close itself:
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()
    }
}

You can also make this Activity almost invisible to the user using this theme:

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

Sadly, this is a weird workaround. I hope to find a nicer workaround to this.

The restriction talks about starting Activity, so my current idea is that maybe if I start a foreground service for a split of a second it will also help, and for this I won't even need SAW permission.

EDIT: OK I tried with a foreground service (sample here), and it didn't work. No idea why an Activity is working but not a service. I even tried to re-schedule the alarm there and tried to let the service stay for a bit, even after re-schedule. Also tried a normal service but of course it closed right away, as the task was removed, and it didn't work at all (even if I created a thread to run in the background).

Another possible solution that I didn't try is to have a foreground service forever, or at least till the task is removed, but this is a bit weird and I don't see the apps I've mentioned using it.

EDIT: tried to have a foreground service running before removal of the app's task, and for a bit afterwards, and the alarm still worked. Also tried to have this service to be the one in charge of task-removed event, and to close itself right away when it occurs, and it still worked (sample here). The advantage of this workaround is that you don't have to have the SAW permission at all. The disadvantage is that you have a service with a notification while the app is already visible to the user. I wonder if it's possible to hide the notification while the app is already in the foreground via the Activity.


EDIT: Seems it's a bug on Android Studio (reported here, including videos comparing versions). When you launch the app from the problematic version I tried, it could cause the alarms to be cleared.

If you launch the app from the launcher, it works fine.

This is the current code to set the alarm:

        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)

I don't even have to use "pendingShowList". Using null is also ok.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...