1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.incallui; 18 19 import com.google.common.base.Preconditions; 20 21 import android.Manifest; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.PackageManager; 26 import android.graphics.drawable.Drawable; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.support.annotation.Nullable; 30 import android.telecom.Call.Details; 31 import android.telecom.DisconnectCause; 32 import android.telecom.PhoneAccount; 33 import android.telecom.PhoneAccountHandle; 34 import android.telecom.StatusHints; 35 import android.telecom.TelecomManager; 36 import android.telecom.VideoProfile; 37 import android.telephony.PhoneNumberUtils; 38 import android.text.TextUtils; 39 import android.view.View; 40 import android.view.accessibility.AccessibilityManager; 41 import android.widget.ListAdapter; 42 43 import com.android.contacts.common.ContactsUtils; 44 import com.android.contacts.common.compat.telecom.TelecomManagerCompat; 45 import com.android.contacts.common.preference.ContactsPreferences; 46 import com.android.contacts.common.testing.NeededForTesting; 47 import com.android.contacts.common.util.ContactDisplayUtils; 48 import com.android.dialer.R; 49 import com.android.incallui.Call.State; 50 import com.android.incallui.ContactInfoCache.ContactCacheEntry; 51 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; 52 import com.android.incallui.InCallPresenter.InCallDetailsListener; 53 import com.android.incallui.InCallPresenter.InCallEventListener; 54 import com.android.incallui.InCallPresenter.InCallState; 55 import com.android.incallui.InCallPresenter.InCallStateListener; 56 import com.android.incallui.InCallPresenter.IncomingCallListener; 57 import com.android.incalluibind.ObjectFactory; 58 59 import java.lang.ref.WeakReference; 60 61 import 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 */ 67 public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> 68 implements InCallStateListener, IncomingCallListener, InCallDetailsListener, 69 InCallEventListener, CallList.CallUpdateListener, DistanceHelper.Listener { 70 71 public interface EmergencyCallListener { onCallUpdated(BaseFragment fragment, boolean isEmergency)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 ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary)98 public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) { 99 mCallCardPresenter = new WeakReference<CallCardPresenter>(callCardPresenter); 100 mIsPrimary = isPrimary; 101 } 102 103 @Override onContactInfoComplete(String callId, ContactCacheEntry entry)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 onImageLoadComplete(String callId, ContactCacheEntry entry)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 onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry)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 CallCardPresenter()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 init(Context context, Call call)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 onUiReady(CallCardUi ui)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 onUiUnready(CallCardUi ui)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 onIncomingCall(InCallState oldState, InCallState newState, Call call)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 onStateChange(InCallState oldState, InCallState newState, CallList callList)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 onDetailsChanged(Call call, Details details)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 onCallChanged(Call call)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 onSessionModificationStateChange(int sessionModificationState)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 onLastForwardedNumberChange()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 onChildNumberChange()394 public void onChildNumberChange() { 395 Log.v(this, "onChildNumberChange"); 396 397 if (mPrimary == null) { 398 return; 399 } 400 updatePrimaryDisplayInfo(); 401 } 402 shouldRefreshPrimaryInfo(boolean primaryChanged, CallCardUi ui, boolean shouldShowCallSubject)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 getSubscriptionNumber()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 updatePrimaryCallState()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 */ maybeShowHdAudioIcon()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 */ maybeShowManageConferenceCallButton()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 */ maybeShowProgressSpinner(int callState, int sessionModificationState)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 */ shouldShowManageConference()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 setCallbackNumber()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 updateCallTime()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 onCallStateButtonTouched()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 */ onContactPhotoClick()561 public void onContactPhotoClick() { 562 if (mPrimary != null && mPrimary.isVideoCall(mContext)) { 563 InCallPresenter.getInstance().toggleFullscreenMode(); 564 } 565 } 566 maybeStartSearch(Call call, boolean isPrimary)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 maybeClearSessionModificationState(Call call)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 */ startContactInfoSearch(final Call call, final boolean isPrimary, boolean isIncoming)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 onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary)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 onImageLoadComplete(String callId, ContactCacheEntry entry)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 onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry)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 onLocationReady()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 updateContactInteractions()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 */ setInCallContactInteractionsType(boolean isBusiness)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 updateContactEntry(ContactCacheEntry entry, boolean isPrimary)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 */ getCallToDisplay(CallList callList, Call ignore, boolean skipDisconnected)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 updatePrimaryDisplayInfo()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 updateSecondaryDisplayInfo()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 */ getAccountForCall(Call call)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 */ getGatewayNumber()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 */ getCallProviderLabel(Call call)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 */ getConnectionLabel()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 getCallStateIcon()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 hasOutgoingGatewayCall()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 getNameForCall(ContactCacheEntry contactInfo)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 getNumberForCall(ContactCacheEntry contactInfo)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 secondaryInfoClicked()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 endCallClicked()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 getNumberFromHandle(Uri handle)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 onFullscreenModeChanged(boolean isFullscreenMode)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 onSecondaryCallerInfoVisibilityChanged(boolean isVisible, int height)1038 public void onSecondaryCallerInfoVisibilityChanged(boolean isVisible, int height) { 1039 // No-op - the Call Card is the origin of this event. 1040 } 1041 isPrimaryCallActive()1042 private boolean isPrimaryCallActive() { 1043 return mPrimary != null && mPrimary.getState() == Call.State.ACTIVE; 1044 } 1045 getConferenceString(Call call)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 getConferencePhoto(Call call)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 shouldShowEndCallButton(Call primary, int callState)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 maybeSendAccessibilityEvent(InCallState oldState, InCallState newState, boolean primaryChanged)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 */ shouldShowCallSubject(Call call)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 */ shouldShowNoteSentToast(Call call)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 hasCallSubject(Call call)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 { setVisible(boolean on)1140 void setVisible(boolean on); setContactContextTitle(View listHeaderView)1141 void setContactContextTitle(View listHeaderView); setContactContextContent(ListAdapter listAdapter)1142 void setContactContextContent(ListAdapter listAdapter); showContactContext(boolean show)1143 void showContactContext(boolean show); setCallCardVisible(boolean visible)1144 void setCallCardVisible(boolean visible); setPrimary(String number, String name, boolean nameIsNumber, String label, Drawable photo, boolean isSipCall, boolean isContactPhotoShown, boolean isWorkCall)1145 void setPrimary(String number, String name, boolean nameIsNumber, String label, 1146 Drawable photo, boolean isSipCall, boolean isContactPhotoShown, boolean isWorkCall); setSecondary(boolean show, String name, boolean nameIsNumber, String label, String providerLabel, boolean isConference, boolean isVideoCall, boolean isFullscreen)1147 void setSecondary(boolean show, String name, boolean nameIsNumber, String label, 1148 String providerLabel, boolean isConference, boolean isVideoCall, 1149 boolean isFullscreen); setSecondaryInfoVisible(boolean visible)1150 void setSecondaryInfoVisible(boolean visible); setCallState(int state, int videoState, int sessionModificationState, DisconnectCause disconnectCause, String connectionLabel, Drawable connectionIcon, String gatewayNumber, boolean isWifi, boolean isConference, boolean isWorkCall)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); setPrimaryCallElapsedTime(boolean show, long duration)1155 void setPrimaryCallElapsedTime(boolean show, long duration); setPrimaryName(String name, boolean nameIsNumber)1156 void setPrimaryName(String name, boolean nameIsNumber); setPrimaryImage(Drawable image, boolean isVisible)1157 void setPrimaryImage(Drawable image, boolean isVisible); setPrimaryPhoneNumber(String phoneNumber)1158 void setPrimaryPhoneNumber(String phoneNumber); setPrimaryLabel(String label)1159 void setPrimaryLabel(String label); setEndCallButtonEnabled(boolean enabled, boolean animate)1160 void setEndCallButtonEnabled(boolean enabled, boolean animate); setCallbackNumber(String number, boolean isEmergencyCalls)1161 void setCallbackNumber(String number, boolean isEmergencyCalls); setCallSubject(String callSubject)1162 void setCallSubject(String callSubject); setProgressSpinnerVisible(boolean visible)1163 void setProgressSpinnerVisible(boolean visible); showHdAudioIndicator(boolean visible)1164 void showHdAudioIndicator(boolean visible); showForwardIndicator(boolean visible)1165 void showForwardIndicator(boolean visible); showManageConferenceCallButton(boolean visible)1166 void showManageConferenceCallButton(boolean visible); isManageConferenceVisible()1167 boolean isManageConferenceVisible(); isCallSubjectVisible()1168 boolean isCallSubjectVisible(); animateForNewOutgoingCall()1169 void animateForNewOutgoingCall(); sendAccessibilityAnnouncement()1170 void sendAccessibilityAnnouncement(); showNoteSentToast()1171 void showNoteSentToast(); 1172 } 1173 } 1174