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