1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.deskclock.data 18 19 import android.annotation.TargetApi 20 import android.app.AlarmManager 21 import android.app.Notification 22 import android.app.NotificationChannel 23 import android.app.NotificationManager 24 import android.app.PendingIntent 25 import android.content.Context 26 import android.content.Intent 27 import android.content.res.Resources 28 import android.os.Build 29 import android.os.SystemClock 30 import android.text.TextUtils 31 import android.text.format.DateUtils.MINUTE_IN_MILLIS 32 import android.text.format.DateUtils.SECOND_IN_MILLIS 33 import android.widget.RemoteViews 34 import androidx.annotation.DrawableRes 35 import androidx.core.app.NotificationCompat 36 import androidx.core.app.NotificationCompat.Action 37 import androidx.core.app.NotificationCompat.Builder 38 import androidx.core.app.NotificationManagerCompat 39 import androidx.core.content.ContextCompat 40 41 import com.android.deskclock.AlarmUtils 42 import com.android.deskclock.R 43 import com.android.deskclock.Utils 44 import com.android.deskclock.events.Events 45 import com.android.deskclock.timer.ExpiredTimersActivity 46 import com.android.deskclock.timer.TimerService 47 48 /** 49 * Builds notifications to reflect the latest state of the timers. 50 */ 51 internal class TimerNotificationBuilder { 52 isChannelCreatednull53 fun isChannelCreated(notificationManager: NotificationManagerCompat): Boolean { 54 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { 55 if(notificationManager.getNotificationChannelCompat(TIMER_MODEL_NOTIFICATION_CHANNEL_ID) != null) { 56 return true 57 } else { 58 return false 59 } 60 } 61 return false 62 } 63 buildChannelnull64 fun buildChannel(context: Context, notificationManager: NotificationManagerCompat) { 65 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { 66 val channel = NotificationChannel( 67 TIMER_MODEL_NOTIFICATION_CHANNEL_ID, 68 context.getString(R.string.default_label), 69 NotificationManagerCompat.IMPORTANCE_DEFAULT) 70 notificationManager.createNotificationChannel(channel) 71 } 72 } 73 buildnull74 fun build(context: Context, nm: NotificationModel, unexpired: List<Timer>): Notification { 75 val timer = unexpired[0] 76 val count = unexpired.size 77 78 // Compute some values required below. 79 val running = timer.isRunning 80 val res: Resources = context.getResources() 81 82 val base = getChronometerBase(timer) 83 val pname: String = context.getPackageName() 84 85 val actions: MutableList<Action> = ArrayList<Action>(2) 86 87 val stateText: CharSequence 88 if (count == 1) { 89 if (running) { 90 // Single timer is running. 91 stateText = if (timer.label.isNullOrEmpty()) { 92 res.getString(R.string.timer_notification_label) 93 } else { 94 timer.label 95 } 96 97 // Left button: Pause 98 val pause: Intent = Intent(context, TimerService::class.java) 99 .setAction(TimerService.ACTION_PAUSE_TIMER) 100 .putExtra(TimerService.EXTRA_TIMER_ID, timer.id) 101 102 @DrawableRes val icon1: Int = R.drawable.ic_pause_24dp 103 val title1: CharSequence = res.getText(R.string.timer_pause) 104 val intent1: PendingIntent = Utils.pendingServiceIntent(context, pause) 105 actions.add(Action.Builder(icon1, title1, intent1).build()) 106 107 // Right Button: +1 Minute 108 val addMinute: Intent = Intent(context, TimerService::class.java) 109 .setAction(TimerService.ACTION_ADD_MINUTE_TIMER) 110 .putExtra(TimerService.EXTRA_TIMER_ID, timer.id) 111 112 @DrawableRes val icon2: Int = R.drawable.ic_add_24dp 113 val title2: CharSequence = res.getText(R.string.timer_plus_1_min) 114 val intent2: PendingIntent = Utils.pendingServiceIntent(context, addMinute) 115 actions.add(Action.Builder(icon2, title2, intent2).build()) 116 } else { 117 // Single timer is paused. 118 stateText = res.getString(R.string.timer_paused) 119 120 // Left button: Start 121 val start: Intent = Intent(context, TimerService::class.java) 122 .setAction(TimerService.ACTION_START_TIMER) 123 .putExtra(TimerService.EXTRA_TIMER_ID, timer.id) 124 125 @DrawableRes val icon1: Int = R.drawable.ic_start_24dp 126 val title1: CharSequence = res.getText(R.string.sw_resume_button) 127 val intent1: PendingIntent = Utils.pendingServiceIntent(context, start) 128 actions.add(Action.Builder(icon1, title1, intent1).build()) 129 130 // Right Button: Reset 131 val reset: Intent = Intent(context, TimerService::class.java) 132 .setAction(TimerService.ACTION_RESET_TIMER) 133 .putExtra(TimerService.EXTRA_TIMER_ID, timer.id) 134 135 @DrawableRes val icon2: Int = R.drawable.ic_reset_24dp 136 val title2: CharSequence = res.getText(R.string.sw_reset_button) 137 val intent2: PendingIntent = Utils.pendingServiceIntent(context, reset) 138 actions.add(Action.Builder(icon2, title2, intent2).build()) 139 } 140 } else { 141 stateText = if (running) { 142 // At least one timer is running. 143 res.getString(R.string.timers_in_use, count) 144 } else { 145 // All timers are paused. 146 res.getString(R.string.timers_stopped, count) 147 } 148 149 val reset: Intent = TimerService.createResetUnexpiredTimersIntent(context) 150 151 @DrawableRes val icon1: Int = R.drawable.ic_reset_24dp 152 val title1: CharSequence = res.getText(R.string.timer_reset_all) 153 val intent1: PendingIntent = Utils.pendingServiceIntent(context, reset) 154 actions.add(Action.Builder(icon1, title1, intent1).build()) 155 } 156 157 // Intent to load the app and show the timer when the notification is tapped. 158 val showApp: Intent = Intent(context, TimerService::class.java) 159 .setAction(TimerService.ACTION_SHOW_TIMER) 160 .putExtra(TimerService.EXTRA_TIMER_ID, timer.id) 161 .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_notification) 162 163 val pendingShowApp: PendingIntent = 164 PendingIntent.getService(context, REQUEST_CODE_UPCOMING, showApp, 165 PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT) 166 167 val notification: Builder = Builder( 168 context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID) 169 .setOngoing(true) 170 .setLocalOnly(true) 171 .setShowWhen(false) 172 .setAutoCancel(false) 173 .setContentIntent(pendingShowApp) 174 .setPriority(NotificationManager.IMPORTANCE_HIGH) 175 .setCategory(NotificationCompat.CATEGORY_ALARM) 176 .setSmallIcon(R.drawable.stat_notify_timer) 177 .setSortKey(nm.timerNotificationSortKey) 178 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) 179 .setStyle(NotificationCompat.DecoratedCustomViewStyle()) 180 .setColor(ContextCompat.getColor(context, R.color.default_background)) 181 182 for (action in actions) { 183 notification.addAction(action) 184 } 185 186 if (Utils.isNOrLater) { 187 notification.setCustomContentView(buildChronometer(pname, base, running, stateText)) 188 .setGroup(nm.timerNotificationGroupKey) 189 } else { 190 val contentTextPreN: CharSequence? 191 contentTextPreN = when { 192 count == 1 -> { 193 TimerStringFormatter.formatTimeRemaining(context, timer.remainingTime, false) 194 } 195 running -> { 196 val timeRemaining = TimerStringFormatter.formatTimeRemaining(context, 197 timer.remainingTime, false) 198 context.getString(R.string.next_timer_notif, timeRemaining) 199 } 200 else -> context.getString(R.string.all_timers_stopped_notif) 201 } 202 203 notification.setContentTitle(stateText).setContentText(contentTextPreN) 204 205 val am: AlarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 206 val updateNotification: Intent = TimerService.createUpdateNotificationIntent(context) 207 val remainingTime = timer.remainingTime 208 if (timer.isRunning && remainingTime > MINUTE_IN_MILLIS) { 209 // Schedule a callback to update the time-sensitive information of the running timer 210 val pi: PendingIntent = 211 PendingIntent.getService(context, REQUEST_CODE_UPCOMING, updateNotification, 212 PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT) 213 214 val nextMinuteChange: Long = remainingTime % MINUTE_IN_MILLIS 215 val triggerTime: Long = SystemClock.elapsedRealtime() + nextMinuteChange 216 TimerModel.schedulePendingIntent(am, triggerTime, pi) 217 } else { 218 // Cancel the update notification callback. 219 val pi: PendingIntent? = PendingIntent.getService(context, 0, updateNotification, 220 PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_NO_CREATE) 221 if (pi != null) { 222 am.cancel(pi) 223 pi.cancel() 224 } 225 } 226 } 227 return notification.build() 228 } 229 buildHeadsUpnull230 fun buildHeadsUp(context: Context, expired: List<Timer>): Notification { 231 val timer = expired[0] 232 233 // First action intent is to reset all timers. 234 @DrawableRes val icon1: Int = R.drawable.ic_stop_24dp 235 val reset: Intent = TimerService.createResetExpiredTimersIntent(context) 236 val intent1: PendingIntent = Utils.pendingServiceIntent(context, reset) 237 238 // Generate some descriptive text, a title, and an action name based on the timer count. 239 val stateText: CharSequence 240 val count = expired.size 241 val actions: MutableList<Action> = ArrayList<Action>(2) 242 if (count == 1) { 243 val label = timer.label 244 stateText = if (label.isNullOrEmpty()) { 245 context.getString(R.string.timer_times_up) 246 } else { 247 label 248 } 249 250 // Left button: Reset single timer 251 val title1: CharSequence = context.getString(R.string.timer_stop) 252 actions.add(Action.Builder(icon1, title1, intent1).build()) 253 254 // Right button: Add minute 255 val addTime: Intent = TimerService.createAddMinuteTimerIntent(context, timer.id) 256 val intent2: PendingIntent = Utils.pendingServiceIntent(context, addTime) 257 @DrawableRes val icon2: Int = R.drawable.ic_add_24dp 258 val title2: CharSequence = context.getString(R.string.timer_plus_1_min) 259 actions.add(Action.Builder(icon2, title2, intent2).build()) 260 } else { 261 stateText = context.getString(R.string.timer_multi_times_up, count) 262 263 // Left button: Reset all timers 264 val title1: CharSequence = context.getString(R.string.timer_stop_all) 265 actions.add(Action.Builder(icon1, title1, intent1).build()) 266 } 267 268 val base = getChronometerBase(timer) 269 270 val pname: String = context.getPackageName() 271 272 // Content intent shows the timer full screen when clicked. 273 val content = Intent(context, ExpiredTimersActivity::class.java) 274 val contentIntent: PendingIntent = Utils.pendingActivityIntent(context, content) 275 276 // Full screen intent has flags so it is different than the content intent. 277 val fullScreen: Intent = Intent(context, ExpiredTimersActivity::class.java) 278 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION) 279 val pendingFullScreen: PendingIntent = Utils.pendingActivityIntent(context, fullScreen) 280 281 val notification: Builder = Builder( 282 context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID) 283 .setOngoing(true) 284 .setLocalOnly(true) 285 .setShowWhen(false) 286 .setAutoCancel(false) 287 .setContentIntent(contentIntent) 288 .setPriority(NotificationManager.IMPORTANCE_HIGH) 289 .setDefaults(Notification.DEFAULT_LIGHTS) 290 .setSmallIcon(R.drawable.stat_notify_timer) 291 .setFullScreenIntent(pendingFullScreen, true) 292 .setStyle(NotificationCompat.DecoratedCustomViewStyle()) 293 .setColor(ContextCompat.getColor(context, R.color.default_background)) 294 295 for (action in actions) { 296 notification.addAction(action) 297 } 298 299 if (Utils.isNOrLater) { 300 notification.setCustomContentView(buildChronometer(pname, base, true, stateText)) 301 } else { 302 val contentTextPreN: CharSequence = if (count == 1) { 303 context.getString(R.string.timer_times_up) 304 } else { 305 context.getString(R.string.timer_multi_times_up, count) 306 } 307 notification.setContentTitle(stateText).setContentText(contentTextPreN) 308 } 309 310 return notification.build() 311 } 312 buildMissednull313 fun buildMissed( 314 context: Context, 315 nm: NotificationModel, 316 missedTimers: List<Timer> 317 ): Notification { 318 val timer = missedTimers[0] 319 val count = missedTimers.size 320 321 // Compute some values required below. 322 val base = getChronometerBase(timer) 323 val pname: String = context.getPackageName() 324 val res: Resources = context.getResources() 325 326 val action: Action 327 328 val stateText: CharSequence 329 if (count == 1) { 330 // Single timer is missed. 331 stateText = if (TextUtils.isEmpty(timer.label)) { 332 res.getString(R.string.missed_timer_notification_label) 333 } else { 334 res.getString(R.string.missed_named_timer_notification_label, 335 timer.label) 336 } 337 338 // Reset button 339 val reset: Intent = Intent(context, TimerService::class.java) 340 .setAction(TimerService.ACTION_RESET_TIMER) 341 .putExtra(TimerService.EXTRA_TIMER_ID, timer.id) 342 343 @DrawableRes val icon1: Int = R.drawable.ic_reset_24dp 344 val title1: CharSequence = res.getText(R.string.timer_reset) 345 val intent1: PendingIntent = Utils.pendingServiceIntent(context, reset) 346 action = Action.Builder(icon1, title1, intent1).build() 347 } else { 348 // Multiple missed timers. 349 stateText = res.getString(R.string.timer_multi_missed, count) 350 351 val reset: Intent = TimerService.createResetMissedTimersIntent(context) 352 353 @DrawableRes val icon1: Int = R.drawable.ic_reset_24dp 354 val title1: CharSequence = res.getText(R.string.timer_reset_all) 355 val intent1: PendingIntent = Utils.pendingServiceIntent(context, reset) 356 action = Action.Builder(icon1, title1, intent1).build() 357 } 358 359 // Intent to load the app and show the timer when the notification is tapped. 360 val showApp: Intent = Intent(context, TimerService::class.java) 361 .setAction(TimerService.ACTION_SHOW_TIMER) 362 .putExtra(TimerService.EXTRA_TIMER_ID, timer.id) 363 .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_notification) 364 365 val pendingShowApp: PendingIntent = 366 PendingIntent.getService(context, REQUEST_CODE_MISSING, showApp, 367 PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT) 368 369 val notification: Builder = Builder( 370 context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID) 371 .setLocalOnly(true) 372 .setShowWhen(false) 373 .setAutoCancel(false) 374 .setContentIntent(pendingShowApp) 375 .setPriority(NotificationManager.IMPORTANCE_HIGH) 376 .setCategory(NotificationCompat.CATEGORY_ALARM) 377 .setSmallIcon(R.drawable.stat_notify_timer) 378 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) 379 .setSortKey(nm.timerNotificationMissedSortKey) 380 .setStyle(NotificationCompat.DecoratedCustomViewStyle()) 381 .addAction(action) 382 .setColor(ContextCompat.getColor(context, R.color.default_background)) 383 384 if (Utils.isNOrLater) { 385 notification.setCustomContentView(buildChronometer(pname, base, true, stateText)) 386 .setGroup(nm.timerNotificationGroupKey) 387 } else { 388 val contentText: CharSequence = AlarmUtils.getFormattedTime(context, 389 timer.wallClockExpirationTime) 390 notification.setContentText(contentText).setContentTitle(stateText) 391 } 392 393 return notification.build() 394 } 395 396 @TargetApi(Build.VERSION_CODES.N) buildChronometernull397 private fun buildChronometer( 398 pname: String, 399 base: Long, 400 running: Boolean, 401 stateText: CharSequence 402 ): RemoteViews { 403 val content = RemoteViews(pname, R.layout.chronometer_notif_content) 404 content.setChronometerCountDown(R.id.chronometer, true) 405 content.setChronometer(R.id.chronometer, base, null, running) 406 content.setTextViewText(R.id.state, stateText) 407 return content 408 } 409 410 companion object { 411 /** 412 * Notification channel containing all TimerModel notifications. 413 */ 414 private const val TIMER_MODEL_NOTIFICATION_CHANNEL_ID = "TimerModelNotification" 415 416 private const val REQUEST_CODE_UPCOMING = 0 417 private const val REQUEST_CODE_MISSING = 1 418 419 /** 420 * @param timer the timer on which to base the chronometer display 421 * @return the time at which the chronometer will/did reach 0:00 in realtime 422 */ getChronometerBasenull423 private fun getChronometerBase(timer: Timer): Long { 424 // The in-app timer display rounds *up* to the next second for positive timer values. 425 // Mirror that behavior in the notification's Chronometer by padding in an extra second 426 // as needed. 427 val remaining = timer.remainingTime 428 val adjustedRemaining = if (remaining < 0) remaining else remaining + SECOND_IN_MILLIS 429 430 // Chronometer will/did reach 0:00 adjustedRemaining milliseconds from now. 431 return SystemClock.elapsedRealtime() + adjustedRemaining 432 } 433 } 434 } 435