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