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