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 android.telecom.Call.Details.PROPERTY_HIGH_DEF_AUDIO; 20 import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL; 21 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST; 22 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VIDEO_INCOMING_CALL; 23 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VOICE_INCOMING_CALL; 24 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_INCOMING_CALL; 25 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST; 26 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL; 27 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_TURN_OFF_SPEAKER; 28 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_TURN_ON_SPEAKER; 29 30 import android.Manifest; 31 import android.app.Notification; 32 import android.app.PendingIntent; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.content.res.Resources; 36 import android.graphics.Bitmap; 37 import android.graphics.drawable.BitmapDrawable; 38 import android.graphics.drawable.Drawable; 39 import android.graphics.drawable.Icon; 40 import android.media.AudioAttributes; 41 import android.net.Uri; 42 import android.os.Build.VERSION; 43 import android.os.Build.VERSION_CODES; 44 import android.os.Trace; 45 import android.support.annotation.ColorRes; 46 import android.support.annotation.NonNull; 47 import android.support.annotation.Nullable; 48 import android.support.annotation.RequiresPermission; 49 import android.support.annotation.StringRes; 50 import android.support.annotation.VisibleForTesting; 51 import android.support.v4.os.BuildCompat; 52 import android.telecom.Call.Details; 53 import android.telecom.CallAudioState; 54 import android.telecom.PhoneAccount; 55 import android.telecom.TelecomManager; 56 import android.telecom.VideoProfile; 57 import android.text.BidiFormatter; 58 import android.text.Spannable; 59 import android.text.SpannableString; 60 import android.text.TextDirectionHeuristics; 61 import android.text.TextUtils; 62 import android.text.style.ForegroundColorSpan; 63 import com.android.contacts.common.ContactsUtils; 64 import com.android.contacts.common.ContactsUtils.UserType; 65 import com.android.contacts.common.preference.ContactsPreferences; 66 import com.android.contacts.common.util.ContactDisplayUtils; 67 import com.android.dialer.common.Assert; 68 import com.android.dialer.common.LogUtil; 69 import com.android.dialer.configprovider.ConfigProviderBindings; 70 import com.android.dialer.contactphoto.BitmapUtil; 71 import com.android.dialer.enrichedcall.EnrichedCallManager; 72 import com.android.dialer.enrichedcall.Session; 73 import com.android.dialer.lettertile.LetterTileDrawable; 74 import com.android.dialer.lettertile.LetterTileDrawable.ContactType; 75 import com.android.dialer.multimedia.MultimediaData; 76 import com.android.dialer.notification.NotificationChannelId; 77 import com.android.dialer.oem.MotorolaUtils; 78 import com.android.dialer.util.DrawableConverter; 79 import com.android.incallui.ContactInfoCache.ContactCacheEntry; 80 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; 81 import com.android.incallui.InCallPresenter.InCallState; 82 import com.android.incallui.async.PausableExecutorImpl; 83 import com.android.incallui.audiomode.AudioModeProvider; 84 import com.android.incallui.call.CallList; 85 import com.android.incallui.call.DialerCall; 86 import com.android.incallui.call.DialerCallListener; 87 import com.android.incallui.call.TelecomAdapter; 88 import com.android.incallui.ringtone.DialerRingtoneManager; 89 import com.android.incallui.ringtone.InCallTonePlayer; 90 import com.android.incallui.ringtone.ToneGeneratorFactory; 91 import com.android.incallui.videotech.utils.SessionModificationState; 92 import java.util.Objects; 93 94 /** This class adds Notifications to the status bar for the in-call experience. */ 95 public class StatusBarNotifier 96 implements InCallPresenter.InCallStateListener, 97 EnrichedCallManager.StateChangedListener, 98 ContactInfoCacheCallback { 99 100 private static final int NOTIFICATION_ID = 1; 101 102 // Notification types 103 // Indicates that no notification is currently showing. 104 private static final int NOTIFICATION_NONE = 0; 105 // Notification for an active call. This is non-interruptive, but cannot be dismissed. 106 private static final int NOTIFICATION_IN_CALL = 1; 107 // Notification for incoming calls. This is interruptive and will show up as a HUN. 108 private static final int NOTIFICATION_INCOMING_CALL = 2; 109 // Notification for incoming calls in the case where there is already an active call. 110 // This is non-interruptive, but otherwise behaves the same as NOTIFICATION_INCOMING_CALL 111 private static final int NOTIFICATION_INCOMING_CALL_QUIET = 3; 112 113 private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000}; 114 115 private final Context context; 116 private final ContactInfoCache contactInfoCache; 117 private final DialerRingtoneManager dialerRingtoneManager; 118 @Nullable private ContactsPreferences contactsPreferences; 119 private int currentNotification = NOTIFICATION_NONE; 120 private int callState = DialerCall.State.INVALID; 121 private int videoState = VideoProfile.STATE_AUDIO_ONLY; 122 private int savedIcon = 0; 123 private String savedContent = null; 124 private Bitmap savedLargeIcon; 125 private String savedContentTitle; 126 private CallAudioState savedCallAudioState; 127 private Uri ringtone; 128 private StatusBarCallListener statusBarCallListener; 129 StatusBarNotifier(@onNull Context context, @NonNull ContactInfoCache contactInfoCache)130 public StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) { 131 Trace.beginSection("StatusBarNotifier.Constructor"); 132 this.context = Assert.isNotNull(context); 133 contactsPreferences = ContactsPreferencesFactory.newContactsPreferences(this.context); 134 this.contactInfoCache = contactInfoCache; 135 dialerRingtoneManager = 136 new DialerRingtoneManager( 137 new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()), 138 CallList.getInstance()); 139 currentNotification = NOTIFICATION_NONE; 140 Trace.endSection(); 141 } 142 143 /** 144 * Should only be called from a irrecoverable state where it is necessary to dismiss all 145 * notifications. 146 */ clearAllCallNotifications()147 static void clearAllCallNotifications() { 148 LogUtil.e( 149 "StatusBarNotifier.clearAllCallNotifications", 150 "something terrible happened, clear all InCall notifications"); 151 152 TelecomAdapter.getInstance().stopForegroundNotification(); 153 } 154 getWorkStringFromPersonalString(int resId)155 private static int getWorkStringFromPersonalString(int resId) { 156 if (resId == R.string.notification_ongoing_call) { 157 return R.string.notification_ongoing_work_call; 158 } else if (resId == R.string.notification_incoming_call) { 159 return R.string.notification_incoming_work_call; 160 } else { 161 return resId; 162 } 163 } 164 165 /** 166 * Returns PendingIntent for answering a phone call. This will typically be used from Notification 167 * context. 168 */ createNotificationPendingIntent(Context context, String action)169 private static PendingIntent createNotificationPendingIntent(Context context, String action) { 170 final Intent intent = new Intent(action, null, context, NotificationBroadcastReceiver.class); 171 return PendingIntent.getBroadcast(context, 0, intent, 0); 172 } 173 174 /** Creates notifications according to the state we receive from {@link InCallPresenter}. */ 175 @Override 176 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) onStateChange(InCallState oldState, InCallState newState, CallList callList)177 public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { 178 LogUtil.d("StatusBarNotifier.onStateChange", "%s->%s", oldState, newState); 179 updateNotification(); 180 } 181 182 @Override onEnrichedCallStateChanged()183 public void onEnrichedCallStateChanged() { 184 LogUtil.enterBlock("StatusBarNotifier.onEnrichedCallStateChanged"); 185 updateNotification(); 186 } 187 188 /** 189 * Updates the phone app's status bar notification *and* launches the incoming call UI in response 190 * to a new incoming call. 191 * 192 * <p>If an incoming call is ringing (or call-waiting), the notification will also include a 193 * "fullScreenIntent" that will cause the InCallScreen to be launched, unless the current 194 * foreground activity is marked as "immersive". 195 * 196 * <p>(This is the mechanism that actually brings up the incoming call UI when we receive a "new 197 * ringing connection" event from the telephony layer.) 198 * 199 * <p>Also note that this method is safe to call even if the phone isn't actually ringing (or, 200 * more likely, if an incoming call *was* ringing briefly but then disconnected). In that case, 201 * we'll simply update or cancel the in-call notification based on the current phone state. 202 * 203 * @see #updateInCallNotification() 204 */ 205 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) updateNotification()206 public void updateNotification() { 207 updateInCallNotification(); 208 } 209 210 /** 211 * Take down the in-call notification. 212 * 213 * @see #updateInCallNotification() 214 */ cancelNotification()215 private void cancelNotification() { 216 if (statusBarCallListener != null) { 217 setStatusBarCallListener(null); 218 } 219 if (currentNotification != NOTIFICATION_NONE) { 220 TelecomAdapter.getInstance().stopForegroundNotification(); 221 currentNotification = NOTIFICATION_NONE; 222 } 223 } 224 225 /** 226 * Helper method for updateInCallNotification() and updateNotification(): Update the phone app's 227 * status bar notification based on the current telephony state, or cancels the notification if 228 * the phone is totally idle. 229 */ 230 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) updateInCallNotification()231 private void updateInCallNotification() { 232 LogUtil.d("StatusBarNotifier.updateInCallNotification", ""); 233 234 final DialerCall call = getCallToShow(CallList.getInstance()); 235 236 if (call != null) { 237 showNotification(call); 238 } else { 239 cancelNotification(); 240 } 241 } 242 243 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) showNotification(final DialerCall call)244 private void showNotification(final DialerCall call) { 245 Trace.beginSection("StatusBarNotifier.showNotification"); 246 final boolean isIncoming = 247 (call.getState() == DialerCall.State.INCOMING 248 || call.getState() == DialerCall.State.CALL_WAITING); 249 setStatusBarCallListener(new StatusBarCallListener(call)); 250 251 // we make a call to the contact info cache to query for supplemental data to what the 252 // call provides. This includes the contact name and photo. 253 // This callback will always get called immediately and synchronously with whatever data 254 // it has available, and may make a subsequent call later (same thread) if it had to 255 // call into the contacts provider for more data. 256 contactInfoCache.findInfo(call, isIncoming, this); 257 Trace.endSection(); 258 } 259 260 /** Sets up the main Ui for the notification */ 261 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) buildAndSendNotification( CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo)262 private void buildAndSendNotification( 263 CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo) { 264 Trace.beginSection("StatusBarNotifier.buildAndSendNotification"); 265 // This can get called to update an existing notification after contact information has come 266 // back. However, it can happen much later. Before we continue, we need to make sure that 267 // the call being passed in is still the one we want to show in the notification. 268 final DialerCall call = getCallToShow(callList); 269 if (call == null || !call.getId().equals(originalCall.getId())) { 270 Trace.endSection(); 271 return; 272 } 273 274 Trace.beginSection("prepare work"); 275 final int callState = call.getState(); 276 final CallAudioState callAudioState = AudioModeProvider.getInstance().getAudioState(); 277 278 Trace.beginSection("read icon and strings"); 279 // Check if data has changed; if nothing is different, don't issue another notification. 280 final int iconResId = getIconToDisplay(call); 281 Bitmap largeIcon = getLargeIconToDisplay(context, contactInfo, call); 282 final CharSequence content = getContentString(call, contactInfo.userType); 283 final String contentTitle = getContentTitle(contactInfo, call); 284 Trace.endSection(); 285 286 final boolean isVideoUpgradeRequest = 287 call.getVideoTech().getSessionModificationState() 288 == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST; 289 final int notificationType; 290 if (callState == DialerCall.State.INCOMING 291 || callState == DialerCall.State.CALL_WAITING 292 || isVideoUpgradeRequest) { 293 if (ConfigProviderBindings.get(context) 294 .getBoolean("quiet_incoming_call_if_ui_showing", true)) { 295 notificationType = 296 InCallPresenter.getInstance().isShowingInCallUi() 297 ? NOTIFICATION_INCOMING_CALL_QUIET 298 : NOTIFICATION_INCOMING_CALL; 299 } else { 300 boolean alreadyActive = 301 callList.getActiveOrBackgroundCall() != null 302 && InCallPresenter.getInstance().isShowingInCallUi(); 303 notificationType = 304 alreadyActive ? NOTIFICATION_INCOMING_CALL_QUIET : NOTIFICATION_INCOMING_CALL; 305 } 306 } else { 307 notificationType = NOTIFICATION_IN_CALL; 308 } 309 Trace.endSection(); // prepare work 310 311 if (!checkForChangeAndSaveData( 312 iconResId, 313 content.toString(), 314 largeIcon, 315 contentTitle, 316 callState, 317 call.getVideoState(), 318 notificationType, 319 contactInfo.contactRingtoneUri, 320 callAudioState)) { 321 Trace.endSection(); 322 return; 323 } 324 325 if (largeIcon != null) { 326 largeIcon = getRoundedIcon(largeIcon); 327 } 328 329 // This builder is used for the notification shown when the device is locked and the user 330 // has set their notification settings to 'hide sensitive content' 331 // {@see Notification.Builder#setPublicVersion}. 332 Notification.Builder publicBuilder = new Notification.Builder(context); 333 publicBuilder 334 .setSmallIcon(iconResId) 335 .setColor(context.getResources().getColor(R.color.dialer_theme_color, context.getTheme())) 336 // Hide work call state for the lock screen notification 337 .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT)); 338 setNotificationWhen(call, callState, publicBuilder); 339 340 // Builder for the notification shown when the device is unlocked or the user has set their 341 // notification settings to 'show all notification content'. 342 final Notification.Builder builder = getNotificationBuilder(); 343 builder.setPublicVersion(publicBuilder.build()); 344 345 // Set up the main intent to send the user to the in-call screen 346 builder.setContentIntent(createLaunchPendingIntent(false /* isFullScreen */)); 347 348 LogUtil.i("StatusBarNotifier.buildAndSendNotification", "notificationType=" + notificationType); 349 switch (notificationType) { 350 case NOTIFICATION_INCOMING_CALL: 351 if (BuildCompat.isAtLeastO()) { 352 builder.setChannelId(NotificationChannelId.INCOMING_CALL); 353 } 354 // Set the intent as a full screen intent as well if a call is incoming 355 configureFullScreenIntent(builder, createLaunchPendingIntent(true /* isFullScreen */)); 356 // Set the notification category and bump the priority for incoming calls 357 builder.setCategory(Notification.CATEGORY_CALL); 358 // This will be ignored on O+ and handled by the channel 359 builder.setPriority(Notification.PRIORITY_MAX); 360 if (currentNotification != NOTIFICATION_INCOMING_CALL) { 361 LogUtil.i( 362 "StatusBarNotifier.buildAndSendNotification", 363 "Canceling old notification so this one can be noisy"); 364 // Moving from a non-interuptive notification (or none) to a noisy one. Cancel the old 365 // notification (if there is one) so the fullScreenIntent or HUN will show 366 TelecomAdapter.getInstance().stopForegroundNotification(); 367 } 368 break; 369 case NOTIFICATION_INCOMING_CALL_QUIET: 370 if (BuildCompat.isAtLeastO()) { 371 builder.setChannelId(NotificationChannelId.ONGOING_CALL); 372 } 373 break; 374 case NOTIFICATION_IN_CALL: 375 if (BuildCompat.isAtLeastO()) { 376 publicBuilder.setColorized(true); 377 builder.setColorized(true); 378 builder.setChannelId(NotificationChannelId.ONGOING_CALL); 379 } 380 break; 381 default: 382 break; 383 } 384 385 // Set the content 386 builder.setContentText(content); 387 builder.setSmallIcon(iconResId); 388 builder.setContentTitle(contentTitle); 389 builder.setLargeIcon(largeIcon); 390 builder.setColor(InCallPresenter.getInstance().getThemeColorManager().getPrimaryColor()); 391 392 if (isVideoUpgradeRequest) { 393 builder.setUsesChronometer(false); 394 addDismissUpgradeRequestAction(builder); 395 addAcceptUpgradeRequestAction(builder); 396 } else { 397 createIncomingCallNotification(call, callState, callAudioState, builder); 398 } 399 400 addPersonReference(builder, contactInfo, call); 401 402 Trace.beginSection("fire notification"); 403 // Fire off the notification 404 Notification notification = builder.build(); 405 406 if (dialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) { 407 notification.flags |= Notification.FLAG_INSISTENT; 408 notification.sound = contactInfo.contactRingtoneUri; 409 AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder(); 410 audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC); 411 audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE); 412 notification.audioAttributes = audioAttributes.build(); 413 if (dialerRingtoneManager.shouldVibrate(context.getContentResolver())) { 414 notification.vibrate = VIBRATE_PATTERN; 415 } 416 } 417 if (dialerRingtoneManager.shouldPlayCallWaitingTone(callState)) { 418 LogUtil.v("StatusBarNotifier.buildAndSendNotification", "playing call waiting tone"); 419 dialerRingtoneManager.playCallWaitingTone(); 420 } 421 422 LogUtil.i( 423 "StatusBarNotifier.buildAndSendNotification", 424 "displaying notification for " + notificationType); 425 426 // If a notification exists, this will only update it. 427 TelecomAdapter.getInstance().startForegroundNotification(NOTIFICATION_ID, notification); 428 429 Trace.endSection(); 430 call.getLatencyReport().onNotificationShown(); 431 currentNotification = notificationType; 432 Trace.endSection(); 433 } 434 createIncomingCallNotification( DialerCall call, int state, CallAudioState callAudioState, Notification.Builder builder)435 private void createIncomingCallNotification( 436 DialerCall call, int state, CallAudioState callAudioState, Notification.Builder builder) { 437 setNotificationWhen(call, state, builder); 438 439 // Add hang up option for any active calls (active | onhold), outgoing calls (dialing). 440 if (state == DialerCall.State.ACTIVE 441 || state == DialerCall.State.ONHOLD 442 || DialerCall.State.isDialing(state)) { 443 addHangupAction(builder); 444 addSpeakerAction(builder, callAudioState); 445 } else if (state == DialerCall.State.INCOMING || state == DialerCall.State.CALL_WAITING) { 446 addDismissAction(builder); 447 if (call.isVideoCall()) { 448 addVideoCallAction(builder); 449 } else { 450 addAnswerAction(builder); 451 } 452 } 453 } 454 455 /** 456 * Sets the notification's when section as needed. For active calls, this is explicitly set as the 457 * duration of the call. For all other states, the notification will automatically show the time 458 * at which the notification was created. 459 */ setNotificationWhen(DialerCall call, int state, Notification.Builder builder)460 private void setNotificationWhen(DialerCall call, int state, Notification.Builder builder) { 461 if (state == DialerCall.State.ACTIVE) { 462 builder.setUsesChronometer(true); 463 builder.setWhen(call.getConnectTimeMillis()); 464 } else { 465 builder.setUsesChronometer(false); 466 } 467 } 468 469 /** 470 * Checks the new notification data and compares it against any notification that we are already 471 * displaying. If the data is exactly the same, we return false so that we do not issue a new 472 * notification for the exact same data. 473 */ checkForChangeAndSaveData( int icon, String content, Bitmap largeIcon, String contentTitle, int state, int videoState, int notificationType, Uri ringtone, CallAudioState callAudioState)474 private boolean checkForChangeAndSaveData( 475 int icon, 476 String content, 477 Bitmap largeIcon, 478 String contentTitle, 479 int state, 480 int videoState, 481 int notificationType, 482 Uri ringtone, 483 CallAudioState callAudioState) { 484 485 // The two are different: 486 // if new title is not null, it should be different from saved version OR 487 // if new title is null, the saved version should not be null 488 final boolean contentTitleChanged = 489 (contentTitle != null && !contentTitle.equals(savedContentTitle)) 490 || (contentTitle == null && savedContentTitle != null); 491 492 boolean largeIconChanged; 493 if (savedLargeIcon == null) { 494 largeIconChanged = largeIcon != null; 495 } else { 496 largeIconChanged = largeIcon == null || !savedLargeIcon.sameAs(largeIcon); 497 } 498 499 // any change means we are definitely updating 500 boolean retval = 501 (savedIcon != icon) 502 || !Objects.equals(savedContent, content) 503 || (callState != state) 504 || (this.videoState != videoState) 505 || largeIconChanged 506 || contentTitleChanged 507 || !Objects.equals(this.ringtone, ringtone) 508 || !Objects.equals(savedCallAudioState, callAudioState); 509 510 LogUtil.d( 511 "StatusBarNotifier.checkForChangeAndSaveData", 512 "data changed: icon: %b, content: %b, state: %b, videoState: %b, largeIcon: %b, title: %b," 513 + "ringtone: %b, audioState: %b, type: %b", 514 (savedIcon != icon), 515 !Objects.equals(savedContent, content), 516 (callState != state), 517 (this.videoState != videoState), 518 largeIconChanged, 519 contentTitleChanged, 520 !Objects.equals(this.ringtone, ringtone), 521 !Objects.equals(savedCallAudioState, callAudioState), 522 currentNotification != notificationType); 523 // If we aren't showing a notification right now or the notification type is changing, 524 // definitely do an update. 525 if (currentNotification != notificationType) { 526 if (currentNotification == NOTIFICATION_NONE) { 527 LogUtil.d( 528 "StatusBarNotifier.checkForChangeAndSaveData", "showing notification for first time."); 529 } 530 retval = true; 531 } 532 533 savedIcon = icon; 534 savedContent = content; 535 callState = state; 536 this.videoState = videoState; 537 savedLargeIcon = largeIcon; 538 savedContentTitle = contentTitle; 539 this.ringtone = ringtone; 540 savedCallAudioState = callAudioState; 541 542 if (retval) { 543 LogUtil.d( 544 "StatusBarNotifier.checkForChangeAndSaveData", "data changed. Showing notification"); 545 } 546 547 return retval; 548 } 549 550 /** Returns the main string to use in the notification. */ 551 @VisibleForTesting 552 @Nullable getContentTitle(ContactCacheEntry contactInfo, DialerCall call)553 String getContentTitle(ContactCacheEntry contactInfo, DialerCall call) { 554 if (call.isConferenceCall()) { 555 return CallerInfoUtils.getConferenceString( 556 context, call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)); 557 } 558 559 String preferredName = 560 ContactDisplayUtils.getPreferredDisplayName( 561 contactInfo.namePrimary, contactInfo.nameAlternative, contactsPreferences); 562 if (TextUtils.isEmpty(preferredName)) { 563 return TextUtils.isEmpty(contactInfo.number) 564 ? null 565 : BidiFormatter.getInstance() 566 .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR); 567 } 568 return preferredName; 569 } 570 addPersonReference( Notification.Builder builder, ContactCacheEntry contactInfo, DialerCall call)571 private void addPersonReference( 572 Notification.Builder builder, ContactCacheEntry contactInfo, DialerCall call) { 573 // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed. 574 // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid 575 // NotificationManager using it. 576 if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) { 577 builder.addPerson(contactInfo.lookupUri.toString()); 578 } else if (!TextUtils.isEmpty(call.getNumber())) { 579 builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null).toString()); 580 } 581 } 582 583 /** Gets a large icon from the contact info object to display in the notification. */ getLargeIconToDisplay( Context context, ContactCacheEntry contactInfo, DialerCall call)584 private static Bitmap getLargeIconToDisplay( 585 Context context, ContactCacheEntry contactInfo, DialerCall call) { 586 Trace.beginSection("StatusBarNotifier.getLargeIconToDisplay"); 587 Resources resources = context.getResources(); 588 Bitmap largeIcon = null; 589 if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) { 590 largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap(); 591 } 592 if (contactInfo.photo == null) { 593 int width = (int) resources.getDimension(android.R.dimen.notification_large_icon_width); 594 int height = (int) resources.getDimension(android.R.dimen.notification_large_icon_height); 595 @ContactType 596 int contactType = 597 LetterTileDrawable.getContactTypeFromPrimitives( 598 call.isVoiceMailNumber(), 599 call.isSpam(), 600 contactInfo.isBusiness, 601 call.getNumberPresentation(), 602 call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)); 603 LetterTileDrawable lettertile = new LetterTileDrawable(resources); 604 605 lettertile.setCanonicalDialerLetterTileDetails( 606 contactInfo.namePrimary == null ? contactInfo.number : contactInfo.namePrimary, 607 contactInfo.lookupKey, 608 LetterTileDrawable.SHAPE_CIRCLE, 609 contactType); 610 largeIcon = lettertile.getBitmap(width, height); 611 } 612 613 if (call.isSpam()) { 614 Drawable drawable = resources.getDrawable(R.drawable.blocked_contact, context.getTheme()); 615 largeIcon = DrawableConverter.drawableToBitmap(drawable); 616 } 617 Trace.endSection(); 618 return largeIcon; 619 } 620 getRoundedIcon(Bitmap bitmap)621 private Bitmap getRoundedIcon(Bitmap bitmap) { 622 if (bitmap == null) { 623 return null; 624 } 625 final int height = 626 (int) context.getResources().getDimension(android.R.dimen.notification_large_icon_height); 627 final int width = 628 (int) context.getResources().getDimension(android.R.dimen.notification_large_icon_width); 629 return BitmapUtil.getRoundedBitmap(bitmap, width, height); 630 } 631 632 /** 633 * Returns the appropriate icon res Id to display based on the call for which we want to display 634 * information. 635 */ 636 @VisibleForTesting getIconToDisplay(DialerCall call)637 public int getIconToDisplay(DialerCall call) { 638 // Even if both lines are in use, we only show a single item in 639 // the expanded Notifications UI. It's labeled "Ongoing call" 640 // (or "On hold" if there's only one call, and it's on hold.) 641 // Also, we don't have room to display caller-id info from two 642 // different calls. So if both lines are in use, display info 643 // from the foreground call. And if there's a ringing call, 644 // display that regardless of the state of the other calls. 645 if (call.getState() == DialerCall.State.ONHOLD) { 646 return R.drawable.quantum_ic_phone_paused_vd_theme_24; 647 } else if (call.getVideoTech().getSessionModificationState() 648 == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST 649 || call.isVideoCall()) { 650 return R.drawable.quantum_ic_videocam_white_24; 651 } else if (call.hasProperty(PROPERTY_HIGH_DEF_AUDIO) 652 && MotorolaUtils.shouldShowHdIconInNotification(context)) { 653 // Normally when a call is ongoing the status bar displays an icon of a phone. This is a 654 // helpful hint for users so they know how to get back to the call. For Sprint HD calls, we 655 // replace this icon with an icon of a phone with a HD badge. This is a carrier requirement. 656 return R.drawable.ic_hd_call; 657 } else if (call.hasProperty(Details.PROPERTY_HAS_CDMA_VOICE_PRIVACY)) { 658 return R.drawable.quantum_ic_phone_locked_vd_theme_24; 659 } 660 // If NewReturnToCall is enabled, use the static icon. The animated one will show in the bubble. 661 if (NewReturnToCallController.isEnabled(context)) { 662 return R.drawable.quantum_ic_call_vd_theme_24; 663 } else { 664 return R.drawable.on_going_call; 665 } 666 } 667 668 /** Returns the message to use with the notification. */ getContentString(DialerCall call, @UserType long userType)669 private CharSequence getContentString(DialerCall call, @UserType long userType) { 670 boolean isIncomingOrWaiting = 671 call.getState() == DialerCall.State.INCOMING 672 || call.getState() == DialerCall.State.CALL_WAITING; 673 674 if (isIncomingOrWaiting 675 && call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) { 676 677 if (!TextUtils.isEmpty(call.getChildNumber())) { 678 return context.getString(R.string.child_number, call.getChildNumber()); 679 } else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) { 680 return call.getCallSubject(); 681 } 682 } 683 684 int resId = R.string.notification_ongoing_call; 685 String wifiBrand = context.getString(R.string.notification_call_wifi_brand); 686 if (call.hasProperty(Details.PROPERTY_WIFI)) { 687 resId = R.string.notification_ongoing_call_wifi_template; 688 } 689 690 if (isIncomingOrWaiting) { 691 if (call.isSpam()) { 692 resId = R.string.notification_incoming_spam_call; 693 } else if (shouldShowEnrichedCallNotification(call.getEnrichedCallSession())) { 694 resId = getECIncomingCallText(call.getEnrichedCallSession()); 695 } else if (call.hasProperty(Details.PROPERTY_WIFI)) { 696 resId = R.string.notification_incoming_call_wifi_template; 697 } else if (call.getAccountHandle() != null && hasMultiplePhoneAccounts(call)) { 698 return getMultiSimIncomingText(call); 699 } else if (call.isVideoCall()) { 700 resId = R.string.notification_incoming_video_call; 701 } else { 702 resId = R.string.notification_incoming_call; 703 } 704 } else if (call.getState() == DialerCall.State.ONHOLD) { 705 resId = R.string.notification_on_hold; 706 } else if (call.isVideoCall()) { 707 resId = 708 call.getVideoTech().isPaused() 709 ? R.string.notification_ongoing_paused_video_call 710 : R.string.notification_ongoing_video_call; 711 } else if (DialerCall.State.isDialing(call.getState())) { 712 resId = R.string.notification_dialing; 713 } else if (call.getVideoTech().getSessionModificationState() 714 == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 715 resId = R.string.notification_requesting_video_call; 716 } 717 718 // Is the call placed through work connection service. 719 boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL); 720 if (userType == ContactsUtils.USER_TYPE_WORK || isWorkCall) { 721 resId = getWorkStringFromPersonalString(resId); 722 wifiBrand = context.getString(R.string.notification_call_wifi_work_brand); 723 } 724 725 if (resId == R.string.notification_incoming_call_wifi_template 726 || resId == R.string.notification_ongoing_call_wifi_template) { 727 // TODO(a bug): Potentially apply this template logic everywhere. 728 return context.getString(resId, wifiBrand); 729 } 730 731 return context.getString(resId); 732 } 733 shouldShowEnrichedCallNotification(Session session)734 private boolean shouldShowEnrichedCallNotification(Session session) { 735 if (session == null) { 736 return false; 737 } 738 return session.getMultimediaData().hasData() || session.getMultimediaData().isImportant(); 739 } 740 getECIncomingCallText(Session session)741 private int getECIncomingCallText(Session session) { 742 int resId; 743 MultimediaData data = session.getMultimediaData(); 744 boolean hasImage = data.hasImageData(); 745 boolean hasSubject = !TextUtils.isEmpty(data.getText()); 746 boolean hasMap = data.getLocation() != null; 747 if (data.isImportant()) { 748 if (hasMap) { 749 if (hasImage) { 750 if (hasSubject) { 751 resId = R.string.important_notification_incoming_call_with_photo_message_location; 752 } else { 753 resId = R.string.important_notification_incoming_call_with_photo_location; 754 } 755 } else if (hasSubject) { 756 resId = R.string.important_notification_incoming_call_with_message_location; 757 } else { 758 resId = R.string.important_notification_incoming_call_with_location; 759 } 760 } else if (hasImage) { 761 if (hasSubject) { 762 resId = R.string.important_notification_incoming_call_with_photo_message; 763 } else { 764 resId = R.string.important_notification_incoming_call_with_photo; 765 } 766 } else if (hasSubject) { 767 resId = R.string.important_notification_incoming_call_with_message; 768 } else { 769 resId = R.string.important_notification_incoming_call; 770 } 771 if (context.getString(resId).length() > 50) { 772 resId = R.string.important_notification_incoming_call_attachments; 773 } 774 } else { 775 if (hasMap) { 776 if (hasImage) { 777 if (hasSubject) { 778 resId = R.string.notification_incoming_call_with_photo_message_location; 779 } else { 780 resId = R.string.notification_incoming_call_with_photo_location; 781 } 782 } else if (hasSubject) { 783 resId = R.string.notification_incoming_call_with_message_location; 784 } else { 785 resId = R.string.notification_incoming_call_with_location; 786 } 787 } else if (hasImage) { 788 if (hasSubject) { 789 resId = R.string.notification_incoming_call_with_photo_message; 790 } else { 791 resId = R.string.notification_incoming_call_with_photo; 792 } 793 } else { 794 resId = R.string.notification_incoming_call_with_message; 795 } 796 } 797 if (context.getString(resId).length() > 50) { 798 resId = R.string.notification_incoming_call_attachments; 799 } 800 return resId; 801 } 802 getMultiSimIncomingText(DialerCall call)803 private CharSequence getMultiSimIncomingText(DialerCall call) { 804 PhoneAccount phoneAccount = 805 context.getSystemService(TelecomManager.class).getPhoneAccount(call.getAccountHandle()); 806 SpannableString string = 807 new SpannableString( 808 context.getString( 809 R.string.notification_incoming_call_mutli_sim, phoneAccount.getLabel())); 810 int accountStart = string.toString().lastIndexOf(phoneAccount.getLabel().toString()); 811 int accountEnd = accountStart + phoneAccount.getLabel().length(); 812 813 string.setSpan( 814 new ForegroundColorSpan(phoneAccount.getHighlightColor()), 815 accountStart, 816 accountEnd, 817 Spannable.SPAN_INCLUSIVE_EXCLUSIVE); 818 return string; 819 } 820 821 /** Gets the most relevant call to display in the notification. */ getCallToShow(CallList callList)822 private DialerCall getCallToShow(CallList callList) { 823 if (callList == null) { 824 return null; 825 } 826 DialerCall call = callList.getIncomingCall(); 827 if (call == null) { 828 call = callList.getOutgoingCall(); 829 } 830 if (call == null) { 831 call = callList.getVideoUpgradeRequestCall(); 832 } 833 if (call == null) { 834 call = callList.getActiveOrBackgroundCall(); 835 } 836 return call; 837 } 838 getActionText(@tringRes int stringRes, @ColorRes int colorRes)839 private Spannable getActionText(@StringRes int stringRes, @ColorRes int colorRes) { 840 Spannable spannable = new SpannableString(context.getText(stringRes)); 841 if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) { 842 // This will only work for cases where the Notification.Builder has a fullscreen intent set 843 // Notification.Builder that does not have a full screen intent will take the color of the 844 // app and the following leads to a no-op. 845 spannable.setSpan( 846 new ForegroundColorSpan(context.getColor(colorRes)), 0, spannable.length(), 0); 847 } 848 return spannable; 849 } 850 addAnswerAction(Notification.Builder builder)851 private void addAnswerAction(Notification.Builder builder) { 852 LogUtil.d( 853 "StatusBarNotifier.addAnswerAction", 854 "will show \"answer\" action in the incoming call Notification"); 855 PendingIntent answerVoicePendingIntent = 856 createNotificationPendingIntent(context, ACTION_ANSWER_VOICE_INCOMING_CALL); 857 builder.addAction( 858 new Notification.Action.Builder( 859 Icon.createWithResource(context, R.drawable.quantum_ic_call_white_24), 860 getActionText( 861 R.string.notification_action_answer, R.color.notification_action_accept), 862 answerVoicePendingIntent) 863 .build()); 864 } 865 addDismissAction(Notification.Builder builder)866 private void addDismissAction(Notification.Builder builder) { 867 LogUtil.d( 868 "StatusBarNotifier.addDismissAction", 869 "will show \"decline\" action in the incoming call Notification"); 870 PendingIntent declinePendingIntent = 871 createNotificationPendingIntent(context, ACTION_DECLINE_INCOMING_CALL); 872 builder.addAction( 873 new Notification.Action.Builder( 874 Icon.createWithResource(context, R.drawable.quantum_ic_close_white_24), 875 getActionText( 876 R.string.notification_action_dismiss, R.color.notification_action_dismiss), 877 declinePendingIntent) 878 .build()); 879 } 880 addHangupAction(Notification.Builder builder)881 private void addHangupAction(Notification.Builder builder) { 882 LogUtil.d( 883 "StatusBarNotifier.addHangupAction", 884 "will show \"hang-up\" action in the ongoing active call Notification"); 885 PendingIntent hangupPendingIntent = 886 createNotificationPendingIntent(context, ACTION_HANG_UP_ONGOING_CALL); 887 builder.addAction( 888 new Notification.Action.Builder( 889 Icon.createWithResource(context, R.drawable.quantum_ic_call_end_white_24), 890 context.getText(R.string.notification_action_end_call), 891 hangupPendingIntent) 892 .build()); 893 } 894 addSpeakerAction(Notification.Builder builder, CallAudioState callAudioState)895 private void addSpeakerAction(Notification.Builder builder, CallAudioState callAudioState) { 896 if ((callAudioState.getSupportedRouteMask() & CallAudioState.ROUTE_BLUETOOTH) 897 == CallAudioState.ROUTE_BLUETOOTH) { 898 // Don't add speaker button if bluetooth is connected 899 return; 900 } 901 if (callAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) { 902 addSpeakerOffAction(builder); 903 } else if ((callAudioState.getRoute() & CallAudioState.ROUTE_WIRED_OR_EARPIECE) != 0) { 904 addSpeakerOnAction(builder); 905 } 906 } 907 addSpeakerOnAction(Notification.Builder builder)908 private void addSpeakerOnAction(Notification.Builder builder) { 909 LogUtil.d( 910 "StatusBarNotifier.addSpeakerOnAction", 911 "will show \"Speaker on\" action in the ongoing active call Notification"); 912 PendingIntent speakerOnPendingIntent = 913 createNotificationPendingIntent(context, ACTION_TURN_ON_SPEAKER); 914 builder.addAction( 915 new Notification.Action.Builder( 916 Icon.createWithResource(context, R.drawable.quantum_ic_volume_up_white_24), 917 context.getText(R.string.notification_action_speaker_on), 918 speakerOnPendingIntent) 919 .build()); 920 } 921 addSpeakerOffAction(Notification.Builder builder)922 private void addSpeakerOffAction(Notification.Builder builder) { 923 LogUtil.d( 924 "StatusBarNotifier.addSpeakerOffAction", 925 "will show \"Speaker off\" action in the ongoing active call Notification"); 926 PendingIntent speakerOffPendingIntent = 927 createNotificationPendingIntent(context, ACTION_TURN_OFF_SPEAKER); 928 builder.addAction( 929 new Notification.Action.Builder( 930 Icon.createWithResource(context, R.drawable.quantum_ic_phone_in_talk_white_24), 931 context.getText(R.string.notification_action_speaker_off), 932 speakerOffPendingIntent) 933 .build()); 934 } 935 addVideoCallAction(Notification.Builder builder)936 private void addVideoCallAction(Notification.Builder builder) { 937 LogUtil.i( 938 "StatusBarNotifier.addVideoCallAction", 939 "will show \"video\" action in the incoming call Notification"); 940 PendingIntent answerVideoPendingIntent = 941 createNotificationPendingIntent(context, ACTION_ANSWER_VIDEO_INCOMING_CALL); 942 builder.addAction( 943 new Notification.Action.Builder( 944 Icon.createWithResource(context, R.drawable.quantum_ic_videocam_white_24), 945 getActionText( 946 R.string.notification_action_answer_video, 947 R.color.notification_action_answer_video), 948 answerVideoPendingIntent) 949 .build()); 950 } 951 addAcceptUpgradeRequestAction(Notification.Builder builder)952 private void addAcceptUpgradeRequestAction(Notification.Builder builder) { 953 LogUtil.i( 954 "StatusBarNotifier.addAcceptUpgradeRequestAction", 955 "will show \"accept upgrade\" action in the incoming call Notification"); 956 PendingIntent acceptVideoPendingIntent = 957 createNotificationPendingIntent(context, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST); 958 builder.addAction( 959 new Notification.Action.Builder( 960 Icon.createWithResource(context, R.drawable.quantum_ic_videocam_white_24), 961 getActionText( 962 R.string.notification_action_accept, R.color.notification_action_accept), 963 acceptVideoPendingIntent) 964 .build()); 965 } 966 addDismissUpgradeRequestAction(Notification.Builder builder)967 private void addDismissUpgradeRequestAction(Notification.Builder builder) { 968 LogUtil.i( 969 "StatusBarNotifier.addDismissUpgradeRequestAction", 970 "will show \"dismiss upgrade\" action in the incoming call Notification"); 971 PendingIntent declineVideoPendingIntent = 972 createNotificationPendingIntent(context, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST); 973 builder.addAction( 974 new Notification.Action.Builder( 975 Icon.createWithResource(context, R.drawable.quantum_ic_videocam_white_24), 976 getActionText( 977 R.string.notification_action_dismiss, R.color.notification_action_dismiss), 978 declineVideoPendingIntent) 979 .build()); 980 } 981 982 /** Adds fullscreen intent to the builder. */ configureFullScreenIntent(Notification.Builder builder, PendingIntent intent)983 private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent) { 984 // Ok, we actually want to launch the incoming call 985 // UI at this point (in addition to simply posting a notification 986 // to the status bar). Setting fullScreenIntent will cause 987 // the InCallScreen to be launched immediately *unless* the 988 // current foreground activity is marked as "immersive". 989 LogUtil.d("StatusBarNotifier.configureFullScreenIntent", "setting fullScreenIntent: " + intent); 990 builder.setFullScreenIntent(intent, true); 991 } 992 getNotificationBuilder()993 private Notification.Builder getNotificationBuilder() { 994 final Notification.Builder builder = new Notification.Builder(context); 995 builder.setOngoing(true); 996 builder.setOnlyAlertOnce(true); 997 // This will be ignored on O+ and handled by the channel 998 // noinspection deprecation 999 builder.setPriority(Notification.PRIORITY_HIGH); 1000 1001 return builder; 1002 } 1003 createLaunchPendingIntent(boolean isFullScreen)1004 private PendingIntent createLaunchPendingIntent(boolean isFullScreen) { 1005 Intent intent = 1006 InCallActivity.getIntent( 1007 context, false /* showDialpad */, false /* newOutgoingCall */, isFullScreen); 1008 1009 int requestCode = InCallActivity.PendingIntentRequestCodes.NON_FULL_SCREEN; 1010 if (isFullScreen) { 1011 // Use a unique request code so that the pending intent isn't clobbered by the 1012 // non-full screen pending intent. 1013 requestCode = InCallActivity.PendingIntentRequestCodes.FULL_SCREEN; 1014 } 1015 1016 // PendingIntent that can be used to launch the InCallActivity. The 1017 // system fires off this intent if the user pulls down the windowshade 1018 // and clicks the notification's expanded view. It's also used to 1019 // launch the InCallActivity immediately when when there's an incoming 1020 // call (see the "fullScreenIntent" field below). 1021 return PendingIntent.getActivity(context, requestCode, intent, 0); 1022 } 1023 setStatusBarCallListener(StatusBarCallListener listener)1024 private void setStatusBarCallListener(StatusBarCallListener listener) { 1025 if (statusBarCallListener != null) { 1026 statusBarCallListener.cleanup(); 1027 } 1028 statusBarCallListener = listener; 1029 } 1030 hasMultiplePhoneAccounts(DialerCall call)1031 private boolean hasMultiplePhoneAccounts(DialerCall call) { 1032 if (call.getCallCapableAccounts() == null) { 1033 return false; 1034 } 1035 return call.getCallCapableAccounts().size() > 1; 1036 } 1037 1038 @Override 1039 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) onContactInfoComplete(String callId, ContactCacheEntry entry)1040 public void onContactInfoComplete(String callId, ContactCacheEntry entry) { 1041 DialerCall call = CallList.getInstance().getCallById(callId); 1042 if (call != null) { 1043 call.getLogState().contactLookupResult = entry.contactLookupResult; 1044 buildAndSendNotification(CallList.getInstance(), call, entry); 1045 } 1046 } 1047 1048 @Override 1049 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) onImageLoadComplete(String callId, ContactCacheEntry entry)1050 public void onImageLoadComplete(String callId, ContactCacheEntry entry) { 1051 DialerCall call = CallList.getInstance().getCallById(callId); 1052 if (call != null) { 1053 buildAndSendNotification(CallList.getInstance(), call, entry); 1054 } 1055 } 1056 1057 private class StatusBarCallListener implements DialerCallListener { 1058 1059 private DialerCall dialerCall; 1060 StatusBarCallListener(DialerCall dialerCall)1061 StatusBarCallListener(DialerCall dialerCall) { 1062 this.dialerCall = dialerCall; 1063 this.dialerCall.addListener(this); 1064 } 1065 cleanup()1066 void cleanup() { 1067 dialerCall.removeListener(this); 1068 } 1069 1070 @Override onDialerCallDisconnect()1071 public void onDialerCallDisconnect() {} 1072 1073 @Override onDialerCallUpdate()1074 public void onDialerCallUpdate() { 1075 if (CallList.getInstance().getIncomingCall() == null) { 1076 dialerRingtoneManager.stopCallWaitingTone(); 1077 } 1078 } 1079 1080 @Override onDialerCallChildNumberChange()1081 public void onDialerCallChildNumberChange() {} 1082 1083 @Override onDialerCallLastForwardedNumberChange()1084 public void onDialerCallLastForwardedNumberChange() {} 1085 1086 @Override onDialerCallUpgradeToVideo()1087 public void onDialerCallUpgradeToVideo() {} 1088 1089 @Override onWiFiToLteHandover()1090 public void onWiFiToLteHandover() {} 1091 1092 @Override onHandoverToWifiFailure()1093 public void onHandoverToWifiFailure() {} 1094 1095 @Override onInternationalCallOnWifi()1096 public void onInternationalCallOnWifi() {} 1097 1098 @Override onEnrichedCallSessionUpdate()1099 public void onEnrichedCallSessionUpdate() {} 1100 1101 /** 1102 * Responds to changes in the session modification state for the call by dismissing the status 1103 * bar notification as required. 1104 */ 1105 @Override onDialerCallSessionModificationStateChange()1106 public void onDialerCallSessionModificationStateChange() { 1107 if (dialerCall.getVideoTech().getSessionModificationState() 1108 == SessionModificationState.NO_REQUEST) { 1109 cleanup(); 1110 updateNotification(); 1111 } 1112 } 1113 } 1114 } 1115