Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to set an alarm on Android Q?

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))
    }
}
like image 548
android developer Avatar asked Sep 16 '19 22:09

android developer


People also ask

Where is the alarm setting on this phone?

To set an alarm on Android, first open the Clock app. If it's not already on your homescreen, you can find it by swiping up from the bottom of the screen and going through your App menu. 1. Tap on the "ALARM" tab at the top-left of the Clock app.


1 Answers

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)

like image 169
CommonsWare Avatar answered Sep 22 '22 09:09

CommonsWare