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.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
    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