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