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