1 /* 2 * Copyright (C) 2017 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.call; 18 19 import android.content.Context; 20 import android.os.Handler; 21 import android.os.Message; 22 import android.os.Trace; 23 import android.support.annotation.NonNull; 24 import android.support.annotation.Nullable; 25 import android.support.annotation.VisibleForTesting; 26 import android.telecom.Call; 27 import android.telecom.DisconnectCause; 28 import android.telecom.PhoneAccount; 29 import android.util.ArrayMap; 30 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; 31 import com.android.dialer.common.Assert; 32 import com.android.dialer.common.LogUtil; 33 import com.android.dialer.common.concurrent.DialerExecutorComponent; 34 import com.android.dialer.enrichedcall.EnrichedCallComponent; 35 import com.android.dialer.enrichedcall.EnrichedCallManager; 36 import com.android.dialer.logging.DialerImpression; 37 import com.android.dialer.logging.Logger; 38 import com.android.dialer.metrics.Metrics; 39 import com.android.dialer.metrics.MetricsComponent; 40 import com.android.dialer.promotion.impl.RttPromotion; 41 import com.android.dialer.shortcuts.ShortcutUsageReporter; 42 import com.android.dialer.spam.SpamComponent; 43 import com.android.dialer.spam.status.SpamStatus; 44 import com.android.dialer.telecom.TelecomCallUtil; 45 import com.android.incallui.call.state.DialerCallState; 46 import com.android.incallui.latencyreport.LatencyReport; 47 import com.android.incallui.videotech.utils.SessionModificationState; 48 import com.google.common.util.concurrent.FutureCallback; 49 import com.google.common.util.concurrent.Futures; 50 import com.google.common.util.concurrent.ListenableFuture; 51 import java.util.Collection; 52 import java.util.Collections; 53 import java.util.Iterator; 54 import java.util.Map; 55 import java.util.Objects; 56 import java.util.Set; 57 import java.util.concurrent.ConcurrentHashMap; 58 59 /** 60 * Maintains the list of active calls and notifies interested classes of changes to the call list as 61 * they are received from the telephony stack. Primary listener of changes to this class is 62 * InCallPresenter. 63 */ 64 public class CallList implements DialerCallDelegate { 65 66 private static final int DISCONNECTED_CALL_SHORT_TIMEOUT_MS = 200; 67 private static final int DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS = 2000; 68 private static final int DISCONNECTED_CALL_LONG_TIMEOUT_MS = 5000; 69 70 private static final int EVENT_DISCONNECTED_TIMEOUT = 1; 71 72 private static CallList instance = new CallList(); 73 74 private final Map<String, DialerCall> callById = new ArrayMap<>(); 75 private final Map<android.telecom.Call, DialerCall> callByTelecomCall = new ArrayMap<>(); 76 77 /** 78 * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is load factor before 79 * resizing, 1 means we only expect a single thread to access the map so make only a single shard 80 */ 81 private final Set<Listener> listeners = 82 Collections.newSetFromMap(new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1)); 83 84 private final Set<DialerCall> pendingDisconnectCalls = 85 Collections.newSetFromMap(new ConcurrentHashMap<DialerCall, Boolean>(8, 0.9f, 1)); 86 87 private UiListener uiListeners; 88 /** Handles the timeout for destroying disconnected calls. */ 89 private final Handler handler = 90 new Handler() { 91 @Override 92 public void handleMessage(Message msg) { 93 switch (msg.what) { 94 case EVENT_DISCONNECTED_TIMEOUT: 95 LogUtil.d("CallList.handleMessage", "EVENT_DISCONNECTED_TIMEOUT ", msg.obj); 96 finishDisconnectedCall((DialerCall) msg.obj); 97 break; 98 default: 99 LogUtil.e("CallList.handleMessage", "Message not expected: " + msg.what); 100 break; 101 } 102 } 103 }; 104 105 /** 106 * USED ONLY FOR TESTING Testing-only constructor. Instance should only be acquired through 107 * getRunningInstance(). 108 */ 109 @VisibleForTesting CallList()110 public CallList() {} 111 112 @VisibleForTesting setCallListInstance(CallList callList)113 public static void setCallListInstance(CallList callList) { 114 instance = callList; 115 } 116 117 /** Static singleton accessor method. */ getInstance()118 public static CallList getInstance() { 119 return instance; 120 } 121 onCallAdded( final Context context, final android.telecom.Call telecomCall, LatencyReport latencyReport)122 public void onCallAdded( 123 final Context context, final android.telecom.Call telecomCall, LatencyReport latencyReport) { 124 Trace.beginSection("CallList.onCallAdded"); 125 if (telecomCall.getState() == Call.STATE_CONNECTING) { 126 MetricsComponent.get(context) 127 .metrics() 128 .startTimer(Metrics.ON_CALL_ADDED_TO_ON_INCALL_UI_SHOWN_OUTGOING); 129 } else if (telecomCall.getState() == Call.STATE_RINGING) { 130 MetricsComponent.get(context) 131 .metrics() 132 .startTimer(Metrics.ON_CALL_ADDED_TO_ON_INCALL_UI_SHOWN_INCOMING); 133 } 134 if (uiListeners != null) { 135 uiListeners.onCallAdded(); 136 } 137 final DialerCall call = 138 new DialerCall(context, this, telecomCall, latencyReport, true /* registerCallback */); 139 if (getFirstCall() != null) { 140 logSecondIncomingCall(context, getFirstCall(), call); 141 } 142 143 EnrichedCallManager manager = EnrichedCallComponent.get(context).getEnrichedCallManager(); 144 manager.registerCapabilitiesListener(call); 145 manager.registerStateChangedListener(call); 146 147 Trace.beginSection("checkSpam"); 148 call.addListener(new DialerCallListenerImpl(call)); 149 LogUtil.d("CallList.onCallAdded", "callState=" + call.getState()); 150 if (SpamComponent.get(context).spamSettings().isSpamEnabled()) { 151 String number = TelecomCallUtil.getNumber(telecomCall); 152 ListenableFuture<SpamStatus> futureSpamStatus = 153 SpamComponent.get(context).spam().checkSpamStatus(number, call.getCountryIso()); 154 155 Futures.addCallback( 156 futureSpamStatus, 157 new FutureCallback<SpamStatus>() { 158 @Override 159 public void onSuccess(@Nullable SpamStatus result) { 160 boolean isIncomingCall = 161 call.getState() == DialerCallState.INCOMING 162 || call.getState() == DialerCallState.CALL_WAITING; 163 boolean isSpam = result.isSpam(); 164 call.setSpamStatus(result); 165 166 if (isIncomingCall) { 167 Logger.get(context) 168 .logCallImpression( 169 isSpam 170 ? DialerImpression.Type.INCOMING_SPAM_CALL 171 : DialerImpression.Type.INCOMING_NON_SPAM_CALL, 172 call.getUniqueCallId(), 173 call.getTimeAddedMs()); 174 } 175 onUpdateCall(call); 176 notifyGenericListeners(); 177 } 178 179 @Override 180 public void onFailure(Throwable t) { 181 LogUtil.e("CallList.onFailure", "unable to query spam status", t); 182 } 183 }, 184 DialerExecutorComponent.get(context).uiExecutor()); 185 186 Trace.beginSection("updateUserMarkedSpamStatus"); 187 Trace.endSection(); 188 } 189 Trace.endSection(); 190 191 Trace.beginSection("checkBlock"); 192 FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler = 193 new FilteredNumberAsyncQueryHandler(context); 194 195 filteredNumberAsyncQueryHandler.isBlockedNumber( 196 new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() { 197 @Override 198 public void onCheckComplete(Integer id) { 199 if (id != null && id != FilteredNumberAsyncQueryHandler.INVALID_ID) { 200 call.setBlockedStatus(true); 201 // No need to update UI since it's only used for logging. 202 } 203 } 204 }, 205 call.getNumber(), 206 call.getCountryIso()); 207 Trace.endSection(); 208 209 if (call.getState() == DialerCallState.INCOMING 210 || call.getState() == DialerCallState.CALL_WAITING) { 211 if (call.isActiveRttCall()) { 212 if (!call.isPhoneAccountRttCapable()) { 213 RttPromotion.setEnabled(context); 214 } 215 Logger.get(context) 216 .logCallImpression( 217 DialerImpression.Type.INCOMING_RTT_CALL, 218 call.getUniqueCallId(), 219 call.getTimeAddedMs()); 220 } 221 onIncoming(call); 222 } else { 223 if (call.isActiveRttCall()) { 224 Logger.get(context) 225 .logCallImpression( 226 DialerImpression.Type.OUTGOING_RTT_CALL, 227 call.getUniqueCallId(), 228 call.getTimeAddedMs()); 229 } 230 onUpdateCall(call); 231 notifyGenericListeners(); 232 } 233 234 if (call.getState() != DialerCallState.INCOMING) { 235 // Only report outgoing calls 236 ShortcutUsageReporter.onOutgoingCallAdded(context, call.getNumber()); 237 } 238 239 Trace.endSection(); 240 } 241 logSecondIncomingCall( @onNull Context context, @NonNull DialerCall firstCall, @NonNull DialerCall incomingCall)242 private void logSecondIncomingCall( 243 @NonNull Context context, @NonNull DialerCall firstCall, @NonNull DialerCall incomingCall) { 244 DialerImpression.Type impression; 245 if (firstCall.isVideoCall()) { 246 if (incomingCall.isVideoCall()) { 247 impression = DialerImpression.Type.VIDEO_CALL_WITH_INCOMING_VIDEO_CALL; 248 } else { 249 impression = DialerImpression.Type.VIDEO_CALL_WITH_INCOMING_VOICE_CALL; 250 } 251 } else { 252 if (incomingCall.isVideoCall()) { 253 impression = DialerImpression.Type.VOICE_CALL_WITH_INCOMING_VIDEO_CALL; 254 } else { 255 impression = DialerImpression.Type.VOICE_CALL_WITH_INCOMING_VOICE_CALL; 256 } 257 } 258 Assert.checkArgument(impression != null); 259 Logger.get(context) 260 .logCallImpression( 261 impression, incomingCall.getUniqueCallId(), incomingCall.getTimeAddedMs()); 262 } 263 264 @Override getDialerCallFromTelecomCall(Call telecomCall)265 public DialerCall getDialerCallFromTelecomCall(Call telecomCall) { 266 return callByTelecomCall.get(telecomCall); 267 } 268 onCallRemoved(Context context, android.telecom.Call telecomCall)269 public void onCallRemoved(Context context, android.telecom.Call telecomCall) { 270 if (callByTelecomCall.containsKey(telecomCall)) { 271 DialerCall call = callByTelecomCall.get(telecomCall); 272 Assert.checkArgument(!call.isExternalCall()); 273 274 EnrichedCallManager manager = EnrichedCallComponent.get(context).getEnrichedCallManager(); 275 manager.unregisterCapabilitiesListener(call); 276 manager.unregisterStateChangedListener(call); 277 278 // Don't log an already logged call. logCall() might be called multiple times 279 // for the same call due to a bug. 280 if (call.getLogState() != null && !call.getLogState().isLogged) { 281 getLegacyBindings(context).logCall(call); 282 call.getLogState().isLogged = true; 283 } 284 285 if (updateCallInMap(call)) { 286 LogUtil.w( 287 "CallList.onCallRemoved", "Removing call not previously disconnected " + call.getId()); 288 } 289 290 call.onRemovedFromCallList(); 291 } 292 293 if (!hasLiveCall()) { 294 DialerCall.clearRestrictedCount(); 295 } 296 } 297 getLegacyBindings(Context context)298 InCallUiLegacyBindings getLegacyBindings(Context context) { 299 Objects.requireNonNull(context); 300 301 Context application = context.getApplicationContext(); 302 InCallUiLegacyBindings legacyInstance = null; 303 if (application instanceof InCallUiLegacyBindingsFactory) { 304 legacyInstance = ((InCallUiLegacyBindingsFactory) application).newInCallUiLegacyBindings(); 305 } 306 307 if (legacyInstance == null) { 308 legacyInstance = new InCallUiLegacyBindingsStub(); 309 } 310 return legacyInstance; 311 } 312 313 /** 314 * Handles the case where an internal call has become an exteral call. We need to 315 * 316 * @param context 317 * @param telecomCall 318 */ onInternalCallMadeExternal(Context context, android.telecom.Call telecomCall)319 public void onInternalCallMadeExternal(Context context, android.telecom.Call telecomCall) { 320 321 if (callByTelecomCall.containsKey(telecomCall)) { 322 DialerCall call = callByTelecomCall.get(telecomCall); 323 324 // Don't log an already logged call. logCall() might be called multiple times 325 // for the same call due to a bug. 326 if (call.getLogState() != null && !call.getLogState().isLogged) { 327 getLegacyBindings(context).logCall(call); 328 call.getLogState().isLogged = true; 329 } 330 331 // When removing a call from the call list because it became an external call, we need to 332 // ensure the callback is unregistered -- this is normally only done when calls disconnect. 333 // However, the call won't be disconnected in this case. Also, logic in updateCallInMap 334 // would just re-add the call anyways. 335 call.unregisterCallback(); 336 callById.remove(call.getId()); 337 callByTelecomCall.remove(telecomCall); 338 } 339 } 340 341 /** Called when a single call has changed. */ onIncoming(DialerCall call)342 private void onIncoming(DialerCall call) { 343 Trace.beginSection("CallList.onIncoming"); 344 if (updateCallInMap(call)) { 345 LogUtil.i("CallList.onIncoming", String.valueOf(call)); 346 } 347 348 for (Listener listener : listeners) { 349 listener.onIncomingCall(call); 350 } 351 Trace.endSection(); 352 } 353 addListener(@onNull Listener listener)354 public void addListener(@NonNull Listener listener) { 355 Objects.requireNonNull(listener); 356 357 listeners.add(listener); 358 359 // Let the listener know about the active calls immediately. 360 listener.onCallListChange(this); 361 } 362 setUiListener(UiListener uiListener)363 public void setUiListener(UiListener uiListener) { 364 uiListeners = uiListener; 365 } 366 removeListener(@ullable Listener listener)367 public void removeListener(@Nullable Listener listener) { 368 if (listener != null) { 369 listeners.remove(listener); 370 } 371 } 372 373 /** 374 * TODO: Change so that this function is not needed. Instead of assuming there is an active call, 375 * the code should rely on the status of a specific DialerCall and allow the presenters to update 376 * the DialerCall object when the active call changes. 377 */ getIncomingOrActive()378 public DialerCall getIncomingOrActive() { 379 DialerCall retval = getIncomingCall(); 380 if (retval == null) { 381 retval = getActiveCall(); 382 } 383 return retval; 384 } 385 getOutgoingOrActive()386 public DialerCall getOutgoingOrActive() { 387 DialerCall retval = getOutgoingCall(); 388 if (retval == null) { 389 retval = getActiveCall(); 390 } 391 return retval; 392 } 393 394 /** A call that is waiting for {@link PhoneAccount} selection */ getWaitingForAccountCall()395 public DialerCall getWaitingForAccountCall() { 396 return getFirstCallWithState(DialerCallState.SELECT_PHONE_ACCOUNT); 397 } 398 getPendingOutgoingCall()399 public DialerCall getPendingOutgoingCall() { 400 return getFirstCallWithState(DialerCallState.CONNECTING); 401 } 402 getOutgoingCall()403 public DialerCall getOutgoingCall() { 404 DialerCall call = getFirstCallWithState(DialerCallState.DIALING); 405 if (call == null) { 406 call = getFirstCallWithState(DialerCallState.REDIALING); 407 } 408 if (call == null) { 409 call = getFirstCallWithState(DialerCallState.PULLING); 410 } 411 return call; 412 } 413 getActiveCall()414 public DialerCall getActiveCall() { 415 return getFirstCallWithState(DialerCallState.ACTIVE); 416 } 417 getSecondActiveCall()418 public DialerCall getSecondActiveCall() { 419 return getCallWithState(DialerCallState.ACTIVE, 1); 420 } 421 getBackgroundCall()422 public DialerCall getBackgroundCall() { 423 return getFirstCallWithState(DialerCallState.ONHOLD); 424 } 425 getDisconnectedCall()426 public DialerCall getDisconnectedCall() { 427 return getFirstCallWithState(DialerCallState.DISCONNECTED); 428 } 429 getDisconnectingCall()430 public DialerCall getDisconnectingCall() { 431 return getFirstCallWithState(DialerCallState.DISCONNECTING); 432 } 433 getSecondBackgroundCall()434 public DialerCall getSecondBackgroundCall() { 435 return getCallWithState(DialerCallState.ONHOLD, 1); 436 } 437 getActiveOrBackgroundCall()438 public DialerCall getActiveOrBackgroundCall() { 439 DialerCall call = getActiveCall(); 440 if (call == null) { 441 call = getBackgroundCall(); 442 } 443 return call; 444 } 445 getIncomingCall()446 public DialerCall getIncomingCall() { 447 DialerCall call = getFirstCallWithState(DialerCallState.INCOMING); 448 if (call == null) { 449 call = getFirstCallWithState(DialerCallState.CALL_WAITING); 450 } 451 452 return call; 453 } 454 getFirstCall()455 public DialerCall getFirstCall() { 456 DialerCall result = getIncomingCall(); 457 if (result == null) { 458 result = getPendingOutgoingCall(); 459 } 460 if (result == null) { 461 result = getOutgoingCall(); 462 } 463 if (result == null) { 464 result = getFirstCallWithState(DialerCallState.ACTIVE); 465 } 466 if (result == null) { 467 result = getDisconnectingCall(); 468 } 469 if (result == null) { 470 result = getDisconnectedCall(); 471 } 472 return result; 473 } 474 hasLiveCall()475 public boolean hasLiveCall() { 476 DialerCall call = getFirstCall(); 477 return call != null && call != getDisconnectingCall() && call != getDisconnectedCall(); 478 } 479 hasActiveRttCall()480 boolean hasActiveRttCall() { 481 for (DialerCall call : getAllCalls()) { 482 if (call.isActiveRttCall()) { 483 return true; 484 } 485 } 486 return false; 487 } 488 489 /** 490 * Returns the first call found in the call map with the upgrade to video modification state. 491 * 492 * @return The first call with the upgrade to video state. 493 */ getVideoUpgradeRequestCall()494 public DialerCall getVideoUpgradeRequestCall() { 495 for (DialerCall call : callById.values()) { 496 if (call.getVideoTech().getSessionModificationState() 497 == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 498 return call; 499 } 500 } 501 return null; 502 } 503 getCallById(String callId)504 public DialerCall getCallById(String callId) { 505 return callById.get(callId); 506 } 507 getAllCalls()508 public Collection<DialerCall> getAllCalls() { 509 return callById.values(); 510 } 511 512 /** Returns first call found in the call map with the specified state. */ getFirstCallWithState(int state)513 public DialerCall getFirstCallWithState(int state) { 514 return getCallWithState(state, 0); 515 } 516 517 /** 518 * Returns the [position]th call found in the call map with the specified state. TODO: Improve 519 * this logic to sort by call time. 520 */ getCallWithState(int state, int positionToFind)521 public DialerCall getCallWithState(int state, int positionToFind) { 522 DialerCall retval = null; 523 int position = 0; 524 for (DialerCall call : callById.values()) { 525 if (call.getState() == state) { 526 if (position >= positionToFind) { 527 retval = call; 528 break; 529 } else { 530 position++; 531 } 532 } 533 } 534 535 return retval; 536 } 537 538 /** 539 * Return if there is any active or background call which was not a parent call (never had a child 540 * call) 541 */ hasNonParentActiveOrBackgroundCall()542 public boolean hasNonParentActiveOrBackgroundCall() { 543 for (DialerCall call : callById.values()) { 544 if ((call.getState() == DialerCallState.ACTIVE 545 || call.getState() == DialerCallState.ONHOLD 546 || call.getState() == DialerCallState.CONFERENCED) 547 && !call.wasParentCall()) { 548 return true; 549 } 550 } 551 return false; 552 } 553 554 /** 555 * This is called when the service disconnects, either expectedly or unexpectedly. For the 556 * expected case, it's because we have no calls left. For the unexpected case, it is likely a 557 * crash of phone and we need to clean up our calls manually. Without phone, there can be no 558 * active calls, so this is relatively safe thing to do. 559 */ clearOnDisconnect()560 public void clearOnDisconnect() { 561 for (DialerCall call : callById.values()) { 562 final int state = call.getState(); 563 if (state != DialerCallState.IDLE 564 && state != DialerCallState.INVALID 565 && state != DialerCallState.DISCONNECTED) { 566 567 call.setState(DialerCallState.DISCONNECTED); 568 call.setDisconnectCause(new DisconnectCause(DisconnectCause.UNKNOWN)); 569 updateCallInMap(call); 570 } 571 } 572 notifyGenericListeners(); 573 } 574 575 /** 576 * Called when the user has dismissed an error dialog. This indicates acknowledgement of the 577 * disconnect cause, and that any pending disconnects should immediately occur. 578 */ onErrorDialogDismissed()579 public void onErrorDialogDismissed() { 580 final Iterator<DialerCall> iterator = pendingDisconnectCalls.iterator(); 581 while (iterator.hasNext()) { 582 DialerCall call = iterator.next(); 583 iterator.remove(); 584 finishDisconnectedCall(call); 585 } 586 } 587 588 /** 589 * Processes an update for a single call. 590 * 591 * @param call The call to update. 592 */ 593 @VisibleForTesting onUpdateCall(DialerCall call)594 void onUpdateCall(DialerCall call) { 595 Trace.beginSection("CallList.onUpdateCall"); 596 LogUtil.d("CallList.onUpdateCall", String.valueOf(call)); 597 if (!callById.containsKey(call.getId()) && call.isExternalCall()) { 598 // When a regular call becomes external, it is removed from the call list, and there may be 599 // pending updates to Telecom which are queued up on the Telecom call's handler which we no 600 // longer wish to cause updates to the call in the CallList. Bail here if the list of tracked 601 // calls doesn't contain the call which received the update. 602 return; 603 } 604 605 if (updateCallInMap(call)) { 606 LogUtil.i("CallList.onUpdateCall", String.valueOf(call)); 607 } 608 Trace.endSection(); 609 } 610 611 /** 612 * Sends a generic notification to all listeners that something has changed. It is up to the 613 * listeners to call back to determine what changed. 614 */ notifyGenericListeners()615 private void notifyGenericListeners() { 616 Trace.beginSection("CallList.notifyGenericListeners"); 617 for (Listener listener : listeners) { 618 listener.onCallListChange(this); 619 } 620 Trace.endSection(); 621 } 622 notifyListenersOfDisconnect(DialerCall call)623 private void notifyListenersOfDisconnect(DialerCall call) { 624 for (Listener listener : listeners) { 625 listener.onDisconnect(call); 626 } 627 } 628 629 /** 630 * Updates the call entry in the local map. 631 * 632 * @return false if no call previously existed and no call was added, otherwise true. 633 */ updateCallInMap(DialerCall call)634 private boolean updateCallInMap(DialerCall call) { 635 Trace.beginSection("CallList.updateCallInMap"); 636 Objects.requireNonNull(call); 637 638 boolean updated = false; 639 640 if (call.getState() == DialerCallState.DISCONNECTED) { 641 // update existing (but do not add!!) disconnected calls 642 if (callById.containsKey(call.getId())) { 643 // For disconnected calls, we want to keep them alive for a few seconds so that the 644 // UI has a chance to display anything it needs when a call is disconnected. 645 646 // Set up a timer to destroy the call after X seconds. 647 final Message msg = handler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call); 648 handler.sendMessageDelayed(msg, getDelayForDisconnect(call)); 649 pendingDisconnectCalls.add(call); 650 651 callById.put(call.getId(), call); 652 callByTelecomCall.put(call.getTelecomCall(), call); 653 updated = true; 654 } 655 } else if (!isCallDead(call)) { 656 callById.put(call.getId(), call); 657 callByTelecomCall.put(call.getTelecomCall(), call); 658 updated = true; 659 } else if (callById.containsKey(call.getId())) { 660 callById.remove(call.getId()); 661 callByTelecomCall.remove(call.getTelecomCall()); 662 updated = true; 663 } 664 665 Trace.endSection(); 666 return updated; 667 } 668 getDelayForDisconnect(DialerCall call)669 private int getDelayForDisconnect(DialerCall call) { 670 if (call.getState() != DialerCallState.DISCONNECTED) { 671 throw new IllegalStateException(); 672 } 673 674 final int cause = call.getDisconnectCause().getCode(); 675 final int delay; 676 switch (cause) { 677 case DisconnectCause.LOCAL: 678 delay = DISCONNECTED_CALL_SHORT_TIMEOUT_MS; 679 break; 680 case DisconnectCause.REMOTE: 681 case DisconnectCause.ERROR: 682 delay = DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS; 683 break; 684 case DisconnectCause.REJECTED: 685 case DisconnectCause.MISSED: 686 case DisconnectCause.CANCELED: 687 // no delay for missed/rejected incoming calls and canceled outgoing calls. 688 delay = 0; 689 break; 690 default: 691 delay = DISCONNECTED_CALL_LONG_TIMEOUT_MS; 692 break; 693 } 694 695 return delay; 696 } 697 isCallDead(DialerCall call)698 private boolean isCallDead(DialerCall call) { 699 final int state = call.getState(); 700 return DialerCallState.IDLE == state || DialerCallState.INVALID == state; 701 } 702 703 /** Sets up a call for deletion and notifies listeners of change. */ finishDisconnectedCall(DialerCall call)704 private void finishDisconnectedCall(DialerCall call) { 705 if (pendingDisconnectCalls.contains(call)) { 706 pendingDisconnectCalls.remove(call); 707 } 708 call.setState(DialerCallState.IDLE); 709 updateCallInMap(call); 710 notifyGenericListeners(); 711 } 712 713 /** 714 * Notifies all video calls of a change in device orientation. 715 * 716 * @param rotation The new rotation angle (in degrees). 717 */ notifyCallsOfDeviceRotation(int rotation)718 public void notifyCallsOfDeviceRotation(int rotation) { 719 for (DialerCall call : callById.values()) { 720 call.getVideoTech().setDeviceOrientation(rotation); 721 } 722 } 723 onInCallUiShown(boolean forFullScreenIntent)724 public void onInCallUiShown(boolean forFullScreenIntent) { 725 for (DialerCall call : callById.values()) { 726 call.getLatencyReport().onInCallUiShown(forFullScreenIntent); 727 } 728 if (uiListeners != null) { 729 uiListeners.onInCallUiShown(); 730 } 731 } 732 733 /** Listener interface for any class that wants to be notified of changes to the call list. */ 734 public interface Listener { 735 736 /** 737 * Called when a new incoming call comes in. This is the only method that gets called for 738 * incoming calls. Listeners that want to perform an action on incoming call should respond in 739 * this method because {@link #onCallListChange} does not automatically get called for incoming 740 * calls. 741 */ onIncomingCall(DialerCall call)742 void onIncomingCall(DialerCall call); 743 744 /** 745 * Called when a new modify call request comes in This is the only method that gets called for 746 * modify requests. 747 */ onUpgradeToVideo(DialerCall call)748 void onUpgradeToVideo(DialerCall call); 749 750 /** 751 * Called when a new RTT call request comes in This is the only method that gets called for RTT 752 * requests. 753 */ onUpgradeToRtt(DialerCall call, int rttRequestId)754 default void onUpgradeToRtt(DialerCall call, int rttRequestId) {} 755 756 /** Called when the SpeakEasy state of a Dialer call is mutated. */ onSpeakEasyStateChange()757 default void onSpeakEasyStateChange() {} 758 759 /** Called when the session modification state of a call changes. */ onSessionModificationStateChange(DialerCall call)760 void onSessionModificationStateChange(DialerCall call); 761 762 /** 763 * Called anytime there are changes to the call list. The change can be switching call states, 764 * updating information, etc. This method will NOT be called for new incoming calls and for 765 * calls that switch to disconnected state. Listeners must add actions to those method 766 * implementations if they want to deal with those actions. 767 */ onCallListChange(CallList callList)768 void onCallListChange(CallList callList); 769 770 /** 771 * Called when a call switches to the disconnected state. This is the only method that will get 772 * called upon disconnection. 773 */ onDisconnect(DialerCall call)774 void onDisconnect(DialerCall call); 775 onWiFiToLteHandover(DialerCall call)776 void onWiFiToLteHandover(DialerCall call); 777 778 /** 779 * Called when a user is in a video call and the call is unable to be handed off successfully to 780 * WiFi 781 */ onHandoverToWifiFailed(DialerCall call)782 void onHandoverToWifiFailed(DialerCall call); 783 784 /** Called when the user initiates a call to an international number while on WiFi. */ onInternationalCallOnWifi(@onNull DialerCall call)785 void onInternationalCallOnWifi(@NonNull DialerCall call); 786 } 787 788 /** UiListener interface for measuring incall latency.(used by testing only) */ 789 public interface UiListener { 790 791 /** Called when a new call gets added into call list from IncallServiceImpl */ onCallAdded()792 void onCallAdded(); 793 794 /** Called in the end of onResume method of IncallActivityCommon. */ onInCallUiShown()795 void onInCallUiShown(); 796 } 797 798 private class DialerCallListenerImpl implements DialerCallListener { 799 800 @NonNull private final DialerCall call; 801 DialerCallListenerImpl(@onNull DialerCall call)802 DialerCallListenerImpl(@NonNull DialerCall call) { 803 this.call = Assert.isNotNull(call); 804 } 805 806 @Override onDialerCallDisconnect()807 public void onDialerCallDisconnect() { 808 if (updateCallInMap(call)) { 809 LogUtil.i("DialerCallListenerImpl.onDialerCallDisconnect", String.valueOf(call)); 810 // notify those listening for all disconnects 811 notifyListenersOfDisconnect(call); 812 } 813 } 814 815 @Override onDialerCallUpdate()816 public void onDialerCallUpdate() { 817 Trace.beginSection("CallList.onDialerCallUpdate"); 818 onUpdateCall(call); 819 notifyGenericListeners(); 820 Trace.endSection(); 821 } 822 823 @Override onDialerCallChildNumberChange()824 public void onDialerCallChildNumberChange() {} 825 826 @Override onDialerCallLastForwardedNumberChange()827 public void onDialerCallLastForwardedNumberChange() {} 828 829 @Override onDialerCallUpgradeToRtt(int rttRequestId)830 public void onDialerCallUpgradeToRtt(int rttRequestId) { 831 for (Listener listener : listeners) { 832 listener.onUpgradeToRtt(call, rttRequestId); 833 } 834 } 835 836 @Override onDialerCallSpeakEasyStateChange()837 public void onDialerCallSpeakEasyStateChange() { 838 for (Listener listener : listeners) { 839 listener.onSpeakEasyStateChange(); 840 } 841 } 842 843 @Override onDialerCallUpgradeToVideo()844 public void onDialerCallUpgradeToVideo() { 845 for (Listener listener : listeners) { 846 listener.onUpgradeToVideo(call); 847 } 848 } 849 850 @Override onWiFiToLteHandover()851 public void onWiFiToLteHandover() { 852 for (Listener listener : listeners) { 853 listener.onWiFiToLteHandover(call); 854 } 855 } 856 857 @Override onHandoverToWifiFailure()858 public void onHandoverToWifiFailure() { 859 for (Listener listener : listeners) { 860 listener.onHandoverToWifiFailed(call); 861 } 862 } 863 864 @Override onInternationalCallOnWifi()865 public void onInternationalCallOnWifi() { 866 LogUtil.enterBlock("DialerCallListenerImpl.onInternationalCallOnWifi"); 867 for (Listener listener : listeners) { 868 listener.onInternationalCallOnWifi(call); 869 } 870 } 871 872 @Override onEnrichedCallSessionUpdate()873 public void onEnrichedCallSessionUpdate() {} 874 875 @Override onDialerCallSessionModificationStateChange()876 public void onDialerCallSessionModificationStateChange() { 877 for (Listener listener : listeners) { 878 listener.onSessionModificationStateChange(call); 879 } 880 } 881 } 882 } 883