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