11

Background

Android Q seems to have plenty of new restrictions, but alarms shouldn't be one of them:

https://developer.android.com/guide/components/activities/background-starts

The problem

It seems that old code that I made for setting an alarm, which worked fine on P, can't work well anymore:

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var manager: AlarmManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        manager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
        button.setOnClickListener {
            Log.d("AppLog", "alarm set")
            Toast.makeText(this, "alarm set", Toast.LENGTH_SHORT).show()
            val timeToTrigger = System.currentTimeMillis() + 10 * 1000
            setAlarm(this, timeToTrigger, 1)
        }
    }

    companion object {
        fun setAlarm(context: Context, timeToTrigger: Long, requestId: Int) {
            val manager = 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.LOLLIPOP -> manager.setAlarmClock(AlarmClockInfo(timeToTrigger, pendingIntent), pendingIntent)
                VERSION.SDK_INT >= VERSION_CODES.KITKAT -> manager.setExact(AlarmManager.RTC_WAKEUP, timeToTrigger, pendingIntent)
                else -> manager.set(AlarmManager.RTC_WAKEUP, timeToTrigger, pendingIntent)
            }
        }
    }
}

The receiver does get the Intent, but when it tries to open the Activity, sometimes nothing occurs:

AlarmReceiver.kt

class AlarmReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        Log.d("AppLog", "AlarmReceiver onReceive")
        context.startActivity(Intent(context, Main2Activity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
    }
}

Seeing this as a bug, I reported here (including sample code)

What I've tried

I tried to find what's new on Q, to see what could cause it, and I couldn't find it.

I also tried (if you look at the code) to directly open the Activity instead of via a BroadcastReceiver.

And, I tried to set the BroadcastReceiver to run on a different process.

All of those didn't help.

What I have found is that while some alarm clock apps fail to work properly (such as Timely), some apps work just fine (such as "Alarm Clock Xtreme").

The questions

  1. On Android Q, is there an official way to let alarms work correctly? To open an Activity that will be shown to the user, exactly as an alarm clock app should?

  2. What's wrong in the code I've made? How come it works on P but not always on Q?


EDIT: OK after being adviced to have a notification shown while I start the Activity, and also use FullScreenIntent, I got something to work, but it's only working when the screen is turned off. When the screen is turned on, it only shows the notification, which is a bad thing because the whole point is to have an alarm being shown to the user, and some users (like me) don't want to have heads-up-notification for alarms, popping out in the middle of something and not pausing anything. I hope someone can help with this, as this used to be a very easy thing to do, and now it got way too complex...

Here's the current code (available here) :

NotificationId

object NotificationId {
    const val ALARM_TRIGGERED = 1
    @JvmStatic
    private var hasInitialized = false

    @UiThread
    @JvmStatic
    fun initNotificationsChannels(context: Context) {
        if (hasInitialized || Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
            return
        hasInitialized = true
        val channelsToUpdateOrAdd = HashMap<String, NotificationChannel>()
        val channel = NotificationChannel(context.getString(R.string.channel_id__alarm_triggered), context.getString(R.string.channel_name__alarm_triggered), NotificationManager.IMPORTANCE_HIGH)
        channel.description = context.getString(R.string.channel_description__alarm_triggered)
        channel.enableLights(true)
        channel.setSound(null, null)
        channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
        channel.enableVibration(false)
        channel.setShowBadge(false)
        channelsToUpdateOrAdd[channel.id] = channel
        //
        val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        val existingChannels = notificationManager.notificationChannels
        if (existingChannels != null)
            for (existingChannel in existingChannels) {
                //                The importance of an existing channel will only be changed if the new importance is lower than the current value and the user has not altered any settings on this channel.
                //                The group an existing channel will only be changed if the channel does not already belong to a group. All other fields are ignored for channels that already exist.
                val channelToUpdateOrAdd = channelsToUpdateOrAdd[existingChannel.id]
                if (channelToUpdateOrAdd == null) //|| channelToUpdateOrAdd.importance > existingChannel.importance || (existingChannel.group != null && channelToUpdateOrAdd.group != existingChannel.group))
                    notificationManager.deleteNotificationChannel(existingChannel.id)
            }
        for (notificationChannel in channelsToUpdateOrAdd.values) {
            notificationManager.createNotificationChannel(notificationChannel)
        }
    }
}

MyService.kt

class MyService : Service() {
    override fun onBind(p0: Intent?): IBinder? = null
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d("AppLog", "MyService onStartCommand")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationId.initNotificationsChannels(this)
            val builder = NotificationCompat.Builder(this, getString(R.string.channel_id__alarm_triggered)).setSmallIcon(android.R.drawable.sym_def_app_icon) //
                    .setPriority(NotificationCompat.PRIORITY_HIGH).setCategory(NotificationCompat.CATEGORY_ALARM)
            builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
            builder.setShowWhen(false)
            builder.setContentText("Alarm is triggered!")
            builder.setContentTitle("Alarm!!!")
            val fullScreenIntent = Intent(this, Main2Activity::class.java)
            val fullScreenPendingIntent = PendingIntent.getActivity(this, 0,
                    fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT)
            builder.setFullScreenIntent(fullScreenPendingIntent, true)
            startForeground(NotificationId.ALARM_TRIGGERED, builder.build())
            startActivity(Intent(this, Main2Activity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
            Handler().postDelayed({
                stopForeground(true)
                stopSelf()
            }, 2000L)
        } else {
            startActivity(Intent(this, Main2Activity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
        }
        return super.onStartCommand(intent, flags, startId)
    }
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var manager: AlarmManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        manager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
        button.setOnClickListener {
            Log.d("AppLog", "alarm set")
            Toast.makeText(this, "alarm set", Toast.LENGTH_SHORT).show()
            val timeToTrigger = System.currentTimeMillis() + 10 * 1000
            setAlarm(this, timeToTrigger, 1)
        }
    }

    companion object {
        fun setAlarm(context: Context, timeToTrigger: Long, requestId: Int) {
            val manager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
                        val pendingIntent = PendingIntent.getBroadcast(context, requestId, Intent(context, AlarmReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
            //            val pendingIntent = PendingIntent.getBroadcast(context, requestId, Intent(context, Main2Activity::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
//            val pendingIntent = PendingIntent.getService(context, requestId, Intent(context, MyService::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
            when {
                VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP -> manager.setAlarmClock(AlarmClockInfo(timeToTrigger, pendingIntent), pendingIntent)
                VERSION.SDK_INT >= VERSION_CODES.KITKAT -> manager.setExact(AlarmManager.RTC_WAKEUP, timeToTrigger, pendingIntent)
                else -> manager.set(AlarmManager.RTC_WAKEUP, timeToTrigger, pendingIntent)
            }
        }
    }
}

AlarmReceiver.kt

class AlarmReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        Log.d("AppLog", "AlarmReceiver onReceive")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            context.startForegroundService(Intent(context, MyService::class.java))
        } else context.startService(Intent(context, MyService::class.java))
    }
}
android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • 3
    Why aren't you using a notification with a full screen intent as [described in the documentation](https://developer.android.com/guide/components/activities/background-starts#display-notification)? – ianhanniballake Sep 16 '19 at 22:11
  • @ianhanniballake I don't need to show a notification. I want to show an Activity. Where would I put this notification anyway? In the BroadcastReceiver? – android developer Sep 16 '19 at 22:45
  • 2
    That's what a full screen intent attached to a notification does and what the Clock app uses. – ianhanniballake Sep 16 '19 at 22:47
  • @ianhanniballake Again, how could I put it as an alarm? And why would I show a notification, if I want to show an Activity? – android developer Sep 16 '19 at 22:55
  • 2
    Create your notification in your BroadcastReceiver. Full screen intent has been the recommended best practice for alarms since it was introduced in API 9 and was even more important with the introduction of heads up notifications (where your alarm shows as a heads up notification if the user is actively using their device). – ianhanniballake Sep 16 '19 at 23:02
  • @ianhanniballake And then what should I do with a notification, if what I want is to show an Activity? When would the Activity show up, if what you say is to just show a notification? Why show a notification? You didn't explain what's the point of having it. An alarm clock is supposed to show an Activity, not a notification, when there is an alarm. Otherwise it won't wake you up with an easy way to dismiss it. A notification is harder to dismiss. Suppose I wish to show the Activity 10 seconds from now. Where does the notification gets into action? – android developer Sep 16 '19 at 23:05
  • 1
    @ianhanniballake As I wrote, showing just the Activity used to work fine before Q. Something has changed on Q, and I want to find what. – android developer Sep 16 '19 at 23:13
  • 2
    Clearly it was the background activity starts. Setting an alarm with AlarmManager doesn't give you the ability to start activities from the background. – ianhanniballake Sep 16 '19 at 23:14
  • @ianhanniballake Not according to the docs. According to the docs, alarms shouldn't be harmed. I even put a link to it. See "Exceptions to the restriction : The app receives a notification PendingIntent from the system. In the case of pending intents for services and broadcast receivers, the app can start activities for a few seconds after the pending intent is sent." , and indeed there are alarm clock apps that still work. But again, why do you keep talking about a notification when I talk about an Activity I wish to show from an alarm? This code worked for all versions before Q. – android developer Sep 16 '19 at 23:22

1 Answers1

10

What's wrong in the code I've made? How come it works on P but not always on Q?

You are attempting to start an activity from the background. That is banned on Android 10+ for the most part.

According to the docs, alarms shouldn't be harmed.

From the material that you quoted, with emphasis added: "The app receives a notification PendingIntent from the system". You are not using notifications. And, therefore, this exception does not apply.

On Android Q, is there an official way to let alarms work correctly? To open an Activity that will be shown to the user, exactly as an alarm clock app should?

Use a notification with a full-screen Intent, as is covered in the documentation. If the screen is locked, your activity will be displayed when the notification is raised. If the screen is unlocked, a high-priority ("heads up") notification will be displayed instead. In other words:

  • If the device is not being used, you get what you want

  • If the device is probably being used, the user find out about the event without your taking over the screen, so you do not interfere with whatever the user is doing (e.g., relying on a navigation app while driving)

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • 1
    It doesn't show me any error when I tried without a notification. Anyway, I tried with a notification and FullScreenIntent, and it still doesn't get shown unless I'm on the lock screen. I tried to delay starting the Activity, and it still didn't work. I've updated my question to show the current code. Please check it out, and tell me what's wrong now. It used to be so easy. Now it's very complex. Need the alarm to trigger BroadcastReceiver, which opens a service, which creates a notification and starts as foreground, which then only works when screen is turned off for some reason. – android developer Sep 17 '19 at 18:48
  • 1
    I mean, even for foreground service I know that they made it very complex and it could crash in some cases (for example if the app is already in the foreground). When such a thing occurs here, the alarm might not be shown at all to the user. And how come I couldn't see any log about the issues I've faced? When the service tries to start the Activity, nothing occurred. Nothing was written to the logs... – android developer Sep 17 '19 at 18:50
  • 1
    @androiddeveloper: [This sample app](https://gitlab.com/commonsguy/cw-android-q/tree/v0.5/PayAttention) from [this book](https://commonsware.com/Q) demonstrates the use of a full-screen `Intent` notification. It works for me on Android 10 (tested on a Google Pixel), showing the heads-up bubble. I happen to use `WorkManager`, not `AlarmManager`, though. It also demonstrates what happens if you try starting an activity from the background. I get `W/ActivityTaskManager: Background activity start` in Logcat. – CommonsWare Sep 17 '19 at 22:58
  • 1
    Well I get the heads-up-notification instead of an Activity when the device is unlocked, and if I don't use the Intent you mentioned, I get nothing at all.This is even though the docs say "The app receives a notification PendingIntent from the system. In the case of pending intents for services and broadcast receivers, the app can start activities for a few seconds after the pending intent is sent." . It should have let me start an Activity. It's also very vague about "few seconds". Should I wait a few seconds and then start the Activity? If so, how can I know how many seconds? And why ? – android developer Sep 17 '19 at 23:55
  • 1
    @androiddeveloper: "Well I get the heads-up-notification instead of an Activity when the device is unlocked" -- correct. That is the expected behavior. "It should have let me start an Activity" -- the `PendingIntent` *in the `Notification`* can start an activity, as my app demonstrates. A `PendingIntent` executed by other things, such as `AlarmManager`, cannot. – CommonsWare Sep 18 '19 at 12:23
  • 1
    @androiddeveloper: If you are saying that you attached a broadcast or service `PendingIntent` to the `Notification`, and when the user clicked the `Notification` you were unable to start an activity from the receiver or service, that might be a bug. As you cite, you should have a few seconds' eligibility to start an activity. Unfortunately, this sort of "timing thing" often has problems (see the whole "did not call `startForegorund()` after using `startForegroundService()`" fiasco). I would recommend having your `Notification` use an activity `PendingIntent`. – CommonsWare Sep 18 '19 at 12:25
  • 1
    I'm not talking about clicking on the notification. I'm talking about showing the Activity. Some people, like me, would prefer sometimes to have the alarm in full screen, and not in some notification. I noticed that if I have other means that are written on the docs, such as SAW permission (was easiest), it works fine. Why after all of these steps, of having an alarm trigger BroadcastReceiver and then a service and then making it a foreground service with a notification - I still can't open an Activity? It's clear to the user which app shows the Activity. – android developer Sep 19 '19 at 08:31
  • 1
    @androiddeveloper: "such as SAW permission (was easiest)" -- I suspect that `SYSTEM_ALERT_WINDOW` is going away entirely in Android R. It already is gone for some Android 10 devices. – CommonsWare Sep 19 '19 at 11:02
  • 1
    Right. I've heard it on their lectures. As for "some Android 10 devices", this is for Android Go devices, as Google said that it could affect performance too much. Doesn't make sense to me, because they allow various other things that can affect performance, and it should always be the user's choice. I used it here only to test it out, as it's easiest to find the change . As a side note, I hope Google will not remove this permission, because it's very useful and many apps will suffer from it being missing, some depend on it completely. And Bubble API is just a tiny fraction of what it covers. – android developer Sep 19 '19 at 14:22
  • 1
    Anyway, you say that it's impossible to use notification for being full screen, and that I just didn't understand the text correctly, right? I select this answer to be correct, then. Do you know a lot about Q's changes? If so, I'm still struggling with storage related question: https://stackoverflow.com/q/56309165/878126 Please have a look. I will even grant the bounty if what you find works. – android developer Sep 19 '19 at 14:26