Home | History | Annotate | Download | only in call
      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