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