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