TimerReceiver.java revision 74be96baee671f1f62b4ed543ba793336012ef24
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        int timer;
50        String actionType = intent.getAction();
51
52        // Get the updated timers data.
53        if (mTimers == null) {
54            mTimers = new ArrayList<TimerObj> ();
55        }
56        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
57        TimerObj.getTimersFromSharedPrefs(prefs, mTimers);
58
59
60        if (intent.hasExtra(Timers.TIMER_INTENT_EXTRA)) {
61            // Get the alarm out of the Intent
62            timer = intent.getIntExtra(Timers.TIMER_INTENT_EXTRA, -1);
63            if (timer == -1) {
64                Log.d(TAG, " got intent without Timer data: "+actionType);
65            }
66        } else if (Timers.NOTIF_IN_USE_SHOW.equals(actionType)){
67            showInUseNotification(context);
68            return;
69        } else if (Timers.NOTIF_IN_USE_CANCEL.equals(actionType)) {
70            cancelInUseNotification(context);
71            return;
72        } else {
73            // No data to work with, do nothing
74            Log.d(TAG, " got intent without Timer data");
75            return;
76        }
77
78        TimerObj t = Timers.findTimer(mTimers, timer);
79
80        if (intent.getBooleanExtra(Timers.UPDATE_NOTIFICATION, false)) {
81            if (Timers.TIMER_STOP.equals(actionType)) {
82                if (t == null) {
83                    Log.d(TAG, "timer not found in list - can't stop it.");
84                    return;
85                }
86                t.mState = TimerObj.STATE_DONE;
87                t.writeToSharedPref(prefs);
88                SharedPreferences.Editor editor = prefs.edit();
89                editor.putBoolean(Timers.FROM_NOTIFICATION, true);
90                editor.putLong(Timers.NOTIF_TIME, Utils.getTimeNow());
91                editor.putInt(Timers.NOTIF_ID, timer);
92                editor.apply();
93
94                stopRingtoneIfNoTimesup(context);
95
96                Intent activityIntent = new Intent(context, DeskClock.class);
97                activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
98                activityIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX);
99                context.startActivity(activityIntent);
100            }
101             return;
102        }
103
104        if (Timers.TIMES_UP.equals(actionType)) {
105            // Find the timer (if it doesn't exists, it was probably deleted).
106            if (t == null) {
107                Log.d(TAG, " timer not found in list - do nothing");
108                return;
109            }
110
111            t.mState = TimerObj.STATE_TIMESUP;
112            t.writeToSharedPref(prefs);
113            // Play ringtone by using TimerRingService service with a default alarm.
114            Log.d(TAG, "playing ringtone");
115            Intent si = new Intent();
116            si.setClass(context, TimerRingService.class);
117            context.startService(si);
118
119            // Update the in-use notification
120            if (getNextRunningTimer(mTimers, false, Utils.getTimeNow()) == null) {
121                // Found no running timers.
122                cancelInUseNotification(context);
123            } else {
124                showInUseNotification(context);
125            }
126
127            // Start the TimerAlertFullScreen activity.
128            Intent timersAlert = new Intent(context, TimerAlertFullScreen.class);
129            timersAlert.setFlags(
130                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
131            context.startActivity(timersAlert);
132        } else if (Timers.TIMER_RESET.equals(actionType)
133                || Timers.DELETE_TIMER.equals(actionType)
134                || Timers.TIMER_DONE.equals(actionType)) {
135            // Stop Ringtone if all timers are not in times-up status
136            stopRingtoneIfNoTimesup(context);
137        }
138        // Update the next "Times up" alarm
139        updateNextTimesup(context);
140    }
141
142    private void stopRingtoneIfNoTimesup(final Context context) {
143        if (Timers.findExpiredTimer(mTimers) == null) {
144            // Stop ringtone
145            Log.d(TAG, "stopping ringtone");
146            Intent si = new Intent();
147            si.setClass(context, TimerRingService.class);
148            context.stopService(si);
149        }
150    }
151
152    // Scan all timers and find the one that will expire next.
153    // Tell AlarmManager to send a "Time's up" message to this receiver when this timer expires.
154    // If no timer exists, clear "time's up" message.
155    private void updateNextTimesup(Context context) {
156        TimerObj t = getNextRunningTimer(mTimers, false, Utils.getTimeNow());
157        long nextTimesup = (t == null) ? -1 : t.getTimesupTime();
158        int timerId = (t == null) ? -1 : t.mTimerId;
159
160        Intent intent = new Intent();
161        intent.setAction(Timers.TIMES_UP);
162        intent.setClass(context, TimerReceiver.class);
163        if (!mTimers.isEmpty()) {
164            intent.putExtra(Timers.TIMER_INTENT_EXTRA, timerId);
165        }
166        AlarmManager mngr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
167        PendingIntent p = PendingIntent.getBroadcast(context,
168                0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
169        if (t != null) {
170            if (Utils.isKeyLimePieOrLater()) {
171                mngr.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p);
172            } else {
173                mngr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p);
174            }
175            Log.d(TAG,"Setting times up to " + nextTimesup);
176        } else {
177            Log.d(TAG,"canceling times up");
178            mngr.cancel(p);
179        }
180    }
181
182    private void showInUseNotification(final Context context) {
183        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
184        boolean appOpen = prefs.getBoolean(Timers.NOTIF_APP_OPEN, false);
185        ArrayList<TimerObj> timersInUse = Timers.timersInUse(mTimers);
186        int numTimersInUse = timersInUse.size();
187
188        if (appOpen || numTimersInUse == 0) {
189            return;
190        }
191
192        String title, contentText;
193        Long nextBroadcastTime = null;
194        long now = Utils.getTimeNow();
195        if (timersInUse.size() == 1) {
196            TimerObj timer = timersInUse.get(0);
197            boolean timerIsTicking = timer.isTicking();
198            String label = timer.mLabel.equals("") ?
199                    context.getString(R.string.timer_notification_label) : timer.mLabel;
200            title = timerIsTicking ? label : context.getString(R.string.timer_stopped);
201            long timeLeft = timerIsTicking ? timer.getTimesupTime() - now : timer.mTimeLeft;
202            contentText = buildTimeRemaining(context, timeLeft);
203            if (timerIsTicking && timeLeft > TimerObj.MINUTE_IN_MILLIS) {
204                nextBroadcastTime = getBroadcastTime(now, timeLeft);
205            }
206        } else {
207            TimerObj timer = getNextRunningTimer(timersInUse, false, now);
208            if (timer == null) {
209                // No running timers.
210                title = String.format(
211                        context.getString(R.string.timers_stopped), numTimersInUse);
212                contentText = context.getString(R.string.all_timers_stopped_notif);
213            } else {
214                // We have at least one timer running and other timers stopped.
215                title = String.format(
216                        context.getString(R.string.timers_in_use), numTimersInUse);
217                long completionTime = timer.getTimesupTime();
218                long timeLeft = completionTime - now;
219                contentText = String.format(context.getString(R.string.next_timer_notif),
220                        buildTimeRemaining(context, timeLeft));
221                if (timeLeft <= TimerObj.MINUTE_IN_MILLIS) {
222                    TimerObj timerWithUpdate = getNextRunningTimer(timersInUse, true, now);
223                    if (timerWithUpdate != null) {
224                        completionTime = timerWithUpdate.getTimesupTime();
225                        timeLeft = completionTime - now;
226                        nextBroadcastTime = getBroadcastTime(now, timeLeft);
227                    }
228                } else {
229                    nextBroadcastTime = getBroadcastTime(now, timeLeft);
230                }
231            }
232        }
233        showCollapsedNotificationWithNext(context, title, contentText, nextBroadcastTime);
234    }
235
236    private long getBroadcastTime(long now, long timeUntilBroadcast) {
237        long seconds = timeUntilBroadcast / 1000;
238        seconds = seconds - ( (seconds / 60) * 60 );
239        return now + (seconds * 1000);
240    }
241
242    /** Public and static to allow timer fragment to update notification with new label. **/
243    public static void showExpiredAlarmNotification(Context context, TimerObj t) {
244        Intent broadcastIntent = new Intent();
245        broadcastIntent.putExtra(Timers.TIMER_INTENT_EXTRA, t.mTimerId);
246        broadcastIntent.setAction(Timers.TIMER_STOP);
247        broadcastIntent.putExtra(Timers.UPDATE_NOTIFICATION, true);
248        PendingIntent pendingBroadcastIntent = PendingIntent.getBroadcast(
249                context, 0, broadcastIntent, 0);
250        String label = t.mLabel.equals("") ? context.getString(R.string.timer_notification_label) :
251            t.mLabel;
252        String contentText = context.getString(R.string.timer_times_up);
253        showCollapsedNotification(context, label, contentText, Notification.PRIORITY_MAX,
254                pendingBroadcastIntent, t.mTimerId, true);
255    }
256
257    private void showCollapsedNotificationWithNext(
258            final Context context, String title, String text, Long nextBroadcastTime) {
259        Intent activityIntent = new Intent(context, DeskClock.class);
260        activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
261        activityIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX);
262        PendingIntent pendingActivityIntent = PendingIntent.getActivity(context, 0, activityIntent,
263                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
264        showCollapsedNotification(context, title, text, Notification.PRIORITY_HIGH,
265                pendingActivityIntent, IN_USE_NOTIFICATION_ID, false);
266
267        if (nextBroadcastTime == null) {
268            return;
269        }
270        Intent nextBroadcast = new Intent();
271        nextBroadcast.setAction(Timers.NOTIF_IN_USE_SHOW);
272        PendingIntent pendingNextBroadcast =
273                PendingIntent.getBroadcast(context, 0, nextBroadcast, 0);
274        AlarmManager alarmManager =
275                (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
276        if (Utils.isKeyLimePieOrLater()) {
277            alarmManager.setExact(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast);
278        } else {
279            alarmManager.set(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast);
280        }
281    }
282
283    private static void showCollapsedNotification(final Context context, String title, String text,
284            int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker) {
285        Notification.Builder builder = new Notification.Builder(context)
286        .setAutoCancel(false)
287        .setContentTitle(title)
288        .setContentText(text)
289        .setDeleteIntent(pendingIntent)
290        .setOngoing(true)
291        .setPriority(priority)
292        .setShowWhen(false)
293        .setSmallIcon(R.drawable.stat_notify_timer);
294        if (showTicker) {
295            builder.setTicker(text);
296        }
297
298        Notification notification = builder.build();
299        notification.contentIntent = pendingIntent;
300        NotificationManager notificationManager =
301                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
302        notificationManager.notify(notificationId, notification);
303    }
304
305    private String buildTimeRemaining(Context context, long timeLeft) {
306        if (timeLeft < 0) {
307            // We should never be here...
308            Log.v(TAG, "Will not show notification for timer already expired.");
309            return null;
310        }
311
312        long hundreds, seconds, minutes, hours;
313        seconds = timeLeft / 1000;
314        minutes = seconds / 60;
315        seconds = seconds - minutes * 60;
316        hours = minutes / 60;
317        minutes = minutes - hours * 60;
318        if (hours > 99) {
319            hours = 0;
320        }
321
322        String hourSeq = (hours == 0) ? "" :
323            ( (hours == 1) ? context.getString(R.string.hour) :
324                context.getString(R.string.hours, Long.toString(hours)) );
325        String minSeq = (minutes == 0) ? "" :
326            ( (minutes == 1) ? context.getString(R.string.minute) :
327                context.getString(R.string.minutes, Long.toString(minutes)) );
328
329        boolean dispHour = hours > 0;
330        boolean dispMinute = minutes > 0;
331        int index = (dispHour ? 1 : 0) | (dispMinute ? 2 : 0);
332        String[] formats = context.getResources().getStringArray(R.array.timer_notifications);
333        return String.format(formats[index], hourSeq, minSeq);
334    }
335
336    private TimerObj getNextRunningTimer(
337            ArrayList<TimerObj> timers, boolean requireNextUpdate, long now) {
338        long nextTimesup = Long.MAX_VALUE;
339        boolean nextTimerFound = false;
340        Iterator<TimerObj> i = timers.iterator();
341        TimerObj t = null;
342        while(i.hasNext()) {
343            TimerObj tmp = i.next();
344            if (tmp.mState == TimerObj.STATE_RUNNING) {
345                long timesupTime = tmp.getTimesupTime();
346                long timeLeft = timesupTime - now;
347                if (timesupTime < nextTimesup && (!requireNextUpdate || timeLeft > 60) ) {
348                    nextTimesup = timesupTime;
349                    nextTimerFound = true;
350                    t = tmp;
351                }
352            }
353        }
354        if (nextTimerFound) {
355            return t;
356        } else {
357            return null;
358        }
359    }
360
361    private void cancelInUseNotification(final Context context) {
362        NotificationManager notificationManager =
363                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
364        notificationManager.cancel(IN_USE_NOTIFICATION_ID);
365    }
366}
367