Home | History | Annotate | Download | only in timer
      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
     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             mngr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p);
    171             Log.d(TAG,"Setting times up to " + nextTimesup);
    172         } else {
    173             Log.d(TAG,"canceling times up");
    174             mngr.cancel(p);
    175         }
    176     }
    177 
    178     private void showInUseNotification(final Context context) {
    179         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
    180         boolean appOpen = prefs.getBoolean(Timers.NOTIF_APP_OPEN, false);
    181         ArrayList<TimerObj> timersInUse = Timers.timersInUse(mTimers);
    182         int numTimersInUse = timersInUse.size();
    183 
    184         if (appOpen || numTimersInUse == 0) {
    185             return;
    186         }
    187 
    188         String title, contentText;
    189         Long nextBroadcastTime = null;
    190         long now = Utils.getTimeNow();
    191         if (timersInUse.size() == 1) {
    192             TimerObj timer = timersInUse.get(0);
    193             boolean timerIsTicking = timer.isTicking();
    194             String label = timer.mLabel.equals("") ?
    195                     context.getString(R.string.timer_notification_label) : timer.mLabel;
    196             title = timerIsTicking ? label : context.getString(R.string.timer_stopped);
    197             long timeLeft = timerIsTicking ? timer.getTimesupTime() - now : timer.mTimeLeft;
    198             contentText = buildTimeRemaining(context, timeLeft);
    199             if (timerIsTicking && timeLeft > 60 * 1000) {
    200                 nextBroadcastTime = getBroadcastTime(now, timeLeft);
    201             }
    202         } else {
    203             TimerObj timer = getNextRunningTimer(timersInUse, false, now);
    204             if (timer == null) {
    205                 // No running timers.
    206                 title = String.format(
    207                         context.getString(R.string.timers_stopped), numTimersInUse);
    208                 contentText = context.getString(R.string.all_timers_stopped_notif);
    209             } else {
    210                 // We have at least one timer running and other timers stopped.
    211                 title = String.format(
    212                         context.getString(R.string.timers_in_use), numTimersInUse);
    213                 long completionTime = timer.getTimesupTime();
    214                 long timeLeft = completionTime - now;
    215                 contentText = String.format(context.getString(R.string.next_timer_notif),
    216                         buildTimeRemaining(context, timeLeft));
    217                 if (timeLeft <= 60 * 1000) {
    218                     TimerObj timerWithUpdate = getNextRunningTimer(timersInUse, true, now);
    219                     if (timerWithUpdate != null) {
    220                         completionTime = timerWithUpdate.getTimesupTime();
    221                         timeLeft = completionTime - now;
    222                         nextBroadcastTime = getBroadcastTime(now, timeLeft);
    223                     }
    224                 } else {
    225                     nextBroadcastTime = getBroadcastTime(now, timeLeft);
    226                 }
    227             }
    228         }
    229         showCollapsedNotificationWithNext(context, title, contentText, nextBroadcastTime);
    230     }
    231 
    232     private long getBroadcastTime(long now, long timeUntilBroadcast) {
    233         long seconds = timeUntilBroadcast / 1000;
    234         seconds = seconds - ( (seconds / 60) * 60 );
    235         return now + (seconds * 1000);
    236     }
    237 
    238     /** Public and static to allow timer fragment to update notification with new label. **/
    239     public static void showExpiredAlarmNotification(Context context, TimerObj t) {
    240         Intent broadcastIntent = new Intent();
    241         broadcastIntent.putExtra(Timers.TIMER_INTENT_EXTRA, t.mTimerId);
    242         broadcastIntent.setAction(Timers.TIMER_STOP);
    243         broadcastIntent.putExtra(Timers.UPDATE_NOTIFICATION, true);
    244         PendingIntent pendingBroadcastIntent = PendingIntent.getBroadcast(
    245                 context, 0, broadcastIntent, 0);
    246         String label = t.mLabel.equals("") ? context.getString(R.string.timer_notification_label) :
    247             t.mLabel;
    248         String contentText = context.getString(R.string.timer_times_up);
    249         showCollapsedNotification(context, label, contentText, Notification.PRIORITY_MAX,
    250                 pendingBroadcastIntent, t.mTimerId, true);
    251     }
    252 
    253     private void showCollapsedNotificationWithNext(
    254             final Context context, String title, String text, Long nextBroadcastTime) {
    255         Intent activityIntent = new Intent(context, DeskClock.class);
    256         activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    257         activityIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX);
    258         PendingIntent pendingActivityIntent = PendingIntent.getActivity(context, 0, activityIntent,
    259                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
    260         showCollapsedNotification(context, title, text, Notification.PRIORITY_HIGH,
    261                 pendingActivityIntent, IN_USE_NOTIFICATION_ID, false);
    262 
    263         if (nextBroadcastTime == null) {
    264             return;
    265         }
    266         Intent nextBroadcast = new Intent();
    267         nextBroadcast.setAction(Timers.NOTIF_IN_USE_SHOW);
    268         PendingIntent pendingNextBroadcast =
    269                 PendingIntent.getBroadcast(context, 0, nextBroadcast, 0);
    270         AlarmManager alarmManager =
    271                 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    272         alarmManager.set(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast);
    273     }
    274 
    275     private static void showCollapsedNotification(final Context context, String title, String text,
    276             int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker) {
    277         Notification.Builder builder = new Notification.Builder(context)
    278         .setAutoCancel(false)
    279         .setContentTitle(title)
    280         .setContentText(text)
    281         .setDeleteIntent(pendingIntent)
    282         .setOngoing(true)
    283         .setPriority(priority)
    284         .setShowWhen(false)
    285         .setSmallIcon(R.drawable.stat_notify_timer);
    286         if (showTicker) {
    287             builder.setTicker(text);
    288         }
    289 
    290         Notification notification = builder.build();
    291         notification.contentIntent = pendingIntent;
    292         NotificationManager notificationManager =
    293                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    294         notificationManager.notify(notificationId, notification);
    295     }
    296 
    297     private String buildTimeRemaining(Context context, long timeLeft) {
    298         if (timeLeft < 0) {
    299             // We should never be here...
    300             Log.v(TAG, "Will not show notification for timer already expired.");
    301             return null;
    302         }
    303 
    304         long hundreds, seconds, minutes, hours;
    305         seconds = timeLeft / 1000;
    306         minutes = seconds / 60;
    307         seconds = seconds - minutes * 60;
    308         hours = minutes / 60;
    309         minutes = minutes - hours * 60;
    310         if (hours > 99) {
    311             hours = 0;
    312         }
    313 
    314         String hourSeq = (hours == 0) ? "" :
    315             ( (hours == 1) ? context.getString(R.string.hour) :
    316                 context.getString(R.string.hours, Long.toString(hours)) );
    317         String minSeq = (minutes == 0) ? "" :
    318             ( (minutes == 1) ? context.getString(R.string.minute) :
    319                 context.getString(R.string.minutes, Long.toString(minutes)) );
    320 
    321         boolean dispHour = hours > 0;
    322         boolean dispMinute = minutes > 0;
    323         int index = (dispHour ? 1 : 0) | (dispMinute ? 2 : 0);
    324         String[] formats = context.getResources().getStringArray(R.array.timer_notifications);
    325         return String.format(formats[index], hourSeq, minSeq);
    326     }
    327 
    328     private TimerObj getNextRunningTimer(
    329             ArrayList<TimerObj> timers, boolean requireNextUpdate, long now) {
    330         long nextTimesup = Long.MAX_VALUE;
    331         boolean nextTimerFound = false;
    332         Iterator<TimerObj> i = timers.iterator();
    333         TimerObj t = null;
    334         while(i.hasNext()) {
    335             TimerObj tmp = i.next();
    336             if (tmp.mState == TimerObj.STATE_RUNNING) {
    337                 long timesupTime = tmp.getTimesupTime();
    338                 long timeLeft = timesupTime - now;
    339                 if (timesupTime < nextTimesup && (!requireNextUpdate || timeLeft > 60) ) {
    340                     nextTimesup = timesupTime;
    341                     nextTimerFound = true;
    342                     t = tmp;
    343                 }
    344             }
    345         }
    346         if (nextTimerFound) {
    347             return t;
    348         } else {
    349             return null;
    350         }
    351     }
    352 
    353     private void cancelInUseNotification(final Context context) {
    354         NotificationManager notificationManager =
    355                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    356         notificationManager.cancel(IN_USE_NOTIFICATION_ID);
    357     }
    358 }
    359