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