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