AOSP/DeskClock - Add Kotlin for AlarmNotifications and AlarmService

Test: manual - Ran the following on Sargo phone. Tested the alarm
setting and ringing behaviour.

$ source build/envsetup.sh
$ lunch aosp_sargo-userdebug
$ make DeskClockKotlin -j
$ adb install out/target/product/sargo/product/app/DeskClockKotlin/DeskClockKotlin.apk

BUG: 157255731

Change-Id: I5ca2145621c8a08fa675e570b1f8ebe24e3dbccd
diff --git a/Android.bp b/Android.bp
index 1795461..bc7a6bf 100644
--- a/Android.bp
+++ b/Android.bp
@@ -44,6 +44,8 @@
         "src/**/alarmclock/*.java",
         "src/**/deskclock/alarms/AlarmActivity.java",
         "src/**/deskclock/alarms/AlarmKlaxon.java",
+        "src/**/deskclock/alarms/AlarmNotifications.java",
+        "src/**/deskclock/alarms/AlarmService.java",
         "src/**/deskclock/alarms/AlarmTimeClickHandler.java",
         "src/**/deskclock/alarms/AlarmUpdateHandler.java",
         "src/**/deskclock/alarms/ScrollHandler.java",
diff --git a/src/com/android/deskclock/alarms/AlarmNotifications.kt b/src/com/android/deskclock/alarms/AlarmNotifications.kt
new file mode 100644
index 0000000..364a24b
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmNotifications.kt
@@ -0,0 +1,604 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.alarms
+
+import android.annotation.TargetApi
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.os.Build
+import android.service.notification.StatusBarNotification
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+
+import com.android.deskclock.AlarmClockFragment
+import com.android.deskclock.AlarmUtils
+import com.android.deskclock.DeskClock
+import com.android.deskclock.LogUtils
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+internal object AlarmNotifications {
+    const val EXTRA_NOTIFICATION_ID = "extra_notification_id"
+
+    /**
+     * Notification channel containing all low priority notifications.
+     */
+    private const val ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID = "alarmLowPriorityNotification"
+
+    /**
+     * Notification channel containing all high priority notifications.
+     */
+    private const val ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID = "alarmHighPriorityNotification"
+
+    /**
+     * Notification channel containing all snooze notifications.
+     */
+    private const val ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID = "alarmSnoozeNotification"
+
+    /**
+     * Notification channel containing all missed notifications.
+     */
+    private const val ALARM_MISSED_NOTIFICATION_CHANNEL_ID = "alarmMissedNotification"
+
+    /**
+     * Notification channel containing all alarm notifications.
+     */
+    private const val ALARM_NOTIFICATION_CHANNEL_ID = "alarmNotification"
+
+    /**
+     * Formats times such that chronological order and lexicographical order agree.
+     */
+    private val SORT_KEY_FORMAT: DateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
+
+    /**
+     * This value is coordinated with group ids from
+     * [com.android.deskclock.data.NotificationModel]
+     */
+    private const val UPCOMING_GROUP_KEY = "1"
+
+    /**
+     * This value is coordinated with group ids from
+     * [com.android.deskclock.data.NotificationModel]
+     */
+    private const val MISSED_GROUP_KEY = "4"
+
+    /**
+     * This value is coordinated with notification ids from
+     * [com.android.deskclock.data.NotificationModel]
+     */
+    private const val ALARM_GROUP_NOTIFICATION_ID = Int.MAX_VALUE - 4
+
+    /**
+     * This value is coordinated with notification ids from
+     * [com.android.deskclock.data.NotificationModel]
+     */
+    private const val ALARM_GROUP_MISSED_NOTIFICATION_ID = Int.MAX_VALUE - 5
+
+    /**
+     * This value is coordinated with notification ids from
+     * [com.android.deskclock.data.NotificationModel]
+     */
+    private const val ALARM_FIRING_NOTIFICATION_ID = Int.MAX_VALUE - 7
+
+    @JvmStatic
+    @Synchronized
+    fun showLowPriorityNotification(
+        context: Context,
+        instance: AlarmInstance
+    ) {
+        LogUtils.v("Displaying low priority notification for alarm instance: " + instance.mId)
+
+        val builder: NotificationCompat.Builder = NotificationCompat.Builder(
+                context, ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID)
+                .setShowWhen(false)
+                .setContentTitle(context.getString(
+                        R.string.alarm_alert_predismiss_title))
+                .setContentText(AlarmUtils.getAlarmText(
+                        context, instance, true /* includeLabel */))
+                .setColor(ContextCompat.getColor(context, R.color.default_background))
+                .setSmallIcon(R.drawable.stat_notify_alarm)
+                .setAutoCancel(false)
+                .setSortKey(createSortKey(instance))
+                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+                .setCategory(NotificationCompat.CATEGORY_EVENT)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setLocalOnly(true)
+
+        if (Utils.isNOrLater()) {
+            builder.setGroup(UPCOMING_GROUP_KEY)
+        }
+
+        // Setup up hide notification
+        val hideIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+                AlarmStateManager.ALARM_DELETE_TAG, instance,
+                InstancesColumns.HIDE_NOTIFICATION_STATE)
+        val id = instance.hashCode()
+        builder.setDeleteIntent(PendingIntent.getService(context, id,
+                hideIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        // Setup up dismiss action
+        val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+                AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.PREDISMISSED_STATE)
+        builder.addAction(R.drawable.ic_alarm_off_24dp,
+                context.getString(R.string.alarm_alert_dismiss_text),
+                PendingIntent.getService(context, id,
+                        dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        // Setup content action if instance is owned by alarm
+        val viewAlarmIntent: Intent = createViewAlarmIntent(context, instance)
+        builder.setContentIntent(PendingIntent.getActivity(context, id,
+                viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            val channel = NotificationChannel(
+                    ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID,
+                    context.getString(R.string.default_label),
+                    NotificationManagerCompat.IMPORTANCE_DEFAULT)
+            nm.createNotificationChannel(channel)
+        }
+        val notification: Notification = builder.build()
+        nm.notify(id, notification)
+        updateUpcomingAlarmGroupNotification(context, -1, notification)
+    }
+
+    @JvmStatic
+    @Synchronized
+    fun showHighPriorityNotification(
+        context: Context,
+        instance: AlarmInstance
+    ) {
+        LogUtils.v("Displaying high priority notification for alarm instance: " + instance.mId)
+
+        val builder: NotificationCompat.Builder = NotificationCompat.Builder(
+                context, ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID)
+                .setShowWhen(false)
+                .setContentTitle(context.getString(
+                        R.string.alarm_alert_predismiss_title))
+                .setContentText(AlarmUtils.getAlarmText(
+                        context, instance, true /* includeLabel */))
+                .setColor(ContextCompat.getColor(context, R.color.default_background))
+                .setSmallIcon(R.drawable.stat_notify_alarm)
+                .setAutoCancel(false)
+                .setSortKey(createSortKey(instance))
+                .setPriority(NotificationCompat.PRIORITY_HIGH)
+                .setCategory(NotificationCompat.CATEGORY_EVENT)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setLocalOnly(true)
+
+        if (Utils.isNOrLater()) {
+            builder.setGroup(UPCOMING_GROUP_KEY)
+        }
+
+        // Setup up dismiss action
+        val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+                AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.PREDISMISSED_STATE)
+        val id = instance.hashCode()
+        builder.addAction(R.drawable.ic_alarm_off_24dp,
+                context.getString(R.string.alarm_alert_dismiss_text),
+                PendingIntent.getService(context, id,
+                        dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        // Setup content action if instance is owned by alarm
+        val viewAlarmIntent: Intent = createViewAlarmIntent(context, instance)
+        builder.setContentIntent(PendingIntent.getActivity(context, id,
+                viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            val channel = NotificationChannel(
+                    ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID,
+                    context.getString(R.string.default_label),
+                    NotificationManagerCompat.IMPORTANCE_DEFAULT)
+            nm.createNotificationChannel(channel)
+        }
+        val notification: Notification = builder.build()
+        nm.notify(id, notification)
+        updateUpcomingAlarmGroupNotification(context, -1, notification)
+    }
+
+    @TargetApi(Build.VERSION_CODES.N)
+    private fun isGroupSummary(n: Notification): Boolean {
+        return n.flags and Notification.FLAG_GROUP_SUMMARY == Notification.FLAG_GROUP_SUMMARY
+    }
+
+    /**
+     * Method which returns the first active notification for a given group. If a notification was
+     * just posted, provide it to make sure it is included as a potential result. If a notification
+     * was just canceled, provide the id so that it is not included as a potential result. These
+     * extra parameters are needed due to a race condition which exists in
+     * [NotificationManager.getActiveNotifications].
+     *
+     * @param context Context from which to grab the NotificationManager
+     * @param group The group key to query for notifications
+     * @param canceledNotificationId The id of the just-canceled notification (-1 if none)
+     * @param postedNotification The notification that was just posted
+     * @return The first active notification for the group
+     */
+    @TargetApi(Build.VERSION_CODES.N)
+    private fun getFirstActiveNotification(
+        context: Context,
+        group: String,
+        canceledNotificationId: Int,
+        postedNotification: Notification?
+    ): Notification? {
+        val nm: NotificationManager =
+                context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+        val notifications: Array<StatusBarNotification> = nm.getActiveNotifications()
+        var firstActiveNotification: Notification? = postedNotification
+        for (statusBarNotification in notifications) {
+            val n: Notification = statusBarNotification.getNotification()
+            if (!isGroupSummary(n) && group == n.getGroup() &&
+                    statusBarNotification.getId() != canceledNotificationId) {
+                if (firstActiveNotification == null ||
+                        n.getSortKey().compareTo(firstActiveNotification.getSortKey()) < 0) {
+                    firstActiveNotification = n
+                }
+            }
+        }
+        return firstActiveNotification
+    }
+
+    @TargetApi(Build.VERSION_CODES.N)
+    private fun getActiveGroupSummaryNotification(context: Context, group: String): Notification? {
+        val nm: NotificationManager =
+                context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+        val notifications: Array<StatusBarNotification> = nm.getActiveNotifications()
+        for (statusBarNotification in notifications) {
+            val n: Notification = statusBarNotification.getNotification()
+            if (isGroupSummary(n) && group == n.getGroup()) {
+                return n
+            }
+        }
+        return null
+    }
+
+    private fun updateUpcomingAlarmGroupNotification(
+        context: Context,
+        canceledNotificationId: Int,
+        postedNotification: Notification?
+    ) {
+        if (!Utils.isNOrLater()) {
+            return
+        }
+
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            val channel = NotificationChannel(
+                    ALARM_NOTIFICATION_CHANNEL_ID,
+                    context.getString(R.string.default_label),
+                    NotificationManagerCompat.IMPORTANCE_DEFAULT)
+            nm.createNotificationChannel(channel)
+        }
+
+        val firstUpcoming: Notification? = getFirstActiveNotification(context, UPCOMING_GROUP_KEY,
+                canceledNotificationId, postedNotification)
+        if (firstUpcoming == null) {
+            nm.cancel(ALARM_GROUP_NOTIFICATION_ID)
+            return
+        }
+
+        var summary: Notification? = getActiveGroupSummaryNotification(context, UPCOMING_GROUP_KEY)
+        if (summary == null ||
+                summary.contentIntent != firstUpcoming.contentIntent) {
+            summary = NotificationCompat.Builder(context, ALARM_NOTIFICATION_CHANNEL_ID)
+                    .setShowWhen(false)
+                    .setContentIntent(firstUpcoming.contentIntent)
+                    .setColor(ContextCompat.getColor(context, R.color.default_background))
+                    .setSmallIcon(R.drawable.stat_notify_alarm)
+                    .setGroup(UPCOMING_GROUP_KEY)
+                    .setGroupSummary(true)
+                    .setPriority(NotificationCompat.PRIORITY_HIGH)
+                    .setCategory(NotificationCompat.CATEGORY_EVENT)
+                    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                    .setLocalOnly(true)
+                    .build()
+            nm.notify(ALARM_GROUP_NOTIFICATION_ID, summary)
+        }
+    }
+
+    private fun updateMissedAlarmGroupNotification(
+        context: Context,
+        canceledNotificationId: Int,
+        postedNotification: Notification?
+    ) {
+        if (!Utils.isNOrLater()) {
+            return
+        }
+
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            val channel = NotificationChannel(
+                    ALARM_NOTIFICATION_CHANNEL_ID,
+                    context.getString(R.string.default_label),
+                    NotificationManagerCompat.IMPORTANCE_DEFAULT)
+            nm.createNotificationChannel(channel)
+        }
+
+        val firstMissed: Notification? = getFirstActiveNotification(context, MISSED_GROUP_KEY,
+                canceledNotificationId, postedNotification)
+        if (firstMissed == null) {
+            nm.cancel(ALARM_GROUP_MISSED_NOTIFICATION_ID)
+            return
+        }
+
+        var summary: Notification? = getActiveGroupSummaryNotification(context, MISSED_GROUP_KEY)
+        if (summary == null ||
+                summary.contentIntent != firstMissed.contentIntent) {
+            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+                val channel = NotificationChannel(
+                        ALARM_MISSED_NOTIFICATION_CHANNEL_ID,
+                        context.getString(R.string.default_label),
+                        NotificationManagerCompat.IMPORTANCE_DEFAULT)
+                nm.createNotificationChannel(channel)
+            }
+            summary = NotificationCompat.Builder(context, ALARM_NOTIFICATION_CHANNEL_ID)
+                    .setShowWhen(false)
+                    .setContentIntent(firstMissed.contentIntent)
+                    .setColor(ContextCompat.getColor(context, R.color.default_background))
+                    .setSmallIcon(R.drawable.stat_notify_alarm)
+                    .setGroup(MISSED_GROUP_KEY)
+                    .setGroupSummary(true)
+                    .setPriority(NotificationCompat.PRIORITY_HIGH)
+                    .setCategory(NotificationCompat.CATEGORY_EVENT)
+                    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                    .setLocalOnly(true)
+                    .build()
+            nm.notify(ALARM_GROUP_MISSED_NOTIFICATION_ID, summary)
+        }
+    }
+
+    @JvmStatic
+    @Synchronized
+    fun showSnoozeNotification(
+        context: Context,
+        instance: AlarmInstance
+    ) {
+        LogUtils.v("Displaying snoozed notification for alarm instance: " + instance.mId)
+
+        val builder: NotificationCompat.Builder = NotificationCompat.Builder(
+                context, ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID)
+                .setShowWhen(false)
+                .setContentTitle(instance.getLabelOrDefault(context))
+                .setContentText(context.getString(R.string.alarm_alert_snooze_until,
+                        AlarmUtils.getFormattedTime(context, instance.alarmTime)))
+                .setColor(ContextCompat.getColor(context, R.color.default_background))
+                .setSmallIcon(R.drawable.stat_notify_alarm)
+                .setAutoCancel(false)
+                .setSortKey(createSortKey(instance))
+                .setPriority(NotificationCompat.PRIORITY_MAX)
+                .setCategory(NotificationCompat.CATEGORY_EVENT)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setLocalOnly(true)
+
+        if (Utils.isNOrLater()) {
+            builder.setGroup(UPCOMING_GROUP_KEY)
+        }
+
+        // Setup up dismiss action
+        val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+                AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.DISMISSED_STATE)
+        val id = instance.hashCode()
+        builder.addAction(R.drawable.ic_alarm_off_24dp,
+                context.getString(R.string.alarm_alert_dismiss_text),
+                PendingIntent.getService(context, id,
+                        dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        // Setup content action if instance is owned by alarm
+        val viewAlarmIntent: Intent = createViewAlarmIntent(context, instance)
+        builder.setContentIntent(PendingIntent.getActivity(context, id,
+                viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            val channel = NotificationChannel(
+                    ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID,
+                    context.getString(R.string.default_label),
+                    NotificationManagerCompat.IMPORTANCE_DEFAULT)
+            nm.createNotificationChannel(channel)
+        }
+        val notification: Notification = builder.build()
+        nm.notify(id, notification)
+        updateUpcomingAlarmGroupNotification(context, -1, notification)
+    }
+
+    @JvmStatic
+    @Synchronized
+    fun showMissedNotification(
+        context: Context,
+        instance: AlarmInstance
+    ) {
+        LogUtils.v("Displaying missed notification for alarm instance: " + instance.mId)
+
+        val label = instance.mLabel
+        val alarmTime: String = AlarmUtils.getFormattedTime(context, instance.alarmTime)
+        val builder: NotificationCompat.Builder = NotificationCompat.Builder(
+                context, ALARM_MISSED_NOTIFICATION_CHANNEL_ID)
+                .setShowWhen(false)
+                .setContentTitle(context.getString(R.string.alarm_missed_title))
+                .setContentText(if (instance.mLabel!!.isEmpty()) {
+                    alarmTime
+                } else {
+                    context.getString(R.string.alarm_missed_text, alarmTime, label)
+                })
+                .setColor(ContextCompat.getColor(context, R.color.default_background))
+                .setSortKey(createSortKey(instance))
+                .setSmallIcon(R.drawable.stat_notify_alarm)
+                .setPriority(NotificationCompat.PRIORITY_HIGH)
+                .setCategory(NotificationCompat.CATEGORY_EVENT)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setLocalOnly(true)
+
+        if (Utils.isNOrLater()) {
+            builder.setGroup(MISSED_GROUP_KEY)
+        }
+
+        val id = instance.hashCode()
+
+        // Setup dismiss intent
+        val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+                AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.DISMISSED_STATE)
+        builder.setDeleteIntent(PendingIntent.getService(context, id,
+                dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        // Setup content intent
+        val showAndDismiss: Intent = AlarmInstance.createIntent(context,
+                AlarmStateManager::class.java, instance.mId)
+        showAndDismiss.putExtra(EXTRA_NOTIFICATION_ID, id)
+        showAndDismiss.setAction(AlarmStateManager.SHOW_AND_DISMISS_ALARM_ACTION)
+        builder.setContentIntent(PendingIntent.getBroadcast(context, id,
+                showAndDismiss, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            val channel = NotificationChannel(
+                    ALARM_MISSED_NOTIFICATION_CHANNEL_ID,
+                    context.getString(R.string.default_label),
+                    NotificationManagerCompat.IMPORTANCE_DEFAULT)
+            nm.createNotificationChannel(channel)
+        }
+        val notification: Notification = builder.build()
+        nm.notify(id, notification)
+        updateMissedAlarmGroupNotification(context, -1, notification)
+    }
+
+    @Synchronized
+    fun showAlarmNotification(service: Service, instance: AlarmInstance) {
+        LogUtils.v("Displaying alarm notification for alarm instance: " + instance.mId)
+
+        val resources: Resources = service.getResources()
+        val notification: NotificationCompat.Builder = NotificationCompat.Builder(
+                service, ALARM_NOTIFICATION_CHANNEL_ID)
+                .setContentTitle(instance.getLabelOrDefault(service))
+                .setContentText(AlarmUtils.getFormattedTime(
+                        service, instance.alarmTime))
+                .setColor(ContextCompat.getColor(service, R.color.default_background))
+                .setSmallIcon(R.drawable.stat_notify_alarm)
+                .setOngoing(true)
+                .setAutoCancel(false)
+                .setDefaults(NotificationCompat.DEFAULT_LIGHTS)
+                .setWhen(0)
+                .setCategory(NotificationCompat.CATEGORY_ALARM)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setLocalOnly(true)
+
+        // Setup Snooze Action
+        val snoozeIntent: Intent = AlarmStateManager.createStateChangeIntent(service,
+                AlarmStateManager.ALARM_SNOOZE_TAG, instance, InstancesColumns.SNOOZE_STATE)
+        snoozeIntent.putExtra(AlarmStateManager.FROM_NOTIFICATION_EXTRA, true)
+        val snoozePendingIntent: PendingIntent = PendingIntent.getService(service,
+                ALARM_FIRING_NOTIFICATION_ID, snoozeIntent, PendingIntent.FLAG_UPDATE_CURRENT)
+        notification.addAction(R.drawable.ic_snooze_24dp,
+                resources.getString(R.string.alarm_alert_snooze_text), snoozePendingIntent)
+
+        // Setup Dismiss Action
+        val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(service,
+                AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.DISMISSED_STATE)
+        dismissIntent.putExtra(AlarmStateManager.FROM_NOTIFICATION_EXTRA, true)
+        val dismissPendingIntent: PendingIntent = PendingIntent.getService(service,
+                ALARM_FIRING_NOTIFICATION_ID, dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT)
+        notification.addAction(R.drawable.ic_alarm_off_24dp,
+                resources.getString(R.string.alarm_alert_dismiss_text),
+                dismissPendingIntent)
+
+        // Setup Content Action
+        val contentIntent: Intent = AlarmInstance.createIntent(service, AlarmActivity::class.java,
+                instance.mId)
+        notification.setContentIntent(PendingIntent.getActivity(service,
+                ALARM_FIRING_NOTIFICATION_ID, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        // Setup fullscreen intent
+        val fullScreenIntent: Intent =
+                AlarmInstance.createIntent(service, AlarmActivity::class.java, instance.mId)
+        // set action, so we can be different then content pending intent
+        fullScreenIntent.setAction("fullscreen_activity")
+        fullScreenIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
+                Intent.FLAG_ACTIVITY_NO_USER_ACTION)
+        notification.setFullScreenIntent(PendingIntent.getActivity(service,
+                ALARM_FIRING_NOTIFICATION_ID, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT),
+                true)
+        notification.setPriority(NotificationCompat.PRIORITY_MAX)
+
+        clearNotification(service, instance)
+        service.startForeground(ALARM_FIRING_NOTIFICATION_ID, notification.build())
+    }
+
+    @JvmStatic
+    @Synchronized
+    fun clearNotification(context: Context, instance: AlarmInstance) {
+        LogUtils.v("Clearing notifications for alarm instance: " + instance.mId)
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        val id = instance.hashCode()
+        nm.cancel(id)
+        updateUpcomingAlarmGroupNotification(context, id, null)
+        updateMissedAlarmGroupNotification(context, id, null)
+    }
+
+    /**
+     * Updates the notification for an existing alarm. Use if the label has changed.
+     */
+    @JvmStatic
+    fun updateNotification(context: Context, instance: AlarmInstance) {
+        when (instance.mAlarmState) {
+            InstancesColumns.LOW_NOTIFICATION_STATE -> {
+                showLowPriorityNotification(context, instance)
+            }
+            InstancesColumns.HIGH_NOTIFICATION_STATE -> {
+                showHighPriorityNotification(context, instance)
+            }
+            InstancesColumns.SNOOZE_STATE -> showSnoozeNotification(context, instance)
+            InstancesColumns.MISSED_STATE -> showMissedNotification(context, instance)
+            else -> LogUtils.d("No notification to update")
+        }
+    }
+
+    @JvmStatic
+    fun createViewAlarmIntent(context: Context?, instance: AlarmInstance): Intent {
+        val alarmId = instance.mAlarmId ?: Alarm.INVALID_ID
+        return Alarm.createIntent(context, DeskClock::class.java, alarmId)
+                .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+    }
+
+    /**
+     * Alarm notifications are sorted chronologically. Missed alarms are sorted chronologically
+     * **after** all upcoming/snoozed alarms by including the "MISSED" prefix on the
+     * sort key.
+     *
+     * @param instance the alarm instance for which the notification is generated
+     * @return the sort key that specifies the order of this alarm notification
+     */
+    private fun createSortKey(instance: AlarmInstance): String {
+        val timeKey = SORT_KEY_FORMAT.format(instance.alarmTime.time)
+        val missedAlarm = instance.mAlarmState == InstancesColumns.MISSED_STATE
+        return if (missedAlarm) "MISSED $timeKey" else timeKey
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmService.kt b/src/com/android/deskclock/alarms/AlarmService.kt
new file mode 100644
index 0000000..cd1493f
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmService.kt
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.alarms
+
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Binder
+import android.os.IBinder
+import android.telephony.PhoneStateListener
+import android.telephony.TelephonyManager
+
+import com.android.deskclock.AlarmAlertWakeLock
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.events.Events
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+
+/**
+ * This service is in charge of starting/stopping the alarm. It will bring up and manage the
+ * [AlarmActivity] as well as [AlarmKlaxon].
+ *
+ * Registers a broadcast receiver to listen for snooze/dismiss intents. The broadcast receiver
+ * exits early if AlarmActivity is bound to prevent double-processing of the snooze/dismiss intents.
+ */
+class AlarmService : Service() {
+    /** Binder given to AlarmActivity.  */
+    private val mBinder: IBinder = Binder()
+
+    /** Whether the service is currently bound to AlarmActivity  */
+    private var mIsBound = false
+
+    /** Listener for changes in phone state.  */
+    private val mPhoneStateListener = PhoneStateChangeListener()
+
+    /** Whether the receiver is currently registered  */
+    private var mIsRegistered = false
+
+    override fun onBind(intent: Intent?): IBinder {
+        mIsBound = true
+        return mBinder
+    }
+
+    override fun onUnbind(intent: Intent?): Boolean {
+        mIsBound = false
+        return super.onUnbind(intent)
+    }
+
+    private lateinit var mTelephonyManager: TelephonyManager
+    private var mCurrentAlarm: AlarmInstance? = null
+
+    private fun startAlarm(instance: AlarmInstance) {
+        LogUtils.v("AlarmService.start with instance: " + instance.mId)
+        if (mCurrentAlarm != null) {
+            AlarmStateManager.setMissedState(this, mCurrentAlarm)
+            stopCurrentAlarm()
+        }
+
+        AlarmAlertWakeLock.acquireCpuWakeLock(this)
+
+        mCurrentAlarm = instance
+        AlarmNotifications.showAlarmNotification(this, mCurrentAlarm!!)
+        mTelephonyManager.listen(mPhoneStateListener.init(), PhoneStateListener.LISTEN_CALL_STATE)
+        AlarmKlaxon.start(this, mCurrentAlarm!!)
+        sendBroadcast(Intent(ALARM_ALERT_ACTION))
+    }
+
+    private fun stopCurrentAlarm() {
+        if (mCurrentAlarm == null) {
+            LogUtils.v("There is no current alarm to stop")
+            return
+        }
+
+        val instanceId = mCurrentAlarm!!.mId
+        LogUtils.v("AlarmService.stop with instance: %s", instanceId)
+
+        AlarmKlaxon.stop(this)
+        mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE)
+        sendBroadcast(Intent(ALARM_DONE_ACTION))
+
+        stopForeground(true /* removeNotification */)
+
+        mCurrentAlarm = null
+        AlarmAlertWakeLock.releaseCpuLock()
+    }
+
+    private val mActionsReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent) {
+            val action: String? = intent.getAction()
+            LogUtils.i("AlarmService received intent %s", action)
+            if (mCurrentAlarm == null ||
+                    mCurrentAlarm!!.mAlarmState != InstancesColumns.FIRED_STATE) {
+                LogUtils.i("No valid firing alarm")
+                return
+            }
+
+            if (mIsBound) {
+                LogUtils.i("AlarmActivity bound; AlarmService no-op")
+                return
+            }
+
+            when (action) {
+                ALARM_SNOOZE_ACTION -> {
+                    // Set the alarm state to snoozed.
+                    // If this broadcast receiver is handling the snooze intent then AlarmActivity
+                    // must not be showing, so always show snooze toast.
+                    AlarmStateManager.setSnoozeState(context, mCurrentAlarm, true /* showToast */)
+                    Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent)
+                }
+                ALARM_DISMISS_ACTION -> {
+                    // Set the alarm state to dismissed.
+                    AlarmStateManager.deleteInstanceAndUpdateParent(context, mCurrentAlarm)
+                    Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent)
+                }
+            }
+        }
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        mTelephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
+
+        // Register the broadcast receiver
+        val filter = IntentFilter(ALARM_SNOOZE_ACTION)
+        filter.addAction(ALARM_DISMISS_ACTION)
+        registerReceiver(mActionsReceiver, filter)
+        mIsRegistered = true
+    }
+
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        LogUtils.v("AlarmService.onStartCommand() with %s", intent)
+        if (intent == null) {
+            return Service.START_NOT_STICKY
+        }
+
+        val instanceId = AlarmInstance.getId(intent.getData()!!)
+        when (intent.getAction()) {
+            AlarmStateManager.CHANGE_STATE_ACTION -> {
+                AlarmStateManager.handleIntent(this, intent)
+
+                // If state is changed to firing, actually fire the alarm!
+                val alarmState: Int = intent.getIntExtra(AlarmStateManager.ALARM_STATE_EXTRA, -1)
+                if (alarmState == InstancesColumns.FIRED_STATE) {
+                    val cr: ContentResolver = this.getContentResolver()
+                    val instance: AlarmInstance? = AlarmInstance.getInstance(cr, instanceId)
+                    if (instance == null) {
+                        LogUtils.e("No instance found to start alarm: %d", instanceId)
+                        if (mCurrentAlarm != null) {
+                            // Only release lock if we are not firing alarm
+                            AlarmAlertWakeLock.releaseCpuLock()
+                        }
+                    } else if (mCurrentAlarm != null && mCurrentAlarm!!.mId == instanceId) {
+                        LogUtils.e("Alarm already started for instance: %d", instanceId)
+                    } else {
+                        startAlarm(instance)
+                    }
+                }
+            }
+            STOP_ALARM_ACTION -> {
+                if (mCurrentAlarm != null && mCurrentAlarm!!.mId != instanceId) {
+                    LogUtils.e("Can't stop alarm for instance: %d because current alarm is: %d",
+                            instanceId, mCurrentAlarm!!.mId)
+                } else {
+                    stopCurrentAlarm()
+                    stopSelf()
+                }
+            }
+        }
+
+        return Service.START_NOT_STICKY
+    }
+
+    override fun onDestroy() {
+        LogUtils.v("AlarmService.onDestroy() called")
+        super.onDestroy()
+        if (mCurrentAlarm != null) {
+            stopCurrentAlarm()
+        }
+
+        if (mIsRegistered) {
+            unregisterReceiver(mActionsReceiver)
+            mIsRegistered = false
+        }
+    }
+
+    private inner class PhoneStateChangeListener : PhoneStateListener() {
+        private var mPhoneCallState = 0
+
+        fun init(): PhoneStateChangeListener {
+            mPhoneCallState = -1
+            return this
+        }
+
+        override fun onCallStateChanged(state: Int, ignored: String?) {
+            if (mPhoneCallState == -1) {
+                mPhoneCallState = state
+            }
+
+            if (state != TelephonyManager.CALL_STATE_IDLE && state != mPhoneCallState) {
+                startService(AlarmStateManager.createStateChangeIntent(this@AlarmService,
+                        "AlarmService", mCurrentAlarm, InstancesColumns.MISSED_STATE))
+            }
+        }
+    }
+
+    companion object {
+        /**
+         * AlarmActivity and AlarmService (when unbound) listen for this broadcast intent
+         * so that other applications can snooze the alarm (after ALARM_ALERT_ACTION and before
+         * ALARM_DONE_ACTION).
+         */
+        const val ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE"
+
+        /**
+         * AlarmActivity and AlarmService listen for this broadcast intent so that other
+         * applications can dismiss the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
+         */
+        const val ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS"
+
+        /** A public action sent by AlarmService when the alarm has started.  */
+        const val ALARM_ALERT_ACTION = "com.android.deskclock.ALARM_ALERT"
+
+        /** A public action sent by AlarmService when the alarm has stopped for any reason.  */
+        const val ALARM_DONE_ACTION = "com.android.deskclock.ALARM_DONE"
+
+        /** Private action used to stop an alarm with this service.  */
+        const val STOP_ALARM_ACTION = "STOP_ALARM"
+
+        /**
+         * Utility method to help stop an alarm properly. Nothing will happen, if alarm is not firing
+         * or using a different instance.
+         *
+         * @param context application context
+         * @param instance you are trying to stop
+         */
+        @JvmStatic
+        fun stopAlarm(context: Context, instance: AlarmInstance) {
+            val intent: Intent =
+                    AlarmInstance.createIntent(context, AlarmService::class.java, instance.mId)
+                            .setAction(STOP_ALARM_ACTION)
+
+            // We don't need a wake lock here, since we are trying to kill an alarm
+            context.startService(intent)
+        }
+    }
+}
\ No newline at end of file