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         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 
    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.
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    406     private void showTimesUpNotification(final Context context) {
    407         for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) {
    408             showTimesUpNotification(context, timerObj);
    409         }
    410     }
    411 
    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 
    465     private void cancelTimesUpNotification(final Context context) {
    466         for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) {
    467             cancelTimesUpNotification(context, timerObj);
    468         }
    469     }
    470 
    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