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 String label = timer.mLabel.equals("") ? 194 context.getString(R.string.timer_notification_label) : timer.mLabel; 195 title = timer.isTicking() ? label : context.getString(R.string.timer_stopped); 196 long timeLeft = timer.isTicking() ? timer.getTimesupTime() - now : timer.mTimeLeft; 197 contentText = buildTimeRemaining(context, timeLeft); 198 if (timeLeft > 60) { 199 nextBroadcastTime = getBroadcastTime(now, timeLeft); 200 } 201 } else { 202 TimerObj timer = getNextRunningTimer(timersInUse, false, now); 203 if (timer == null) { 204 // No running timers. 205 title = String.format( 206 context.getString(R.string.timers_stopped), numTimersInUse); 207 contentText = context.getString(R.string.all_timers_stopped_notif); 208 } else { 209 // We have at least one timer running and other timers stopped. 210 title = String.format( 211 context.getString(R.string.timers_in_use), numTimersInUse); 212 long completionTime = timer.getTimesupTime(); 213 long timeLeft = completionTime - now; 214 contentText = String.format(context.getString(R.string.next_timer_notif), 215 buildTimeRemaining(context, timeLeft)); 216 if (timeLeft <= 60) { 217 TimerObj timerWithUpdate = getNextRunningTimer(timersInUse, true, now); 218 if (timerWithUpdate != null) { 219 completionTime = timerWithUpdate.getTimesupTime(); 220 timeLeft = completionTime - now; 221 nextBroadcastTime = getBroadcastTime(now, timeLeft); 222 } 223 } else { 224 nextBroadcastTime = getBroadcastTime(now, timeLeft); 225 } 226 } 227 } 228 showCollapsedNotificationWithNext(context, title, contentText, nextBroadcastTime); 229 } 230 231 private long getBroadcastTime(long now, long timeUntilBroadcast) { 232 long seconds = timeUntilBroadcast / 1000; 233 seconds = seconds - ( (seconds / 60) * 60 ); 234 return now + (seconds * 1000); 235 } 236 237 /** Public and static to allow timer fragment to update notification with new label. **/ 238 public static void showExpiredAlarmNotification(Context context, TimerObj t) { 239 Intent broadcastIntent = new Intent(); 240 broadcastIntent.putExtra(Timers.TIMER_INTENT_EXTRA, t.mTimerId); 241 broadcastIntent.setAction(Timers.TIMER_STOP); 242 broadcastIntent.putExtra(Timers.UPDATE_NOTIFICATION, true); 243 PendingIntent pendingBroadcastIntent = PendingIntent.getBroadcast( 244 context, 0, broadcastIntent, 0); 245 String label = t.mLabel.equals("") ? context.getString(R.string.timer_notification_label) : 246 t.mLabel; 247 String contentText = context.getString(R.string.timer_times_up); 248 showCollapsedNotification(context, label, contentText, Notification.PRIORITY_MAX, 249 pendingBroadcastIntent, t.mTimerId, true); 250 } 251 252 private void showCollapsedNotificationWithNext( 253 final Context context, String title, String text, Long nextBroadcastTime) { 254 Intent activityIntent = new Intent(context, DeskClock.class); 255 activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 256 activityIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX); 257 PendingIntent pendingActivityIntent = PendingIntent.getActivity(context, 0, activityIntent, 258 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 259 showCollapsedNotification(context, title, text, Notification.PRIORITY_HIGH, 260 pendingActivityIntent, IN_USE_NOTIFICATION_ID, false); 261 262 if (nextBroadcastTime == null) { 263 return; 264 } 265 Intent nextBroadcast = new Intent(); 266 nextBroadcast.setAction(Timers.NOTIF_IN_USE_SHOW); 267 PendingIntent pendingNextBroadcast = 268 PendingIntent.getBroadcast(context, 0, nextBroadcast, 0); 269 AlarmManager alarmManager = 270 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 271 alarmManager.set(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast); 272 } 273 274 private static void showCollapsedNotification(final Context context, String title, String text, 275 int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker) { 276 Notification.Builder builder = new Notification.Builder(context) 277 .setAutoCancel(false) 278 .setContentTitle(title) 279 .setContentText(text) 280 .setDeleteIntent(pendingIntent) 281 .setOngoing(true) 282 .setPriority(priority) 283 .setShowWhen(false) 284 .setSmallIcon(R.drawable.stat_notify_timer); 285 if (showTicker) { 286 builder.setTicker(text); 287 } 288 289 Notification notification = builder.build(); 290 notification.contentIntent = pendingIntent; 291 NotificationManager notificationManager = 292 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 293 notificationManager.notify(notificationId, notification); 294 } 295 296 private String buildTimeRemaining(Context context, long timeLeft) { 297 if (timeLeft < 0) { 298 // We should never be here... 299 Log.v(TAG, "Will not show notification for timer already expired."); 300 return null; 301 } 302 303 long hundreds, seconds, minutes, hours; 304 seconds = timeLeft / 1000; 305 minutes = seconds / 60; 306 seconds = seconds - minutes * 60; 307 hours = minutes / 60; 308 minutes = minutes - hours * 60; 309 if (hours > 99) { 310 hours = 0; 311 } 312 313 String hourSeq = (hours == 0) ? "" : 314 ( (hours == 1) ? context.getString(R.string.hour) : 315 context.getString(R.string.hours, Long.toString(hours)) ); 316 String minSeq = (minutes == 0) ? "" : 317 ( (minutes == 1) ? context.getString(R.string.minute) : 318 context.getString(R.string.minutes, Long.toString(minutes)) ); 319 320 boolean dispHour = hours > 0; 321 boolean dispMinute = minutes > 0; 322 int index = (dispHour ? 1 : 0) | (dispMinute ? 2 : 0); 323 String[] formats = context.getResources().getStringArray(R.array.timer_notifications); 324 return String.format(formats[index], hourSeq, minSeq); 325 } 326 327 private TimerObj getNextRunningTimer( 328 ArrayList<TimerObj> timers, boolean requireNextUpdate, long now) { 329 long nextTimesup = Long.MAX_VALUE; 330 boolean nextTimerFound = false; 331 Iterator<TimerObj> i = timers.iterator(); 332 TimerObj t = null; 333 while(i.hasNext()) { 334 TimerObj tmp = i.next(); 335 if (tmp.mState == TimerObj.STATE_RUNNING) { 336 long timesupTime = tmp.getTimesupTime(); 337 long timeLeft = timesupTime - now; 338 if (timesupTime < nextTimesup && (!requireNextUpdate || timeLeft > 60) ) { 339 nextTimesup = timesupTime; 340 nextTimerFound = true; 341 t = tmp; 342 } 343 } 344 } 345 if (nextTimerFound) { 346 return t; 347 } else { 348 return null; 349 } 350 } 351 352 private void cancelInUseNotification(final Context context) { 353 NotificationManager notificationManager = 354 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 355 notificationManager.cancel(IN_USE_NOTIFICATION_ID); 356 } 357 } 358