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_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 stopRingtoneIfNoTimesup(final Context context)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. updateNextTimesup(Context context)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 showInUseNotification(final Context context)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 getBroadcastTime(long now, long timeUntilBroadcast)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 showCollapsedNotificationWithNext( final Context context, String title, String text, Long nextBroadcastTime)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 showCollapsedNotification(final Context context, String title, String text, int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker)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 buildTimeRemaining(Context context, long timeLeft)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 getNextRunningTimer( ArrayList<TimerObj> timers, boolean requireNextUpdate, long now)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 cancelInUseNotification(final Context context)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 showTimesUpNotification(final Context context)406 private void showTimesUpNotification(final Context context) { 407 for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) { 408 showTimesUpNotification(context, timerObj); 409 } 410 } 411 showTimesUpNotification(final Context context, TimerObj timerObj)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 cancelTimesUpNotification(final Context context)465 private void cancelTimesUpNotification(final Context context) { 466 for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) { 467 cancelTimesUpNotification(context, timerObj); 468 } 469 } 470 cancelTimesUpNotification(final Context context, TimerObj timerObj)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