TimerReceiver.java revision bae6862c87f851be4bbcbab57ac71ac5385c9850
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
17package com.android.deskclock.timer;
18
19import android.app.AlarmManager;
20import android.app.Notification;
21import android.app.NotificationManager;
22import android.app.PendingIntent;
23import android.content.BroadcastReceiver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.SharedPreferences;
27import android.preference.PreferenceManager;
28import android.util.Log;
29
30import com.android.deskclock.DeskClock;
31import com.android.deskclock.R;
32import com.android.deskclock.TimerRingService;
33import com.android.deskclock.Utils;
34
35import java.util.ArrayList;
36import java.util.Iterator;
37
38public 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
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.setState(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.setState(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.REFRESH_UI_WITH_LATEST_DATA, 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.setState(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.REFRESH_UI_WITH_LATEST_DATA, 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
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.
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        // Time-critical, should be foreground
221        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
222        if (!mTimers.isEmpty()) {
223            intent.putExtra(Timers.TIMER_INTENT_EXTRA, timerId);
224        }
225        AlarmManager mngr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
226        PendingIntent p = PendingIntent.getBroadcast(context,
227                0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
228        if (t != null) {
229            if (Utils.isKitKatOrLater()) {
230                mngr.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p);
231            } else {
232                mngr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p);
233            }
234            if (Timers.LOGGING) {
235                Log.d(TAG, "Setting times up to " + nextTimesup);
236            }
237        } else {
238            mngr.cancel(p);
239            if (Timers.LOGGING) {
240                Log.v(TAG, "no next times up");
241            }
242        }
243    }
244
245    private void showInUseNotification(final Context context) {
246        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
247        boolean appOpen = prefs.getBoolean(Timers.NOTIF_APP_OPEN, false);
248        ArrayList<TimerObj> timersInUse = Timers.timersInUse(mTimers);
249        int numTimersInUse = timersInUse.size();
250
251        if (appOpen || numTimersInUse == 0) {
252            return;
253        }
254
255        String title, contentText;
256        Long nextBroadcastTime = null;
257        long now = Utils.getTimeNow();
258        if (timersInUse.size() == 1) {
259            TimerObj timer = timersInUse.get(0);
260            boolean timerIsTicking = timer.isTicking();
261            String label = timer.getLabelOrDefault(context);
262            title = timerIsTicking ? label : context.getString(R.string.timer_stopped);
263            long timeLeft = timerIsTicking ? timer.getTimesupTime() - now : timer.mTimeLeft;
264            contentText = buildTimeRemaining(context, timeLeft);
265            if (timerIsTicking && timeLeft > TimerObj.MINUTE_IN_MILLIS) {
266                nextBroadcastTime = getBroadcastTime(now, timeLeft);
267            }
268        } else {
269            TimerObj timer = getNextRunningTimer(timersInUse, false, now);
270            if (timer == null) {
271                // No running timers.
272                title = String.format(
273                        context.getString(R.string.timers_stopped), numTimersInUse);
274                contentText = context.getString(R.string.all_timers_stopped_notif);
275            } else {
276                // We have at least one timer running and other timers stopped.
277                title = String.format(
278                        context.getString(R.string.timers_in_use), numTimersInUse);
279                long completionTime = timer.getTimesupTime();
280                long timeLeft = completionTime - now;
281                contentText = String.format(context.getString(R.string.next_timer_notif),
282                        buildTimeRemaining(context, timeLeft));
283                if (timeLeft <= TimerObj.MINUTE_IN_MILLIS) {
284                    TimerObj timerWithUpdate = getNextRunningTimer(timersInUse, true, now);
285                    if (timerWithUpdate != null) {
286                        completionTime = timerWithUpdate.getTimesupTime();
287                        timeLeft = completionTime - now;
288                        nextBroadcastTime = getBroadcastTime(now, timeLeft);
289                    }
290                } else {
291                    nextBroadcastTime = getBroadcastTime(now, timeLeft);
292                }
293            }
294        }
295        showCollapsedNotificationWithNext(context, title, contentText, nextBroadcastTime);
296    }
297
298    private long getBroadcastTime(long now, long timeUntilBroadcast) {
299        long seconds = timeUntilBroadcast / 1000;
300        seconds = seconds - ( (seconds / 60) * 60 );
301        return now + (seconds * 1000);
302    }
303
304    private void showCollapsedNotificationWithNext(
305            final Context context, String title, String text, Long nextBroadcastTime) {
306        Intent activityIntent = new Intent(context, DeskClock.class);
307        activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
308        activityIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX);
309        PendingIntent pendingActivityIntent = PendingIntent.getActivity(context, 0, activityIntent,
310                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
311        showCollapsedNotification(context, title, text, Notification.PRIORITY_HIGH,
312                pendingActivityIntent, IN_USE_NOTIFICATION_ID, false);
313
314        if (nextBroadcastTime == null) {
315            return;
316        }
317        Intent nextBroadcast = new Intent();
318        nextBroadcast.setAction(Timers.NOTIF_IN_USE_SHOW);
319        PendingIntent pendingNextBroadcast =
320                PendingIntent.getBroadcast(context, 0, nextBroadcast, 0);
321        AlarmManager alarmManager =
322                (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
323        if (Utils.isKitKatOrLater()) {
324            alarmManager.setExact(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast);
325        } else {
326            alarmManager.set(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast);
327        }
328    }
329
330    private static void showCollapsedNotification(final Context context, String title, String text,
331            int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker) {
332        Notification.Builder builder = new Notification.Builder(context)
333                .setAutoCancel(false)
334                .setContentTitle(title)
335                .setContentText(text)
336                .setDeleteIntent(pendingIntent)
337                .setOngoing(true)
338                .setPriority(priority)
339                .setShowWhen(false)
340                .setSmallIcon(R.drawable.stat_notify_timer)
341                .setCategory(Notification.CATEGORY_ALARM)
342                .setVisibility(Notification.VISIBILITY_PUBLIC)
343                .setLocalOnly(true);
344        if (showTicker) {
345            builder.setTicker(text);
346        }
347
348        Notification notification = builder.build();
349        notification.contentIntent = pendingIntent;
350        NotificationManager notificationManager =
351                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
352        notificationManager.notify(notificationId, notification);
353    }
354
355    private String buildTimeRemaining(Context context, long timeLeft) {
356        if (timeLeft < 0) {
357            // We should never be here...
358            Log.v(TAG, "Will not show notification for timer already expired.");
359            return null;
360        }
361
362        long hundreds, seconds, minutes, hours;
363        seconds = timeLeft / 1000;
364        minutes = seconds / 60;
365        seconds = seconds - minutes * 60;
366        hours = minutes / 60;
367        minutes = minutes - hours * 60;
368        if (hours > 99) {
369            hours = 0;
370        }
371
372        String hourSeq = (hours == 0) ? "" :
373            ( (hours == 1) ? context.getString(R.string.hour) :
374                context.getString(R.string.hours, Long.toString(hours)) );
375        String minSeq = (minutes == 0) ? "" :
376            ( (minutes == 1) ? context.getString(R.string.minute) :
377                context.getString(R.string.minutes, Long.toString(minutes)) );
378
379        boolean dispHour = hours > 0;
380        boolean dispMinute = minutes > 0;
381        int index = (dispHour ? 1 : 0) | (dispMinute ? 2 : 0);
382        String[] formats = context.getResources().getStringArray(R.array.timer_notifications);
383        return String.format(formats[index], hourSeq, minSeq);
384    }
385
386    private TimerObj getNextRunningTimer(
387            ArrayList<TimerObj> timers, boolean requireNextUpdate, long now) {
388        long nextTimesup = Long.MAX_VALUE;
389        boolean nextTimerFound = false;
390        Iterator<TimerObj> i = timers.iterator();
391        TimerObj t = null;
392        while(i.hasNext()) {
393            TimerObj tmp = i.next();
394            if (tmp.mState == TimerObj.STATE_RUNNING) {
395                long timesupTime = tmp.getTimesupTime();
396                long timeLeft = timesupTime - now;
397                if (timesupTime < nextTimesup && (!requireNextUpdate || timeLeft > 60) ) {
398                    nextTimesup = timesupTime;
399                    nextTimerFound = true;
400                    t = tmp;
401                }
402            }
403        }
404        if (nextTimerFound) {
405            return t;
406        } else {
407            return null;
408        }
409    }
410
411    private void cancelInUseNotification(final Context context) {
412        NotificationManager notificationManager =
413                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
414        notificationManager.cancel(IN_USE_NOTIFICATION_ID);
415    }
416
417    private void showTimesUpNotification(final Context context) {
418        for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) {
419            showTimesUpNotification(context, timerObj);
420        }
421    }
422
423    private void showTimesUpNotification(final Context context, TimerObj timerObj) {
424        // Content Intent. When clicked will show the timer full screen
425        PendingIntent contentIntent = PendingIntent.getActivity(context, timerObj.mTimerId,
426                new Intent(context, TimerAlertFullScreen.class).putExtra(
427                        Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId),
428                PendingIntent.FLAG_UPDATE_CURRENT);
429
430        // Add one minute action button
431        PendingIntent addOneMinuteAction = PendingIntent.getBroadcast(context, timerObj.mTimerId,
432                new Intent(Timers.NOTIF_TIMES_UP_PLUS_ONE)
433                        .putExtra(Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId),
434                PendingIntent.FLAG_UPDATE_CURRENT);
435
436        // Add stop/done action button
437        PendingIntent stopIntent = PendingIntent.getBroadcast(context, timerObj.mTimerId,
438                new Intent(Timers.NOTIF_TIMES_UP_STOP)
439                        .putExtra(Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId),
440                PendingIntent.FLAG_UPDATE_CURRENT);
441
442        // Notification creation
443        Notification notification = new Notification.Builder(context)
444                .setContentIntent(contentIntent)
445                .addAction(R.drawable.ic_menu_add,
446                        context.getResources().getString(R.string.timer_plus_1_min),
447                        addOneMinuteAction)
448                .addAction(
449                        timerObj.getDeleteAfterUse()
450                                ? android.R.drawable.ic_menu_close_clear_cancel
451                                : R.drawable.ic_notify_stop,
452                        timerObj.getDeleteAfterUse()
453                                ? context.getResources().getString(R.string.timer_done)
454                                : context.getResources().getString(R.string.timer_stop),
455                        stopIntent)
456                .setContentTitle(timerObj.getLabelOrDefault(context))
457                .setContentText(context.getResources().getString(R.string.timer_times_up))
458                .setSmallIcon(R.drawable.stat_notify_timer)
459                .setOngoing(true)
460                .setAutoCancel(false)
461                .setPriority(Notification.PRIORITY_MAX)
462                .setDefaults(Notification.DEFAULT_LIGHTS)
463                .setWhen(0)
464                .setCategory(Notification.CATEGORY_ALARM)
465                .setVisibility(Notification.VISIBILITY_PUBLIC)
466                .setLocalOnly(true)
467                .build();
468
469        // Send the notification using the timer's id to identify the
470        // correct notification
471        ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(
472                timerObj.mTimerId, notification);
473        if (Timers.LOGGING) {
474            Log.v(TAG, "Setting times-up notification for "
475                    + timerObj.getLabelOrDefault(context) + " #" + timerObj.mTimerId);
476        }
477    }
478
479    private void cancelTimesUpNotification(final Context context) {
480        for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) {
481            cancelTimesUpNotification(context, timerObj);
482        }
483    }
484
485    private void cancelTimesUpNotification(final Context context, TimerObj timerObj) {
486        NotificationManager notificationManager =
487                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
488        notificationManager.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