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 com.google.common.base.Preconditions; 20 21import android.Manifest; 22import android.content.Context; 23import android.content.Intent; 24import android.content.pm.ApplicationInfo; 25import android.content.pm.PackageManager; 26import android.graphics.drawable.Drawable; 27import android.net.Uri; 28import android.os.Bundle; 29import android.support.annotation.Nullable; 30import android.telecom.Call.Details; 31import android.telecom.DisconnectCause; 32import android.telecom.PhoneAccount; 33import android.telecom.PhoneAccountHandle; 34import android.telecom.StatusHints; 35import android.telecom.TelecomManager; 36import android.telecom.VideoProfile; 37import android.telephony.PhoneNumberUtils; 38import android.text.TextUtils; 39import android.view.View; 40import android.view.accessibility.AccessibilityManager; 41import android.widget.ListAdapter; 42 43import com.android.contacts.common.ContactsUtils; 44import com.android.contacts.common.compat.telecom.TelecomManagerCompat; 45import com.android.contacts.common.preference.ContactsPreferences; 46import com.android.contacts.common.testing.NeededForTesting; 47import com.android.contacts.common.util.ContactDisplayUtils; 48import com.android.dialer.R; 49import com.android.incallui.Call.State; 50import com.android.incallui.ContactInfoCache.ContactCacheEntry; 51import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; 52import com.android.incallui.InCallPresenter.InCallDetailsListener; 53import com.android.incallui.InCallPresenter.InCallEventListener; 54import com.android.incallui.InCallPresenter.InCallState; 55import com.android.incallui.InCallPresenter.InCallStateListener; 56import com.android.incallui.InCallPresenter.IncomingCallListener; 57import com.android.incalluibind.ObjectFactory; 58 59import java.lang.ref.WeakReference; 60 61import static com.android.contacts.common.compat.CallSdkCompat.Details.PROPERTY_ENTERPRISE_CALL; 62/** 63 * Presenter for the Call Card Fragment. 64 * <p> 65 * This class listens for changes to InCallState and passes it along to the fragment. 66 */ 67public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> 68 implements InCallStateListener, IncomingCallListener, InCallDetailsListener, 69 InCallEventListener, CallList.CallUpdateListener, DistanceHelper.Listener { 70 71 public interface EmergencyCallListener { 72 public void onCallUpdated(BaseFragment fragment, boolean isEmergency); 73 } 74 75 private static final String TAG = CallCardPresenter.class.getSimpleName(); 76 private static final long CALL_TIME_UPDATE_INTERVAL_MS = 1000; 77 78 private final EmergencyCallListener mEmergencyCallListener = 79 ObjectFactory.newEmergencyCallListener(); 80 private DistanceHelper mDistanceHelper; 81 82 private Call mPrimary; 83 private Call mSecondary; 84 private ContactCacheEntry mPrimaryContactInfo; 85 private ContactCacheEntry mSecondaryContactInfo; 86 private CallTimer mCallTimer; 87 private Context mContext; 88 @Nullable private ContactsPreferences mContactsPreferences; 89 private boolean mSpinnerShowing = false; 90 private boolean mHasShownToast = false; 91 private InCallContactInteractions mInCallContactInteractions; 92 private boolean mIsFullscreen = false; 93 94 public static class ContactLookupCallback implements ContactInfoCacheCallback { 95 private final WeakReference<CallCardPresenter> mCallCardPresenter; 96 private final boolean mIsPrimary; 97 98 public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) { 99 mCallCardPresenter = new WeakReference<CallCardPresenter>(callCardPresenter); 100 mIsPrimary = isPrimary; 101 } 102 103 @Override 104 public void onContactInfoComplete(String callId, ContactCacheEntry entry) { 105 CallCardPresenter presenter = mCallCardPresenter.get(); 106 if (presenter != null) { 107 presenter.onContactInfoComplete(callId, entry, mIsPrimary); 108 } 109 } 110 111 @Override 112 public void onImageLoadComplete(String callId, ContactCacheEntry entry) { 113 CallCardPresenter presenter = mCallCardPresenter.get(); 114 if (presenter != null) { 115 presenter.onImageLoadComplete(callId, entry); 116 } 117 } 118 119 @Override 120 public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) { 121 CallCardPresenter presenter = mCallCardPresenter.get(); 122 if (presenter != null) { 123 presenter.onContactInteractionsInfoComplete(callId, entry); 124 } 125 } 126 } 127 128 public CallCardPresenter() { 129 // create the call timer 130 mCallTimer = new CallTimer(new Runnable() { 131 @Override 132 public void run() { 133 updateCallTime(); 134 } 135 }); 136 } 137 138 public void init(Context context, Call call) { 139 mContext = Preconditions.checkNotNull(context); 140 mDistanceHelper = ObjectFactory.newDistanceHelper(mContext, this); 141 mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); 142 143 // Call may be null if disconnect happened already. 144 if (call != null) { 145 mPrimary = call; 146 if (shouldShowNoteSentToast(mPrimary)) { 147 final CallCardUi ui = getUi(); 148 if (ui != null) { 149 ui.showNoteSentToast(); 150 } 151 } 152 CallList.getInstance().addCallUpdateListener(call.getId(), this); 153 154 // start processing lookups right away. 155 if (!call.isConferenceCall()) { 156 startContactInfoSearch(call, true, call.getState() == Call.State.INCOMING); 157 } else { 158 updateContactEntry(null, true); 159 } 160 } 161 162 onStateChange(null, InCallPresenter.getInstance().getInCallState(), CallList.getInstance()); 163 } 164 165 @Override 166 public void onUiReady(CallCardUi ui) { 167 super.onUiReady(ui); 168 169 if (mContactsPreferences != null) { 170 mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); 171 } 172 173 // Contact search may have completed before ui is ready. 174 if (mPrimaryContactInfo != null) { 175 updatePrimaryDisplayInfo(); 176 } 177 178 // Register for call state changes last 179 InCallPresenter.getInstance().addListener(this); 180 InCallPresenter.getInstance().addIncomingCallListener(this); 181 InCallPresenter.getInstance().addDetailsListener(this); 182 InCallPresenter.getInstance().addInCallEventListener(this); 183 } 184 185 @Override 186 public void onUiUnready(CallCardUi ui) { 187 super.onUiUnready(ui); 188 189 // stop getting call state changes 190 InCallPresenter.getInstance().removeListener(this); 191 InCallPresenter.getInstance().removeIncomingCallListener(this); 192 InCallPresenter.getInstance().removeDetailsListener(this); 193 InCallPresenter.getInstance().removeInCallEventListener(this); 194 if (mPrimary != null) { 195 CallList.getInstance().removeCallUpdateListener(mPrimary.getId(), this); 196 } 197 198 if (mDistanceHelper != null) { 199 mDistanceHelper.cleanUp(); 200 } 201 202 mPrimary = null; 203 mPrimaryContactInfo = null; 204 mSecondaryContactInfo = null; 205 } 206 207 @Override 208 public void onIncomingCall(InCallState oldState, InCallState newState, Call call) { 209 // same logic should happen as with onStateChange() 210 onStateChange(oldState, newState, CallList.getInstance()); 211 } 212 213 @Override 214 public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { 215 Log.d(this, "onStateChange() " + newState); 216 final CallCardUi ui = getUi(); 217 if (ui == null) { 218 return; 219 } 220 221 Call primary = null; 222 Call secondary = null; 223 224 if (newState == InCallState.INCOMING) { 225 primary = callList.getIncomingCall(); 226 } else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) { 227 primary = callList.getOutgoingCall(); 228 if (primary == null) { 229 primary = callList.getPendingOutgoingCall(); 230 } 231 232 // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the 233 // highest priority call to display as the secondary call. 234 secondary = getCallToDisplay(callList, null, true); 235 } else if (newState == InCallState.INCALL) { 236 primary = getCallToDisplay(callList, null, false); 237 secondary = getCallToDisplay(callList, primary, true); 238 } 239 240 if (mInCallContactInteractions != null && 241 (oldState == InCallState.INCOMING || newState == InCallState.INCOMING)) { 242 ui.showContactContext(newState != InCallState.INCOMING); 243 } 244 245 Log.d(this, "Primary call: " + primary); 246 Log.d(this, "Secondary call: " + secondary); 247 248 final boolean primaryChanged = !(Call.areSame(mPrimary, primary) && 249 Call.areSameNumber(mPrimary, primary)); 250 final boolean secondaryChanged = !(Call.areSame(mSecondary, secondary) && 251 Call.areSameNumber(mSecondary, secondary)); 252 253 mSecondary = secondary; 254 Call previousPrimary = mPrimary; 255 mPrimary = primary; 256 257 if (primaryChanged && shouldShowNoteSentToast(primary)) { 258 ui.showNoteSentToast(); 259 } 260 261 // Refresh primary call information if either: 262 // 1. Primary call changed. 263 // 2. The call's ability to manage conference has changed. 264 // 3. The call subject should be shown or hidden. 265 if (shouldRefreshPrimaryInfo(primaryChanged, ui, shouldShowCallSubject(mPrimary))) { 266 // primary call has changed 267 if (previousPrimary != null) { 268 //clear progess spinner (if any) related to previous primary call 269 maybeShowProgressSpinner(previousPrimary.getState(), 270 Call.SessionModificationState.NO_REQUEST); 271 CallList.getInstance().removeCallUpdateListener(previousPrimary.getId(), this); 272 } 273 CallList.getInstance().addCallUpdateListener(mPrimary.getId(), this); 274 275 mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mPrimary, 276 mPrimary.getState() == Call.State.INCOMING); 277 updatePrimaryDisplayInfo(); 278 maybeStartSearch(mPrimary, true); 279 maybeClearSessionModificationState(mPrimary); 280 } 281 282 if (previousPrimary != null && mPrimary == null) { 283 //clear progess spinner (if any) related to previous primary call 284 maybeShowProgressSpinner(previousPrimary.getState(), 285 Call.SessionModificationState.NO_REQUEST); 286 CallList.getInstance().removeCallUpdateListener(previousPrimary.getId(), this); 287 } 288 289 if (mSecondary == null) { 290 // Secondary call may have ended. Update the ui. 291 mSecondaryContactInfo = null; 292 updateSecondaryDisplayInfo(); 293 } else if (secondaryChanged) { 294 // secondary call has changed 295 mSecondaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mSecondary, 296 mSecondary.getState() == Call.State.INCOMING); 297 updateSecondaryDisplayInfo(); 298 maybeStartSearch(mSecondary, false); 299 maybeClearSessionModificationState(mSecondary); 300 } 301 302 // Start/stop timers. 303 if (isPrimaryCallActive()) { 304 Log.d(this, "Starting the calltime timer"); 305 mCallTimer.start(CALL_TIME_UPDATE_INTERVAL_MS); 306 } else { 307 Log.d(this, "Canceling the calltime timer"); 308 mCallTimer.cancel(); 309 ui.setPrimaryCallElapsedTime(false, 0); 310 } 311 312 // Set the call state 313 int callState = Call.State.IDLE; 314 if (mPrimary != null) { 315 callState = mPrimary.getState(); 316 updatePrimaryCallState(); 317 } else { 318 getUi().setCallState( 319 callState, 320 VideoProfile.STATE_AUDIO_ONLY, 321 Call.SessionModificationState.NO_REQUEST, 322 new DisconnectCause(DisconnectCause.UNKNOWN), 323 null, 324 null, 325 null, 326 false /* isWifi */, 327 false /* isConference */, 328 false /* isWorkCall */); 329 getUi().showHdAudioIndicator(false); 330 } 331 332 maybeShowManageConferenceCallButton(); 333 334 // Hide the end call button instantly if we're receiving an incoming call. 335 getUi().setEndCallButtonEnabled(shouldShowEndCallButton(mPrimary, callState), 336 callState != Call.State.INCOMING /* animate */); 337 338 maybeSendAccessibilityEvent(oldState, newState, primaryChanged); 339 } 340 341 @Override 342 public void onDetailsChanged(Call call, Details details) { 343 updatePrimaryCallState(); 344 345 if (call.can(Details.CAPABILITY_MANAGE_CONFERENCE) != 346 details.can(Details.CAPABILITY_MANAGE_CONFERENCE)) { 347 maybeShowManageConferenceCallButton(); 348 } 349 } 350 351 @Override 352 public void onCallChanged(Call call) { 353 // No-op; specific call updates handled elsewhere. 354 } 355 356 /** 357 * Handles a change to the session modification state for a call. Triggers showing the progress 358 * spinner, as well as updating the call state label. 359 * 360 * @param sessionModificationState The new session modification state. 361 */ 362 @Override 363 public void onSessionModificationStateChange(int sessionModificationState) { 364 Log.d(this, "onSessionModificationStateChange : sessionModificationState = " + 365 sessionModificationState); 366 367 if (mPrimary == null) { 368 return; 369 } 370 maybeShowProgressSpinner(mPrimary.getState(), sessionModificationState); 371 getUi().setEndCallButtonEnabled(sessionModificationState != 372 Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST, 373 true /* shouldAnimate */); 374 updatePrimaryCallState(); 375 } 376 377 /** 378 * Handles a change to the last forwarding number by refreshing the primary call info. 379 */ 380 @Override 381 public void onLastForwardedNumberChange() { 382 Log.v(this, "onLastForwardedNumberChange"); 383 384 if (mPrimary == null) { 385 return; 386 } 387 updatePrimaryDisplayInfo(); 388 } 389 390 /** 391 * Handles a change to the child number by refreshing the primary call info. 392 */ 393 @Override 394 public void onChildNumberChange() { 395 Log.v(this, "onChildNumberChange"); 396 397 if (mPrimary == null) { 398 return; 399 } 400 updatePrimaryDisplayInfo(); 401 } 402 403 private boolean shouldRefreshPrimaryInfo(boolean primaryChanged, CallCardUi ui, 404 boolean shouldShowCallSubject) { 405 if (mPrimary == null) { 406 return false; 407 } 408 return primaryChanged || 409 ui.isManageConferenceVisible() != shouldShowManageConference() || 410 ui.isCallSubjectVisible() != shouldShowCallSubject; 411 } 412 413 private String getSubscriptionNumber() { 414 // If it's an emergency call, and they're not populating the callback number, 415 // then try to fall back to the phone sub info (to hopefully get the SIM's 416 // number directly from the telephony layer). 417 PhoneAccountHandle accountHandle = mPrimary.getAccountHandle(); 418 if (accountHandle != null) { 419 TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager(); 420 PhoneAccount account = TelecomManagerCompat.getPhoneAccount(mgr, accountHandle); 421 if (account != null) { 422 return getNumberFromHandle(account.getSubscriptionAddress()); 423 } 424 } 425 return null; 426 } 427 428 private void updatePrimaryCallState() { 429 if (getUi() != null && mPrimary != null) { 430 boolean isWorkCall = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL) 431 || (mPrimaryContactInfo == null ? false 432 : mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK); 433 getUi().setCallState( 434 mPrimary.getState(), 435 mPrimary.getVideoState(), 436 mPrimary.getSessionModificationState(), 437 mPrimary.getDisconnectCause(), 438 getConnectionLabel(), 439 getCallStateIcon(), 440 getGatewayNumber(), 441 mPrimary.hasProperty(Details.PROPERTY_WIFI), 442 mPrimary.isConferenceCall(), 443 isWorkCall); 444 445 maybeShowHdAudioIcon(); 446 setCallbackNumber(); 447 } 448 } 449 450 /** 451 * Show the HD icon if the call is active and has {@link Details#PROPERTY_HIGH_DEF_AUDIO}, 452 * except if the call has a last forwarded number (we will show that icon instead). 453 */ 454 private void maybeShowHdAudioIcon() { 455 boolean showHdAudioIndicator = 456 isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO) && 457 TextUtils.isEmpty(mPrimary.getLastForwardedNumber()); 458 getUi().showHdAudioIndicator(showHdAudioIndicator); 459 } 460 461 /** 462 * Only show the conference call button if we can manage the conference. 463 */ 464 private void maybeShowManageConferenceCallButton() { 465 getUi().showManageConferenceCallButton(shouldShowManageConference()); 466 } 467 468 /** 469 * Determines if a pending session modification exists for the current call. If so, the 470 * progress spinner is shown, and the call state is updated. 471 * 472 * @param callState The call state. 473 * @param sessionModificationState The session modification state. 474 */ 475 private void maybeShowProgressSpinner(int callState, int sessionModificationState) { 476 final boolean show = sessionModificationState == 477 Call.SessionModificationState.WAITING_FOR_RESPONSE 478 && callState == Call.State.ACTIVE; 479 if (show != mSpinnerShowing) { 480 getUi().setProgressSpinnerVisible(show); 481 mSpinnerShowing = show; 482 } 483 } 484 485 /** 486 * Determines if the manage conference button should be visible, based on the current primary 487 * call. 488 * 489 * @return {@code True} if the manage conference button should be visible. 490 */ 491 private boolean shouldShowManageConference() { 492 if (mPrimary == null) { 493 return false; 494 } 495 496 return mPrimary.can(android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE) 497 && !mIsFullscreen; 498 } 499 500 private void setCallbackNumber() { 501 String callbackNumber = null; 502 503 // Show the emergency callback number if either: 504 // 1. This is an emergency call. 505 // 2. The phone is in Emergency Callback Mode, which means we should show the callback 506 // number. 507 boolean showCallbackNumber = mPrimary.hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE); 508 509 if (mPrimary.isEmergencyCall() || showCallbackNumber) { 510 callbackNumber = getSubscriptionNumber(); 511 } else { 512 StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints(); 513 if (statusHints != null) { 514 Bundle extras = statusHints.getExtras(); 515 if (extras != null) { 516 callbackNumber = extras.getString(TelecomManager.EXTRA_CALL_BACK_NUMBER); 517 } 518 } 519 } 520 521 final String simNumber = TelecomManagerCompat.getLine1Number( 522 InCallPresenter.getInstance().getTelecomManager(), 523 InCallPresenter.getInstance().getTelephonyManager(), 524 mPrimary.getAccountHandle()); 525 if (!showCallbackNumber && PhoneNumberUtils.compare(callbackNumber, simNumber)) { 526 Log.d(this, "Numbers are the same (and callback number is not being forced to show);" + 527 " not showing the callback number"); 528 callbackNumber = null; 529 } 530 531 getUi().setCallbackNumber(callbackNumber, mPrimary.isEmergencyCall() || showCallbackNumber); 532 } 533 534 public void updateCallTime() { 535 final CallCardUi ui = getUi(); 536 537 if (ui == null) { 538 mCallTimer.cancel(); 539 } else if (!isPrimaryCallActive()) { 540 ui.setPrimaryCallElapsedTime(false, 0); 541 mCallTimer.cancel(); 542 } else { 543 final long callStart = mPrimary.getConnectTimeMillis(); 544 final long duration = System.currentTimeMillis() - callStart; 545 ui.setPrimaryCallElapsedTime(true, duration); 546 } 547 } 548 549 public void onCallStateButtonTouched() { 550 Intent broadcastIntent = ObjectFactory.getCallStateButtonBroadcastIntent(mContext); 551 if (broadcastIntent != null) { 552 Log.d(this, "Sending call state button broadcast: ", broadcastIntent); 553 mContext.sendBroadcast(broadcastIntent, Manifest.permission.READ_PHONE_STATE); 554 } 555 } 556 557 /** 558 * Handles click on the contact photo by toggling fullscreen mode if the current call is a video 559 * call. 560 */ 561 public void onContactPhotoClick() { 562 if (mPrimary != null && mPrimary.isVideoCall(mContext)) { 563 InCallPresenter.getInstance().toggleFullscreenMode(); 564 } 565 } 566 567 private void maybeStartSearch(Call call, boolean isPrimary) { 568 // no need to start search for conference calls which show generic info. 569 if (call != null && !call.isConferenceCall()) { 570 startContactInfoSearch(call, isPrimary, call.getState() == Call.State.INCOMING); 571 } 572 } 573 574 private void maybeClearSessionModificationState(Call call) { 575 if (call.getSessionModificationState() != 576 Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 577 call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST); 578 } 579 } 580 581 /** 582 * Starts a query for more contact data for the save primary and secondary calls. 583 */ 584 private void startContactInfoSearch(final Call call, final boolean isPrimary, 585 boolean isIncoming) { 586 final ContactInfoCache cache = ContactInfoCache.getInstance(mContext); 587 588 cache.findInfo(call, isIncoming, new ContactLookupCallback(this, isPrimary)); 589 } 590 591 private void onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary) { 592 final boolean entryMatchesExistingCall = 593 (isPrimary && mPrimary != null && TextUtils.equals(callId, mPrimary.getId())) || 594 (!isPrimary && mSecondary != null && TextUtils.equals(callId, mSecondary.getId())); 595 if (entryMatchesExistingCall) { 596 updateContactEntry(entry, isPrimary); 597 } else { 598 Log.w(this, "Dropping stale contact lookup info for " + callId); 599 } 600 601 final Call call = CallList.getInstance().getCallById(callId); 602 if (call != null) { 603 call.getLogState().contactLookupResult = entry.contactLookupResult; 604 } 605 if (entry.contactUri != null) { 606 CallerInfoUtils.sendViewNotification(mContext, entry.contactUri); 607 } 608 } 609 610 private void onImageLoadComplete(String callId, ContactCacheEntry entry) { 611 if (getUi() == null) { 612 return; 613 } 614 615 if (entry.photo != null) { 616 if (mPrimary != null && callId.equals(mPrimary.getId())) { 617 boolean showContactPhoto = !VideoCallPresenter.showIncomingVideo( 618 mPrimary.getVideoState(), mPrimary.getState()); 619 getUi().setPrimaryImage(entry.photo, showContactPhoto); 620 } 621 } 622 } 623 624 private void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) { 625 if (getUi() == null) { 626 return; 627 } 628 629 if (mPrimary != null && callId.equals(mPrimary.getId())) { 630 mPrimaryContactInfo.locationAddress = entry.locationAddress; 631 updateContactInteractions(); 632 } 633 } 634 635 @Override 636 public void onLocationReady() { 637 // This will only update the contacts interactions data if the location returns after 638 // the contact information is found. 639 updateContactInteractions(); 640 } 641 642 private void updateContactInteractions() { 643 if (mPrimary != null && mPrimaryContactInfo != null 644 && (mPrimaryContactInfo.locationAddress != null 645 || mPrimaryContactInfo.openingHours != null)) { 646 // TODO: This is hardcoded to "isBusiness" because functionality to differentiate 647 // between business and personal has not yet been added. 648 if (setInCallContactInteractionsType(true /* isBusiness */)) { 649 getUi().setContactContextTitle( 650 mInCallContactInteractions.getBusinessListHeaderView()); 651 } 652 653 mInCallContactInteractions.setBusinessInfo( 654 mPrimaryContactInfo.locationAddress, 655 mDistanceHelper.calculateDistance(mPrimaryContactInfo.locationAddress), 656 mPrimaryContactInfo.openingHours); 657 getUi().setContactContextContent(mInCallContactInteractions.getListAdapter()); 658 getUi().showContactContext(mPrimary.getState() != State.INCOMING); 659 } else { 660 getUi().showContactContext(false); 661 } 662 } 663 664 /** 665 * Update the contact interactions type so that the correct UI is shown. 666 * 667 * @param isBusiness {@code true} if the interaction is a business interaction, {@code false} if 668 * it is a personal contact. 669 * 670 * @return {@code true} if this is a new type of contact interaction (business or personal). 671 * {@code false} if it hasn't changed. 672 */ 673 private boolean setInCallContactInteractionsType(boolean isBusiness) { 674 if (mInCallContactInteractions == null) { 675 mInCallContactInteractions = 676 new InCallContactInteractions(mContext, isBusiness); 677 return true; 678 } 679 680 return mInCallContactInteractions.switchContactType(isBusiness); 681 } 682 683 private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary) { 684 if (isPrimary) { 685 mPrimaryContactInfo = entry; 686 updatePrimaryDisplayInfo(); 687 } else { 688 mSecondaryContactInfo = entry; 689 updateSecondaryDisplayInfo(); 690 } 691 } 692 693 /** 694 * Get the highest priority call to display. 695 * Goes through the calls and chooses which to return based on priority of which type of call 696 * to display to the user. Callers can use the "ignore" feature to get the second best call 697 * by passing a previously found primary call as ignore. 698 * 699 * @param ignore A call to ignore if found. 700 */ 701 private Call getCallToDisplay(CallList callList, Call ignore, boolean skipDisconnected) { 702 // Active calls come second. An active call always gets precedent. 703 Call retval = callList.getActiveCall(); 704 if (retval != null && retval != ignore) { 705 return retval; 706 } 707 708 // Sometimes there is intemediate state that two calls are in active even one is about 709 // to be on hold. 710 retval = callList.getSecondActiveCall(); 711 if (retval != null && retval != ignore) { 712 return retval; 713 } 714 715 // Disconnected calls get primary position if there are no active calls 716 // to let user know quickly what call has disconnected. Disconnected 717 // calls are very short lived. 718 if (!skipDisconnected) { 719 retval = callList.getDisconnectingCall(); 720 if (retval != null && retval != ignore) { 721 return retval; 722 } 723 retval = callList.getDisconnectedCall(); 724 if (retval != null && retval != ignore) { 725 return retval; 726 } 727 } 728 729 // Then we go to background call (calls on hold) 730 retval = callList.getBackgroundCall(); 731 if (retval != null && retval != ignore) { 732 return retval; 733 } 734 735 // Lastly, we go to a second background call. 736 retval = callList.getSecondBackgroundCall(); 737 738 return retval; 739 } 740 741 private void updatePrimaryDisplayInfo() { 742 final CallCardUi ui = getUi(); 743 if (ui == null) { 744 // TODO: May also occur if search result comes back after ui is destroyed. Look into 745 // removing that case completely. 746 Log.d(TAG, "updatePrimaryDisplayInfo called but ui is null!"); 747 return; 748 } 749 750 if (mPrimary == null) { 751 // Clear the primary display info. 752 ui.setPrimary(null, null, false, null, null, false, false, false); 753 return; 754 } 755 756 // Hide the contact photo if we are in a video call and the incoming video surface is 757 // showing. 758 boolean showContactPhoto = !VideoCallPresenter 759 .showIncomingVideo(mPrimary.getVideoState(), mPrimary.getState()); 760 761 // Call placed through a work phone account. 762 boolean hasWorkCallProperty = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL); 763 764 if (mPrimary.isConferenceCall()) { 765 Log.d(TAG, "Update primary display info for conference call."); 766 767 ui.setPrimary( 768 null /* number */, 769 getConferenceString(mPrimary), 770 false /* nameIsNumber */, 771 null /* label */, 772 getConferencePhoto(mPrimary), 773 false /* isSipCall */, 774 showContactPhoto, 775 hasWorkCallProperty); 776 } else if (mPrimaryContactInfo != null) { 777 Log.d(TAG, "Update primary display info for " + mPrimaryContactInfo); 778 779 String name = getNameForCall(mPrimaryContactInfo); 780 String number; 781 782 boolean isChildNumberShown = !TextUtils.isEmpty(mPrimary.getChildNumber()); 783 boolean isForwardedNumberShown = !TextUtils.isEmpty(mPrimary.getLastForwardedNumber()); 784 boolean isCallSubjectShown = shouldShowCallSubject(mPrimary); 785 786 if (isCallSubjectShown) { 787 ui.setCallSubject(mPrimary.getCallSubject()); 788 } else { 789 ui.setCallSubject(null); 790 } 791 792 if (isCallSubjectShown) { 793 number = null; 794 } else if (isChildNumberShown) { 795 number = mContext.getString(R.string.child_number, mPrimary.getChildNumber()); 796 } else if (isForwardedNumberShown) { 797 // Use last forwarded number instead of second line, if present. 798 number = mPrimary.getLastForwardedNumber(); 799 } else { 800 number = getNumberForCall(mPrimaryContactInfo); 801 } 802 803 ui.showForwardIndicator(isForwardedNumberShown); 804 maybeShowHdAudioIcon(); 805 806 boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number); 807 // Call with caller that is a work contact. 808 boolean isWorkContact = (mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK); 809 ui.setPrimary( 810 number, 811 name, 812 nameIsNumber, 813 isChildNumberShown || isCallSubjectShown ? null : mPrimaryContactInfo.label, 814 mPrimaryContactInfo.photo, 815 mPrimaryContactInfo.isSipCall, 816 showContactPhoto, 817 hasWorkCallProperty || isWorkContact); 818 819 updateContactInteractions(); 820 } else { 821 // Clear the primary display info. 822 ui.setPrimary(null, null, false, null, null, false, false, false); 823 } 824 825 if (mEmergencyCallListener != null) { 826 boolean isEmergencyCall = mPrimary.isEmergencyCall(); 827 mEmergencyCallListener.onCallUpdated((BaseFragment) ui, isEmergencyCall); 828 } 829 } 830 831 private void updateSecondaryDisplayInfo() { 832 final CallCardUi ui = getUi(); 833 if (ui == null) { 834 return; 835 } 836 837 if (mSecondary == null) { 838 // Clear the secondary display info. 839 ui.setSecondary(false, null, false, null, null, false /* isConference */, 840 false /* isVideoCall */, mIsFullscreen); 841 return; 842 } 843 844 if (mSecondary.isConferenceCall()) { 845 ui.setSecondary( 846 true /* show */, 847 getConferenceString(mSecondary), 848 false /* nameIsNumber */, 849 null /* label */, 850 getCallProviderLabel(mSecondary), 851 true /* isConference */, 852 mSecondary.isVideoCall(mContext), 853 mIsFullscreen); 854 } else if (mSecondaryContactInfo != null) { 855 Log.d(TAG, "updateSecondaryDisplayInfo() " + mSecondaryContactInfo); 856 String name = getNameForCall(mSecondaryContactInfo); 857 boolean nameIsNumber = name != null && name.equals(mSecondaryContactInfo.number); 858 ui.setSecondary( 859 true /* show */, 860 name, 861 nameIsNumber, 862 mSecondaryContactInfo.label, 863 getCallProviderLabel(mSecondary), 864 false /* isConference */, 865 mSecondary.isVideoCall(mContext), 866 mIsFullscreen); 867 } else { 868 // Clear the secondary display info. 869 ui.setSecondary(false, null, false, null, null, false /* isConference */, 870 false /* isVideoCall */, mIsFullscreen); 871 } 872 } 873 874 875 /** 876 * Gets the phone account to display for a call. 877 */ 878 private PhoneAccount getAccountForCall(Call call) { 879 PhoneAccountHandle accountHandle = call.getAccountHandle(); 880 if (accountHandle == null) { 881 return null; 882 } 883 return TelecomManagerCompat.getPhoneAccount( 884 InCallPresenter.getInstance().getTelecomManager(), 885 accountHandle); 886 } 887 888 /** 889 * Returns the gateway number for any existing outgoing call. 890 */ 891 private String getGatewayNumber() { 892 if (hasOutgoingGatewayCall()) { 893 return getNumberFromHandle(mPrimary.getGatewayInfo().getGatewayAddress()); 894 } 895 return null; 896 } 897 898 /** 899 * Return the string label to represent the call provider 900 */ 901 private String getCallProviderLabel(Call call) { 902 PhoneAccount account = getAccountForCall(call); 903 TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager(); 904 if (account != null && !TextUtils.isEmpty(account.getLabel()) 905 && TelecomManagerCompat.getCallCapablePhoneAccounts(mgr).size() > 1) { 906 return account.getLabel().toString(); 907 } 908 return null; 909 } 910 911 /** 912 * Returns the label (line of text above the number/name) for any given call. 913 * For example, "calling via [Account/Google Voice]" for outgoing calls. 914 */ 915 private String getConnectionLabel() { 916 StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints(); 917 if (statusHints != null && !TextUtils.isEmpty(statusHints.getLabel())) { 918 return statusHints.getLabel().toString(); 919 } 920 921 if (hasOutgoingGatewayCall() && getUi() != null) { 922 // Return the label for the gateway app on outgoing calls. 923 final PackageManager pm = mContext.getPackageManager(); 924 try { 925 ApplicationInfo info = pm.getApplicationInfo( 926 mPrimary.getGatewayInfo().getGatewayProviderPackageName(), 0); 927 return pm.getApplicationLabel(info).toString(); 928 } catch (PackageManager.NameNotFoundException e) { 929 Log.e(this, "Gateway Application Not Found.", e); 930 return null; 931 } 932 } 933 return getCallProviderLabel(mPrimary); 934 } 935 936 private Drawable getCallStateIcon() { 937 // Return connection icon if one exists. 938 StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints(); 939 if (statusHints != null && statusHints.getIcon() != null) { 940 Drawable icon = statusHints.getIcon().loadDrawable(mContext); 941 if (icon != null) { 942 return icon; 943 } 944 } 945 946 return null; 947 } 948 949 private boolean hasOutgoingGatewayCall() { 950 // We only display the gateway information while STATE_DIALING so return false for any other 951 // call state. 952 // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which 953 // is also called after a contact search completes (call is not present yet). Split the 954 // UI update so it can receive independent updates. 955 if (mPrimary == null) { 956 return false; 957 } 958 return Call.State.isDialing(mPrimary.getState()) && mPrimary.getGatewayInfo() != null && 959 !mPrimary.getGatewayInfo().isEmpty(); 960 } 961 962 /** 963 * Gets the name to display for the call. 964 */ 965 @NeededForTesting 966 String getNameForCall(ContactCacheEntry contactInfo) { 967 String preferredName = ContactDisplayUtils.getPreferredDisplayName( 968 contactInfo.namePrimary, 969 contactInfo.nameAlternative, 970 mContactsPreferences); 971 if (TextUtils.isEmpty(preferredName)) { 972 return contactInfo.number; 973 } 974 return preferredName; 975 } 976 977 /** 978 * Gets the number to display for a call. 979 */ 980 @NeededForTesting 981 String getNumberForCall(ContactCacheEntry contactInfo) { 982 // If the name is empty, we use the number for the name...so don't show a second 983 // number in the number field 984 String preferredName = ContactDisplayUtils.getPreferredDisplayName( 985 contactInfo.namePrimary, 986 contactInfo.nameAlternative, 987 mContactsPreferences); 988 if (TextUtils.isEmpty(preferredName)) { 989 return contactInfo.location; 990 } 991 return contactInfo.number; 992 } 993 994 public void secondaryInfoClicked() { 995 if (mSecondary == null) { 996 Log.w(this, "Secondary info clicked but no secondary call."); 997 return; 998 } 999 1000 Log.i(this, "Swapping call to foreground: " + mSecondary); 1001 TelecomAdapter.getInstance().unholdCall(mSecondary.getId()); 1002 } 1003 1004 public void endCallClicked() { 1005 if (mPrimary == null) { 1006 return; 1007 } 1008 1009 Log.i(this, "Disconnecting call: " + mPrimary); 1010 final String callId = mPrimary.getId(); 1011 mPrimary.setState(Call.State.DISCONNECTING); 1012 CallList.getInstance().onUpdate(mPrimary); 1013 TelecomAdapter.getInstance().disconnectCall(callId); 1014 } 1015 1016 private String getNumberFromHandle(Uri handle) { 1017 return handle == null ? "" : handle.getSchemeSpecificPart(); 1018 } 1019 1020 /** 1021 * Handles a change to the fullscreen mode of the in-call UI. 1022 * 1023 * @param isFullscreenMode {@code True} if the in-call UI is entering full screen mode. 1024 */ 1025 @Override 1026 public void onFullscreenModeChanged(boolean isFullscreenMode) { 1027 mIsFullscreen = isFullscreenMode; 1028 final CallCardUi ui = getUi(); 1029 if (ui == null) { 1030 return; 1031 } 1032 ui.setCallCardVisible(!isFullscreenMode); 1033 ui.setSecondaryInfoVisible(!isFullscreenMode); 1034 maybeShowManageConferenceCallButton(); 1035 } 1036 1037 @Override 1038 public void onSecondaryCallerInfoVisibilityChanged(boolean isVisible, int height) { 1039 // No-op - the Call Card is the origin of this event. 1040 } 1041 1042 private boolean isPrimaryCallActive() { 1043 return mPrimary != null && mPrimary.getState() == Call.State.ACTIVE; 1044 } 1045 1046 private String getConferenceString(Call call) { 1047 boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE); 1048 Log.v(this, "getConferenceString: " + isGenericConference); 1049 1050 final int resId = isGenericConference 1051 ? R.string.card_title_in_call : R.string.card_title_conf_call; 1052 return mContext.getResources().getString(resId); 1053 } 1054 1055 private Drawable getConferencePhoto(Call call) { 1056 boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE); 1057 Log.v(this, "getConferencePhoto: " + isGenericConference); 1058 1059 final int resId = isGenericConference 1060 ? R.drawable.img_phone : R.drawable.img_conference; 1061 Drawable photo = mContext.getResources().getDrawable(resId); 1062 photo.setAutoMirrored(true); 1063 return photo; 1064 } 1065 1066 private boolean shouldShowEndCallButton(Call primary, int callState) { 1067 if (primary == null) { 1068 return false; 1069 } 1070 if ((!Call.State.isConnectingOrConnected(callState) 1071 && callState != Call.State.DISCONNECTING) || callState == Call.State.INCOMING) { 1072 return false; 1073 } 1074 if (mPrimary.getSessionModificationState() 1075 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 1076 return false; 1077 } 1078 return true; 1079 } 1080 1081 private void maybeSendAccessibilityEvent(InCallState oldState, InCallState newState, 1082 boolean primaryChanged) { 1083 if (mContext == null) { 1084 return; 1085 } 1086 final AccessibilityManager am = (AccessibilityManager) mContext.getSystemService( 1087 Context.ACCESSIBILITY_SERVICE); 1088 if (!am.isEnabled()) { 1089 return; 1090 } 1091 // Announce the current call if it's new incoming/outgoing call or primary call is changed 1092 // due to switching calls between two ongoing calls (one is on hold). 1093 if ((oldState != InCallState.OUTGOING && newState == InCallState.OUTGOING) 1094 || (oldState != InCallState.INCOMING && newState == InCallState.INCOMING) 1095 || primaryChanged) { 1096 if (getUi() != null) { 1097 getUi().sendAccessibilityAnnouncement(); 1098 } 1099 } 1100 } 1101 1102 /** 1103 * Determines whether the call subject should be visible on the UI. For the call subject to be 1104 * visible, the call has to be in an incoming or waiting state, and the subject must not be 1105 * empty. 1106 * 1107 * @param call The call. 1108 * @return {@code true} if the subject should be shown, {@code false} otherwise. 1109 */ 1110 private boolean shouldShowCallSubject(Call call) { 1111 if (call == null) { 1112 return false; 1113 } 1114 1115 boolean isIncomingOrWaiting = mPrimary.getState() == Call.State.INCOMING || 1116 mPrimary.getState() == Call.State.CALL_WAITING; 1117 return isIncomingOrWaiting && !TextUtils.isEmpty(call.getCallSubject()) && 1118 call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED && 1119 call.isCallSubjectSupported(); 1120 } 1121 1122 /** 1123 * Determines whether the "note sent" toast should be shown. It should be shown for a new 1124 * outgoing call with a subject. 1125 * 1126 * @param call The call 1127 * @return {@code true} if the toast should be shown, {@code false} otherwise. 1128 */ 1129 private boolean shouldShowNoteSentToast(Call call) { 1130 return call != null && hasCallSubject(call) && (call.getState() == Call.State.DIALING 1131 || call.getState() == Call.State.CONNECTING); 1132 } 1133 1134 private static boolean hasCallSubject(Call call) { 1135 return !TextUtils.isEmpty(call.getTelecomCall().getDetails().getIntentExtras() 1136 .getString(TelecomManager.EXTRA_CALL_SUBJECT)); 1137 } 1138 1139 public interface CallCardUi extends Ui { 1140 void setVisible(boolean on); 1141 void setContactContextTitle(View listHeaderView); 1142 void setContactContextContent(ListAdapter listAdapter); 1143 void showContactContext(boolean show); 1144 void setCallCardVisible(boolean visible); 1145 void setPrimary(String number, String name, boolean nameIsNumber, String label, 1146 Drawable photo, boolean isSipCall, boolean isContactPhotoShown, boolean isWorkCall); 1147 void setSecondary(boolean show, String name, boolean nameIsNumber, String label, 1148 String providerLabel, boolean isConference, boolean isVideoCall, 1149 boolean isFullscreen); 1150 void setSecondaryInfoVisible(boolean visible); 1151 void setCallState(int state, int videoState, int sessionModificationState, 1152 DisconnectCause disconnectCause, String connectionLabel, 1153 Drawable connectionIcon, String gatewayNumber, boolean isWifi, 1154 boolean isConference, boolean isWorkCall); 1155 void setPrimaryCallElapsedTime(boolean show, long duration); 1156 void setPrimaryName(String name, boolean nameIsNumber); 1157 void setPrimaryImage(Drawable image, boolean isVisible); 1158 void setPrimaryPhoneNumber(String phoneNumber); 1159 void setPrimaryLabel(String label); 1160 void setEndCallButtonEnabled(boolean enabled, boolean animate); 1161 void setCallbackNumber(String number, boolean isEmergencyCalls); 1162 void setCallSubject(String callSubject); 1163 void setProgressSpinnerVisible(boolean visible); 1164 void showHdAudioIndicator(boolean visible); 1165 void showForwardIndicator(boolean visible); 1166 void showManageConferenceCallButton(boolean visible); 1167 boolean isManageConferenceVisible(); 1168 boolean isCallSubjectVisible(); 1169 void animateForNewOutgoingCall(); 1170 void sendAccessibilityAnnouncement(); 1171 void showNoteSentToast(); 1172 } 1173} 1174