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