1/* 2 * Copyright (C) 2016 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 17package com.android.deskclock.data; 18 19import android.annotation.TargetApi; 20import android.app.AlarmManager; 21import android.app.Notification; 22import android.app.PendingIntent; 23import android.content.Context; 24import android.content.Intent; 25import android.content.res.Resources; 26import android.os.Build; 27import android.os.SystemClock; 28import android.support.annotation.DrawableRes; 29import android.support.v4.app.NotificationCompat; 30import android.support.v4.content.ContextCompat; 31import android.text.TextUtils; 32import android.widget.RemoteViews; 33 34import com.android.deskclock.AlarmUtils; 35import com.android.deskclock.R; 36import com.android.deskclock.Utils; 37import com.android.deskclock.events.Events; 38import com.android.deskclock.timer.ExpiredTimersActivity; 39import com.android.deskclock.timer.TimerService; 40 41import java.util.ArrayList; 42import java.util.List; 43 44import static android.support.v4.app.NotificationCompat.Action; 45import static android.support.v4.app.NotificationCompat.Builder; 46import static android.text.format.DateUtils.MINUTE_IN_MILLIS; 47import static android.text.format.DateUtils.SECOND_IN_MILLIS; 48 49/** 50 * Builds notifications to reflect the latest state of the timers. 51 */ 52class TimerNotificationBuilder { 53 54 private static final int REQUEST_CODE_UPCOMING = 0; 55 private static final int REQUEST_CODE_MISSING = 1; 56 57 public Notification build(Context context, NotificationModel nm, List<Timer> unexpired) { 58 final Timer timer = unexpired.get(0); 59 final int count = unexpired.size(); 60 61 // Compute some values required below. 62 final boolean running = timer.isRunning(); 63 final Resources res = context.getResources(); 64 65 final long base = getChronometerBase(timer); 66 final String pname = context.getPackageName(); 67 68 final List<Action> actions = new ArrayList<>(2); 69 70 final CharSequence stateText; 71 if (count == 1) { 72 if (running) { 73 // Single timer is running. 74 if (TextUtils.isEmpty(timer.getLabel())) { 75 stateText = res.getString(R.string.timer_notification_label); 76 } else { 77 stateText = timer.getLabel(); 78 } 79 80 // Left button: Pause 81 final Intent pause = new Intent(context, TimerService.class) 82 .setAction(TimerService.ACTION_PAUSE_TIMER) 83 .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId()); 84 85 @DrawableRes final int icon1 = R.drawable.ic_pause_24dp; 86 final CharSequence title1 = res.getText(R.string.timer_pause); 87 final PendingIntent intent1 = Utils.pendingServiceIntent(context, pause); 88 actions.add(new Action.Builder(icon1, title1, intent1).build()); 89 90 // Right Button: +1 Minute 91 final Intent addMinute = new Intent(context, TimerService.class) 92 .setAction(TimerService.ACTION_ADD_MINUTE_TIMER) 93 .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId()); 94 95 @DrawableRes final int icon2 = R.drawable.ic_add_24dp; 96 final CharSequence title2 = res.getText(R.string.timer_plus_1_min); 97 final PendingIntent intent2 = Utils.pendingServiceIntent(context, addMinute); 98 actions.add(new Action.Builder(icon2, title2, intent2).build()); 99 100 } else { 101 // Single timer is paused. 102 stateText = res.getString(R.string.timer_paused); 103 104 // Left button: Start 105 final Intent start = new Intent(context, TimerService.class) 106 .setAction(TimerService.ACTION_START_TIMER) 107 .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId()); 108 109 @DrawableRes final int icon1 = R.drawable.ic_start_24dp; 110 final CharSequence title1 = res.getText(R.string.sw_resume_button); 111 final PendingIntent intent1 = Utils.pendingServiceIntent(context, start); 112 actions.add(new Action.Builder(icon1, title1, intent1).build()); 113 114 // Right Button: Reset 115 final Intent reset = new Intent(context, TimerService.class) 116 .setAction(TimerService.ACTION_RESET_TIMER) 117 .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId()); 118 119 @DrawableRes final int icon2 = R.drawable.ic_reset_24dp; 120 final CharSequence title2 = res.getText(R.string.sw_reset_button); 121 final PendingIntent intent2 = Utils.pendingServiceIntent(context, reset); 122 actions.add(new Action.Builder(icon2, title2, intent2).build()); 123 } 124 } else { 125 if (running) { 126 // At least one timer is running. 127 stateText = res.getString(R.string.timers_in_use, count); 128 } else { 129 // All timers are paused. 130 stateText = res.getString(R.string.timers_stopped, count); 131 } 132 133 final Intent reset = TimerService.createResetUnexpiredTimersIntent(context); 134 135 @DrawableRes final int icon1 = R.drawable.ic_reset_24dp; 136 final CharSequence title1 = res.getText(R.string.timer_reset_all); 137 final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset); 138 actions.add(new Action.Builder(icon1, title1, intent1).build()); 139 } 140 141 // Intent to load the app and show the timer when the notification is tapped. 142 final Intent showApp = new Intent(context, TimerService.class) 143 .setAction(TimerService.ACTION_SHOW_TIMER) 144 .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId()) 145 .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_notification); 146 147 final PendingIntent pendingShowApp = 148 PendingIntent.getService(context, REQUEST_CODE_UPCOMING, showApp, 149 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 150 151 final Builder notification = new NotificationCompat.Builder(context) 152 .setOngoing(true) 153 .setLocalOnly(true) 154 .setShowWhen(false) 155 .setAutoCancel(false) 156 .setContentIntent(pendingShowApp) 157 .setPriority(Notification.PRIORITY_HIGH) 158 .setCategory(NotificationCompat.CATEGORY_ALARM) 159 .setSmallIcon(R.drawable.stat_notify_timer) 160 .setSortKey(nm.getTimerNotificationSortKey()) 161 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) 162 .setStyle(new NotificationCompat.DecoratedCustomViewStyle()) 163 .setColor(ContextCompat.getColor(context, R.color.default_background)); 164 165 for (Action action : actions) { 166 notification.addAction(action); 167 } 168 169 if (Utils.isNOrLater()) { 170 notification.setCustomContentView(buildChronometer(pname, base, running, stateText)) 171 .setGroup(nm.getTimerNotificationGroupKey()); 172 } else { 173 final CharSequence contentTextPreN; 174 if (count == 1) { 175 contentTextPreN = TimerStringFormatter.formatTimeRemaining(context, 176 timer.getRemainingTime(), false); 177 } else if (running) { 178 final String timeRemaining = TimerStringFormatter.formatTimeRemaining(context, 179 timer.getRemainingTime(), false); 180 contentTextPreN = context.getString(R.string.next_timer_notif, timeRemaining); 181 } else { 182 contentTextPreN = context.getString(R.string.all_timers_stopped_notif); 183 } 184 185 notification.setContentTitle(stateText).setContentText(contentTextPreN); 186 187 final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 188 final Intent updateNotification = TimerService.createUpdateNotificationIntent(context); 189 final long remainingTime = timer.getRemainingTime(); 190 if (timer.isRunning() && remainingTime > MINUTE_IN_MILLIS) { 191 // Schedule a callback to update the time-sensitive information of the running timer 192 final PendingIntent pi = 193 PendingIntent.getService(context, REQUEST_CODE_UPCOMING, updateNotification, 194 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 195 196 final long nextMinuteChange = remainingTime % MINUTE_IN_MILLIS; 197 final long triggerTime = SystemClock.elapsedRealtime() + nextMinuteChange; 198 TimerModel.schedulePendingIntent(am, triggerTime, pi); 199 } else { 200 // Cancel the update notification callback. 201 final PendingIntent pi = PendingIntent.getService(context, 0, updateNotification, 202 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE); 203 if (pi != null) { 204 am.cancel(pi); 205 pi.cancel(); 206 } 207 } 208 } 209 210 return notification.build(); 211 } 212 213 Notification buildHeadsUp(Context context, List<Timer> expired) { 214 final Timer timer = expired.get(0); 215 216 // First action intent is to reset all timers. 217 @DrawableRes final int icon1 = R.drawable.ic_stop_24dp; 218 final Intent reset = TimerService.createResetExpiredTimersIntent(context); 219 final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset); 220 221 // Generate some descriptive text, a title, and an action name based on the timer count. 222 final CharSequence stateText; 223 final int count = expired.size(); 224 final List<Action> actions = new ArrayList<>(2); 225 if (count == 1) { 226 final String label = timer.getLabel(); 227 if (TextUtils.isEmpty(label)) { 228 stateText = context.getString(R.string.timer_times_up); 229 } else { 230 stateText = label; 231 } 232 233 // Left button: Reset single timer 234 final CharSequence title1 = context.getString(R.string.timer_stop); 235 actions.add(new Action.Builder(icon1, title1, intent1).build()); 236 237 // Right button: Add minute 238 final Intent addTime = TimerService.createAddMinuteTimerIntent(context, timer.getId()); 239 final PendingIntent intent2 = Utils.pendingServiceIntent(context, addTime); 240 @DrawableRes final int icon2 = R.drawable.ic_add_24dp; 241 final CharSequence title2 = context.getString(R.string.timer_plus_1_min); 242 actions.add(new Action.Builder(icon2, title2, intent2).build()); 243 } else { 244 stateText = context.getString(R.string.timer_multi_times_up, count); 245 246 // Left button: Reset all timers 247 final CharSequence title1 = context.getString(R.string.timer_stop_all); 248 actions.add(new Action.Builder(icon1, title1, intent1).build()); 249 } 250 251 final long base = getChronometerBase(timer); 252 253 final String pname = context.getPackageName(); 254 255 // Content intent shows the timer full screen when clicked. 256 final Intent content = new Intent(context, ExpiredTimersActivity.class); 257 final PendingIntent contentIntent = Utils.pendingActivityIntent(context, content); 258 259 // Full screen intent has flags so it is different than the content intent. 260 final Intent fullScreen = new Intent(context, ExpiredTimersActivity.class) 261 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION); 262 final PendingIntent pendingFullScreen = Utils.pendingActivityIntent(context, fullScreen); 263 264 final Builder notification = new NotificationCompat.Builder(context) 265 .setOngoing(true) 266 .setLocalOnly(true) 267 .setShowWhen(false) 268 .setAutoCancel(false) 269 .setContentIntent(contentIntent) 270 .setPriority(Notification.PRIORITY_MAX) 271 .setDefaults(Notification.DEFAULT_LIGHTS) 272 .setSmallIcon(R.drawable.stat_notify_timer) 273 .setFullScreenIntent(pendingFullScreen, true) 274 .setStyle(new NotificationCompat.DecoratedCustomViewStyle()) 275 .setColor(ContextCompat.getColor(context, R.color.default_background)); 276 277 for (Action action : actions) { 278 notification.addAction(action); 279 } 280 281 if (Utils.isNOrLater()) { 282 notification.setCustomContentView(buildChronometer(pname, base, true, stateText)); 283 } else { 284 final CharSequence contentTextPreN = count == 1 285 ? context.getString(R.string.timer_times_up) 286 : context.getString(R.string.timer_multi_times_up, count); 287 288 notification.setContentTitle(stateText).setContentText(contentTextPreN); 289 } 290 291 return notification.build(); 292 } 293 294 Notification buildMissed(Context context, NotificationModel nm, 295 List<Timer> missedTimers) { 296 final Timer timer = missedTimers.get(0); 297 final int count = missedTimers.size(); 298 299 // Compute some values required below. 300 final long base = getChronometerBase(timer); 301 final String pname = context.getPackageName(); 302 final Resources res = context.getResources(); 303 304 final Action action; 305 306 final CharSequence stateText; 307 if (count == 1) { 308 // Single timer is missed. 309 if (TextUtils.isEmpty(timer.getLabel())) { 310 stateText = res.getString(R.string.missed_timer_notification_label); 311 } else { 312 stateText = res.getString(R.string.missed_named_timer_notification_label, 313 timer.getLabel()); 314 } 315 316 // Reset button 317 final Intent reset = new Intent(context, TimerService.class) 318 .setAction(TimerService.ACTION_RESET_TIMER) 319 .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId()); 320 321 @DrawableRes final int icon1 = R.drawable.ic_reset_24dp; 322 final CharSequence title1 = res.getText(R.string.timer_reset); 323 final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset); 324 action = new Action.Builder(icon1, title1, intent1).build(); 325 } else { 326 // Multiple missed timers. 327 stateText = res.getString(R.string.timer_multi_missed, count); 328 329 final Intent reset = TimerService.createResetMissedTimersIntent(context); 330 331 @DrawableRes final int icon1 = R.drawable.ic_reset_24dp; 332 final CharSequence title1 = res.getText(R.string.timer_reset_all); 333 final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset); 334 action = new Action.Builder(icon1, title1, intent1).build(); 335 } 336 337 // Intent to load the app and show the timer when the notification is tapped. 338 final Intent showApp = new Intent(context, TimerService.class) 339 .setAction(TimerService.ACTION_SHOW_TIMER) 340 .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId()) 341 .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_notification); 342 343 final PendingIntent pendingShowApp = 344 PendingIntent.getService(context, REQUEST_CODE_MISSING, showApp, 345 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); 346 347 final Builder notification = new NotificationCompat.Builder(context) 348 .setLocalOnly(true) 349 .setShowWhen(false) 350 .setAutoCancel(false) 351 .setContentIntent(pendingShowApp) 352 .setPriority(Notification.PRIORITY_HIGH) 353 .setCategory(NotificationCompat.CATEGORY_ALARM) 354 .setSmallIcon(R.drawable.stat_notify_timer) 355 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) 356 .setSortKey(nm.getTimerNotificationMissedSortKey()) 357 .setStyle(new NotificationCompat.DecoratedCustomViewStyle()) 358 .addAction(action) 359 .setColor(ContextCompat.getColor(context, R.color.default_background)); 360 361 if (Utils.isNOrLater()) { 362 notification.setCustomContentView(buildChronometer(pname, base, true, stateText)) 363 .setGroup(nm.getTimerNotificationGroupKey()); 364 } else { 365 final CharSequence contentText = AlarmUtils.getFormattedTime(context, 366 timer.getWallClockExpirationTime()); 367 notification.setContentText(contentText).setContentTitle(stateText); 368 } 369 370 return notification.build(); 371 } 372 373 /** 374 * @param timer the timer on which to base the chronometer display 375 * @return the time at which the chronometer will/did reach 0:00 in realtime 376 */ 377 private static long getChronometerBase(Timer timer) { 378 // The in-app timer display rounds *up* to the next second for positive timer values. Mirror 379 // that behavior in the notification's Chronometer by padding in an extra second as needed. 380 final long remaining = timer.getRemainingTime(); 381 final long adjustedRemaining = remaining < 0 ? remaining : remaining + SECOND_IN_MILLIS; 382 383 // Chronometer will/did reach 0:00 adjustedRemaining milliseconds from now. 384 return SystemClock.elapsedRealtime() + adjustedRemaining; 385 } 386 387 @TargetApi(Build.VERSION_CODES.N) 388 private RemoteViews buildChronometer(String pname, long base, boolean running, 389 CharSequence stateText) { 390 final RemoteViews content = new RemoteViews(pname, R.layout.chronometer_notif_content); 391 content.setChronometerCountDown(R.id.chronometer, true); 392 content.setChronometer(R.id.chronometer, base, null, running); 393 content.setTextViewText(R.id.state, stateText); 394 return content; 395 } 396} 397