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