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 onReceive(final Context context, final Intent intent)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 stopRingtoneIfNoTimesup(final Context context)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. updateNextTimesup(Context context)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 showInUseNotification(final Context context)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 getBroadcastTime(long now, long timeUntilBroadcast)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 showCollapsedNotificationWithNext( final Context context, String title, String text, Long nextBroadcastTime)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 showCollapsedNotification(final Context context, String title, String text, int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker)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 buildTimeRemaining(Context context, long timeLeft)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 getNextRunningTimer( ArrayList<TimerObj> timers, boolean requireNextUpdate, long now)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 cancelInUseNotification(final Context context)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 showTimesUpNotification(final Context context)413 private void showTimesUpNotification(final Context context) { 414 for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) { 415 showTimesUpNotification(context, timerObj); 416 } 417 } 418 showTimesUpNotification(final Context context, TimerObj timerObj)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_notify_stop, 448 timerObj.getDeleteAfterUse() 449 ? context.getResources().getString(R.string.timer_done) 450 : context.getResources().getString(R.string.timer_stop), 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 cancelTimesUpNotification(final Context context)473 private void cancelTimesUpNotification(final Context context) { 474 for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) { 475 cancelTimesUpNotification(context, timerObj); 476 } 477 } 478 cancelTimesUpNotification(final Context context, TimerObj timerObj)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