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