1 /* 2 * Copyright (C) 2013 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.incallui; 18 19 import com.google.common.base.Preconditions; 20 21 import android.app.Notification; 22 import android.app.NotificationManager; 23 import android.app.PendingIntent; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.graphics.Bitmap; 27 import android.graphics.BitmapFactory; 28 import android.graphics.drawable.BitmapDrawable; 29 import android.os.Handler; 30 import android.os.Message; 31 import android.text.TextUtils; 32 33 import com.android.incallui.ContactInfoCache.ContactCacheEntry; 34 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; 35 import com.android.incallui.InCallApp.NotificationBroadcastReceiver; 36 import com.android.incallui.InCallPresenter.InCallState; 37 import com.android.services.telephony.common.Call; 38 39 /** 40 * This class adds Notifications to the status bar for the in-call experience. 41 */ 42 public class StatusBarNotifier implements InCallPresenter.InCallStateListener { 43 // notification types 44 private static final int IN_CALL_NOTIFICATION = 1; 45 46 private static final long IN_CALL_TIMEOUT = 1000L; 47 48 private interface NotificationTimer { 49 enum State { 50 SCHEDULED, 51 FIRED, 52 CLEAR; 53 } 54 State getState(); 55 void schedule(); 56 void clear(); 57 } 58 59 private NotificationTimer mNotificationTimer = new NotificationTimer() { 60 private final Handler mHandler = new Handler(new Handler.Callback() { 61 public boolean handleMessage(Message m) { 62 fire(); 63 return true; 64 } 65 }); 66 private State mState = State.CLEAR; 67 public State getState() { return mState; } 68 public void schedule() { 69 if (mState == State.CLEAR) { 70 Log.d(this, "updateInCallNotification: timer scheduled"); 71 mHandler.sendEmptyMessageDelayed(0, IN_CALL_TIMEOUT); 72 mState = State.SCHEDULED; 73 } 74 } 75 public void clear() { 76 Log.d(this, "updateInCallNotification: timer cleared"); 77 mHandler.removeMessages(0); 78 mState = State.CLEAR; 79 } 80 private void fire() { 81 Log.d(this, "updateInCallNotification: timer fired"); 82 mState = State.FIRED; 83 updateNotification( 84 InCallPresenter.getInstance().getInCallState(), 85 InCallPresenter.getInstance().getCallList()); 86 } 87 }; 88 89 private final Context mContext; 90 private final ContactInfoCache mContactInfoCache; 91 private final NotificationManager mNotificationManager; 92 private boolean mIsShowingNotification = false; 93 private int mCallState = Call.State.INVALID; 94 private int mSavedIcon = 0; 95 private int mSavedContent = 0; 96 private Bitmap mSavedLargeIcon; 97 private String mSavedContentTitle; 98 99 public StatusBarNotifier(Context context, ContactInfoCache contactInfoCache) { 100 Preconditions.checkNotNull(context); 101 102 mContext = context; 103 mContactInfoCache = contactInfoCache; 104 mNotificationManager = 105 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); 106 } 107 108 /** 109 * Creates notifications according to the state we receive from {@link InCallPresenter}. 110 */ 111 @Override 112 public void onStateChange(InCallState state, CallList callList) { 113 Log.d(this, "onStateChange"); 114 115 updateNotification(state, callList); 116 } 117 118 /** 119 * Updates the phone app's status bar notification based on the 120 * current telephony state, or cancels the notification if the phone 121 * is totally idle. 122 * 123 * This method will never actually launch the incoming-call UI. 124 * (Use updateNotificationAndLaunchIncomingCallUi() for that.) 125 */ 126 public void updateNotification(InCallState state, CallList callList) { 127 Log.d(this, "updateNotification"); 128 // allowFullScreenIntent=false means *don't* allow the incoming 129 // call UI to be launched. 130 updateInCallNotification(false, state, callList); 131 } 132 133 /** 134 * Updates the phone app's status bar notification *and* launches the 135 * incoming call UI in response to a new incoming call. 136 * 137 * This is just like updateInCallNotification(), with one exception: 138 * If an incoming call is ringing (or call-waiting), the notification 139 * will also include a "fullScreenIntent" that will cause the 140 * InCallScreen to be launched immediately, unless the current 141 * foreground activity is marked as "immersive". 142 * 143 * (This is the mechanism that actually brings up the incoming call UI 144 * when we receive a "new ringing connection" event from the telephony 145 * layer.) 146 * 147 * Watch out: this method should ONLY be called directly from the code 148 * path in CallNotifier that handles the "new ringing connection" 149 * event from the telephony layer. All other places that update the 150 * in-call notification (like for phone state changes) should call 151 * updateInCallNotification() instead. (This ensures that we don't 152 * end up launching the InCallScreen multiple times for a single 153 * incoming call, which could cause slow responsiveness and/or visible 154 * glitches.) 155 * 156 * Also note that this method is safe to call even if the phone isn't 157 * actually ringing (or, more likely, if an incoming call *was* 158 * ringing briefly but then disconnected). In that case, we'll simply 159 * update or cancel the in-call notification based on the current 160 * phone state. 161 * 162 * @see #updateInCallNotification(boolean,InCallState,CallList) 163 */ 164 public void updateNotificationAndLaunchIncomingCallUi(InCallState state, CallList callList) { 165 // Set allowFullScreenIntent=true to indicate that we *should* 166 // launch the incoming call UI if necessary. 167 updateInCallNotification(true, state, callList); 168 } 169 170 /** 171 * Take down the in-call notification. 172 * @see #updateInCallNotification(boolean,InCallState,CallList) 173 */ 174 private void cancelInCall() { 175 Log.d(this, "cancelInCall()..."); 176 mNotificationManager.cancel(IN_CALL_NOTIFICATION); 177 mIsShowingNotification = false; 178 } 179 180 /* package */ static void clearInCallNotification(Context backupContext) { 181 Log.i(StatusBarNotifier.class.getSimpleName(), 182 "Something terrible happened. Clear all InCall notifications"); 183 184 NotificationManager notificationManager = 185 (NotificationManager) backupContext.getSystemService(Context.NOTIFICATION_SERVICE); 186 notificationManager.cancel(IN_CALL_NOTIFICATION); 187 } 188 189 /** 190 * Helper method for updateInCallNotification() and 191 * updateNotificationAndLaunchIncomingCallUi(): Update the phone app's 192 * status bar notification based on the current telephony state, or 193 * cancels the notification if the phone is totally idle. 194 * 195 * @param allowFullScreenIntent If true, *and* an incoming call is 196 * ringing, the notification will include a "fullScreenIntent" 197 * pointing at the InCallActivity (which will cause the InCallActivity 198 * to be launched.) 199 * Watch out: This should be set to true *only* when directly 200 * handling a new incoming call for the first time. 201 */ 202 private void updateInCallNotification(final boolean allowFullScreenIntent, 203 final InCallState state, CallList callList) { 204 Log.d(this, "updateInCallNotification(allowFullScreenIntent = " 205 + allowFullScreenIntent + ")..."); 206 207 Call call = getCallToShow(callList); 208 209 // Whether we have an outgoing call but the incall UI has yet to show up. 210 // Since we don't normally show a notification while the incall screen is 211 // in the foreground, if we show the outgoing notification before the activity 212 // comes up the user will see it flash on and off on an outgoing call. We therefore 213 // do not show the notification for outgoing calls before the activity has started. 214 boolean isOutgoingWithoutIncallUi = 215 state == InCallState.OUTGOING && 216 !InCallPresenter.getInstance().isActivityPreviouslyStarted(); 217 218 // Whether to show a notification immediately. 219 boolean showNotificationNow = 220 221 // We can still be in the INCALL state when a call is disconnected (in order to show 222 // the "Call ended" screen. So check that we have an active connection too. 223 (call != null) && 224 225 // We show a notification iff there is an active call. 226 state.isConnectingOrConnected() && 227 228 // If the UI is already showing, then for most cases we do not want to show 229 // a notification since that would be redundant, unless it is an incoming call, 230 // in which case the notification is actually an important alert. 231 (!InCallPresenter.getInstance().isShowingInCallUi() || state.isIncoming()) && 232 233 // If we have an outgoing call with no UI but the timer has fired, we show 234 // a notification anyway. 235 (!isOutgoingWithoutIncallUi || 236 mNotificationTimer.getState() == NotificationTimer.State.FIRED); 237 238 if (showNotificationNow) { 239 showNotification(call, allowFullScreenIntent); 240 } else { 241 cancelInCall(); 242 if (isOutgoingWithoutIncallUi && 243 mNotificationTimer.getState() == NotificationTimer.State.CLEAR) { 244 mNotificationTimer.schedule(); 245 } 246 } 247 248 // If we see a UI, or we are done with calls for now, reset to ground state. 249 if (InCallPresenter.getInstance().isShowingInCallUi() || call == null) { 250 mNotificationTimer.clear(); 251 } 252 } 253 254 private void showNotification(final Call call, final boolean allowFullScreenIntent) { 255 final boolean isIncoming = (call.getState() == Call.State.INCOMING || 256 call.getState() == Call.State.CALL_WAITING); 257 258 // we make a call to the contact info cache to query for supplemental data to what the 259 // call provides. This includes the contact name and photo. 260 // This callback will always get called immediately and synchronously with whatever data 261 // it has available, and may make a subsequent call later (same thread) if it had to 262 // call into the contacts provider for more data. 263 mContactInfoCache.findInfo(call.getIdentification(), isIncoming, 264 new ContactInfoCacheCallback() { 265 private boolean mAllowFullScreenIntent = allowFullScreenIntent; 266 267 @Override 268 public void onContactInfoComplete(int callId, ContactCacheEntry entry) { 269 Call call = CallList.getInstance().getCall(callId); 270 if (call != null) { 271 buildAndSendNotification(call, entry, mAllowFullScreenIntent); 272 } 273 274 // Full screen intents are what bring up the in call screen. We only want 275 // to do this the first time we are called back. 276 mAllowFullScreenIntent = false; 277 } 278 279 @Override 280 public void onImageLoadComplete(int callId, ContactCacheEntry entry) { 281 Call call = CallList.getInstance().getCall(callId); 282 if (call != null) { 283 buildAndSendNotification(call, entry, mAllowFullScreenIntent); 284 } 285 } }); 286 } 287 288 /** 289 * Sets up the main Ui for the notification 290 */ 291 private void buildAndSendNotification(Call originalCall, ContactCacheEntry contactInfo, 292 boolean allowFullScreenIntent) { 293 294 // This can get called to update an existing notification after contact information has come 295 // back. However, it can happen much later. Before we continue, we need to make sure that 296 // the call being passed in is still the one we want to show in the notification. 297 final Call call = getCallToShow(CallList.getInstance()); 298 if (call == null || call.getCallId() != originalCall.getCallId()) { 299 return; 300 } 301 302 final int state = call.getState(); 303 final boolean isConference = call.isConferenceCall(); 304 final int iconResId = getIconToDisplay(call); 305 final Bitmap largeIcon = getLargeIconToDisplay(contactInfo, isConference); 306 final int contentResId = getContentString(call); 307 final String contentTitle = getContentTitle(contactInfo, isConference); 308 309 // If we checked and found that nothing is different, dont issue another notification. 310 if (!checkForChangeAndSaveData(iconResId, contentResId, largeIcon, contentTitle, state, 311 allowFullScreenIntent)) { 312 return; 313 } 314 315 /* 316 * Nothing more to check...build and send it. 317 */ 318 final Notification.Builder builder = getNotificationBuilder(); 319 320 // Set up the main intent to send the user to the in-call screen 321 final PendingIntent inCallPendingIntent = createLaunchPendingIntent(); 322 builder.setContentIntent(inCallPendingIntent); 323 324 // Set the intent as a full screen intent as well if requested 325 if (allowFullScreenIntent) { 326 configureFullScreenIntent(builder, inCallPendingIntent, call); 327 } 328 329 // set the content 330 builder.setContentText(mContext.getString(contentResId)); 331 builder.setSmallIcon(iconResId); 332 builder.setContentTitle(contentTitle); 333 builder.setLargeIcon(largeIcon); 334 335 if (state == Call.State.ACTIVE) { 336 builder.setUsesChronometer(true); 337 builder.setWhen(call.getConnectTime()); 338 } else { 339 builder.setUsesChronometer(false); 340 } 341 342 // Add hang up option for any active calls (active | onhold), outgoing calls (dialing). 343 if (state == Call.State.ACTIVE || 344 state == Call.State.ONHOLD || 345 Call.State.isDialing(state)) { 346 addHangupAction(builder); 347 } 348 349 /* 350 * Fire off the notification 351 */ 352 Notification notification = builder.build(); 353 Log.d(this, "Notifying IN_CALL_NOTIFICATION: " + notification); 354 mNotificationManager.notify(IN_CALL_NOTIFICATION, notification); 355 mIsShowingNotification = true; 356 } 357 358 /** 359 * Checks the new notification data and compares it against any notification that we 360 * are already displaying. If the data is exactly the same, we return false so that 361 * we do not issue a new notification for the exact same data. 362 */ 363 private boolean checkForChangeAndSaveData(int icon, int content, Bitmap largeIcon, 364 String contentTitle, int state, boolean showFullScreenIntent) { 365 366 // The two are different: 367 // if new title is not null, it should be different from saved version OR 368 // if new title is null, the saved version should not be null 369 final boolean contentTitleChanged = 370 (contentTitle != null && !contentTitle.equals(mSavedContentTitle)) || 371 (contentTitle == null && mSavedContentTitle != null); 372 373 // any change means we are definitely updating 374 boolean retval = (mSavedIcon != icon) || (mSavedContent != content) || 375 (mCallState != state) || (mSavedLargeIcon != largeIcon) || 376 contentTitleChanged; 377 378 // A full screen intent means that we have been asked to interrupt an activity, 379 // so we definitely want to show it. 380 if (showFullScreenIntent) { 381 Log.d(this, "Forcing full screen intent"); 382 retval = true; 383 } 384 385 // If we aren't showing a notification right now, definitely start showing one. 386 if (!mIsShowingNotification) { 387 Log.d(this, "Showing notification for first time."); 388 retval = true; 389 } 390 391 mSavedIcon = icon; 392 mSavedContent = content; 393 mCallState = state; 394 mSavedLargeIcon = largeIcon; 395 mSavedContentTitle = contentTitle; 396 397 if (retval) { 398 Log.d(this, "Data changed. Showing notification"); 399 } 400 401 return retval; 402 } 403 404 /** 405 * Returns the main string to use in the notification. 406 */ 407 private String getContentTitle(ContactCacheEntry contactInfo, boolean isConference) { 408 if (isConference) { 409 return mContext.getResources().getString(R.string.card_title_conf_call); 410 } 411 if (TextUtils.isEmpty(contactInfo.name)) { 412 return contactInfo.number; 413 } 414 415 return contactInfo.name; 416 } 417 418 /** 419 * Gets a large icon from the contact info object to display in the notification. 420 */ 421 private Bitmap getLargeIconToDisplay(ContactCacheEntry contactInfo, boolean isConference) { 422 Bitmap largeIcon = null; 423 if (isConference) { 424 largeIcon = BitmapFactory.decodeResource(mContext.getResources(), 425 R.drawable.picture_conference); 426 } 427 if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) { 428 largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap(); 429 } 430 431 if (largeIcon != null) { 432 final int height = (int) mContext.getResources().getDimension( 433 android.R.dimen.notification_large_icon_height); 434 final int width = (int) mContext.getResources().getDimension( 435 android.R.dimen.notification_large_icon_width); 436 largeIcon = Bitmap.createScaledBitmap(largeIcon, width, height, false); 437 } 438 439 return largeIcon; 440 } 441 442 /** 443 * Returns the appropriate icon res Id to display based on the call for which 444 * we want to display information. 445 */ 446 private int getIconToDisplay(Call call) { 447 // Even if both lines are in use, we only show a single item in 448 // the expanded Notifications UI. It's labeled "Ongoing call" 449 // (or "On hold" if there's only one call, and it's on hold.) 450 // Also, we don't have room to display caller-id info from two 451 // different calls. So if both lines are in use, display info 452 // from the foreground call. And if there's a ringing call, 453 // display that regardless of the state of the other calls. 454 if (call.getState() == Call.State.ONHOLD) { 455 return R.drawable.stat_sys_phone_call_on_hold; 456 } 457 return R.drawable.stat_sys_phone_call; 458 } 459 460 /** 461 * Returns the message to use with the notification. 462 */ 463 private int getContentString(Call call) { 464 int resId = R.string.notification_ongoing_call; 465 466 if (call.getState() == Call.State.INCOMING || call.getState() == Call.State.CALL_WAITING) { 467 resId = R.string.notification_incoming_call; 468 469 } else if (call.getState() == Call.State.ONHOLD) { 470 resId = R.string.notification_on_hold; 471 472 } else if (Call.State.isDialing(call.getState())) { 473 resId = R.string.notification_dialing; 474 } 475 476 return resId; 477 } 478 479 /** 480 * Gets the most relevant call to display in the notification. 481 */ 482 private Call getCallToShow(CallList callList) { 483 if (callList == null) { 484 return null; 485 } 486 Call call = callList.getIncomingCall(); 487 if (call == null) { 488 call = callList.getOutgoingCall(); 489 } 490 if (call == null) { 491 call = callList.getActiveOrBackgroundCall(); 492 } 493 return call; 494 } 495 496 private void addHangupAction(Notification.Builder builder) { 497 Log.i(this, "Will show \"hang-up\" action in the ongoing active call Notification"); 498 499 // TODO: use better asset. 500 builder.addAction(R.drawable.stat_sys_phone_call_end, 501 mContext.getText(R.string.notification_action_end_call), 502 createHangUpOngoingCallPendingIntent(mContext)); 503 } 504 505 /** 506 * Adds fullscreen intent to the builder. 507 */ 508 private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent, 509 Call call) { 510 // Ok, we actually want to launch the incoming call 511 // UI at this point (in addition to simply posting a notification 512 // to the status bar). Setting fullScreenIntent will cause 513 // the InCallScreen to be launched immediately *unless* the 514 // current foreground activity is marked as "immersive". 515 Log.d(this, "- Setting fullScreenIntent: " + intent); 516 builder.setFullScreenIntent(intent, true); 517 518 // Ugly hack alert: 519 // 520 // The NotificationManager has the (undocumented) behavior 521 // that it will *ignore* the fullScreenIntent field if you 522 // post a new Notification that matches the ID of one that's 523 // already active. Unfortunately this is exactly what happens 524 // when you get an incoming call-waiting call: the 525 // "ongoing call" notification is already visible, so the 526 // InCallScreen won't get launched in this case! 527 // (The result: if you bail out of the in-call UI while on a 528 // call and then get a call-waiting call, the incoming call UI 529 // won't come up automatically.) 530 // 531 // The workaround is to just notice this exact case (this is a 532 // call-waiting call *and* the InCallScreen is not in the 533 // foreground) and manually cancel the in-call notification 534 // before (re)posting it. 535 // 536 // TODO: there should be a cleaner way of avoiding this 537 // problem (see discussion in bug 3184149.) 538 539 // If a call is onhold during an incoming call, the call actually comes in as 540 // INCOMING. For that case *and* traditional call-waiting, we want to 541 // cancel the notification. 542 boolean isCallWaiting = (call.getState() == Call.State.CALL_WAITING || 543 (call.getState() == Call.State.INCOMING && 544 CallList.getInstance().getBackgroundCall() != null)); 545 546 if (isCallWaiting) { 547 Log.i(this, "updateInCallNotification: call-waiting! force relaunch..."); 548 // Cancel the IN_CALL_NOTIFICATION immediately before 549 // (re)posting it; this seems to force the 550 // NotificationManager to launch the fullScreenIntent. 551 mNotificationManager.cancel(IN_CALL_NOTIFICATION); 552 } 553 } 554 555 private Notification.Builder getNotificationBuilder() { 556 final Notification.Builder builder = new Notification.Builder(mContext); 557 builder.setOngoing(true); 558 559 // Make the notification prioritized over the other normal notifications. 560 builder.setPriority(Notification.PRIORITY_HIGH); 561 562 return builder; 563 } 564 private PendingIntent createLaunchPendingIntent() { 565 566 final Intent intent = InCallPresenter.getInstance().getInCallIntent(/*showdialpad=*/false); 567 568 // PendingIntent that can be used to launch the InCallActivity. The 569 // system fires off this intent if the user pulls down the windowshade 570 // and clicks the notification's expanded view. It's also used to 571 // launch the InCallActivity immediately when when there's an incoming 572 // call (see the "fullScreenIntent" field below). 573 PendingIntent inCallPendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 574 575 return inCallPendingIntent; 576 } 577 578 /** 579 * Returns PendingIntent for hanging up ongoing phone call. This will typically be used from 580 * Notification context. 581 */ 582 private static PendingIntent createHangUpOngoingCallPendingIntent(Context context) { 583 final Intent intent = new Intent(InCallApp.ACTION_HANG_UP_ONGOING_CALL, null, 584 context, NotificationBroadcastReceiver.class); 585 return PendingIntent.getBroadcast(context, 0, intent, 0); 586 } 587 } 588