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