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.location.GeoUtil; 36 import com.android.dialer.logging.DialerImpression; 37 import com.android.dialer.logging.Logger; 38 import com.android.dialer.shortcuts.ShortcutUsageReporter; 39 import com.android.dialer.spam.Spam; 40 import com.android.dialer.spam.SpamBindings; 41 import com.android.incallui.call.DialerCall.State; 42 import com.android.incallui.latencyreport.LatencyReport; 43 import com.android.incallui.util.TelecomCallUtil; 44 import com.android.incallui.videotech.utils.SessionModificationState; 45 import java.util.Collections; 46 import java.util.Iterator; 47 import java.util.Map; 48 import java.util.Objects; 49 import java.util.Set; 50 import 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 */ 57 public 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 CallList()101 public CallList() {} 102 103 @VisibleForTesting setCallListInstance(CallList callList)104 public static void setCallListInstance(CallList callList) { 105 sInstance = callList; 106 } 107 108 /** Static singleton accessor method. */ getInstance()109 public static CallList getInstance() { 110 return sInstance; 111 } 112 onCallAdded( final Context context, final android.telecom.Call telecomCall, LatencyReport latencyReport)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 logSecondIncomingCall(@onNull Context context, @NonNull DialerCall incomingCall)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 isPotentialEmergencyCallback(Context context, DialerCall call)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 getDialerCallFromTelecomCall(Call telecomCall)232 public DialerCall getDialerCallFromTelecomCall(Call telecomCall) { 233 return mCallByTelecomCall.get(telecomCall); 234 } 235 updateUserMarkedSpamStatus( final DialerCall call, final Context context, String number, final DialerCallListenerImpl dialerCallListener)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 onCallRemoved(Context context, android.telecom.Call telecomCall)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 getLegacyBindings(Context context)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 */ onInternalCallMadeExternal(Context context, android.telecom.Call telecomCall)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. */ onIncoming(DialerCall call)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 addListener(@onNull Listener listener)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 removeListener(@ullable Listener listener)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 */ getIncomingOrActive()373 public DialerCall getIncomingOrActive() { 374 DialerCall retval = getIncomingCall(); 375 if (retval == null) { 376 retval = getActiveCall(); 377 } 378 return retval; 379 } 380 getOutgoingOrActive()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 */ getWaitingForAccountCall()390 public DialerCall getWaitingForAccountCall() { 391 return getFirstCallWithState(DialerCall.State.SELECT_PHONE_ACCOUNT); 392 } 393 getPendingOutgoingCall()394 public DialerCall getPendingOutgoingCall() { 395 return getFirstCallWithState(DialerCall.State.CONNECTING); 396 } 397 getOutgoingCall()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 getActiveCall()409 public DialerCall getActiveCall() { 410 return getFirstCallWithState(DialerCall.State.ACTIVE); 411 } 412 getSecondActiveCall()413 public DialerCall getSecondActiveCall() { 414 return getCallWithState(DialerCall.State.ACTIVE, 1); 415 } 416 getBackgroundCall()417 public DialerCall getBackgroundCall() { 418 return getFirstCallWithState(DialerCall.State.ONHOLD); 419 } 420 getDisconnectedCall()421 public DialerCall getDisconnectedCall() { 422 return getFirstCallWithState(DialerCall.State.DISCONNECTED); 423 } 424 getDisconnectingCall()425 public DialerCall getDisconnectingCall() { 426 return getFirstCallWithState(DialerCall.State.DISCONNECTING); 427 } 428 getSecondBackgroundCall()429 public DialerCall getSecondBackgroundCall() { 430 return getCallWithState(DialerCall.State.ONHOLD, 1); 431 } 432 getActiveOrBackgroundCall()433 public DialerCall getActiveOrBackgroundCall() { 434 DialerCall call = getActiveCall(); 435 if (call == null) { 436 call = getBackgroundCall(); 437 } 438 return call; 439 } 440 getIncomingCall()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 getFirstCall()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 hasLiveCall()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 */ getVideoUpgradeRequestCall()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 getCallById(String callId)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. */ getFirstCallWithState(int 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 */ getCallWithState(int state, int positionToFind)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 */ clearOnDisconnect()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 */ onErrorDialogDismissed()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 */ onUpdateCall(DialerCall call)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 */ notifyGenericListeners()578 private void notifyGenericListeners() { 579 for (Listener listener : mListeners) { 580 listener.onCallListChange(this); 581 } 582 } 583 notifyListenersOfDisconnect(DialerCall call)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 */ updateCallInMap(DialerCall call)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 getDelayForDisconnect(DialerCall call)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 isCallDead(DialerCall call)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. */ finishDisconnectedCall(DialerCall call)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 */ notifyCallsOfDeviceRotation(int rotation)677 public void notifyCallsOfDeviceRotation(int rotation) { 678 for (DialerCall call : mCallById.values()) { 679 call.getVideoTech().setDeviceOrientation(rotation); 680 } 681 } 682 onInCallUiShown(boolean forFullScreenIntent)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 */ onIncomingCall(DialerCall call)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 */ onUpgradeToVideo(DialerCall call)704 void onUpgradeToVideo(DialerCall call); 705 706 /** Called when the session modification state of a call changes. */ onSessionModificationStateChange(DialerCall call)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 */ onCallListChange(CallList callList)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 */ onDisconnect(DialerCall call)721 void onDisconnect(DialerCall call); 722 onWiFiToLteHandover(DialerCall call)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 */ onHandoverToWifiFailed(DialerCall call)729 void onHandoverToWifiFailed(DialerCall call); 730 731 /** Called when the user initiates a call to an international number while on WiFi. */ onInternationalCallOnWifi(@onNull DialerCall call)732 void onInternationalCallOnWifi(@NonNull DialerCall call); 733 } 734 735 private class DialerCallListenerImpl implements DialerCallListener { 736 737 @NonNull private final DialerCall mCall; 738 DialerCallListenerImpl(@onNull DialerCall call)739 DialerCallListenerImpl(@NonNull DialerCall call) { 740 mCall = Assert.isNotNull(call); 741 } 742 743 @Override onDialerCallDisconnect()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 onDialerCallUpdate()753 public void onDialerCallUpdate() { 754 Trace.beginSection("onUpdate"); 755 onUpdateCall(mCall); 756 notifyGenericListeners(); 757 Trace.endSection(); 758 } 759 760 @Override onDialerCallChildNumberChange()761 public void onDialerCallChildNumberChange() {} 762 763 @Override onDialerCallLastForwardedNumberChange()764 public void onDialerCallLastForwardedNumberChange() {} 765 766 @Override onDialerCallUpgradeToVideo()767 public void onDialerCallUpgradeToVideo() { 768 for (Listener listener : mListeners) { 769 listener.onUpgradeToVideo(mCall); 770 } 771 } 772 773 @Override onWiFiToLteHandover()774 public void onWiFiToLteHandover() { 775 for (Listener listener : mListeners) { 776 listener.onWiFiToLteHandover(mCall); 777 } 778 } 779 780 @Override onHandoverToWifiFailure()781 public void onHandoverToWifiFailure() { 782 for (Listener listener : mListeners) { 783 listener.onHandoverToWifiFailed(mCall); 784 } 785 } 786 787 @Override onInternationalCallOnWifi()788 public void onInternationalCallOnWifi() { 789 LogUtil.enterBlock("DialerCallListenerImpl.onInternationalCallOnWifi"); 790 for (Listener listener : mListeners) { 791 listener.onInternationalCallOnWifi(mCall); 792 } 793 } 794 795 @Override onDialerCallSessionModificationStateChange()796 public void onDialerCallSessionModificationStateChange() { 797 for (Listener listener : mListeners) { 798 listener.onSessionModificationStateChange(mCall); 799 } 800 } 801 } 802 } 803