TimerReceiver.java revision 3c16add0cffe1f7a537acca8537cdd3bf140ba97
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 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_RESTART; 138 t.mTimeLeft = t.mOriginalLength = t.mSetupLength; 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 // Find the timer (if it doesn't exists, it was probably deleted). 183 if (t == null) { 184 Log.d(TAG, " timer to update not found in list - do nothing"); 185 return; 186 } 187 188 // Refresh buzzing notification 189 if (t.mState == TimerObj.STATE_TIMESUP) { 190 // Must cancel the previous notification to get all updates displayed correctly 191 cancelTimesUpNotification(context, t); 192 showTimesUpNotification(context, t); 193 } 194 } 195 // Update the next "Times up" alarm 196 updateNextTimesup(context); 197 } 198 199 private void stopRingtoneIfNoTimesup(final Context context) { 200 if (Timers.findExpiredTimer(mTimers) == null) { 201 // Stop ringtone 202 Log.d(TAG, "stopping ringtone"); 203 Intent si = new Intent(); 204 si.setClass(context, TimerRingService.class); 205 context.stopService(si); 206 } 207 } 208 209 // Scan all timers and find the one that will expire next. 210 // Tell AlarmManager to send a "Time's up" message to this receiver when this timer expires. 211 // If no timer exists, clear "time's up" message. 212 private void updateNextTimesup(Context context) { 213 TimerObj t = getNextRunningTimer(mTimers, false, Utils.getTimeNow()); 214 long nextTimesup = (t == null) ? -1 : t.getTimesupTime(); 215 int timerId = (t == null) ? -1 : t.mTimerId; 216 217 Intent intent = new Intent(); 218 intent.setAction(Timers.TIMES_UP); 219 intent.setClass(context, TimerReceiver.class); 220 if (!mTimers.isEmpty()) { 221 intent.putExtra(Timers.TIMER_INTENT_EXTRA, timerId); 222 } 223 AlarmManager mngr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); 224 PendingIntent p = PendingIntent.getBroadcast(context, 225 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 226 if (t != null) { 227 if (Utils.isKitKatOrLater()) { 228 mngr.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p); 229 } else { 230 mngr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p); 231 } 232 if (Timers.LOGGING) { 233 Log.d(TAG, "Setting times up to " + nextTimesup); 234 } 235 } else { 236 mngr.cancel(p); 237 if (Timers.LOGGING) { 238 Log.v(TAG, "no next times up"); 239 } 240 } 241 } 242 243 private void showInUseNotification(final Context context) { 244 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 245 boolean appOpen = prefs.getBoolean(Timers.NOTIF_APP_OPEN, false); 246 ArrayList<TimerObj> timersInUse = Timers.timersInUse(mTimers); 247 int numTimersInUse = timersInUse.size(); 248 249 if (appOpen || numTimersInUse == 0) { 250 return; 251 } 252 253 String title, contentText; 254 Long nextBroadcastTime = null; 255 long now = Utils.getTimeNow(); 256 if (timersInUse.size() == 1) { 257 TimerObj timer = timersInUse.get(0); 258 boolean timerIsTicking = timer.isTicking(); 259 String label = timer.getLabelOrDefault(context); 260 title = timerIsTicking ? label : context.getString(R.string.timer_stopped); 261 long timeLeft = timerIsTicking ? timer.getTimesupTime() - now : timer.mTimeLeft; 262 contentText = buildTimeRemaining(context, timeLeft); 263 if (timerIsTicking && timeLeft > TimerObj.MINUTE_IN_MILLIS) { 264 nextBroadcastTime = getBroadcastTime(now, timeLeft); 265 } 266 } else { 267 TimerObj timer = getNextRunningTimer(timersInUse, false, now); 268 if (timer == null) { 269 // No running timers. 270 title = String.format( 271 context.getString(R.string.timers_stopped), numTimersInUse); 272 contentText = context.getString(R.string.all_timers_stopped_notif); 273 } else { 274 // We have at least one timer running and other timers stopped. 275 title = String.format( 276 context.getString(R.string.timers_in_use), numTimersInUse); 277 long completionTime = timer.getTimesupTime(); 278 long timeLeft = completionTime - now; 279 contentText = String.format(context.getString(R.string.next_timer_notif), 280 buildTimeRemaining(context, timeLeft)); 281 if (timeLeft <= TimerObj.MINUTE_IN_MILLIS) { 282 TimerObj timerWithUpdate = getNextRunningTimer(timersInUse, true, now); 283 if (timerWithUpdate != null) { 284 completionTime = timerWithUpdate.getTimesupTime(); 285 timeLeft = completionTime - now; 286 nextBroadcastTime = getBroadcastTime(now, timeLeft); 287 } 288 } else { 289 nextBroadcastTime = getBroadcastTime(now, timeLeft); 290 } 291 } 292 } 293 showCollapsedNotificationWithNext(context, title, contentText, nextBroadcastTime); 294 } 295 296 private long getBroadcastTime(long now, long timeUntilBroadcast) { 297 long seconds = timeUntilBroadcast / 1000; 298 seconds = seconds - ( (seconds / 60) * 60 ); 299 return now + (seconds * 1000); 300 } 301 302 private void showCollapsedNotificationWithNext( 303 final Context context, String title, String text, Long nextBroadcastTime) { 304 Intent activityIntent = new Intent(context, DeskClock.class); 305 activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 306 activityIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX); 307 PendingIntent pendingActivityIntent = PendingIntent.getActivity(context, 0, activityIntent, 308 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 309 showCollapsedNotification(context, title, text, Notification.PRIORITY_HIGH, 310 pendingActivityIntent, IN_USE_NOTIFICATION_ID, false); 311 312 if (nextBroadcastTime == null) { 313 return; 314 } 315 Intent nextBroadcast = new Intent(); 316 nextBroadcast.setAction(Timers.NOTIF_IN_USE_SHOW); 317 PendingIntent pendingNextBroadcast = 318 PendingIntent.getBroadcast(context, 0, nextBroadcast, 0); 319 AlarmManager alarmManager = 320 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 321 if (Utils.isKitKatOrLater()) { 322 alarmManager.setExact(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast); 323 } else { 324 alarmManager.set(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast); 325 } 326 } 327 328 private static void showCollapsedNotification(final Context context, String title, String text, 329 int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker) { 330 Notification.Builder builder = new Notification.Builder(context) 331 .setAutoCancel(false) 332 .setContentTitle(title) 333 .setContentText(text) 334 .setDeleteIntent(pendingIntent) 335 .setOngoing(true) 336 .setPriority(priority) 337 .setShowWhen(false) 338 .setSmallIcon(R.drawable.stat_notify_timer) 339 .setCategory(Notification.CATEGORY_ALARM); 340 if (showTicker) { 341 builder.setTicker(text); 342 } 343 344 Notification notification = builder.build(); 345 notification.contentIntent = pendingIntent; 346 NotificationManager notificationManager = 347 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 348 notificationManager.notify(notificationId, notification); 349 } 350 351 private String buildTimeRemaining(Context context, long timeLeft) { 352 if (timeLeft < 0) { 353 // We should never be here... 354 Log.v(TAG, "Will not show notification for timer already expired."); 355 return null; 356 } 357 358 long hundreds, seconds, minutes, hours; 359 seconds = timeLeft / 1000; 360 minutes = seconds / 60; 361 seconds = seconds - minutes * 60; 362 hours = minutes / 60; 363 minutes = minutes - hours * 60; 364 if (hours > 99) { 365 hours = 0; 366 } 367 368 String hourSeq = (hours == 0) ? "" : 369 ( (hours == 1) ? context.getString(R.string.hour) : 370 context.getString(R.string.hours, Long.toString(hours)) ); 371 String minSeq = (minutes == 0) ? "" : 372 ( (minutes == 1) ? context.getString(R.string.minute) : 373 context.getString(R.string.minutes, Long.toString(minutes)) ); 374 375 boolean dispHour = hours > 0; 376 boolean dispMinute = minutes > 0; 377 int index = (dispHour ? 1 : 0) | (dispMinute ? 2 : 0); 378 String[] formats = context.getResources().getStringArray(R.array.timer_notifications); 379 return String.format(formats[index], hourSeq, minSeq); 380 } 381 382 private TimerObj getNextRunningTimer( 383 ArrayList<TimerObj> timers, boolean requireNextUpdate, long now) { 384 long nextTimesup = Long.MAX_VALUE; 385 boolean nextTimerFound = false; 386 Iterator<TimerObj> i = timers.iterator(); 387 TimerObj t = null; 388 while(i.hasNext()) { 389 TimerObj tmp = i.next(); 390 if (tmp.mState == TimerObj.STATE_RUNNING) { 391 long timesupTime = tmp.getTimesupTime(); 392 long timeLeft = timesupTime - now; 393 if (timesupTime < nextTimesup && (!requireNextUpdate || timeLeft > 60) ) { 394 nextTimesup = timesupTime; 395 nextTimerFound = true; 396 t = tmp; 397 } 398 } 399 } 400 if (nextTimerFound) { 401 return t; 402 } else { 403 return null; 404 } 405 } 406 407 private void cancelInUseNotification(final Context context) { 408 NotificationManager notificationManager = 409 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 410 notificationManager.cancel(IN_USE_NOTIFICATION_ID); 411 } 412 413 private void showTimesUpNotification(final Context context) { 414 for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) { 415 showTimesUpNotification(context, timerObj); 416 } 417 } 418 419 private void showTimesUpNotification(final Context context, TimerObj timerObj) { 420 // Content Intent. When clicked will show the timer full screen 421 PendingIntent contentIntent = PendingIntent.getActivity(context, timerObj.mTimerId, 422 new Intent(context, TimerAlertFullScreen.class).putExtra( 423 Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId), 424 PendingIntent.FLAG_UPDATE_CURRENT); 425 426 // Add one minute action button 427 PendingIntent addOneMinuteAction = PendingIntent.getBroadcast(context, timerObj.mTimerId, 428 new Intent(Timers.NOTIF_TIMES_UP_PLUS_ONE) 429 .putExtra(Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId), 430 PendingIntent.FLAG_UPDATE_CURRENT); 431 432 // Add stop/done action button 433 PendingIntent stopIntent = PendingIntent.getBroadcast(context, timerObj.mTimerId, 434 new Intent(Timers.NOTIF_TIMES_UP_STOP) 435 .putExtra(Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId), 436 PendingIntent.FLAG_UPDATE_CURRENT); 437 438 // Notification creation 439 Notification notification = new Notification.Builder(context) 440 .setContentIntent(contentIntent) 441 .addAction(R.drawable.ic_menu_add, 442 context.getResources().getString(R.string.timer_plus_1_min), 443 addOneMinuteAction) 444 .addAction( 445 timerObj.getDeleteAfterUse() 446 ? android.R.drawable.ic_menu_close_clear_cancel 447 : R.drawable.ic_reset, 448 timerObj.getDeleteAfterUse() 449 ? context.getResources().getString(R.string.timer_done) 450 : context.getResources().getString(R.string.timer_reset), 451 stopIntent) 452 .setContentTitle(timerObj.getLabelOrDefault(context)) 453 .setContentText(context.getResources().getString(R.string.timer_times_up)) 454 .setSmallIcon(R.drawable.stat_notify_timer) 455 .setOngoing(true) 456 .setAutoCancel(false) 457 .setPriority(Notification.PRIORITY_MAX) 458 .setDefaults(Notification.DEFAULT_LIGHTS) 459 .setWhen(0) 460 .setCategory(Notification.CATEGORY_ALARM) 461 .build(); 462 463 // Send the notification using the timer's id to identify the 464 // correct notification 465 ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify( 466 timerObj.mTimerId, notification); 467 if (Timers.LOGGING) { 468 Log.v(TAG, "Setting times-up notification for " 469 + timerObj.getLabelOrDefault(context) + " #" + timerObj.mTimerId); 470 } 471 } 472 473 private void cancelTimesUpNotification(final Context context) { 474 for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) { 475 cancelTimesUpNotification(context, timerObj); 476 } 477 } 478 479 private void cancelTimesUpNotification(final Context context, TimerObj timerObj) { 480 NotificationManager notificationManager = 481 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 482 notificationManager.cancel(timerObj.mTimerId); 483 if (Timers.LOGGING) { 484 Log.v(TAG, "Canceling times-up notification for " 485 + timerObj.getLabelOrDefault(context) + " #" + timerObj.mTimerId); 486 } 487 } 488} 489