• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 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.timer;
18 
19 import android.app.AlarmManager;
20 import android.app.Notification;
21 import android.app.PendingIntent;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.SharedPreferences;
26 import android.preference.PreferenceManager;
27 import android.support.v4.app.NotificationCompat;
28 import android.support.v4.app.NotificationManagerCompat;
29 import android.util.Log;
30 
31 import com.android.deskclock.DeskClock;
32 import com.android.deskclock.R;
33 import com.android.deskclock.TimerRingService;
34 import com.android.deskclock.Utils;
35 import com.android.deskclock.events.Events;
36 
37 import java.util.ArrayList;
38 import java.util.Iterator;
39 
40 public class TimerReceiver extends BroadcastReceiver {
41     private static final String TAG = "TimerReceiver";
42 
43     // Make this a large number to avoid the alarm ID's which seem to be 1, 2, ...
44     // Must also be different than StopwatchService.NOTIFICATION_ID
45     private static final int IN_USE_NOTIFICATION_ID = Integer.MAX_VALUE - 2;
46 
47     ArrayList<TimerObj> mTimers;
48 
49     @Override
onReceive(final Context context, final Intent intent)50     public void onReceive(final Context context, final Intent intent) {
51         if (Timers.LOGGING) {
52             Log.v(TAG, "Received intent " + intent.toString());
53         }
54         String actionType = intent.getAction();
55         // This action does not need the timers data
56         if (Timers.NOTIF_IN_USE_CANCEL.equals(actionType)) {
57             cancelInUseNotification(context);
58             return;
59         }
60 
61         // Get the updated timers data.
62         if (mTimers == null) {
63             mTimers = new ArrayList<>();
64         }
65         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
66         TimerObj.getTimersFromSharedPrefs(prefs, mTimers);
67 
68         // These actions do not provide a timer ID, but do use the timers data
69         if (Timers.NOTIF_IN_USE_SHOW.equals(actionType)) {
70             showInUseNotification(context);
71             return;
72         } else if (Timers.NOTIF_TIMES_UP_SHOW.equals(actionType)) {
73             showTimesUpNotification(context);
74             return;
75         } else if (Timers.NOTIF_TIMES_UP_CANCEL.equals(actionType)) {
76             cancelTimesUpNotification(context);
77             return;
78         }
79 
80         // Remaining actions provide a timer Id
81         if (!intent.hasExtra(Timers.TIMER_INTENT_EXTRA)) {
82             // No data to work with, do nothing
83             Log.e(TAG, "got intent without Timer data");
84             return;
85         }
86 
87         // Get the timer out of the Intent
88         int timerId = intent.getIntExtra(Timers.TIMER_INTENT_EXTRA, -1);
89         if (timerId == -1) {
90             Log.d(TAG, "OnReceive:intent without Timer data for " + actionType);
91         }
92 
93         TimerObj t = Timers.findTimer(mTimers, timerId);
94 
95         if (Timers.TIMES_UP.equals(actionType)) {
96             // Find the timer (if it doesn't exists, it was probably deleted).
97             if (t == null) {
98                 Log.d(TAG, " timer not found in list - do nothing");
99                 return;
100             }
101 
102             t.setState(TimerObj.STATE_TIMESUP);
103             t.writeToSharedPref(prefs);
104             Events.sendEvent(R.string.category_timer, R.string.action_fire, 0);
105 
106             // Play ringtone by using TimerRingService service with a default alarm.
107             Log.d(TAG, "playing ringtone");
108             Intent si = new Intent();
109             si.setClass(context, TimerRingService.class);
110             context.startService(si);
111 
112             // Update the in-use notification
113             if (getNextRunningTimer(mTimers, false, Utils.getTimeNow()) == null) {
114                 // Found no running timers.
115                 cancelInUseNotification(context);
116             } else {
117                 showInUseNotification(context);
118             }
119 
120             // Start the TimerAlertFullScreen activity.
121             Intent timersAlert = new Intent(context, TimerAlertFullScreen.class);
122             timersAlert.setFlags(
123                     Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
124             context.startActivity(timersAlert);
125         } else if (Timers.RESET_TIMER.equals(actionType)
126                 || Timers.DELETE_TIMER.equals(actionType)
127                 || Timers.TIMER_DONE.equals(actionType)) {
128             // Stop Ringtone if all timers are not in times-up status
129             stopRingtoneIfNoTimesup(context);
130 
131             if (t != null) {
132                 cancelTimesUpNotification(context, t);
133             }
134         } else if (Timers.NOTIF_TIMES_UP_STOP.equals(actionType)) {
135             // Find the timer (if it doesn't exists, it was probably deleted).
136             if (t == null) {
137                 Log.d(TAG, "timer to stop not found in list - do nothing");
138                 return;
139             } else if (t.mState != TimerObj.STATE_TIMESUP) {
140                 Log.d(TAG, "action to stop but timer not in times-up state - do nothing");
141                 return;
142             }
143 
144             // Update timer state
145             t.setState(t.getDeleteAfterUse() ? TimerObj.STATE_DELETED : TimerObj.STATE_RESTART);
146             t.mTimeLeft = t.mOriginalLength = t.mSetupLength;
147             t.writeToSharedPref(prefs);
148 
149             // Flag to tell DeskClock to re-sync with the database
150             prefs.edit().putBoolean(Timers.REFRESH_UI_WITH_LATEST_DATA, true).apply();
151 
152             cancelTimesUpNotification(context, t);
153 
154             // Done with timer - delete from data base
155             if (t.getDeleteAfterUse()) {
156                 t.deleteFromSharedPref(prefs);
157             }
158 
159             // Stop Ringtone if no timers are in times-up status
160             stopRingtoneIfNoTimesup(context);
161         } else if (Timers.NOTIF_TIMES_UP_PLUS_ONE.equals(actionType)) {
162             // Find the timer (if it doesn't exists, it was probably deleted).
163             if (t == null) {
164                 Log.d(TAG, "timer to +1m not found in list - do nothing");
165                 return;
166             } else if (t.mState != TimerObj.STATE_TIMESUP) {
167                 Log.d(TAG, "action to +1m but timer not in times up state - do nothing");
168                 return;
169             }
170 
171             // Restarting the timer with 1 minute left.
172             t.setState(TimerObj.STATE_RUNNING);
173             t.mStartTime = Utils.getTimeNow();
174             t.mTimeLeft = t. mOriginalLength = TimerObj.MINUTE_IN_MILLIS;
175             t.writeToSharedPref(prefs);
176 
177             // Flag to tell DeskClock to re-sync with the database
178             prefs.edit().putBoolean(Timers.REFRESH_UI_WITH_LATEST_DATA, true).apply();
179 
180             cancelTimesUpNotification(context, t);
181 
182             // If the app is not open, refresh the in-use notification
183             if (!prefs.getBoolean(Timers.NOTIF_APP_OPEN, false)) {
184                 showInUseNotification(context);
185             }
186 
187             // Stop Ringtone if no timers are in times-up status
188             stopRingtoneIfNoTimesup(context);
189         } else if (Timers.TIMER_UPDATE.equals(actionType)) {
190             // Find the timer (if it doesn't exists, it was probably deleted).
191             if (t == null) {
192                 Log.d(TAG, " timer to update not found in list - do nothing");
193                 return;
194             }
195 
196             // Refresh buzzing notification
197             if (t.mState == TimerObj.STATE_TIMESUP) {
198                 // Must cancel the previous notification to get all updates displayed correctly
199                 cancelTimesUpNotification(context, t);
200                 showTimesUpNotification(context, t);
201             }
202         }
203         if (intent.getBooleanExtra(Timers.UPDATE_NEXT_TIMESUP, true)) {
204             // Update the next "Times up" alarm unless explicitly told not to.
205             updateNextTimesup(context);
206         }
207     }
208 
stopRingtoneIfNoTimesup(final Context context)209     private void stopRingtoneIfNoTimesup(final Context context) {
210         if (Timers.findExpiredTimer(mTimers) == null) {
211             // Stop ringtone
212             Log.d(TAG, "stopping ringtone");
213             Intent si = new Intent();
214             si.setClass(context, TimerRingService.class);
215             context.stopService(si);
216         }
217     }
218 
219     // Scan all timers and find the one that will expire next.
220     // Tell AlarmManager to send a "Time's up" message to this receiver when this timer expires.
221     // If no timer exists, clear "time's up" message.
updateNextTimesup(Context context)222     private void updateNextTimesup(Context context) {
223         TimerObj t = getNextRunningTimer(mTimers, false, Utils.getTimeNow());
224         long nextTimesup = (t == null) ? -1 : t.getTimesupTime();
225         int timerId = (t == null) ? -1 : t.mTimerId;
226 
227         Intent intent = new Intent();
228         intent.setAction(Timers.TIMES_UP);
229         intent.setClass(context, TimerReceiver.class);
230         // Time-critical, should be foreground
231         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
232         if (!mTimers.isEmpty()) {
233             intent.putExtra(Timers.TIMER_INTENT_EXTRA, timerId);
234         }
235         AlarmManager mngr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
236         PendingIntent p = PendingIntent.getBroadcast(context,
237                 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
238         if (t != null) {
239             if (Utils.isKitKatOrLater()) {
240                 mngr.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p);
241             } else {
242                 mngr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p);
243             }
244             if (Timers.LOGGING) {
245                 Log.d(TAG, "Setting times up to " + nextTimesup);
246             }
247         } else {
248             // if no timer is found Pending Intents should be canceled
249             // to keep the internal state consistent with the UI
250             mngr.cancel(p);
251             p.cancel();
252             if (Timers.LOGGING) {
253                 Log.v(TAG, "no next times up");
254             }
255         }
256     }
257 
showInUseNotification(final Context context)258     private void showInUseNotification(final Context context) {
259         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
260         boolean appOpen = prefs.getBoolean(Timers.NOTIF_APP_OPEN, false);
261         ArrayList<TimerObj> timersInUse = Timers.timersInUse(mTimers);
262         int numTimersInUse = timersInUse.size();
263 
264         if (appOpen || numTimersInUse == 0) {
265             return;
266         }
267 
268         String title, contentText;
269         Long nextBroadcastTime = null;
270         long now = Utils.getTimeNow();
271         if (timersInUse.size() == 1) {
272             TimerObj timer = timersInUse.get(0);
273             boolean timerIsTicking = timer.isTicking();
274             String label = timer.getLabelOrDefault(context);
275             title = timerIsTicking ? label : context.getString(R.string.timer_stopped);
276             long timeLeft = timerIsTicking ? timer.getTimesupTime() - now : timer.mTimeLeft;
277             contentText = buildTimeRemaining(context, timeLeft);
278             if (timerIsTicking && timeLeft > TimerObj.MINUTE_IN_MILLIS) {
279                 nextBroadcastTime = getBroadcastTime(now, timeLeft);
280             }
281         } else {
282             TimerObj timer = getNextRunningTimer(timersInUse, false, now);
283             if (timer == null) {
284                 // No running timers.
285                 title = String.format(
286                         context.getString(R.string.timers_stopped), numTimersInUse);
287                 contentText = context.getString(R.string.all_timers_stopped_notif);
288             } else {
289                 // We have at least one timer running and other timers stopped.
290                 title = String.format(
291                         context.getString(R.string.timers_in_use), numTimersInUse);
292                 long completionTime = timer.getTimesupTime();
293                 long timeLeft = completionTime - now;
294                 contentText = String.format(context.getString(R.string.next_timer_notif),
295                         buildTimeRemaining(context, timeLeft));
296                 if (timeLeft <= TimerObj.MINUTE_IN_MILLIS) {
297                     TimerObj timerWithUpdate = getNextRunningTimer(timersInUse, true, now);
298                     if (timerWithUpdate != null) {
299                         completionTime = timerWithUpdate.getTimesupTime();
300                         timeLeft = completionTime - now;
301                         nextBroadcastTime = getBroadcastTime(now, timeLeft);
302                     }
303                 } else {
304                     nextBroadcastTime = getBroadcastTime(now, timeLeft);
305                 }
306             }
307         }
308         showCollapsedNotificationWithNext(context, title, contentText, nextBroadcastTime);
309     }
310 
getBroadcastTime(long now, long timeUntilBroadcast)311     private long getBroadcastTime(long now, long timeUntilBroadcast) {
312         long seconds = timeUntilBroadcast / 1000;
313         seconds = seconds - ( (seconds / 60) * 60 );
314         return now + (seconds * 1000);
315     }
316 
showCollapsedNotificationWithNext( final Context context, String title, String text, Long nextBroadcastTime)317     private void showCollapsedNotificationWithNext(
318             final Context context, String title, String text, Long nextBroadcastTime) {
319         Intent activityIntent = new Intent(context, DeskClock.class);
320         activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
321         activityIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX);
322         PendingIntent pendingActivityIntent = PendingIntent.getActivity(context, 0, activityIntent,
323                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
324         showCollapsedNotification(context, title, text, NotificationCompat.PRIORITY_HIGH,
325                 pendingActivityIntent, IN_USE_NOTIFICATION_ID, false);
326 
327         if (nextBroadcastTime == null) {
328             return;
329         }
330         Intent nextBroadcast = new Intent();
331         nextBroadcast.setAction(Timers.NOTIF_IN_USE_SHOW);
332         PendingIntent pendingNextBroadcast =
333                 PendingIntent.getBroadcast(context, 0, nextBroadcast, 0);
334         AlarmManager alarmManager =
335                 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
336         if (Utils.isKitKatOrLater()) {
337             alarmManager.setExact(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast);
338         } else {
339             alarmManager.set(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast);
340         }
341     }
342 
showCollapsedNotification(final Context context, String title, String text, int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker)343     private static void showCollapsedNotification(final Context context, String title, String text,
344             int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker) {
345         NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
346                 .setAutoCancel(false)
347                 .setContentTitle(title)
348                 .setContentText(text)
349                 .setDeleteIntent(pendingIntent)
350                 .setOngoing(true)
351                 .setPriority(priority)
352                 .setShowWhen(false)
353                 .setSmallIcon(R.drawable.stat_notify_timer)
354                 .setCategory(NotificationCompat.CATEGORY_ALARM)
355                 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
356                 .setLocalOnly(true);
357         if (showTicker) {
358             builder.setTicker(text);
359         }
360 
361         final Notification notification = builder.build();
362         notification.contentIntent = pendingIntent;
363         NotificationManagerCompat.from(context).notify(notificationId, notification);
364     }
365 
buildTimeRemaining(Context context, long timeLeft)366     private String buildTimeRemaining(Context context, long timeLeft) {
367         if (timeLeft < 0) {
368             // We should never be here...
369             Log.v(TAG, "Will not show notification for timer already expired.");
370             return null;
371         }
372 
373         long seconds, minutes, hours;
374         seconds = timeLeft / 1000;
375         minutes = seconds / 60;
376         hours = minutes / 60;
377         minutes = minutes - hours * 60;
378         if (hours > 99) {
379             hours = 0;
380         }
381 
382         String minSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.minutes,
383                 (int) minutes);
384 
385         String hourSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.hours,
386                 (int) hours);
387 
388         boolean dispHour = hours > 0;
389         boolean dispMinute = minutes > 0;
390         int index = (dispHour ? 1 : 0) | (dispMinute ? 2 : 0);
391         String[] formats = context.getResources().getStringArray(R.array.timer_notifications);
392         return String.format(formats[index], hourSeq, minSeq);
393     }
394 
getNextRunningTimer( ArrayList<TimerObj> timers, boolean requireNextUpdate, long now)395     private TimerObj getNextRunningTimer(
396             ArrayList<TimerObj> timers, boolean requireNextUpdate, long now) {
397         long nextTimesup = Long.MAX_VALUE;
398         boolean nextTimerFound = false;
399         Iterator<TimerObj> i = timers.iterator();
400         TimerObj t = null;
401         while(i.hasNext()) {
402             TimerObj tmp = i.next();
403             if (tmp.mState == TimerObj.STATE_RUNNING) {
404                 long timesupTime = tmp.getTimesupTime();
405                 long timeLeft = timesupTime - now;
406                 if (timesupTime < nextTimesup && (!requireNextUpdate || timeLeft > 60) ) {
407                     nextTimesup = timesupTime;
408                     nextTimerFound = true;
409                     t = tmp;
410                 }
411             }
412         }
413         if (nextTimerFound) {
414             return t;
415         } else {
416             return null;
417         }
418     }
419 
cancelInUseNotification(final Context context)420     public static void cancelInUseNotification(final Context context) {
421         NotificationManagerCompat.from(context).cancel(IN_USE_NOTIFICATION_ID);
422     }
423 
showTimesUpNotification(final Context context)424     private void showTimesUpNotification(final Context context) {
425         for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) {
426             showTimesUpNotification(context, timerObj);
427         }
428     }
429 
showTimesUpNotification(final Context context, TimerObj timerObj)430     private void showTimesUpNotification(final Context context, TimerObj timerObj) {
431         // Content Intent. When clicked will show the timer full screen
432         PendingIntent contentIntent = PendingIntent.getActivity(context, timerObj.mTimerId,
433                 new Intent(context, TimerAlertFullScreen.class).putExtra(
434                         Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId),
435                 PendingIntent.FLAG_UPDATE_CURRENT);
436 
437         // Add one minute action button
438         PendingIntent addOneMinuteAction = PendingIntent.getBroadcast(context, timerObj.mTimerId,
439                 new Intent(Timers.NOTIF_TIMES_UP_PLUS_ONE)
440                         .putExtra(Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId),
441                 PendingIntent.FLAG_UPDATE_CURRENT);
442 
443         // Add stop/done action button
444         PendingIntent stopIntent = PendingIntent.getBroadcast(context, timerObj.mTimerId,
445                 new Intent(Timers.NOTIF_TIMES_UP_STOP)
446                         .putExtra(Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId),
447                 PendingIntent.FLAG_UPDATE_CURRENT);
448 
449         // Notification creation
450         final NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
451                 .setContentIntent(contentIntent)
452                 .addAction(R.drawable.ic_add_24dp,
453                         context.getResources().getString(R.string.timer_plus_1_min),
454                         addOneMinuteAction)
455                 .addAction(
456                         timerObj.getDeleteAfterUse()
457                                 ? android.R.drawable.ic_menu_close_clear_cancel
458                                 : R.drawable.ic_stop_24dp,
459                         timerObj.getDeleteAfterUse()
460                                 ? context.getResources().getString(R.string.timer_done)
461                                 : context.getResources().getString(R.string.timer_stop),
462                         stopIntent)
463                 .setContentTitle(timerObj.getLabelOrDefault(context))
464                 .setContentText(context.getResources().getString(R.string.timer_times_up))
465                 .setSmallIcon(R.drawable.stat_notify_timer)
466                 .setOngoing(true)
467                 .setAutoCancel(false)
468                 .setPriority(NotificationCompat.PRIORITY_MAX)
469                 .setDefaults(NotificationCompat.DEFAULT_LIGHTS)
470                 .setWhen(0);
471 
472         // Send the notification using the timer's id to identify the
473         // correct notification
474         NotificationManagerCompat.from(context).notify(timerObj.mTimerId, builder.build());
475         if (Timers.LOGGING) {
476             Log.v(TAG, "Setting times-up notification for "
477                     + timerObj.getLabelOrDefault(context) + " #" + timerObj.mTimerId);
478         }
479     }
480 
cancelTimesUpNotification(final Context context)481     private void cancelTimesUpNotification(final Context context) {
482         for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) {
483             cancelTimesUpNotification(context, timerObj);
484         }
485     }
486 
cancelTimesUpNotification(final Context context, TimerObj timerObj)487     private void cancelTimesUpNotification(final Context context, TimerObj timerObj) {
488         NotificationManagerCompat.from(context).cancel(timerObj.mTimerId);
489         if (Timers.LOGGING) {
490             Log.v(TAG, "Canceling times-up notification for "
491                     + timerObj.getLabelOrDefault(context) + " #" + timerObj.mTimerId);
492         }
493     }
494 }
495