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.PendingIntent; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.SharedPreferences; 26 import android.preference.PreferenceManager; 27 import android.support.v4.app.NotificationCompat; 28 import android.support.v4.app.NotificationManagerCompat; 29 import android.util.Log; 30 31 import com.android.deskclock.DeskClock; 32 import com.android.deskclock.R; 33 import com.android.deskclock.TimerRingService; 34 import com.android.deskclock.Utils; 35 import com.android.deskclock.events.Events; 36 37 import java.util.ArrayList; 38 import java.util.Iterator; 39 40 public class TimerReceiver extends BroadcastReceiver { 41 private static final String TAG = "TimerReceiver"; 42 43 // Make this a large number to avoid the alarm ID's which seem to be 1, 2, ... 44 // Must also be different than StopwatchService.NOTIFICATION_ID 45 private static final int IN_USE_NOTIFICATION_ID = Integer.MAX_VALUE - 2; 46 47 ArrayList<TimerObj> mTimers; 48 49 @Override onReceive(final Context context, final Intent intent)50 public void onReceive(final Context context, final Intent intent) { 51 if (Timers.LOGGING) { 52 Log.v(TAG, "Received intent " + intent.toString()); 53 } 54 String actionType = intent.getAction(); 55 // This action does not need the timers data 56 if (Timers.NOTIF_IN_USE_CANCEL.equals(actionType)) { 57 cancelInUseNotification(context); 58 return; 59 } 60 61 // Get the updated timers data. 62 if (mTimers == null) { 63 mTimers = new ArrayList<>(); 64 } 65 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 66 TimerObj.getTimersFromSharedPrefs(prefs, mTimers); 67 68 // These actions do not provide a timer ID, but do use the timers data 69 if (Timers.NOTIF_IN_USE_SHOW.equals(actionType)) { 70 showInUseNotification(context); 71 return; 72 } else if (Timers.NOTIF_TIMES_UP_SHOW.equals(actionType)) { 73 showTimesUpNotification(context); 74 return; 75 } else if (Timers.NOTIF_TIMES_UP_CANCEL.equals(actionType)) { 76 cancelTimesUpNotification(context); 77 return; 78 } 79 80 // Remaining actions provide a timer Id 81 if (!intent.hasExtra(Timers.TIMER_INTENT_EXTRA)) { 82 // No data to work with, do nothing 83 Log.e(TAG, "got intent without Timer data"); 84 return; 85 } 86 87 // Get the timer out of the Intent 88 int timerId = intent.getIntExtra(Timers.TIMER_INTENT_EXTRA, -1); 89 if (timerId == -1) { 90 Log.d(TAG, "OnReceive:intent without Timer data for " + actionType); 91 } 92 93 TimerObj t = Timers.findTimer(mTimers, timerId); 94 95 if (Timers.TIMES_UP.equals(actionType)) { 96 // Find the timer (if it doesn't exists, it was probably deleted). 97 if (t == null) { 98 Log.d(TAG, " timer not found in list - do nothing"); 99 return; 100 } 101 102 t.setState(TimerObj.STATE_TIMESUP); 103 t.writeToSharedPref(prefs); 104 Events.sendEvent(R.string.category_timer, R.string.action_fire, 0); 105 106 // Play ringtone by using TimerRingService service with a default alarm. 107 Log.d(TAG, "playing ringtone"); 108 Intent si = new Intent(); 109 si.setClass(context, TimerRingService.class); 110 context.startService(si); 111 112 // Update the in-use notification 113 if (getNextRunningTimer(mTimers, false, Utils.getTimeNow()) == null) { 114 // Found no running timers. 115 cancelInUseNotification(context); 116 } else { 117 showInUseNotification(context); 118 } 119 120 // Start the TimerAlertFullScreen activity. 121 Intent timersAlert = new Intent(context, TimerAlertFullScreen.class); 122 timersAlert.setFlags( 123 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION); 124 context.startActivity(timersAlert); 125 } else if (Timers.RESET_TIMER.equals(actionType) 126 || Timers.DELETE_TIMER.equals(actionType) 127 || Timers.TIMER_DONE.equals(actionType)) { 128 // Stop Ringtone if all timers are not in times-up status 129 stopRingtoneIfNoTimesup(context); 130 131 if (t != null) { 132 cancelTimesUpNotification(context, t); 133 } 134 } else if (Timers.NOTIF_TIMES_UP_STOP.equals(actionType)) { 135 // Find the timer (if it doesn't exists, it was probably deleted). 136 if (t == null) { 137 Log.d(TAG, "timer to stop not found in list - do nothing"); 138 return; 139 } else if (t.mState != TimerObj.STATE_TIMESUP) { 140 Log.d(TAG, "action to stop but timer not in times-up state - do nothing"); 141 return; 142 } 143 144 // Update timer state 145 t.setState(t.getDeleteAfterUse() ? TimerObj.STATE_DELETED : TimerObj.STATE_RESTART); 146 t.mTimeLeft = t.mOriginalLength = t.mSetupLength; 147 t.writeToSharedPref(prefs); 148 149 // Flag to tell DeskClock to re-sync with the database 150 prefs.edit().putBoolean(Timers.REFRESH_UI_WITH_LATEST_DATA, true).apply(); 151 152 cancelTimesUpNotification(context, t); 153 154 // Done with timer - delete from data base 155 if (t.getDeleteAfterUse()) { 156 t.deleteFromSharedPref(prefs); 157 } 158 159 // Stop Ringtone if no timers are in times-up status 160 stopRingtoneIfNoTimesup(context); 161 } else if (Timers.NOTIF_TIMES_UP_PLUS_ONE.equals(actionType)) { 162 // Find the timer (if it doesn't exists, it was probably deleted). 163 if (t == null) { 164 Log.d(TAG, "timer to +1m not found in list - do nothing"); 165 return; 166 } else if (t.mState != TimerObj.STATE_TIMESUP) { 167 Log.d(TAG, "action to +1m but timer not in times up state - do nothing"); 168 return; 169 } 170 171 // Restarting the timer with 1 minute left. 172 t.setState(TimerObj.STATE_RUNNING); 173 t.mStartTime = Utils.getTimeNow(); 174 t.mTimeLeft = t. mOriginalLength = TimerObj.MINUTE_IN_MILLIS; 175 t.writeToSharedPref(prefs); 176 177 // Flag to tell DeskClock to re-sync with the database 178 prefs.edit().putBoolean(Timers.REFRESH_UI_WITH_LATEST_DATA, true).apply(); 179 180 cancelTimesUpNotification(context, t); 181 182 // If the app is not open, refresh the in-use notification 183 if (!prefs.getBoolean(Timers.NOTIF_APP_OPEN, false)) { 184 showInUseNotification(context); 185 } 186 187 // Stop Ringtone if no timers are in times-up status 188 stopRingtoneIfNoTimesup(context); 189 } else if (Timers.TIMER_UPDATE.equals(actionType)) { 190 // Find the timer (if it doesn't exists, it was probably deleted). 191 if (t == null) { 192 Log.d(TAG, " timer to update not found in list - do nothing"); 193 return; 194 } 195 196 // Refresh buzzing notification 197 if (t.mState == TimerObj.STATE_TIMESUP) { 198 // Must cancel the previous notification to get all updates displayed correctly 199 cancelTimesUpNotification(context, t); 200 showTimesUpNotification(context, t); 201 } 202 } 203 if (intent.getBooleanExtra(Timers.UPDATE_NEXT_TIMESUP, true)) { 204 // Update the next "Times up" alarm unless explicitly told not to. 205 updateNextTimesup(context); 206 } 207 } 208 stopRingtoneIfNoTimesup(final Context context)209 private void stopRingtoneIfNoTimesup(final Context context) { 210 if (Timers.findExpiredTimer(mTimers) == null) { 211 // Stop ringtone 212 Log.d(TAG, "stopping ringtone"); 213 Intent si = new Intent(); 214 si.setClass(context, TimerRingService.class); 215 context.stopService(si); 216 } 217 } 218 219 // Scan all timers and find the one that will expire next. 220 // Tell AlarmManager to send a "Time's up" message to this receiver when this timer expires. 221 // If no timer exists, clear "time's up" message. updateNextTimesup(Context context)222 private void updateNextTimesup(Context context) { 223 TimerObj t = getNextRunningTimer(mTimers, false, Utils.getTimeNow()); 224 long nextTimesup = (t == null) ? -1 : t.getTimesupTime(); 225 int timerId = (t == null) ? -1 : t.mTimerId; 226 227 Intent intent = new Intent(); 228 intent.setAction(Timers.TIMES_UP); 229 intent.setClass(context, TimerReceiver.class); 230 // Time-critical, should be foreground 231 intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 232 if (!mTimers.isEmpty()) { 233 intent.putExtra(Timers.TIMER_INTENT_EXTRA, timerId); 234 } 235 AlarmManager mngr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); 236 PendingIntent p = PendingIntent.getBroadcast(context, 237 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 238 if (t != null) { 239 if (Utils.isKitKatOrLater()) { 240 mngr.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p); 241 } else { 242 mngr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p); 243 } 244 if (Timers.LOGGING) { 245 Log.d(TAG, "Setting times up to " + nextTimesup); 246 } 247 } else { 248 // if no timer is found Pending Intents should be canceled 249 // to keep the internal state consistent with the UI 250 mngr.cancel(p); 251 p.cancel(); 252 if (Timers.LOGGING) { 253 Log.v(TAG, "no next times up"); 254 } 255 } 256 } 257 showInUseNotification(final Context context)258 private void showInUseNotification(final Context context) { 259 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 260 boolean appOpen = prefs.getBoolean(Timers.NOTIF_APP_OPEN, false); 261 ArrayList<TimerObj> timersInUse = Timers.timersInUse(mTimers); 262 int numTimersInUse = timersInUse.size(); 263 264 if (appOpen || numTimersInUse == 0) { 265 return; 266 } 267 268 String title, contentText; 269 Long nextBroadcastTime = null; 270 long now = Utils.getTimeNow(); 271 if (timersInUse.size() == 1) { 272 TimerObj timer = timersInUse.get(0); 273 boolean timerIsTicking = timer.isTicking(); 274 String label = timer.getLabelOrDefault(context); 275 title = timerIsTicking ? label : context.getString(R.string.timer_stopped); 276 long timeLeft = timerIsTicking ? timer.getTimesupTime() - now : timer.mTimeLeft; 277 contentText = buildTimeRemaining(context, timeLeft); 278 if (timerIsTicking && timeLeft > TimerObj.MINUTE_IN_MILLIS) { 279 nextBroadcastTime = getBroadcastTime(now, timeLeft); 280 } 281 } else { 282 TimerObj timer = getNextRunningTimer(timersInUse, false, now); 283 if (timer == null) { 284 // No running timers. 285 title = String.format( 286 context.getString(R.string.timers_stopped), numTimersInUse); 287 contentText = context.getString(R.string.all_timers_stopped_notif); 288 } else { 289 // We have at least one timer running and other timers stopped. 290 title = String.format( 291 context.getString(R.string.timers_in_use), numTimersInUse); 292 long completionTime = timer.getTimesupTime(); 293 long timeLeft = completionTime - now; 294 contentText = String.format(context.getString(R.string.next_timer_notif), 295 buildTimeRemaining(context, timeLeft)); 296 if (timeLeft <= TimerObj.MINUTE_IN_MILLIS) { 297 TimerObj timerWithUpdate = getNextRunningTimer(timersInUse, true, now); 298 if (timerWithUpdate != null) { 299 completionTime = timerWithUpdate.getTimesupTime(); 300 timeLeft = completionTime - now; 301 nextBroadcastTime = getBroadcastTime(now, timeLeft); 302 } 303 } else { 304 nextBroadcastTime = getBroadcastTime(now, timeLeft); 305 } 306 } 307 } 308 showCollapsedNotificationWithNext(context, title, contentText, nextBroadcastTime); 309 } 310 getBroadcastTime(long now, long timeUntilBroadcast)311 private long getBroadcastTime(long now, long timeUntilBroadcast) { 312 long seconds = timeUntilBroadcast / 1000; 313 seconds = seconds - ( (seconds / 60) * 60 ); 314 return now + (seconds * 1000); 315 } 316 showCollapsedNotificationWithNext( final Context context, String title, String text, Long nextBroadcastTime)317 private void showCollapsedNotificationWithNext( 318 final Context context, String title, String text, Long nextBroadcastTime) { 319 Intent activityIntent = new Intent(context, DeskClock.class); 320 activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 321 activityIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX); 322 PendingIntent pendingActivityIntent = PendingIntent.getActivity(context, 0, activityIntent, 323 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 324 showCollapsedNotification(context, title, text, NotificationCompat.PRIORITY_HIGH, 325 pendingActivityIntent, IN_USE_NOTIFICATION_ID, false); 326 327 if (nextBroadcastTime == null) { 328 return; 329 } 330 Intent nextBroadcast = new Intent(); 331 nextBroadcast.setAction(Timers.NOTIF_IN_USE_SHOW); 332 PendingIntent pendingNextBroadcast = 333 PendingIntent.getBroadcast(context, 0, nextBroadcast, 0); 334 AlarmManager alarmManager = 335 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 336 if (Utils.isKitKatOrLater()) { 337 alarmManager.setExact(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast); 338 } else { 339 alarmManager.set(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast); 340 } 341 } 342 showCollapsedNotification(final Context context, String title, String text, int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker)343 private static void showCollapsedNotification(final Context context, String title, String text, 344 int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker) { 345 NotificationCompat.Builder builder = new NotificationCompat.Builder(context) 346 .setAutoCancel(false) 347 .setContentTitle(title) 348 .setContentText(text) 349 .setDeleteIntent(pendingIntent) 350 .setOngoing(true) 351 .setPriority(priority) 352 .setShowWhen(false) 353 .setSmallIcon(R.drawable.stat_notify_timer) 354 .setCategory(NotificationCompat.CATEGORY_ALARM) 355 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) 356 .setLocalOnly(true); 357 if (showTicker) { 358 builder.setTicker(text); 359 } 360 361 final Notification notification = builder.build(); 362 notification.contentIntent = pendingIntent; 363 NotificationManagerCompat.from(context).notify(notificationId, notification); 364 } 365 buildTimeRemaining(Context context, long timeLeft)366 private String buildTimeRemaining(Context context, long timeLeft) { 367 if (timeLeft < 0) { 368 // We should never be here... 369 Log.v(TAG, "Will not show notification for timer already expired."); 370 return null; 371 } 372 373 long seconds, minutes, hours; 374 seconds = timeLeft / 1000; 375 minutes = seconds / 60; 376 hours = minutes / 60; 377 minutes = minutes - hours * 60; 378 if (hours > 99) { 379 hours = 0; 380 } 381 382 String minSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.minutes, 383 (int) minutes); 384 385 String hourSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.hours, 386 (int) hours); 387 388 boolean dispHour = hours > 0; 389 boolean dispMinute = minutes > 0; 390 int index = (dispHour ? 1 : 0) | (dispMinute ? 2 : 0); 391 String[] formats = context.getResources().getStringArray(R.array.timer_notifications); 392 return String.format(formats[index], hourSeq, minSeq); 393 } 394 getNextRunningTimer( ArrayList<TimerObj> timers, boolean requireNextUpdate, long now)395 private TimerObj getNextRunningTimer( 396 ArrayList<TimerObj> timers, boolean requireNextUpdate, long now) { 397 long nextTimesup = Long.MAX_VALUE; 398 boolean nextTimerFound = false; 399 Iterator<TimerObj> i = timers.iterator(); 400 TimerObj t = null; 401 while(i.hasNext()) { 402 TimerObj tmp = i.next(); 403 if (tmp.mState == TimerObj.STATE_RUNNING) { 404 long timesupTime = tmp.getTimesupTime(); 405 long timeLeft = timesupTime - now; 406 if (timesupTime < nextTimesup && (!requireNextUpdate || timeLeft > 60) ) { 407 nextTimesup = timesupTime; 408 nextTimerFound = true; 409 t = tmp; 410 } 411 } 412 } 413 if (nextTimerFound) { 414 return t; 415 } else { 416 return null; 417 } 418 } 419 cancelInUseNotification(final Context context)420 public static void cancelInUseNotification(final Context context) { 421 NotificationManagerCompat.from(context).cancel(IN_USE_NOTIFICATION_ID); 422 } 423 showTimesUpNotification(final Context context)424 private void showTimesUpNotification(final Context context) { 425 for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) { 426 showTimesUpNotification(context, timerObj); 427 } 428 } 429 showTimesUpNotification(final Context context, TimerObj timerObj)430 private void showTimesUpNotification(final Context context, TimerObj timerObj) { 431 // Content Intent. When clicked will show the timer full screen 432 PendingIntent contentIntent = PendingIntent.getActivity(context, timerObj.mTimerId, 433 new Intent(context, TimerAlertFullScreen.class).putExtra( 434 Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId), 435 PendingIntent.FLAG_UPDATE_CURRENT); 436 437 // Add one minute action button 438 PendingIntent addOneMinuteAction = PendingIntent.getBroadcast(context, timerObj.mTimerId, 439 new Intent(Timers.NOTIF_TIMES_UP_PLUS_ONE) 440 .putExtra(Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId), 441 PendingIntent.FLAG_UPDATE_CURRENT); 442 443 // Add stop/done action button 444 PendingIntent stopIntent = PendingIntent.getBroadcast(context, timerObj.mTimerId, 445 new Intent(Timers.NOTIF_TIMES_UP_STOP) 446 .putExtra(Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId), 447 PendingIntent.FLAG_UPDATE_CURRENT); 448 449 // Notification creation 450 final NotificationCompat.Builder builder = new NotificationCompat.Builder(context) 451 .setContentIntent(contentIntent) 452 .addAction(R.drawable.ic_add_24dp, 453 context.getResources().getString(R.string.timer_plus_1_min), 454 addOneMinuteAction) 455 .addAction( 456 timerObj.getDeleteAfterUse() 457 ? android.R.drawable.ic_menu_close_clear_cancel 458 : R.drawable.ic_stop_24dp, 459 timerObj.getDeleteAfterUse() 460 ? context.getResources().getString(R.string.timer_done) 461 : context.getResources().getString(R.string.timer_stop), 462 stopIntent) 463 .setContentTitle(timerObj.getLabelOrDefault(context)) 464 .setContentText(context.getResources().getString(R.string.timer_times_up)) 465 .setSmallIcon(R.drawable.stat_notify_timer) 466 .setOngoing(true) 467 .setAutoCancel(false) 468 .setPriority(NotificationCompat.PRIORITY_MAX) 469 .setDefaults(NotificationCompat.DEFAULT_LIGHTS) 470 .setWhen(0); 471 472 // Send the notification using the timer's id to identify the 473 // correct notification 474 NotificationManagerCompat.from(context).notify(timerObj.mTimerId, builder.build()); 475 if (Timers.LOGGING) { 476 Log.v(TAG, "Setting times-up notification for " 477 + timerObj.getLabelOrDefault(context) + " #" + timerObj.mTimerId); 478 } 479 } 480 cancelTimesUpNotification(final Context context)481 private void cancelTimesUpNotification(final Context context) { 482 for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) { 483 cancelTimesUpNotification(context, timerObj); 484 } 485 } 486 cancelTimesUpNotification(final Context context, TimerObj timerObj)487 private void cancelTimesUpNotification(final Context context, TimerObj timerObj) { 488 NotificationManagerCompat.from(context).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