• 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_DONE;
138             t.mTimeLeft = t.mOriginalLength - (Utils.getTimeNow() - t.mStartTime);
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             // Refresh buzzing notification
183             if (t.mState == TimerObj.STATE_TIMESUP) {
184                 // Must cancel the previous notification to get all updates displayed correctly
185                 cancelTimesUpNotification(context, t);
186                 showTimesUpNotification(context, t);
187             }
188         }
189         // Update the next "Times up" alarm
190         updateNextTimesup(context);
191     }
192 
stopRingtoneIfNoTimesup(final Context context)193     private void stopRingtoneIfNoTimesup(final Context context) {
194         if (Timers.findExpiredTimer(mTimers) == null) {
195             // Stop ringtone
196             Log.d(TAG, "stopping ringtone");
197             Intent si = new Intent();
198             si.setClass(context, TimerRingService.class);
199             context.stopService(si);
200         }
201     }
202 
203     // Scan all timers and find the one that will expire next.
204     // Tell AlarmManager to send a "Time's up" message to this receiver when this timer expires.
205     // If no timer exists, clear "time's up" message.
updateNextTimesup(Context context)206     private void updateNextTimesup(Context context) {
207         TimerObj t = getNextRunningTimer(mTimers, false, Utils.getTimeNow());
208         long nextTimesup = (t == null) ? -1 : t.getTimesupTime();
209         int timerId = (t == null) ? -1 : t.mTimerId;
210 
211         Intent intent = new Intent();
212         intent.setAction(Timers.TIMES_UP);
213         intent.setClass(context, TimerReceiver.class);
214         if (!mTimers.isEmpty()) {
215             intent.putExtra(Timers.TIMER_INTENT_EXTRA, timerId);
216         }
217         AlarmManager mngr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
218         PendingIntent p = PendingIntent.getBroadcast(context,
219                 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
220         if (t != null) {
221             if (Utils.isKitKatOrLater()) {
222                 mngr.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p);
223             } else {
224                 mngr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p);
225             }
226             if (Timers.LOGGING) {
227                 Log.d(TAG, "Setting times up to " + nextTimesup);
228             }
229         } else {
230             mngr.cancel(p);
231             if (Timers.LOGGING) {
232                 Log.v(TAG, "no next times up");
233             }
234         }
235     }
236 
showInUseNotification(final Context context)237     private void showInUseNotification(final Context context) {
238         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
239         boolean appOpen = prefs.getBoolean(Timers.NOTIF_APP_OPEN, false);
240         ArrayList<TimerObj> timersInUse = Timers.timersInUse(mTimers);
241         int numTimersInUse = timersInUse.size();
242 
243         if (appOpen || numTimersInUse == 0) {
244             return;
245         }
246 
247         String title, contentText;
248         Long nextBroadcastTime = null;
249         long now = Utils.getTimeNow();
250         if (timersInUse.size() == 1) {
251             TimerObj timer = timersInUse.get(0);
252             boolean timerIsTicking = timer.isTicking();
253             String label = timer.getLabelOrDefault(context);
254             title = timerIsTicking ? label : context.getString(R.string.timer_stopped);
255             long timeLeft = timerIsTicking ? timer.getTimesupTime() - now : timer.mTimeLeft;
256             contentText = buildTimeRemaining(context, timeLeft);
257             if (timerIsTicking && timeLeft > TimerObj.MINUTE_IN_MILLIS) {
258                 nextBroadcastTime = getBroadcastTime(now, timeLeft);
259             }
260         } else {
261             TimerObj timer = getNextRunningTimer(timersInUse, false, now);
262             if (timer == null) {
263                 // No running timers.
264                 title = String.format(
265                         context.getString(R.string.timers_stopped), numTimersInUse);
266                 contentText = context.getString(R.string.all_timers_stopped_notif);
267             } else {
268                 // We have at least one timer running and other timers stopped.
269                 title = String.format(
270                         context.getString(R.string.timers_in_use), numTimersInUse);
271                 long completionTime = timer.getTimesupTime();
272                 long timeLeft = completionTime - now;
273                 contentText = String.format(context.getString(R.string.next_timer_notif),
274                         buildTimeRemaining(context, timeLeft));
275                 if (timeLeft <= TimerObj.MINUTE_IN_MILLIS) {
276                     TimerObj timerWithUpdate = getNextRunningTimer(timersInUse, true, now);
277                     if (timerWithUpdate != null) {
278                         completionTime = timerWithUpdate.getTimesupTime();
279                         timeLeft = completionTime - now;
280                         nextBroadcastTime = getBroadcastTime(now, timeLeft);
281                     }
282                 } else {
283                     nextBroadcastTime = getBroadcastTime(now, timeLeft);
284                 }
285             }
286         }
287         showCollapsedNotificationWithNext(context, title, contentText, nextBroadcastTime);
288     }
289 
getBroadcastTime(long now, long timeUntilBroadcast)290     private long getBroadcastTime(long now, long timeUntilBroadcast) {
291         long seconds = timeUntilBroadcast / 1000;
292         seconds = seconds - ( (seconds / 60) * 60 );
293         return now + (seconds * 1000);
294     }
295 
showCollapsedNotificationWithNext( final Context context, String title, String text, Long nextBroadcastTime)296     private void showCollapsedNotificationWithNext(
297             final Context context, String title, String text, Long nextBroadcastTime) {
298         Intent activityIntent = new Intent(context, DeskClock.class);
299         activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
300         activityIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX);
301         PendingIntent pendingActivityIntent = PendingIntent.getActivity(context, 0, activityIntent,
302                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
303         showCollapsedNotification(context, title, text, Notification.PRIORITY_HIGH,
304                 pendingActivityIntent, IN_USE_NOTIFICATION_ID, false);
305 
306         if (nextBroadcastTime == null) {
307             return;
308         }
309         Intent nextBroadcast = new Intent();
310         nextBroadcast.setAction(Timers.NOTIF_IN_USE_SHOW);
311         PendingIntent pendingNextBroadcast =
312                 PendingIntent.getBroadcast(context, 0, nextBroadcast, 0);
313         AlarmManager alarmManager =
314                 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
315         if (Utils.isKitKatOrLater()) {
316             alarmManager.setExact(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast);
317         } else {
318             alarmManager.set(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast);
319         }
320     }
321 
showCollapsedNotification(final Context context, String title, String text, int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker)322     private static void showCollapsedNotification(final Context context, String title, String text,
323             int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker) {
324         Notification.Builder builder = new Notification.Builder(context)
325         .setAutoCancel(false)
326         .setContentTitle(title)
327         .setContentText(text)
328         .setDeleteIntent(pendingIntent)
329         .setOngoing(true)
330         .setPriority(priority)
331         .setShowWhen(false)
332         .setSmallIcon(R.drawable.stat_notify_timer);
333         if (showTicker) {
334             builder.setTicker(text);
335         }
336 
337         Notification notification = builder.build();
338         notification.contentIntent = pendingIntent;
339         NotificationManager notificationManager =
340                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
341         notificationManager.notify(notificationId, notification);
342     }
343 
buildTimeRemaining(Context context, long timeLeft)344     private String buildTimeRemaining(Context context, long timeLeft) {
345         if (timeLeft < 0) {
346             // We should never be here...
347             Log.v(TAG, "Will not show notification for timer already expired.");
348             return null;
349         }
350 
351         long hundreds, seconds, minutes, hours;
352         seconds = timeLeft / 1000;
353         minutes = seconds / 60;
354         seconds = seconds - minutes * 60;
355         hours = minutes / 60;
356         minutes = minutes - hours * 60;
357         if (hours > 99) {
358             hours = 0;
359         }
360 
361         String hourSeq = (hours == 0) ? "" :
362             ( (hours == 1) ? context.getString(R.string.hour) :
363                 context.getString(R.string.hours, Long.toString(hours)) );
364         String minSeq = (minutes == 0) ? "" :
365             ( (minutes == 1) ? context.getString(R.string.minute) :
366                 context.getString(R.string.minutes, Long.toString(minutes)) );
367 
368         boolean dispHour = hours > 0;
369         boolean dispMinute = minutes > 0;
370         int index = (dispHour ? 1 : 0) | (dispMinute ? 2 : 0);
371         String[] formats = context.getResources().getStringArray(R.array.timer_notifications);
372         return String.format(formats[index], hourSeq, minSeq);
373     }
374 
getNextRunningTimer( ArrayList<TimerObj> timers, boolean requireNextUpdate, long now)375     private TimerObj getNextRunningTimer(
376             ArrayList<TimerObj> timers, boolean requireNextUpdate, long now) {
377         long nextTimesup = Long.MAX_VALUE;
378         boolean nextTimerFound = false;
379         Iterator<TimerObj> i = timers.iterator();
380         TimerObj t = null;
381         while(i.hasNext()) {
382             TimerObj tmp = i.next();
383             if (tmp.mState == TimerObj.STATE_RUNNING) {
384                 long timesupTime = tmp.getTimesupTime();
385                 long timeLeft = timesupTime - now;
386                 if (timesupTime < nextTimesup && (!requireNextUpdate || timeLeft > 60) ) {
387                     nextTimesup = timesupTime;
388                     nextTimerFound = true;
389                     t = tmp;
390                 }
391             }
392         }
393         if (nextTimerFound) {
394             return t;
395         } else {
396             return null;
397         }
398     }
399 
cancelInUseNotification(final Context context)400     private void cancelInUseNotification(final Context context) {
401         NotificationManager notificationManager =
402                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
403         notificationManager.cancel(IN_USE_NOTIFICATION_ID);
404     }
405 
showTimesUpNotification(final Context context)406     private void showTimesUpNotification(final Context context) {
407         for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) {
408             showTimesUpNotification(context, timerObj);
409         }
410     }
411 
showTimesUpNotification(final Context context, TimerObj timerObj)412     private void showTimesUpNotification(final Context context, TimerObj timerObj) {
413         // Content Intent. When clicked will show the timer full screen
414         PendingIntent contentIntent = PendingIntent.getActivity(context, timerObj.mTimerId,
415                 new Intent(context, TimerAlertFullScreen.class).putExtra(
416                         Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId),
417                 PendingIntent.FLAG_UPDATE_CURRENT);
418 
419         // Add one minute action button
420         PendingIntent addOneMinuteAction = PendingIntent.getBroadcast(context, timerObj.mTimerId,
421                 new Intent(Timers.NOTIF_TIMES_UP_PLUS_ONE)
422                         .putExtra(Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId),
423                 PendingIntent.FLAG_UPDATE_CURRENT);
424 
425         // Add stop/done action button
426         PendingIntent stopAction = PendingIntent.getBroadcast(context, timerObj.mTimerId,
427                 new Intent(Timers.NOTIF_TIMES_UP_STOP)
428                         .putExtra(Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId),
429                 PendingIntent.FLAG_UPDATE_CURRENT);
430 
431         // Notification creation
432         Notification notification = new Notification.Builder(context)
433                 .setContentIntent(contentIntent)
434                 .addAction(R.drawable.ic_menu_add,
435                         context.getResources().getString(R.string.timer_plus_1_min),
436                         addOneMinuteAction)
437                 .addAction(
438                         timerObj.getDeleteAfterUse()
439                                 ? android.R.drawable.ic_menu_close_clear_cancel
440                                 : R.drawable.ic_stop_normal,
441                         timerObj.getDeleteAfterUse()
442                                 ? context.getResources().getString(R.string.timer_done)
443                                 : context.getResources().getString(R.string.timer_stop),
444                         stopAction)
445                 .setContentTitle(timerObj.getLabelOrDefault(context))
446                 .setContentText(context.getResources().getString(R.string.timer_times_up))
447                 .setSmallIcon(R.drawable.stat_notify_timer)
448                 .setOngoing(true)
449                 .setAutoCancel(false)
450                 .setPriority(Notification.PRIORITY_MAX)
451                 .setDefaults(Notification.DEFAULT_LIGHTS)
452                 .setWhen(0)
453                 .build();
454 
455         // Send the notification using the timer's id to identify the
456         // correct notification
457         ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(
458                 timerObj.mTimerId, notification);
459         if (Timers.LOGGING) {
460             Log.v(TAG, "Setting times-up notification for "
461                     + timerObj.getLabelOrDefault(context) + " #" + timerObj.mTimerId);
462         }
463     }
464 
cancelTimesUpNotification(final Context context)465     private void cancelTimesUpNotification(final Context context) {
466         for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) {
467             cancelTimesUpNotification(context, timerObj);
468         }
469     }
470 
cancelTimesUpNotification(final Context context, TimerObj timerObj)471     private void cancelTimesUpNotification(final Context context, TimerObj timerObj) {
472         NotificationManager notificationManager =
473                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
474         notificationManager.cancel(timerObj.mTimerId);
475         if (Timers.LOGGING) {
476             Log.v(TAG, "Canceling times-up notification for "
477                     + timerObj.getLabelOrDefault(context) + " #" + timerObj.mTimerId);
478         }
479     }
480 }
481