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 static com.android.contacts.common.compat.CallSdkCompat.Details.PROPERTY_ENTERPRISE_CALL; 20 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST; 21 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VIDEO_INCOMING_CALL; 22 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VOICE_INCOMING_CALL; 23 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_INCOMING_CALL; 24 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST; 25 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL; 26 27 import com.google.common.base.Preconditions; 28 29 import android.app.Notification; 30 import android.app.NotificationManager; 31 import android.app.PendingIntent; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.graphics.Bitmap; 35 import android.graphics.BitmapFactory; 36 import android.graphics.drawable.BitmapDrawable; 37 import android.media.AudioAttributes; 38 import android.net.Uri; 39 import android.provider.ContactsContract.Contacts; 40 import android.support.annotation.Nullable; 41 import android.telecom.Call.Details; 42 import android.telecom.PhoneAccount; 43 import android.telecom.TelecomManager; 44 import android.text.BidiFormatter; 45 import android.text.TextDirectionHeuristics; 46 import android.text.TextUtils; 47 48 import com.android.contacts.common.ContactsUtils; 49 import com.android.contacts.common.ContactsUtils.UserType; 50 import com.android.contacts.common.preference.ContactsPreferences; 51 import com.android.contacts.common.testing.NeededForTesting; 52 import com.android.contacts.common.util.BitmapUtil; 53 import com.android.contacts.common.util.ContactDisplayUtils; 54 import com.android.dialer.R; 55 import com.android.incallui.ContactInfoCache.ContactCacheEntry; 56 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; 57 import com.android.incallui.InCallPresenter.InCallState; 58 import com.android.incallui.async.PausableExecutorImpl; 59 import com.android.incallui.ringtone.DialerRingtoneManager; 60 import com.android.incallui.ringtone.InCallTonePlayer; 61 import com.android.incallui.ringtone.ToneGeneratorFactory; 62 63 import java.util.Objects; 64 65 /** 66 * This class adds Notifications to the status bar for the in-call experience. 67 */ 68 public class StatusBarNotifier implements InCallPresenter.InCallStateListener, 69 CallList.CallUpdateListener { 70 71 // Notification types 72 // Indicates that no notification is currently showing. 73 private static final int NOTIFICATION_NONE = 0; 74 // Notification for an active call. This is non-interruptive, but cannot be dismissed. 75 private static final int NOTIFICATION_IN_CALL = 1; 76 // Notification for incoming calls. This is interruptive and will show up as a HUN. 77 private static final int NOTIFICATION_INCOMING_CALL = 2; 78 79 private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000}; 80 81 private final Context mContext; 82 @Nullable private ContactsPreferences mContactsPreferences; 83 private final ContactInfoCache mContactInfoCache; 84 private final NotificationManager mNotificationManager; 85 private final DialerRingtoneManager mDialerRingtoneManager; 86 private int mCurrentNotification = NOTIFICATION_NONE; 87 private int mCallState = Call.State.INVALID; 88 private int mSavedIcon = 0; 89 private String mSavedContent = null; 90 private Bitmap mSavedLargeIcon; 91 private String mSavedContentTitle; 92 private String mCallId = null; 93 private InCallState mInCallState; 94 private Uri mRingtone; 95 StatusBarNotifier(Context context, ContactInfoCache contactInfoCache)96 public StatusBarNotifier(Context context, ContactInfoCache contactInfoCache) { 97 Preconditions.checkNotNull(context); 98 mContext = context; 99 mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); 100 mContactInfoCache = contactInfoCache; 101 mNotificationManager = 102 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); 103 mDialerRingtoneManager = new DialerRingtoneManager( 104 new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()), 105 CallList.getInstance()); 106 mCurrentNotification = NOTIFICATION_NONE; 107 } 108 109 /** 110 * Creates notifications according to the state we receive from {@link InCallPresenter}. 111 */ 112 @Override onStateChange(InCallState oldState, InCallState newState, CallList callList)113 public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { 114 Log.d(this, "onStateChange"); 115 mInCallState = newState; 116 updateNotification(newState, callList); 117 } 118 119 /** 120 * Updates the phone app's status bar notification *and* launches the 121 * incoming call UI in response to a new incoming call. 122 * 123 * If an incoming call is ringing (or call-waiting), the notification 124 * will also include a "fullScreenIntent" that will cause the 125 * InCallScreen to be launched, unless the current foreground activity 126 * is marked as "immersive". 127 * 128 * (This is the mechanism that actually brings up the incoming call UI 129 * when we receive a "new ringing connection" event from the telephony 130 * layer.) 131 * 132 * Also note that this method is safe to call even if the phone isn't 133 * actually ringing (or, more likely, if an incoming call *was* 134 * ringing briefly but then disconnected). In that case, we'll simply 135 * update or cancel the in-call notification based on the current 136 * phone state. 137 * 138 * @see #updateInCallNotification(InCallState,CallList) 139 */ updateNotification(InCallState state, CallList callList)140 public void updateNotification(InCallState state, CallList callList) { 141 updateInCallNotification(state, callList); 142 } 143 144 /** 145 * Take down the in-call notification. 146 * @see #updateInCallNotification(InCallState,CallList) 147 */ cancelNotification()148 private void cancelNotification() { 149 if (!TextUtils.isEmpty(mCallId)) { 150 CallList.getInstance().removeCallUpdateListener(mCallId, this); 151 mCallId = null; 152 } 153 if (mCurrentNotification != NOTIFICATION_NONE) { 154 Log.d(this, "cancelInCall()..."); 155 mNotificationManager.cancel(mCurrentNotification); 156 } 157 mCurrentNotification = NOTIFICATION_NONE; 158 } 159 160 /** 161 * Should only be called from a irrecoverable state where it is necessary to dismiss all 162 * notifications. 163 */ clearAllCallNotifications(Context backupContext)164 static void clearAllCallNotifications(Context backupContext) { 165 Log.i(StatusBarNotifier.class.getSimpleName(), 166 "Something terrible happened. Clear all InCall notifications"); 167 168 NotificationManager notificationManager = 169 (NotificationManager) backupContext.getSystemService(Context.NOTIFICATION_SERVICE); 170 notificationManager.cancel(NOTIFICATION_IN_CALL); 171 notificationManager.cancel(NOTIFICATION_INCOMING_CALL); 172 } 173 174 /** 175 * Helper method for updateInCallNotification() and 176 * updateNotification(): Update the phone app's 177 * status bar notification based on the current telephony state, or 178 * cancels the notification if the phone is totally idle. 179 */ updateInCallNotification(final InCallState state, CallList callList)180 private void updateInCallNotification(final InCallState state, CallList callList) { 181 Log.d(this, "updateInCallNotification..."); 182 183 final Call call = getCallToShow(callList); 184 185 if (call != null) { 186 showNotification(call); 187 } else { 188 cancelNotification(); 189 } 190 } 191 showNotification(final Call call)192 private void showNotification(final Call call) { 193 final boolean isIncoming = (call.getState() == Call.State.INCOMING || 194 call.getState() == Call.State.CALL_WAITING); 195 if (!TextUtils.isEmpty(mCallId)) { 196 CallList.getInstance().removeCallUpdateListener(mCallId, this); 197 } 198 mCallId = call.getId(); 199 CallList.getInstance().addCallUpdateListener(call.getId(), this); 200 201 // we make a call to the contact info cache to query for supplemental data to what the 202 // call provides. This includes the contact name and photo. 203 // This callback will always get called immediately and synchronously with whatever data 204 // it has available, and may make a subsequent call later (same thread) if it had to 205 // call into the contacts provider for more data. 206 mContactInfoCache.findInfo(call, isIncoming, new ContactInfoCacheCallback() { 207 @Override 208 public void onContactInfoComplete(String callId, ContactCacheEntry entry) { 209 Call call = CallList.getInstance().getCallById(callId); 210 if (call != null) { 211 call.getLogState().contactLookupResult = entry.contactLookupResult; 212 buildAndSendNotification(call, entry); 213 } 214 } 215 216 @Override 217 public void onImageLoadComplete(String callId, ContactCacheEntry entry) { 218 Call call = CallList.getInstance().getCallById(callId); 219 if (call != null) { 220 buildAndSendNotification(call, entry); 221 } 222 } 223 224 @Override 225 public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) {} 226 }); 227 } 228 229 /** 230 * Sets up the main Ui for the notification 231 */ buildAndSendNotification(Call originalCall, ContactCacheEntry contactInfo)232 private void buildAndSendNotification(Call originalCall, ContactCacheEntry contactInfo) { 233 // This can get called to update an existing notification after contact information has come 234 // back. However, it can happen much later. Before we continue, we need to make sure that 235 // the call being passed in is still the one we want to show in the notification. 236 final Call call = getCallToShow(CallList.getInstance()); 237 if (call == null || !call.getId().equals(originalCall.getId())) { 238 return; 239 } 240 241 final int callState = call.getState(); 242 243 // Check if data has changed; if nothing is different, don't issue another notification. 244 final int iconResId = getIconToDisplay(call); 245 Bitmap largeIcon = getLargeIconToDisplay(contactInfo, call); 246 final String content = 247 getContentString(call, contactInfo.userType); 248 final String contentTitle = getContentTitle(contactInfo, call); 249 250 final int notificationType; 251 if (callState == Call.State.INCOMING || callState == Call.State.CALL_WAITING) { 252 notificationType = NOTIFICATION_INCOMING_CALL; 253 } else { 254 notificationType = NOTIFICATION_IN_CALL; 255 } 256 257 if (!checkForChangeAndSaveData(iconResId, content, largeIcon, contentTitle, callState, 258 notificationType, contactInfo.contactRingtoneUri)) { 259 return; 260 } 261 262 if (largeIcon != null) { 263 largeIcon = getRoundedIcon(largeIcon); 264 } 265 266 /* 267 * This builder is used for the notification shown when the device is locked and the user 268 * has set their notification settings to 'hide sensitive content' 269 * {@see Notification.Builder#setPublicVersion}. 270 */ 271 Notification.Builder publicBuilder = new Notification.Builder(mContext); 272 publicBuilder.setSmallIcon(iconResId) 273 .setColor(mContext.getResources().getColor(R.color.dialer_theme_color)) 274 // Hide work call state for the lock screen notification 275 .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT)); 276 setNotificationWhen(call, callState, publicBuilder); 277 278 /* 279 * Builder for the notification shown when the device is unlocked or the user has set their 280 * notification settings to 'show all notification content'. 281 */ 282 final Notification.Builder builder = getNotificationBuilder(); 283 builder.setPublicVersion(publicBuilder.build()); 284 285 // Set up the main intent to send the user to the in-call screen 286 final PendingIntent inCallPendingIntent = createLaunchPendingIntent(); 287 builder.setContentIntent(inCallPendingIntent); 288 289 // Set the intent as a full screen intent as well if a call is incoming 290 if (notificationType == NOTIFICATION_INCOMING_CALL 291 && !InCallPresenter.getInstance().isShowingInCallUi()) { 292 configureFullScreenIntent(builder, inCallPendingIntent, call); 293 // Set the notification category for incoming calls 294 builder.setCategory(Notification.CATEGORY_CALL); 295 } 296 297 // Set the content 298 builder.setContentText(content); 299 builder.setSmallIcon(iconResId); 300 builder.setContentTitle(contentTitle); 301 builder.setLargeIcon(largeIcon); 302 builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color)); 303 304 final boolean isVideoUpgradeRequest = call.getSessionModificationState() 305 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST; 306 if (isVideoUpgradeRequest) { 307 builder.setUsesChronometer(false); 308 addDismissUpgradeRequestAction(builder); 309 addAcceptUpgradeRequestAction(builder); 310 } else { 311 createIncomingCallNotification(call, callState, builder); 312 } 313 314 addPersonReference(builder, contactInfo, call); 315 316 /* 317 * Fire off the notification 318 */ 319 Notification notification = builder.build(); 320 321 if (mDialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) { 322 notification.flags |= Notification.FLAG_INSISTENT; 323 notification.sound = contactInfo.contactRingtoneUri; 324 AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder(); 325 audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC); 326 audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE); 327 notification.audioAttributes = audioAttributes.build(); 328 if (mDialerRingtoneManager.shouldVibrate(mContext.getContentResolver())) { 329 notification.vibrate = VIBRATE_PATTERN; 330 } 331 } 332 if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) { 333 Log.v(this, "Playing call waiting tone"); 334 mDialerRingtoneManager.playCallWaitingTone(); 335 } 336 if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) { 337 Log.i(this, "Previous notification already showing - cancelling " 338 + mCurrentNotification); 339 mNotificationManager.cancel(mCurrentNotification); 340 } 341 Log.i(this, "Displaying notification for " + notificationType); 342 mNotificationManager.notify(notificationType, notification); 343 mCurrentNotification = notificationType; 344 } 345 createIncomingCallNotification( Call call, int state, Notification.Builder builder)346 private void createIncomingCallNotification( 347 Call call, int state, Notification.Builder builder) { 348 setNotificationWhen(call, state, builder); 349 350 // Add hang up option for any active calls (active | onhold), outgoing calls (dialing). 351 if (state == Call.State.ACTIVE || 352 state == Call.State.ONHOLD || 353 Call.State.isDialing(state)) { 354 addHangupAction(builder); 355 } else if (state == Call.State.INCOMING || state == Call.State.CALL_WAITING) { 356 addDismissAction(builder); 357 if (call.isVideoCall(mContext)) { 358 addVoiceAction(builder); 359 addVideoCallAction(builder); 360 } else { 361 addAnswerAction(builder); 362 } 363 } 364 } 365 366 /* 367 * Sets the notification's when section as needed. For active calls, this is explicitly set as 368 * the duration of the call. For all other states, the notification will automatically show the 369 * time at which the notification was created. 370 */ setNotificationWhen(Call call, int state, Notification.Builder builder)371 private void setNotificationWhen(Call call, int state, Notification.Builder builder) { 372 if (state == Call.State.ACTIVE) { 373 builder.setUsesChronometer(true); 374 builder.setWhen(call.getConnectTimeMillis()); 375 } else { 376 builder.setUsesChronometer(false); 377 } 378 } 379 380 /** 381 * Checks the new notification data and compares it against any notification that we 382 * are already displaying. If the data is exactly the same, we return false so that 383 * we do not issue a new notification for the exact same data. 384 */ checkForChangeAndSaveData(int icon, String content, Bitmap largeIcon, String contentTitle, int state, int notificationType, Uri ringtone)385 private boolean checkForChangeAndSaveData(int icon, String content, Bitmap largeIcon, 386 String contentTitle, int state, int notificationType, Uri ringtone) { 387 388 // The two are different: 389 // if new title is not null, it should be different from saved version OR 390 // if new title is null, the saved version should not be null 391 final boolean contentTitleChanged = 392 (contentTitle != null && !contentTitle.equals(mSavedContentTitle)) || 393 (contentTitle == null && mSavedContentTitle != null); 394 395 // any change means we are definitely updating 396 boolean retval = (mSavedIcon != icon) || !Objects.equals(mSavedContent, content) 397 || (mCallState != state) || (mSavedLargeIcon != largeIcon) 398 || contentTitleChanged || !Objects.equals(mRingtone, ringtone); 399 400 // If we aren't showing a notification right now or the notification type is changing, 401 // definitely do an update. 402 if (mCurrentNotification != notificationType) { 403 if (mCurrentNotification == NOTIFICATION_NONE) { 404 Log.d(this, "Showing notification for first time."); 405 } 406 retval = true; 407 } 408 409 mSavedIcon = icon; 410 mSavedContent = content; 411 mCallState = state; 412 mSavedLargeIcon = largeIcon; 413 mSavedContentTitle = contentTitle; 414 mRingtone = ringtone; 415 416 if (retval) { 417 Log.d(this, "Data changed. Showing notification"); 418 } 419 420 return retval; 421 } 422 423 /** 424 * Returns the main string to use in the notification. 425 */ 426 @NeededForTesting getContentTitle(ContactCacheEntry contactInfo, Call call)427 String getContentTitle(ContactCacheEntry contactInfo, Call call) { 428 if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) { 429 return mContext.getResources().getString(R.string.card_title_conf_call); 430 } 431 432 String preferredName = ContactDisplayUtils.getPreferredDisplayName(contactInfo.namePrimary, 433 contactInfo.nameAlternative, mContactsPreferences); 434 if (TextUtils.isEmpty(preferredName)) { 435 return TextUtils.isEmpty(contactInfo.number) ? null : BidiFormatter.getInstance() 436 .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR); 437 } 438 return preferredName; 439 } 440 addPersonReference(Notification.Builder builder, ContactCacheEntry contactInfo, Call call)441 private void addPersonReference(Notification.Builder builder, ContactCacheEntry contactInfo, 442 Call call) { 443 // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed. 444 // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid 445 // NotificationManager using it. 446 if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) { 447 builder.addPerson(contactInfo.lookupUri.toString()); 448 } else if (!TextUtils.isEmpty(call.getNumber())) { 449 builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL, 450 call.getNumber(), null).toString()); 451 } 452 } 453 454 /** 455 * Gets a large icon from the contact info object to display in the notification. 456 */ getLargeIconToDisplay(ContactCacheEntry contactInfo, Call call)457 private Bitmap getLargeIconToDisplay(ContactCacheEntry contactInfo, Call call) { 458 Bitmap largeIcon = null; 459 if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) { 460 largeIcon = BitmapFactory.decodeResource(mContext.getResources(), 461 R.drawable.img_conference); 462 } 463 if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) { 464 largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap(); 465 } 466 return largeIcon; 467 } 468 getRoundedIcon(Bitmap bitmap)469 private Bitmap getRoundedIcon(Bitmap bitmap) { 470 if (bitmap == null) { 471 return null; 472 } 473 final int height = (int) mContext.getResources().getDimension( 474 android.R.dimen.notification_large_icon_height); 475 final int width = (int) mContext.getResources().getDimension( 476 android.R.dimen.notification_large_icon_width); 477 return BitmapUtil.getRoundedBitmap(bitmap, width, height); 478 } 479 480 /** 481 * Returns the appropriate icon res Id to display based on the call for which 482 * we want to display information. 483 */ getIconToDisplay(Call call)484 private int getIconToDisplay(Call call) { 485 // Even if both lines are in use, we only show a single item in 486 // the expanded Notifications UI. It's labeled "Ongoing call" 487 // (or "On hold" if there's only one call, and it's on hold.) 488 // Also, we don't have room to display caller-id info from two 489 // different calls. So if both lines are in use, display info 490 // from the foreground call. And if there's a ringing call, 491 // display that regardless of the state of the other calls. 492 if (call.getState() == Call.State.ONHOLD) { 493 return R.drawable.ic_phone_paused_white_24dp; 494 } else if (call.getSessionModificationState() 495 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 496 return R.drawable.ic_videocam; 497 } 498 return R.drawable.ic_call_white_24dp; 499 } 500 501 /** 502 * Returns the message to use with the notification. 503 */ getContentString(Call call, @UserType long userType)504 private String getContentString(Call call, @UserType long userType) { 505 boolean isIncomingOrWaiting = call.getState() == Call.State.INCOMING || 506 call.getState() == Call.State.CALL_WAITING; 507 508 if (isIncomingOrWaiting && 509 call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) { 510 511 if (!TextUtils.isEmpty(call.getChildNumber())) { 512 return mContext.getString(R.string.child_number, call.getChildNumber()); 513 } else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) { 514 return call.getCallSubject(); 515 } 516 } 517 518 int resId = R.string.notification_ongoing_call; 519 if (call.hasProperty(Details.PROPERTY_WIFI)) { 520 resId = R.string.notification_ongoing_call_wifi; 521 } 522 523 if (isIncomingOrWaiting) { 524 if (call.hasProperty(Details.PROPERTY_WIFI)) { 525 resId = R.string.notification_incoming_call_wifi; 526 } else { 527 resId = R.string.notification_incoming_call; 528 } 529 } else if (call.getState() == Call.State.ONHOLD) { 530 resId = R.string.notification_on_hold; 531 } else if (Call.State.isDialing(call.getState())) { 532 resId = R.string.notification_dialing; 533 } else if (call.getSessionModificationState() 534 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 535 resId = R.string.notification_requesting_video_call; 536 } 537 538 // Is the call placed through work connection service. 539 boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL); 540 if(userType == ContactsUtils.USER_TYPE_WORK || isWorkCall) { 541 resId = getWorkStringFromPersonalString(resId); 542 } 543 544 return mContext.getString(resId); 545 } 546 getWorkStringFromPersonalString(int resId)547 private static int getWorkStringFromPersonalString(int resId) { 548 if (resId == R.string.notification_ongoing_call) { 549 return R.string.notification_ongoing_work_call; 550 } else if (resId == R.string.notification_ongoing_call_wifi) { 551 return R.string.notification_ongoing_work_call_wifi; 552 } else if (resId == R.string.notification_incoming_call_wifi) { 553 return R.string.notification_incoming_work_call_wifi; 554 } else if (resId == R.string.notification_incoming_call) { 555 return R.string.notification_incoming_work_call; 556 } else { 557 return resId; 558 } 559 } 560 561 /** 562 * Gets the most relevant call to display in the notification. 563 */ getCallToShow(CallList callList)564 private Call getCallToShow(CallList callList) { 565 if (callList == null) { 566 return null; 567 } 568 Call call = callList.getIncomingCall(); 569 if (call == null) { 570 call = callList.getOutgoingCall(); 571 } 572 if (call == null) { 573 call = callList.getVideoUpgradeRequestCall(); 574 } 575 if (call == null) { 576 call = callList.getActiveOrBackgroundCall(); 577 } 578 return call; 579 } 580 addAnswerAction(Notification.Builder builder)581 private void addAnswerAction(Notification.Builder builder) { 582 Log.d(this, "Will show \"answer\" action in the incoming call Notification"); 583 584 PendingIntent answerVoicePendingIntent = createNotificationPendingIntent( 585 mContext, ACTION_ANSWER_VOICE_INCOMING_CALL); 586 builder.addAction(R.drawable.ic_call_white_24dp, 587 mContext.getText(R.string.notification_action_answer), 588 answerVoicePendingIntent); 589 } 590 addDismissAction(Notification.Builder builder)591 private void addDismissAction(Notification.Builder builder) { 592 Log.d(this, "Will show \"dismiss\" action in the incoming call Notification"); 593 594 PendingIntent declinePendingIntent = 595 createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL); 596 builder.addAction(R.drawable.ic_close_dk, 597 mContext.getText(R.string.notification_action_dismiss), 598 declinePendingIntent); 599 } 600 addHangupAction(Notification.Builder builder)601 private void addHangupAction(Notification.Builder builder) { 602 Log.d(this, "Will show \"hang-up\" action in the ongoing active call Notification"); 603 604 PendingIntent hangupPendingIntent = 605 createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL); 606 builder.addAction(R.drawable.ic_call_end_white_24dp, 607 mContext.getText(R.string.notification_action_end_call), 608 hangupPendingIntent); 609 } 610 addVideoCallAction(Notification.Builder builder)611 private void addVideoCallAction(Notification.Builder builder) { 612 Log.i(this, "Will show \"video\" action in the incoming call Notification"); 613 614 PendingIntent answerVideoPendingIntent = createNotificationPendingIntent( 615 mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL); 616 builder.addAction(R.drawable.ic_videocam, 617 mContext.getText(R.string.notification_action_answer_video), 618 answerVideoPendingIntent); 619 } 620 addVoiceAction(Notification.Builder builder)621 private void addVoiceAction(Notification.Builder builder) { 622 Log.d(this, "Will show \"voice\" action in the incoming call Notification"); 623 624 PendingIntent answerVoicePendingIntent = createNotificationPendingIntent( 625 mContext, ACTION_ANSWER_VOICE_INCOMING_CALL); 626 builder.addAction(R.drawable.ic_call_white_24dp, 627 mContext.getText(R.string.notification_action_answer_voice), 628 answerVoicePendingIntent); 629 } 630 addAcceptUpgradeRequestAction(Notification.Builder builder)631 private void addAcceptUpgradeRequestAction(Notification.Builder builder) { 632 Log.i(this, "Will show \"accept upgrade\" action in the incoming call Notification"); 633 634 PendingIntent acceptVideoPendingIntent = createNotificationPendingIntent( 635 mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST); 636 builder.addAction(0, mContext.getText(R.string.notification_action_accept), 637 acceptVideoPendingIntent); 638 } 639 addDismissUpgradeRequestAction(Notification.Builder builder)640 private void addDismissUpgradeRequestAction(Notification.Builder builder) { 641 Log.i(this, "Will show \"dismiss upgrade\" action in the incoming call Notification"); 642 643 PendingIntent declineVideoPendingIntent = createNotificationPendingIntent( 644 mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST); 645 builder.addAction(0, mContext.getText(R.string.notification_action_dismiss), 646 declineVideoPendingIntent); 647 } 648 649 /** 650 * Adds fullscreen intent to the builder. 651 */ configureFullScreenIntent(Notification.Builder builder, PendingIntent intent, Call call)652 private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent, 653 Call call) { 654 // Ok, we actually want to launch the incoming call 655 // UI at this point (in addition to simply posting a notification 656 // to the status bar). Setting fullScreenIntent will cause 657 // the InCallScreen to be launched immediately *unless* the 658 // current foreground activity is marked as "immersive". 659 Log.d(this, "- Setting fullScreenIntent: " + intent); 660 builder.setFullScreenIntent(intent, true); 661 662 // Ugly hack alert: 663 // 664 // The NotificationManager has the (undocumented) behavior 665 // that it will *ignore* the fullScreenIntent field if you 666 // post a new Notification that matches the ID of one that's 667 // already active. Unfortunately this is exactly what happens 668 // when you get an incoming call-waiting call: the 669 // "ongoing call" notification is already visible, so the 670 // InCallScreen won't get launched in this case! 671 // (The result: if you bail out of the in-call UI while on a 672 // call and then get a call-waiting call, the incoming call UI 673 // won't come up automatically.) 674 // 675 // The workaround is to just notice this exact case (this is a 676 // call-waiting call *and* the InCallScreen is not in the 677 // foreground) and manually cancel the in-call notification 678 // before (re)posting it. 679 // 680 // TODO: there should be a cleaner way of avoiding this 681 // problem (see discussion in bug 3184149.) 682 683 // If a call is onhold during an incoming call, the call actually comes in as 684 // INCOMING. For that case *and* traditional call-waiting, we want to 685 // cancel the notification. 686 boolean isCallWaiting = (call.getState() == Call.State.CALL_WAITING || 687 (call.getState() == Call.State.INCOMING && 688 CallList.getInstance().getBackgroundCall() != null)); 689 690 if (isCallWaiting) { 691 Log.i(this, "updateInCallNotification: call-waiting! force relaunch..."); 692 // Cancel the IN_CALL_NOTIFICATION immediately before 693 // (re)posting it; this seems to force the 694 // NotificationManager to launch the fullScreenIntent. 695 mNotificationManager.cancel(NOTIFICATION_IN_CALL); 696 } 697 } 698 getNotificationBuilder()699 private Notification.Builder getNotificationBuilder() { 700 final Notification.Builder builder = new Notification.Builder(mContext); 701 builder.setOngoing(true); 702 703 // Make the notification prioritized over the other normal notifications. 704 builder.setPriority(Notification.PRIORITY_HIGH); 705 706 return builder; 707 } 708 createLaunchPendingIntent()709 private PendingIntent createLaunchPendingIntent() { 710 711 final Intent intent = InCallPresenter.getInstance().getInCallIntent( 712 false /* showDialpad */, false /* newOutgoingCall */); 713 714 // PendingIntent that can be used to launch the InCallActivity. The 715 // system fires off this intent if the user pulls down the windowshade 716 // and clicks the notification's expanded view. It's also used to 717 // launch the InCallActivity immediately when when there's an incoming 718 // call (see the "fullScreenIntent" field below). 719 PendingIntent inCallPendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 720 721 return inCallPendingIntent; 722 } 723 724 /** 725 * Returns PendingIntent for answering a phone call. This will typically be used from 726 * Notification context. 727 */ createNotificationPendingIntent(Context context, String action)728 private static PendingIntent createNotificationPendingIntent(Context context, String action) { 729 final Intent intent = new Intent(action, null, 730 context, NotificationBroadcastReceiver.class); 731 return PendingIntent.getBroadcast(context, 0, intent, 0); 732 } 733 734 @Override onCallChanged(Call call)735 public void onCallChanged(Call call) { 736 if (CallList.getInstance().getIncomingCall() == null) { 737 mDialerRingtoneManager.stopCallWaitingTone(); 738 } 739 } 740 741 /** 742 * Responds to changes in the session modification state for the call by dismissing the 743 * status bar notification as required. 744 * 745 * @param sessionModificationState The new session modification state. 746 */ 747 @Override onSessionModificationStateChange(int sessionModificationState)748 public void onSessionModificationStateChange(int sessionModificationState) { 749 if (sessionModificationState == Call.SessionModificationState.NO_REQUEST) { 750 if (mCallId != null) { 751 CallList.getInstance().removeCallUpdateListener(mCallId, this); 752 } 753 754 updateNotification(mInCallState, CallList.getInstance()); 755 } 756 } 757 758 @Override onLastForwardedNumberChange()759 public void onLastForwardedNumberChange() { 760 // no-op 761 } 762 763 @Override onChildNumberChange()764 public void onChildNumberChange() { 765 // no-op 766 } 767 } 768