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