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