CallCardPresenter.java revision 183cb71663320f16149d83eeebaff7795a4b55f2
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 17package com.android.incallui; 18 19import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL; 20 21import android.Manifest; 22import android.content.Context; 23import android.content.Intent; 24import android.content.IntentFilter; 25import android.content.pm.ApplicationInfo; 26import android.content.pm.PackageManager; 27import android.graphics.drawable.Drawable; 28import android.hardware.display.DisplayManager; 29import android.os.BatteryManager; 30import android.os.Handler; 31import android.os.Trace; 32import android.support.annotation.NonNull; 33import android.support.annotation.Nullable; 34import android.support.v4.app.Fragment; 35import android.support.v4.content.ContextCompat; 36import android.telecom.Call.Details; 37import android.telecom.StatusHints; 38import android.telecom.TelecomManager; 39import android.text.TextUtils; 40import android.view.Display; 41import android.view.View; 42import android.view.accessibility.AccessibilityEvent; 43import android.view.accessibility.AccessibilityManager; 44import com.android.contacts.common.ContactsUtils; 45import com.android.contacts.common.preference.ContactsPreferences; 46import com.android.contacts.common.util.ContactDisplayUtils; 47import com.android.dialer.common.Assert; 48import com.android.dialer.common.LogUtil; 49import com.android.dialer.compat.ActivityCompat; 50import com.android.dialer.configprovider.ConfigProviderBindings; 51import com.android.dialer.feedback.FeedbackComponent; 52import com.android.dialer.logging.DialerImpression; 53import com.android.dialer.logging.Logger; 54import com.android.dialer.multimedia.MultimediaData; 55import com.android.dialer.oem.MotorolaUtils; 56import com.android.dialer.phonenumberutil.PhoneNumberHelper; 57import com.android.dialer.postcall.PostCall; 58import com.android.incallui.ContactInfoCache.ContactCacheEntry; 59import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; 60import com.android.incallui.InCallPresenter.InCallDetailsListener; 61import com.android.incallui.InCallPresenter.InCallEventListener; 62import com.android.incallui.InCallPresenter.InCallState; 63import com.android.incallui.InCallPresenter.InCallStateListener; 64import com.android.incallui.InCallPresenter.IncomingCallListener; 65import com.android.incallui.call.CallList; 66import com.android.incallui.call.DialerCall; 67import com.android.incallui.call.DialerCall.State; 68import com.android.incallui.call.DialerCallListener; 69import com.android.incallui.calllocation.CallLocation; 70import com.android.incallui.calllocation.CallLocationComponent; 71import com.android.incallui.incall.protocol.ContactPhotoType; 72import com.android.incallui.incall.protocol.InCallScreen; 73import com.android.incallui.incall.protocol.InCallScreenDelegate; 74import com.android.incallui.incall.protocol.PrimaryCallState; 75import com.android.incallui.incall.protocol.PrimaryCallState.ButtonState; 76import com.android.incallui.incall.protocol.PrimaryInfo; 77import com.android.incallui.incall.protocol.SecondaryInfo; 78import com.android.incallui.videotech.utils.SessionModificationState; 79import java.lang.ref.WeakReference; 80 81/** 82 * Controller for the Call Card Fragment. This class listens for changes to InCallState and passes 83 * it along to the fragment. 84 */ 85public class CallCardPresenter 86 implements InCallStateListener, 87 IncomingCallListener, 88 InCallDetailsListener, 89 InCallEventListener, 90 InCallScreenDelegate, 91 DialerCallListener { 92 93 /** 94 * Amount of time to wait before sending an announcement via the accessibility manager. When the 95 * call state changes to an outgoing or incoming state for the first time, the UI can often be 96 * changing due to call updates or contact lookup. This allows the UI to settle to a stable state 97 * to ensure that the correct information is announced. 98 */ 99 private static final long ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS = 500; 100 101 /** Flag to allow the user's current location to be shown during emergency calls. */ 102 private static final String CONFIG_ENABLE_EMERGENCY_LOCATION = "config_enable_emergency_location"; 103 104 private static final boolean CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT = true; 105 106 /** 107 * Make it possible to not get location during an emergency call if the battery is too low, since 108 * doing so could trigger gps and thus potentially cause the phone to die in the middle of the 109 * call. 110 */ 111 private static final String CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION = 112 "min_battery_percent_for_emergency_location"; 113 114 private static final long CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION_DEFAULT = 10; 115 116 private final Context context; 117 private final Handler handler = new Handler(); 118 119 private DialerCall primary; 120 private DialerCall secondary; 121 private ContactCacheEntry primaryContactInfo; 122 private ContactCacheEntry secondaryContactInfo; 123 @Nullable private ContactsPreferences contactsPreferences; 124 private boolean isFullscreen = false; 125 private InCallScreen inCallScreen; 126 private boolean isInCallScreenReady; 127 private boolean shouldSendAccessibilityEvent; 128 129 @NonNull private final CallLocation callLocation; 130 private final Runnable sendAccessibilityEventRunnable = 131 new Runnable() { 132 @Override 133 public void run() { 134 shouldSendAccessibilityEvent = !sendAccessibilityEvent(context, getUi()); 135 LogUtil.i( 136 "CallCardPresenter.sendAccessibilityEventRunnable", 137 "still should send: %b", 138 shouldSendAccessibilityEvent); 139 if (!shouldSendAccessibilityEvent) { 140 handler.removeCallbacks(this); 141 } 142 } 143 }; 144 145 public CallCardPresenter(Context context) { 146 LogUtil.i("CallCardPresenter.constructor", null); 147 this.context = Assert.isNotNull(context).getApplicationContext(); 148 callLocation = CallLocationComponent.get(this.context).getCallLocation(); 149 } 150 151 private static boolean hasCallSubject(DialerCall call) { 152 return !TextUtils.isEmpty(call.getCallSubject()); 153 } 154 155 private void addCallFeedbackListener(Context context) { 156 LogUtil.d("CallCardPresenter.addCallFeedbackListener", "Adding call feedback listener"); 157 CallList.getInstance().addListener(FeedbackComponent.get(context).getCallFeedbackListener()); 158 } 159 160 @Override 161 public void onInCallScreenDelegateInit(InCallScreen inCallScreen) { 162 Assert.isNotNull(inCallScreen); 163 this.inCallScreen = inCallScreen; 164 contactsPreferences = ContactsPreferencesFactory.newContactsPreferences(context); 165 166 // Call may be null if disconnect happened already. 167 DialerCall call = CallList.getInstance().getFirstCall(); 168 if (call != null) { 169 primary = call; 170 if (shouldShowNoteSentToast(primary)) { 171 this.inCallScreen.showNoteSentToast(); 172 } 173 call.addListener(this); 174 addCallFeedbackListener(context); 175 // start processing lookups right away. 176 if (!call.isConferenceCall()) { 177 startContactInfoSearch(call, true, call.getState() == DialerCall.State.INCOMING); 178 } else { 179 updateContactEntry(null, true); 180 } 181 } 182 183 onStateChange(null, InCallPresenter.getInstance().getInCallState(), CallList.getInstance()); 184 } 185 186 @Override 187 public void onInCallScreenReady() { 188 LogUtil.i("CallCardPresenter.onInCallScreenReady", null); 189 Assert.checkState(!isInCallScreenReady); 190 if (contactsPreferences != null) { 191 contactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); 192 } 193 194 // Contact search may have completed before ui is ready. 195 if (primaryContactInfo != null) { 196 updatePrimaryDisplayInfo(); 197 } 198 199 // Register for call state changes last 200 InCallPresenter.getInstance().addListener(this); 201 InCallPresenter.getInstance().addIncomingCallListener(this); 202 InCallPresenter.getInstance().addDetailsListener(this); 203 InCallPresenter.getInstance().addInCallEventListener(this); 204 isInCallScreenReady = true; 205 206 // Log location impressions 207 if (isOutgoingEmergencyCall(primary)) { 208 Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_NEW_EMERGENCY_CALL); 209 } else if (isIncomingEmergencyCall(primary) || isIncomingEmergencyCall(secondary)) { 210 Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_CALLBACK); 211 } 212 213 // Showing the location may have been skipped if the UI wasn't ready during previous layout. 214 if (shouldShowLocation()) { 215 inCallScreen.showLocationUi(getLocationFragment()); 216 217 // Log location impressions 218 if (!hasLocationPermission()) { 219 Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_NO_LOCATION_PERMISSION); 220 } else if (isBatteryTooLowForEmergencyLocation()) { 221 Logger.get(context) 222 .logImpression(DialerImpression.Type.EMERGENCY_BATTERY_TOO_LOW_TO_GET_LOCATION); 223 } else if (!callLocation.canGetLocation(context)) { 224 Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_CANT_GET_LOCATION); 225 } 226 } 227 } 228 229 @Override 230 public void onInCallScreenUnready() { 231 LogUtil.i("CallCardPresenter.onInCallScreenUnready", null); 232 Assert.checkState(isInCallScreenReady); 233 234 // stop getting call state changes 235 InCallPresenter.getInstance().removeListener(this); 236 InCallPresenter.getInstance().removeIncomingCallListener(this); 237 InCallPresenter.getInstance().removeDetailsListener(this); 238 InCallPresenter.getInstance().removeInCallEventListener(this); 239 if (primary != null) { 240 primary.removeListener(this); 241 } 242 243 callLocation.close(); 244 245 primary = null; 246 primaryContactInfo = null; 247 secondaryContactInfo = null; 248 isInCallScreenReady = false; 249 } 250 251 @Override 252 public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) { 253 // same logic should happen as with onStateChange() 254 onStateChange(oldState, newState, CallList.getInstance()); 255 } 256 257 @Override 258 public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { 259 Trace.beginSection("CallCardPresenter.onStateChange"); 260 LogUtil.v("CallCardPresenter.onStateChange", "oldState: %s, newState: %s", oldState, newState); 261 if (inCallScreen == null) { 262 Trace.endSection(); 263 return; 264 } 265 266 DialerCall primary = null; 267 DialerCall secondary = null; 268 269 if (newState == InCallState.INCOMING) { 270 primary = callList.getIncomingCall(); 271 } else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) { 272 primary = callList.getOutgoingCall(); 273 if (primary == null) { 274 primary = callList.getPendingOutgoingCall(); 275 } 276 277 // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the 278 // highest priority call to display as the secondary call. 279 secondary = getCallToDisplay(callList, null, true); 280 } else if (newState == InCallState.INCALL) { 281 primary = getCallToDisplay(callList, null, false); 282 secondary = getCallToDisplay(callList, primary, true); 283 } 284 285 LogUtil.v("CallCardPresenter.onStateChange", "primary call: " + primary); 286 LogUtil.v("CallCardPresenter.onStateChange", "secondary call: " + secondary); 287 288 final boolean primaryChanged = 289 !(DialerCall.areSame(this.primary, primary) 290 && DialerCall.areSameNumber(this.primary, primary)); 291 final boolean secondaryChanged = 292 !(DialerCall.areSame(this.secondary, secondary) 293 && DialerCall.areSameNumber(this.secondary, secondary)); 294 295 this.secondary = secondary; 296 DialerCall previousPrimary = this.primary; 297 this.primary = primary; 298 299 if (this.primary != null) { 300 InCallPresenter.getInstance().onForegroundCallChanged(this.primary); 301 inCallScreen.updateInCallScreenColors(); 302 } 303 304 if (primaryChanged && shouldShowNoteSentToast(primary)) { 305 inCallScreen.showNoteSentToast(); 306 } 307 308 // Refresh primary call information if either: 309 // 1. Primary call changed. 310 // 2. The call's ability to manage conference has changed. 311 if (shouldRefreshPrimaryInfo(primaryChanged)) { 312 // primary call has changed 313 if (previousPrimary != null) { 314 previousPrimary.removeListener(this); 315 } 316 this.primary.addListener(this); 317 318 primaryContactInfo = 319 ContactInfoCache.buildCacheEntryFromCall( 320 context, this.primary, this.primary.getState() == DialerCall.State.INCOMING); 321 updatePrimaryDisplayInfo(); 322 maybeStartSearch(this.primary, true); 323 } 324 325 if (previousPrimary != null && this.primary == null) { 326 previousPrimary.removeListener(this); 327 } 328 329 if (secondaryChanged) { 330 if (this.secondary == null) { 331 // Secondary call may have ended. Update the ui. 332 secondaryContactInfo = null; 333 updateSecondaryDisplayInfo(); 334 } else { 335 // secondary call has changed 336 secondaryContactInfo = 337 ContactInfoCache.buildCacheEntryFromCall( 338 context, this.secondary, this.secondary.getState() == DialerCall.State.INCOMING); 339 updateSecondaryDisplayInfo(); 340 maybeStartSearch(this.secondary, false); 341 } 342 } 343 344 // Set the call state 345 int callState = DialerCall.State.IDLE; 346 if (this.primary != null) { 347 callState = this.primary.getState(); 348 updatePrimaryCallState(); 349 } else { 350 getUi().setCallState(PrimaryCallState.createEmptyPrimaryCallState()); 351 } 352 353 maybeShowManageConferenceCallButton(); 354 355 // Hide the end call button instantly if we're receiving an incoming call. 356 getUi() 357 .setEndCallButtonEnabled( 358 shouldShowEndCallButton(this.primary, callState), 359 callState != DialerCall.State.INCOMING /* animate */); 360 361 maybeSendAccessibilityEvent(oldState, newState, primaryChanged); 362 Trace.endSection(); 363 } 364 365 @Override 366 public void onDetailsChanged(DialerCall call, Details details) { 367 updatePrimaryCallState(); 368 369 if (call.can(Details.CAPABILITY_MANAGE_CONFERENCE) 370 != details.can(Details.CAPABILITY_MANAGE_CONFERENCE)) { 371 maybeShowManageConferenceCallButton(); 372 } 373 } 374 375 @Override 376 public void onDialerCallDisconnect() {} 377 378 @Override 379 public void onDialerCallUpdate() { 380 // No-op; specific call updates handled elsewhere. 381 } 382 383 @Override 384 public void onWiFiToLteHandover() {} 385 386 @Override 387 public void onHandoverToWifiFailure() {} 388 389 @Override 390 public void onInternationalCallOnWifi() {} 391 392 @Override 393 public void onEnrichedCallSessionUpdate() { 394 LogUtil.enterBlock("CallCardPresenter.onEnrichedCallSessionUpdate"); 395 updatePrimaryDisplayInfo(); 396 } 397 398 /** Handles a change to the child number by refreshing the primary call info. */ 399 @Override 400 public void onDialerCallChildNumberChange() { 401 LogUtil.v("CallCardPresenter.onDialerCallChildNumberChange", ""); 402 403 if (primary == null) { 404 return; 405 } 406 updatePrimaryDisplayInfo(); 407 } 408 409 /** Handles a change to the last forwarding number by refreshing the primary call info. */ 410 @Override 411 public void onDialerCallLastForwardedNumberChange() { 412 LogUtil.v("CallCardPresenter.onDialerCallLastForwardedNumberChange", ""); 413 414 if (primary == null) { 415 return; 416 } 417 updatePrimaryDisplayInfo(); 418 updatePrimaryCallState(); 419 } 420 421 @Override 422 public void onDialerCallUpgradeToVideo() {} 423 424 /** Handles a change to the session modification state for a call. */ 425 @Override 426 public void onDialerCallSessionModificationStateChange() { 427 LogUtil.enterBlock("CallCardPresenter.onDialerCallSessionModificationStateChange"); 428 429 if (primary == null) { 430 return; 431 } 432 getUi() 433 .setEndCallButtonEnabled( 434 primary.getVideoTech().getSessionModificationState() 435 != SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST, 436 true /* shouldAnimate */); 437 updatePrimaryCallState(); 438 } 439 440 private boolean shouldRefreshPrimaryInfo(boolean primaryChanged) { 441 if (primary == null) { 442 return false; 443 } 444 return primaryChanged 445 || inCallScreen.isManageConferenceVisible() != shouldShowManageConference(); 446 } 447 448 private void updatePrimaryCallState() { 449 if (getUi() != null && primary != null) { 450 boolean isWorkCall = 451 primary.hasProperty(PROPERTY_ENTERPRISE_CALL) 452 || (primaryContactInfo != null 453 && primaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK); 454 boolean isHdAudioCall = 455 isPrimaryCallActive() && primary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO); 456 boolean isAttemptingHdAudioCall = 457 !isHdAudioCall 458 && !primary.hasProperty(DialerCall.PROPERTY_CODEC_KNOWN) 459 && MotorolaUtils.shouldBlinkHdIconWhenConnectingCall(context); 460 461 boolean isBusiness = primaryContactInfo != null && primaryContactInfo.isBusiness; 462 463 // Check for video state change and update the visibility of the contact photo. The contact 464 // photo is hidden when the incoming video surface is shown. 465 // The contact photo visibility can also change in setPrimary(). 466 boolean shouldShowContactPhoto = 467 !VideoCallPresenter.showIncomingVideo(primary.getVideoState(), primary.getState()); 468 getUi() 469 .setCallState( 470 new PrimaryCallState( 471 primary.getState(), 472 primary.isVideoCall(), 473 primary.getVideoTech().getSessionModificationState(), 474 primary.getDisconnectCause(), 475 getConnectionLabel(), 476 getCallStateIcon(), 477 getGatewayNumber(), 478 shouldShowCallSubject(primary) ? primary.getCallSubject() : null, 479 PhoneNumberHelper.formatNumber( 480 primary.getCallbackNumber(), primary.getSimCountryIso()), 481 primary.hasProperty(Details.PROPERTY_WIFI), 482 primary.isConferenceCall() 483 && !primary.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE), 484 isWorkCall, 485 isAttemptingHdAudioCall, 486 isHdAudioCall, 487 !TextUtils.isEmpty(primary.getLastForwardedNumber()), 488 shouldShowContactPhoto, 489 primary.getConnectTimeMillis(), 490 primary.isVoiceMailNumber(), 491 primary.isRemotelyHeld(), 492 isBusiness, 493 supports2ndCallOnHold(), 494 getSwapToSecondaryButtonState(), 495 primary.isAssistedDialed(), 496 null, 497 primary.getAssistedDialingExtras())); 498 499 InCallActivity activity = 500 (InCallActivity) (inCallScreen.getInCallScreenFragment().getActivity()); 501 if (activity != null) { 502 activity.onPrimaryCallStateChanged(); 503 } 504 } 505 } 506 507 private @ButtonState int getSwapToSecondaryButtonState() { 508 if (secondary == null) { 509 return ButtonState.NOT_SUPPORT; 510 } 511 if (primary.getState() == State.ACTIVE) { 512 return ButtonState.ENABLED; 513 } 514 return ButtonState.DISABLED; 515 } 516 517 /** Only show the conference call button if we can manage the conference. */ 518 private void maybeShowManageConferenceCallButton() { 519 getUi().showManageConferenceCallButton(shouldShowManageConference()); 520 } 521 522 /** 523 * Determines if the manage conference button should be visible, based on the current primary 524 * call. 525 * 526 * @return {@code True} if the manage conference button should be visible. 527 */ 528 private boolean shouldShowManageConference() { 529 if (primary == null) { 530 return false; 531 } 532 533 return primary.can(android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE) && !isFullscreen; 534 } 535 536 private boolean supports2ndCallOnHold() { 537 DialerCall firstCall = CallList.getInstance().getActiveOrBackgroundCall(); 538 DialerCall incomingCall = CallList.getInstance().getIncomingCall(); 539 if (firstCall != null && incomingCall != null && firstCall != incomingCall) { 540 return incomingCall.can(Details.CAPABILITY_HOLD); 541 } 542 return true; 543 } 544 545 @Override 546 public void onCallStateButtonClicked() { 547 Intent broadcastIntent = Bindings.get(context).getCallStateButtonBroadcastIntent(context); 548 if (broadcastIntent != null) { 549 LogUtil.v( 550 "CallCardPresenter.onCallStateButtonClicked", 551 "sending call state button broadcast: " + broadcastIntent); 552 context.sendBroadcast(broadcastIntent, Manifest.permission.READ_PHONE_STATE); 553 } 554 } 555 556 @Override 557 public void onManageConferenceClicked() { 558 InCallActivity activity = 559 (InCallActivity) (inCallScreen.getInCallScreenFragment().getActivity()); 560 activity.showConferenceFragment(true); 561 } 562 563 @Override 564 public void onShrinkAnimationComplete() { 565 InCallPresenter.getInstance().onShrinkAnimationComplete(); 566 } 567 568 private void maybeStartSearch(DialerCall call, boolean isPrimary) { 569 // no need to start search for conference calls which show generic info. 570 if (call != null && !call.isConferenceCall()) { 571 startContactInfoSearch(call, isPrimary, call.getState() == DialerCall.State.INCOMING); 572 } 573 } 574 575 /** Starts a query for more contact data for the save primary and secondary calls. */ 576 private void startContactInfoSearch( 577 final DialerCall call, final boolean isPrimary, boolean isIncoming) { 578 final ContactInfoCache cache = ContactInfoCache.getInstance(context); 579 580 cache.findInfo(call, isIncoming, new ContactLookupCallback(this, isPrimary)); 581 } 582 583 private void onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary) { 584 final boolean entryMatchesExistingCall = 585 (isPrimary && primary != null && TextUtils.equals(callId, primary.getId())) 586 || (!isPrimary && secondary != null && TextUtils.equals(callId, secondary.getId())); 587 if (entryMatchesExistingCall) { 588 updateContactEntry(entry, isPrimary); 589 } else { 590 LogUtil.e( 591 "CallCardPresenter.onContactInfoComplete", 592 "dropping stale contact lookup info for " + callId); 593 } 594 595 final DialerCall call = CallList.getInstance().getCallById(callId); 596 if (call != null) { 597 call.getLogState().contactLookupResult = entry.contactLookupResult; 598 } 599 if (entry.lookupUri != null) { 600 CallerInfoUtils.sendViewNotification(context, entry.lookupUri); 601 } 602 } 603 604 private void onImageLoadComplete(String callId, ContactCacheEntry entry) { 605 if (getUi() == null) { 606 return; 607 } 608 609 if (entry.photo != null) { 610 if (primary != null && callId.equals(primary.getId())) { 611 updateContactEntry(entry, true /* isPrimary */); 612 } else if (secondary != null && callId.equals(secondary.getId())) { 613 updateContactEntry(entry, false /* isPrimary */); 614 } 615 } 616 } 617 618 private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary) { 619 if (isPrimary) { 620 primaryContactInfo = entry; 621 updatePrimaryDisplayInfo(); 622 } else { 623 secondaryContactInfo = entry; 624 updateSecondaryDisplayInfo(); 625 } 626 } 627 628 /** 629 * Get the highest priority call to display. Goes through the calls and chooses which to return 630 * based on priority of which type of call to display to the user. Callers can use the "ignore" 631 * feature to get the second best call by passing a previously found primary call as ignore. 632 * 633 * @param ignore A call to ignore if found. 634 */ 635 private DialerCall getCallToDisplay( 636 CallList callList, DialerCall ignore, boolean skipDisconnected) { 637 // Active calls come second. An active call always gets precedent. 638 DialerCall retval = callList.getActiveCall(); 639 if (retval != null && retval != ignore) { 640 return retval; 641 } 642 643 // Sometimes there is intemediate state that two calls are in active even one is about 644 // to be on hold. 645 retval = callList.getSecondActiveCall(); 646 if (retval != null && retval != ignore) { 647 return retval; 648 } 649 650 // Disconnected calls get primary position if there are no active calls 651 // to let user know quickly what call has disconnected. Disconnected 652 // calls are very short lived. 653 if (!skipDisconnected) { 654 retval = callList.getDisconnectingCall(); 655 if (retval != null && retval != ignore) { 656 return retval; 657 } 658 retval = callList.getDisconnectedCall(); 659 if (retval != null && retval != ignore) { 660 return retval; 661 } 662 } 663 664 // Then we go to background call (calls on hold) 665 retval = callList.getBackgroundCall(); 666 if (retval != null && retval != ignore) { 667 return retval; 668 } 669 670 // Lastly, we go to a second background call. 671 retval = callList.getSecondBackgroundCall(); 672 673 return retval; 674 } 675 676 private void updatePrimaryDisplayInfo() { 677 if (inCallScreen == null) { 678 // TODO: May also occur if search result comes back after ui is destroyed. Look into 679 // removing that case completely. 680 LogUtil.v( 681 "CallCardPresenter.updatePrimaryDisplayInfo", 682 "updatePrimaryDisplayInfo called but ui is null!"); 683 return; 684 } 685 686 if (primary == null) { 687 // Clear the primary display info. 688 inCallScreen.setPrimary(PrimaryInfo.createEmptyPrimaryInfo()); 689 return; 690 } 691 692 // Hide the contact photo if we are in a video call and the incoming video surface is 693 // showing. 694 boolean showContactPhoto = 695 !VideoCallPresenter.showIncomingVideo(primary.getVideoState(), primary.getState()); 696 697 // DialerCall placed through a work phone account. 698 boolean hasWorkCallProperty = primary.hasProperty(PROPERTY_ENTERPRISE_CALL); 699 700 MultimediaData multimediaData = null; 701 if (primary.getEnrichedCallSession() != null) { 702 multimediaData = primary.getEnrichedCallSession().getMultimediaData(); 703 } 704 705 if (primary.isConferenceCall()) { 706 LogUtil.v( 707 "CallCardPresenter.updatePrimaryDisplayInfo", 708 "update primary display info for conference call."); 709 710 inCallScreen.setPrimary( 711 new PrimaryInfo( 712 null /* number */, 713 CallerInfoUtils.getConferenceString( 714 context, primary.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)), 715 false /* nameIsNumber */, 716 null /* location */, 717 null /* label */, 718 null /* photo */, 719 ContactPhotoType.DEFAULT_PLACEHOLDER, 720 false /* isSipCall */, 721 showContactPhoto, 722 hasWorkCallProperty, 723 false /* isSpam */, 724 false /* isLocalContact */, 725 false /* answeringDisconnectsOngoingCall */, 726 shouldShowLocation(), 727 null /* contactInfoLookupKey */, 728 null /* enrichedCallMultimediaData */, 729 true /* showInCallButtonGrid */, 730 primary.getNumberPresentation())); 731 } else if (primaryContactInfo != null) { 732 LogUtil.v( 733 "CallCardPresenter.updatePrimaryDisplayInfo", 734 "update primary display info for " + primaryContactInfo); 735 736 String name = getNameForCall(primaryContactInfo); 737 String number; 738 739 boolean isChildNumberShown = !TextUtils.isEmpty(primary.getChildNumber()); 740 boolean isForwardedNumberShown = !TextUtils.isEmpty(primary.getLastForwardedNumber()); 741 boolean isCallSubjectShown = shouldShowCallSubject(primary); 742 743 if (isCallSubjectShown) { 744 number = null; 745 } else if (isChildNumberShown) { 746 number = context.getString(R.string.child_number, primary.getChildNumber()); 747 } else if (isForwardedNumberShown) { 748 // Use last forwarded number instead of second line, if present. 749 number = primary.getLastForwardedNumber(); 750 } else { 751 number = primaryContactInfo.number; 752 } 753 754 boolean nameIsNumber = name != null && name.equals(primaryContactInfo.number); 755 756 // DialerCall with caller that is a work contact. 757 boolean isWorkContact = (primaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK); 758 inCallScreen.setPrimary( 759 new PrimaryInfo( 760 number, 761 primary.updateNameIfRestricted(name), 762 nameIsNumber, 763 shouldShowLocationAsLabel(nameIsNumber, primaryContactInfo.shouldShowLocation) 764 ? primaryContactInfo.location 765 : null, 766 isChildNumberShown || isCallSubjectShown ? null : primaryContactInfo.label, 767 primaryContactInfo.photo, 768 primaryContactInfo.photoType, 769 primaryContactInfo.isSipCall, 770 showContactPhoto, 771 hasWorkCallProperty || isWorkContact, 772 primary.isSpam(), 773 primaryContactInfo.isLocalContact(), 774 primary.answeringDisconnectsForegroundVideoCall(), 775 shouldShowLocation(), 776 primaryContactInfo.lookupKey, 777 multimediaData, 778 true /* showInCallButtonGrid */, 779 primary.getNumberPresentation())); 780 } else { 781 // Clear the primary display info. 782 inCallScreen.setPrimary(PrimaryInfo.createEmptyPrimaryInfo()); 783 } 784 785 if (isInCallScreenReady) { 786 inCallScreen.showLocationUi(getLocationFragment()); 787 } else { 788 LogUtil.i("CallCardPresenter.updatePrimaryDisplayInfo", "UI not ready, not showing location"); 789 } 790 } 791 792 private static boolean shouldShowLocationAsLabel( 793 boolean nameIsNumber, boolean shouldShowLocation) { 794 if (nameIsNumber) { 795 return true; 796 } 797 if (shouldShowLocation) { 798 return true; 799 } 800 return false; 801 } 802 803 private Fragment getLocationFragment() { 804 if (!shouldShowLocation()) { 805 return null; 806 } 807 LogUtil.i("CallCardPresenter.getLocationFragment", "returning location fragment"); 808 return callLocation.getLocationFragment(context); 809 } 810 811 private boolean shouldShowLocation() { 812 if (!ConfigProviderBindings.get(context) 813 .getBoolean(CONFIG_ENABLE_EMERGENCY_LOCATION, CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT)) { 814 LogUtil.i("CallCardPresenter.getLocationFragment", "disabled by config."); 815 return false; 816 } 817 if (!isPotentialEmergencyCall()) { 818 LogUtil.i("CallCardPresenter.getLocationFragment", "shouldn't show location"); 819 return false; 820 } 821 if (!hasLocationPermission()) { 822 LogUtil.i("CallCardPresenter.getLocationFragment", "no location permission."); 823 return false; 824 } 825 if (isBatteryTooLowForEmergencyLocation()) { 826 LogUtil.i("CallCardPresenter.getLocationFragment", "low battery."); 827 return false; 828 } 829 if (ActivityCompat.isInMultiWindowMode(inCallScreen.getInCallScreenFragment().getActivity())) { 830 LogUtil.i("CallCardPresenter.getLocationFragment", "in multi-window mode"); 831 return false; 832 } 833 if (primary.isVideoCall()) { 834 LogUtil.i("CallCardPresenter.getLocationFragment", "emergency video calls not supported"); 835 return false; 836 } 837 if (!callLocation.canGetLocation(context)) { 838 LogUtil.i("CallCardPresenter.getLocationFragment", "can't get current location"); 839 return false; 840 } 841 return true; 842 } 843 844 private boolean isPotentialEmergencyCall() { 845 if (isOutgoingEmergencyCall(primary)) { 846 LogUtil.i("CallCardPresenter.shouldShowLocation", "new emergency call"); 847 return true; 848 } else if (isIncomingEmergencyCall(primary)) { 849 LogUtil.i("CallCardPresenter.shouldShowLocation", "potential emergency callback"); 850 return true; 851 } else if (isIncomingEmergencyCall(secondary)) { 852 LogUtil.i("CallCardPresenter.shouldShowLocation", "has potential emergency callback"); 853 return true; 854 } 855 return false; 856 } 857 858 private static boolean isOutgoingEmergencyCall(@Nullable DialerCall call) { 859 return call != null && !call.isIncoming() && call.isEmergencyCall(); 860 } 861 862 private static boolean isIncomingEmergencyCall(@Nullable DialerCall call) { 863 return call != null && call.isIncoming() && call.isPotentialEmergencyCallback(); 864 } 865 866 private boolean hasLocationPermission() { 867 return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) 868 == PackageManager.PERMISSION_GRANTED; 869 } 870 871 private boolean isBatteryTooLowForEmergencyLocation() { 872 Intent batteryStatus = 873 context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 874 int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); 875 if (status == BatteryManager.BATTERY_STATUS_CHARGING 876 || status == BatteryManager.BATTERY_STATUS_FULL) { 877 // Plugged in or full battery 878 return false; 879 } 880 int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); 881 int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1); 882 float batteryPercent = (100f * level) / scale; 883 long threshold = 884 ConfigProviderBindings.get(context) 885 .getLong( 886 CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION, 887 CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION_DEFAULT); 888 LogUtil.i( 889 "CallCardPresenter.isBatteryTooLowForEmergencyLocation", 890 "percent charged: " + batteryPercent + ", min required charge: " + threshold); 891 return batteryPercent < threshold; 892 } 893 894 private void updateSecondaryDisplayInfo() { 895 if (inCallScreen == null) { 896 return; 897 } 898 899 if (secondary == null) { 900 // Clear the secondary display info. 901 inCallScreen.setSecondary(SecondaryInfo.createEmptySecondaryInfo(isFullscreen)); 902 return; 903 } 904 905 if (secondary.isMergeInProcess()) { 906 LogUtil.i( 907 "CallCardPresenter.updateSecondaryDisplayInfo", 908 "secondary call is merge in process, clearing info"); 909 inCallScreen.setSecondary(SecondaryInfo.createEmptySecondaryInfo(isFullscreen)); 910 return; 911 } 912 913 if (secondary.isConferenceCall()) { 914 inCallScreen.setSecondary( 915 new SecondaryInfo( 916 true /* show */, 917 CallerInfoUtils.getConferenceString( 918 context, secondary.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)), 919 false /* nameIsNumber */, 920 null /* label */, 921 secondary.getCallProviderLabel(), 922 true /* isConference */, 923 secondary.isVideoCall(), 924 isFullscreen)); 925 } else if (secondaryContactInfo != null) { 926 LogUtil.v("CallCardPresenter.updateSecondaryDisplayInfo", "" + secondaryContactInfo); 927 String name = getNameForCall(secondaryContactInfo); 928 boolean nameIsNumber = name != null && name.equals(secondaryContactInfo.number); 929 inCallScreen.setSecondary( 930 new SecondaryInfo( 931 true /* show */, 932 secondary.updateNameIfRestricted(name), 933 nameIsNumber, 934 secondaryContactInfo.label, 935 secondary.getCallProviderLabel(), 936 false /* isConference */, 937 secondary.isVideoCall(), 938 isFullscreen)); 939 } else { 940 // Clear the secondary display info. 941 inCallScreen.setSecondary(SecondaryInfo.createEmptySecondaryInfo(isFullscreen)); 942 } 943 } 944 945 /** Returns the gateway number for any existing outgoing call. */ 946 private String getGatewayNumber() { 947 if (hasOutgoingGatewayCall()) { 948 return DialerCall.getNumberFromHandle(primary.getGatewayInfo().getGatewayAddress()); 949 } 950 return null; 951 } 952 953 /** 954 * Returns the label (line of text above the number/name) for any given call. For example, 955 * "calling via [Account/Google Voice]" for outgoing calls. 956 */ 957 private String getConnectionLabel() { 958 if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) 959 != PackageManager.PERMISSION_GRANTED) { 960 return null; 961 } 962 StatusHints statusHints = primary.getStatusHints(); 963 if (statusHints != null && !TextUtils.isEmpty(statusHints.getLabel())) { 964 return statusHints.getLabel().toString(); 965 } 966 967 if (hasOutgoingGatewayCall() && getUi() != null) { 968 // Return the label for the gateway app on outgoing calls. 969 final PackageManager pm = context.getPackageManager(); 970 try { 971 ApplicationInfo info = 972 pm.getApplicationInfo(primary.getGatewayInfo().getGatewayProviderPackageName(), 0); 973 return pm.getApplicationLabel(info).toString(); 974 } catch (PackageManager.NameNotFoundException e) { 975 LogUtil.e("CallCardPresenter.getConnectionLabel", "gateway Application Not Found.", e); 976 return null; 977 } 978 } 979 return primary.getCallProviderLabel(); 980 } 981 982 private Drawable getCallStateIcon() { 983 // Return connection icon if one exists. 984 StatusHints statusHints = primary.getStatusHints(); 985 if (statusHints != null && statusHints.getIcon() != null) { 986 Drawable icon = statusHints.getIcon().loadDrawable(context); 987 if (icon != null) { 988 return icon; 989 } 990 } 991 992 return null; 993 } 994 995 private boolean hasOutgoingGatewayCall() { 996 // We only display the gateway information while STATE_DIALING so return false for any other 997 // call state. 998 // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which 999 // is also called after a contact search completes (call is not present yet). Split the 1000 // UI update so it can receive independent updates. 1001 if (primary == null) { 1002 return false; 1003 } 1004 return DialerCall.State.isDialing(primary.getState()) 1005 && primary.getGatewayInfo() != null 1006 && !primary.getGatewayInfo().isEmpty(); 1007 } 1008 1009 /** Gets the name to display for the call. */ 1010 private String getNameForCall(ContactCacheEntry contactInfo) { 1011 String preferredName = 1012 ContactDisplayUtils.getPreferredDisplayName( 1013 contactInfo.namePrimary, contactInfo.nameAlternative, contactsPreferences); 1014 if (TextUtils.isEmpty(preferredName)) { 1015 return contactInfo.number; 1016 } 1017 return preferredName; 1018 } 1019 1020 @Override 1021 public void onSecondaryInfoClicked() { 1022 if (secondary == null) { 1023 LogUtil.e( 1024 "CallCardPresenter.onSecondaryInfoClicked", 1025 "secondary info clicked but no secondary call."); 1026 return; 1027 } 1028 1029 Logger.get(context) 1030 .logCallImpression( 1031 DialerImpression.Type.IN_CALL_SWAP_SECONDARY_BUTTON_PRESSED, 1032 primary.getUniqueCallId(), 1033 primary.getTimeAddedMs()); 1034 LogUtil.i( 1035 "CallCardPresenter.onSecondaryInfoClicked", "swapping call to foreground: " + secondary); 1036 secondary.unhold(); 1037 } 1038 1039 @Override 1040 public void onEndCallClicked() { 1041 LogUtil.i("CallCardPresenter.onEndCallClicked", "disconnecting call: " + primary); 1042 if (primary != null) { 1043 primary.disconnect(); 1044 } 1045 PostCall.onDisconnectPressed(context); 1046 } 1047 1048 /** 1049 * Handles a change to the fullscreen mode of the in-call UI. 1050 * 1051 * @param isFullscreenMode {@code True} if the in-call UI is entering full screen mode. 1052 */ 1053 @Override 1054 public void onFullscreenModeChanged(boolean isFullscreenMode) { 1055 isFullscreen = isFullscreenMode; 1056 if (inCallScreen == null) { 1057 return; 1058 } 1059 maybeShowManageConferenceCallButton(); 1060 } 1061 1062 private boolean isPrimaryCallActive() { 1063 return primary != null && primary.getState() == DialerCall.State.ACTIVE; 1064 } 1065 1066 private boolean shouldShowEndCallButton(DialerCall primary, int callState) { 1067 if (primary == null) { 1068 return false; 1069 } 1070 if ((!DialerCall.State.isConnectingOrConnected(callState) 1071 && callState != DialerCall.State.DISCONNECTING 1072 && callState != DialerCall.State.DISCONNECTED) 1073 || callState == DialerCall.State.INCOMING) { 1074 return false; 1075 } 1076 if (this.primary.getVideoTech().getSessionModificationState() 1077 == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 1078 return false; 1079 } 1080 return true; 1081 } 1082 1083 @Override 1084 public void onInCallScreenResumed() { 1085 updatePrimaryDisplayInfo(); 1086 1087 if (shouldSendAccessibilityEvent) { 1088 handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS); 1089 } 1090 } 1091 1092 @Override 1093 public void onInCallScreenPaused() {} 1094 1095 static boolean sendAccessibilityEvent(Context context, InCallScreen inCallScreen) { 1096 AccessibilityManager am = 1097 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 1098 if (!am.isEnabled()) { 1099 LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "accessibility is off"); 1100 return false; 1101 } 1102 if (inCallScreen == null) { 1103 LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "incallscreen is null"); 1104 return false; 1105 } 1106 Fragment fragment = inCallScreen.getInCallScreenFragment(); 1107 if (fragment == null || fragment.getView() == null || fragment.getView().getParent() == null) { 1108 LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "fragment/view/parent is null"); 1109 return false; 1110 } 1111 1112 DisplayManager displayManager = 1113 (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); 1114 Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); 1115 boolean screenIsOn = display.getState() == Display.STATE_ON; 1116 LogUtil.d("CallCardPresenter.sendAccessibilityEvent", "screen is on: %b", screenIsOn); 1117 if (!screenIsOn) { 1118 return false; 1119 } 1120 1121 AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT); 1122 inCallScreen.dispatchPopulateAccessibilityEvent(event); 1123 View view = inCallScreen.getInCallScreenFragment().getView(); 1124 view.getParent().requestSendAccessibilityEvent(view, event); 1125 return true; 1126 } 1127 1128 private void maybeSendAccessibilityEvent( 1129 InCallState oldState, final InCallState newState, boolean primaryChanged) { 1130 shouldSendAccessibilityEvent = false; 1131 if (context == null) { 1132 return; 1133 } 1134 final AccessibilityManager am = 1135 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 1136 if (!am.isEnabled()) { 1137 return; 1138 } 1139 // Announce the current call if it's new incoming/outgoing call or primary call is changed 1140 // due to switching calls between two ongoing calls (one is on hold). 1141 if ((oldState != InCallState.OUTGOING && newState == InCallState.OUTGOING) 1142 || (oldState != InCallState.INCOMING && newState == InCallState.INCOMING) 1143 || primaryChanged) { 1144 LogUtil.i( 1145 "CallCardPresenter.maybeSendAccessibilityEvent", "schedule accessibility announcement"); 1146 shouldSendAccessibilityEvent = true; 1147 handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS); 1148 } 1149 } 1150 1151 /** 1152 * Determines whether the call subject should be visible on the UI. For the call subject to be 1153 * visible, the call has to be in an incoming or waiting state, and the subject must not be empty. 1154 * 1155 * @param call The call. 1156 * @return {@code true} if the subject should be shown, {@code false} otherwise. 1157 */ 1158 private boolean shouldShowCallSubject(DialerCall call) { 1159 if (call == null) { 1160 return false; 1161 } 1162 1163 boolean isIncomingOrWaiting = 1164 primary.getState() == DialerCall.State.INCOMING 1165 || primary.getState() == DialerCall.State.CALL_WAITING; 1166 return isIncomingOrWaiting 1167 && !TextUtils.isEmpty(call.getCallSubject()) 1168 && call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED 1169 && call.isCallSubjectSupported(); 1170 } 1171 1172 /** 1173 * Determines whether the "note sent" toast should be shown. It should be shown for a new outgoing 1174 * call with a subject. 1175 * 1176 * @param call The call 1177 * @return {@code true} if the toast should be shown, {@code false} otherwise. 1178 */ 1179 private boolean shouldShowNoteSentToast(DialerCall call) { 1180 return call != null 1181 && hasCallSubject(call) 1182 && (call.getState() == DialerCall.State.DIALING 1183 || call.getState() == DialerCall.State.CONNECTING); 1184 } 1185 1186 private InCallScreen getUi() { 1187 return inCallScreen; 1188 } 1189 1190 public static class ContactLookupCallback implements ContactInfoCacheCallback { 1191 1192 private final WeakReference<CallCardPresenter> callCardPresenter; 1193 private final boolean isPrimary; 1194 1195 public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) { 1196 this.callCardPresenter = new WeakReference<CallCardPresenter>(callCardPresenter); 1197 this.isPrimary = isPrimary; 1198 } 1199 1200 @Override 1201 public void onContactInfoComplete(String callId, ContactCacheEntry entry) { 1202 CallCardPresenter presenter = callCardPresenter.get(); 1203 if (presenter != null) { 1204 presenter.onContactInfoComplete(callId, entry, isPrimary); 1205 } 1206 } 1207 1208 @Override 1209 public void onImageLoadComplete(String callId, ContactCacheEntry entry) { 1210 CallCardPresenter presenter = callCardPresenter.get(); 1211 if (presenter != null) { 1212 presenter.onImageLoadComplete(callId, entry); 1213 } 1214 } 1215 } 1216} 1217