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