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 int timer; 50 String actionType = intent.getAction(); 51 52 // Get the updated timers data. 53 if (mTimers == null) { 54 mTimers = new ArrayList<TimerObj> (); 55 } 56 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 57 TimerObj.getTimersFromSharedPrefs(prefs, mTimers); 58 59 60 if (intent.hasExtra(Timers.TIMER_INTENT_EXTRA)) { 61 // Get the alarm out of the Intent 62 timer = intent.getIntExtra(Timers.TIMER_INTENT_EXTRA, -1); 63 if (timer == -1) { 64 Log.d(TAG, " got intent without Timer data: "+actionType); 65 } 66 } else if (Timers.NOTIF_IN_USE_SHOW.equals(actionType)){ 67 showInUseNotification(context); 68 return; 69 } else if (Timers.NOTIF_IN_USE_CANCEL.equals(actionType)) { 70 cancelInUseNotification(context); 71 return; 72 } else { 73 // No data to work with, do nothing 74 Log.d(TAG, " got intent without Timer data"); 75 return; 76 } 77 78 TimerObj t = Timers.findTimer(mTimers, timer); 79 80 if (intent.getBooleanExtra(Timers.UPDATE_NOTIFICATION, false)) { 81 if (Timers.TIMER_STOP.equals(actionType)) { 82 if (t == null) { 83 Log.d(TAG, "timer not found in list - can't stop it."); 84 return; 85 } 86 t.mState = TimerObj.STATE_DONE; 87 t.writeToSharedPref(prefs); 88 SharedPreferences.Editor editor = prefs.edit(); 89 editor.putBoolean(Timers.FROM_NOTIFICATION, true); 90 editor.putLong(Timers.NOTIF_TIME, Utils.getTimeNow()); 91 editor.putInt(Timers.NOTIF_ID, timer); 92 editor.apply(); 93 94 stopRingtoneIfNoTimesup(context); 95 96 Intent activityIntent = new Intent(context, DeskClock.class); 97 activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 98 activityIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX); 99 context.startActivity(activityIntent); 100 } 101 return; 102 } 103 104 if (Timers.TIMES_UP.equals(actionType)) { 105 // Find the timer (if it doesn't exists, it was probably deleted). 106 if (t == null) { 107 Log.d(TAG, " timer not found in list - do nothing"); 108 return; 109 } 110 111 t.mState = TimerObj.STATE_TIMESUP; 112 t.writeToSharedPref(prefs); 113 // Play ringtone by using TimerRingService service with a default alarm. 114 Log.d(TAG, "playing ringtone"); 115 Intent si = new Intent(); 116 si.setClass(context, TimerRingService.class); 117 context.startService(si); 118 119 // Update the in-use notification 120 if (getNextRunningTimer(mTimers, false, Utils.getTimeNow()) == null) { 121 // Found no running timers. 122 cancelInUseNotification(context); 123 } else { 124 showInUseNotification(context); 125 } 126 127 // Start the TimerAlertFullScreen activity. 128 Intent timersAlert = new Intent(context, TimerAlertFullScreen.class); 129 timersAlert.setFlags( 130 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION); 131 context.startActivity(timersAlert); 132 } else if (Timers.TIMER_RESET.equals(actionType) 133 || Timers.DELETE_TIMER.equals(actionType) 134 || Timers.TIMER_DONE.equals(actionType)) { 135 // Stop Ringtone if all timers are not in times-up status 136 stopRingtoneIfNoTimesup(context); 137 } 138 // Update the next "Times up" alarm 139 updateNextTimesup(context); 140 } 141 142 private void stopRingtoneIfNoTimesup(final Context context) { 143 if (Timers.findExpiredTimer(mTimers) == null) { 144 // Stop ringtone 145 Log.d(TAG, "stopping ringtone"); 146 Intent si = new Intent(); 147 si.setClass(context, TimerRingService.class); 148 context.stopService(si); 149 } 150 } 151 152 // Scan all timers and find the one that will expire next. 153 // Tell AlarmManager to send a "Time's up" message to this receiver when this timer expires. 154 // If no timer exists, clear "time's up" message. 155 private void updateNextTimesup(Context context) { 156 TimerObj t = getNextRunningTimer(mTimers, false, Utils.getTimeNow()); 157 long nextTimesup = (t == null) ? -1 : t.getTimesupTime(); 158 int timerId = (t == null) ? -1 : t.mTimerId; 159 160 Intent intent = new Intent(); 161 intent.setAction(Timers.TIMES_UP); 162 intent.setClass(context, TimerReceiver.class); 163 if (!mTimers.isEmpty()) { 164 intent.putExtra(Timers.TIMER_INTENT_EXTRA, timerId); 165 } 166 AlarmManager mngr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); 167 PendingIntent p = PendingIntent.getBroadcast(context, 168 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 169 if (t != null) { 170 mngr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p); 171 Log.d(TAG,"Setting times up to " + nextTimesup); 172 } else { 173 Log.d(TAG,"canceling times up"); 174 mngr.cancel(p); 175 } 176 } 177 178 private void showInUseNotification(final Context context) { 179 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 180 boolean appOpen = prefs.getBoolean(Timers.NOTIF_APP_OPEN, false); 181 ArrayList<TimerObj> timersInUse = Timers.timersInUse(mTimers); 182 int numTimersInUse = timersInUse.size(); 183 184 if (appOpen || numTimersInUse == 0) { 185 return; 186 } 187 188 String title, contentText; 189 Long nextBroadcastTime = null; 190 long now = Utils.getTimeNow(); 191 if (timersInUse.size() == 1) { 192 TimerObj timer = timersInUse.get(0); 193 boolean timerIsTicking = timer.isTicking(); 194 String label = timer.mLabel.equals("") ? 195 context.getString(R.string.timer_notification_label) : timer.mLabel; 196 title = timerIsTicking ? label : context.getString(R.string.timer_stopped); 197 long timeLeft = timerIsTicking ? timer.getTimesupTime() - now : timer.mTimeLeft; 198 contentText = buildTimeRemaining(context, timeLeft); 199 if (timerIsTicking && timeLeft > 60 * 1000) { 200 nextBroadcastTime = getBroadcastTime(now, timeLeft); 201 } 202 } else { 203 TimerObj timer = getNextRunningTimer(timersInUse, false, now); 204 if (timer == null) { 205 // No running timers. 206 title = String.format( 207 context.getString(R.string.timers_stopped), numTimersInUse); 208 contentText = context.getString(R.string.all_timers_stopped_notif); 209 } else { 210 // We have at least one timer running and other timers stopped. 211 title = String.format( 212 context.getString(R.string.timers_in_use), numTimersInUse); 213 long completionTime = timer.getTimesupTime(); 214 long timeLeft = completionTime - now; 215 contentText = String.format(context.getString(R.string.next_timer_notif), 216 buildTimeRemaining(context, timeLeft)); 217 if (timeLeft <= 60 * 1000) { 218 TimerObj timerWithUpdate = getNextRunningTimer(timersInUse, true, now); 219 if (timerWithUpdate != null) { 220 completionTime = timerWithUpdate.getTimesupTime(); 221 timeLeft = completionTime - now; 222 nextBroadcastTime = getBroadcastTime(now, timeLeft); 223 } 224 } else { 225 nextBroadcastTime = getBroadcastTime(now, timeLeft); 226 } 227 } 228 } 229 showCollapsedNotificationWithNext(context, title, contentText, nextBroadcastTime); 230 } 231 232 private long getBroadcastTime(long now, long timeUntilBroadcast) { 233 long seconds = timeUntilBroadcast / 1000; 234 seconds = seconds - ( (seconds / 60) * 60 ); 235 return now + (seconds * 1000); 236 } 237 238 /** Public and static to allow timer fragment to update notification with new label. **/ 239 public static void showExpiredAlarmNotification(Context context, TimerObj t) { 240 Intent broadcastIntent = new Intent(); 241 broadcastIntent.putExtra(Timers.TIMER_INTENT_EXTRA, t.mTimerId); 242 broadcastIntent.setAction(Timers.TIMER_STOP); 243 broadcastIntent.putExtra(Timers.UPDATE_NOTIFICATION, true); 244 PendingIntent pendingBroadcastIntent = PendingIntent.getBroadcast( 245 context, 0, broadcastIntent, 0); 246 String label = t.mLabel.equals("") ? context.getString(R.string.timer_notification_label) : 247 t.mLabel; 248 String contentText = context.getString(R.string.timer_times_up); 249 showCollapsedNotification(context, label, contentText, Notification.PRIORITY_MAX, 250 pendingBroadcastIntent, t.mTimerId, true); 251 } 252 253 private void showCollapsedNotificationWithNext( 254 final Context context, String title, String text, Long nextBroadcastTime) { 255 Intent activityIntent = new Intent(context, DeskClock.class); 256 activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 257 activityIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX); 258 PendingIntent pendingActivityIntent = PendingIntent.getActivity(context, 0, activityIntent, 259 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 260 showCollapsedNotification(context, title, text, Notification.PRIORITY_HIGH, 261 pendingActivityIntent, IN_USE_NOTIFICATION_ID, false); 262 263 if (nextBroadcastTime == null) { 264 return; 265 } 266 Intent nextBroadcast = new Intent(); 267 nextBroadcast.setAction(Timers.NOTIF_IN_USE_SHOW); 268 PendingIntent pendingNextBroadcast = 269 PendingIntent.getBroadcast(context, 0, nextBroadcast, 0); 270 AlarmManager alarmManager = 271 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 272 alarmManager.set(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast); 273 } 274 275 private static void showCollapsedNotification(final Context context, String title, String text, 276 int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker) { 277 Notification.Builder builder = new Notification.Builder(context) 278 .setAutoCancel(false) 279 .setContentTitle(title) 280 .setContentText(text) 281 .setDeleteIntent(pendingIntent) 282 .setOngoing(true) 283 .setPriority(priority) 284 .setShowWhen(false) 285 .setSmallIcon(R.drawable.stat_notify_timer); 286 if (showTicker) { 287 builder.setTicker(text); 288 } 289 290 Notification notification = builder.build(); 291 notification.contentIntent = pendingIntent; 292 NotificationManager notificationManager = 293 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 294 notificationManager.notify(notificationId, notification); 295 } 296 297 private String buildTimeRemaining(Context context, long timeLeft) { 298 if (timeLeft < 0) { 299 // We should never be here... 300 Log.v(TAG, "Will not show notification for timer already expired."); 301 return null; 302 } 303 304 long hundreds, seconds, minutes, hours; 305 seconds = timeLeft / 1000; 306 minutes = seconds / 60; 307 seconds = seconds - minutes * 60; 308 hours = minutes / 60; 309 minutes = minutes - hours * 60; 310 if (hours > 99) { 311 hours = 0; 312 } 313 314 String hourSeq = (hours == 0) ? "" : 315 ( (hours == 1) ? context.getString(R.string.hour) : 316 context.getString(R.string.hours, Long.toString(hours)) ); 317 String minSeq = (minutes == 0) ? "" : 318 ( (minutes == 1) ? context.getString(R.string.minute) : 319 context.getString(R.string.minutes, Long.toString(minutes)) ); 320 321 boolean dispHour = hours > 0; 322 boolean dispMinute = minutes > 0; 323 int index = (dispHour ? 1 : 0) | (dispMinute ? 2 : 0); 324 String[] formats = context.getResources().getStringArray(R.array.timer_notifications); 325 return String.format(formats[index], hourSeq, minSeq); 326 } 327 328 private TimerObj getNextRunningTimer( 329 ArrayList<TimerObj> timers, boolean requireNextUpdate, long now) { 330 long nextTimesup = Long.MAX_VALUE; 331 boolean nextTimerFound = false; 332 Iterator<TimerObj> i = timers.iterator(); 333 TimerObj t = null; 334 while(i.hasNext()) { 335 TimerObj tmp = i.next(); 336 if (tmp.mState == TimerObj.STATE_RUNNING) { 337 long timesupTime = tmp.getTimesupTime(); 338 long timeLeft = timesupTime - now; 339 if (timesupTime < nextTimesup && (!requireNextUpdate || timeLeft > 60) ) { 340 nextTimesup = timesupTime; 341 nextTimerFound = true; 342 t = tmp; 343 } 344 } 345 } 346 if (nextTimerFound) { 347 return t; 348 } else { 349 return null; 350 } 351 } 352 353 private void cancelInUseNotification(final Context context) { 354 NotificationManager notificationManager = 355 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 356 notificationManager.cancel(IN_USE_NOTIFICATION_ID); 357 } 358 } 359