如何在 Android Q 上设置闹钟?

How to set an alarm on Android Q?

背景

Android Q 似乎有很多新限制,但警报不应该是其中之一:

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

问题

我为设置闹钟而编写的旧代码,在 P 上运行良好,但似乎不能再正常运行了:

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

接收方确实获得了 Intent,但是当它尝试打开 Activity 时,有时什么也没有发生:

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

将此视为错误,我报告了 here(包括示例代码)

我试过的

我试图找到 Q 上的新内容,看看是什么导致了它,但我找不到。

我也尝试过(如果您查看代码)直接打开 Activity 而不是通过 BroadcastReceiver。

并且,我尝试在不同的进程中将 BroadcastReceiver 设置为 运行。

所有这些都没有帮助。

我发现虽然有些闹钟应用程序无法正常工作(例如 Timely), some apps work just fine (such as "Alarm Clock Xtreme")。

问题

  1. On Android 问,有没有官方的方法可以让闹钟正常工作?要打开将向用户显示的 Activity,就像闹钟应用程序应该的那样?

  2. 我编写的代码有什么问题?为什么它适用于 P 而不是总是适用于 Q?


编辑:在被建议在我启动 Activity 并使用 FullScreenIntent 时显示通知后,我得到了一些工作,但它只在屏幕关闭时工作。当屏幕打开时,它只显示通知,这是一件坏事,因为重点是向用户显示警报,而一些用户(比如我)不想提醒 -警报通知,在某些事情的中间突然出现并且不会暂停任何事情。我希望有人可以帮助解决这个问题,因为这曾经是一件很容易做的事情,现在却变得太复杂了...

这是当前代码(可用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))
    }
}

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

您正在尝试从后台启动 activity。 That is banned on Android 10+ 大部分。

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

来自您引用的 material,并添加了重点:"The app receives a notification PendingIntent from the system"。您没有使用通知。因此,此例外不适用。

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?

使用全屏通知 Intent,如 the documentation 中所述。如果屏幕被锁定,您的 activity 将在引发通知时显示。如果屏幕已解锁,则会显示高优先级 ("heads up") 通知。换句话说:

  • 如果设备没有被使用,你得到你想要的

  • 如果设备可能正在使用中,用户无需接管屏幕即可了解该事件,因此您不会干扰用户正在做的任何事情(例如,依靠导航开车时应用)