• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
CallList()107   public CallList() {}
108 
109   @VisibleForTesting
setCallListInstance(CallList callList)110   public static void setCallListInstance(CallList callList) {
111     instance = callList;
112   }
113 
114   /** Static singleton accessor method. */
getInstance()115   public static CallList getInstance() {
116     return instance;
117   }
118 
onCallAdded( final Context context, final android.telecom.Call telecomCall, LatencyReport latencyReport)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 
logSecondIncomingCall( @onNull Context context, @NonNull DialerCall firstCall, @NonNull DialerCall incomingCall)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 
isPotentialEmergencyCallback(Context context, DialerCall call)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
getDialerCallFromTelecomCall(Call telecomCall)262   public DialerCall getDialerCallFromTelecomCall(Call telecomCall) {
263     return callByTelecomCall.get(telecomCall);
264   }
265 
updateUserMarkedSpamStatus( final DialerCall call, final Context context, String number)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 
onCallRemoved(Context context, android.telecom.Call telecomCall)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 
getLegacyBindings(Context context)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    */
onInternalCallMadeExternal(Context context, android.telecom.Call telecomCall)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. */
onIncoming(DialerCall call)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 
addListener(@onNull Listener listener)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 
setUiListener(UiListener uiListener)400   public void setUiListener(UiListener uiListener) {
401     uiListeners = uiListener;
402   }
403 
removeListener(@ullable Listener listener)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    */
getIncomingOrActive()415   public DialerCall getIncomingOrActive() {
416     DialerCall retval = getIncomingCall();
417     if (retval == null) {
418       retval = getActiveCall();
419     }
420     return retval;
421   }
422 
getOutgoingOrActive()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 */
getWaitingForAccountCall()432   public DialerCall getWaitingForAccountCall() {
433     return getFirstCallWithState(DialerCall.State.SELECT_PHONE_ACCOUNT);
434   }
435 
getPendingOutgoingCall()436   public DialerCall getPendingOutgoingCall() {
437     return getFirstCallWithState(DialerCall.State.CONNECTING);
438   }
439 
getOutgoingCall()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 
getActiveCall()451   public DialerCall getActiveCall() {
452     return getFirstCallWithState(DialerCall.State.ACTIVE);
453   }
454 
getSecondActiveCall()455   public DialerCall getSecondActiveCall() {
456     return getCallWithState(DialerCall.State.ACTIVE, 1);
457   }
458 
getBackgroundCall()459   public DialerCall getBackgroundCall() {
460     return getFirstCallWithState(DialerCall.State.ONHOLD);
461   }
462 
getDisconnectedCall()463   public DialerCall getDisconnectedCall() {
464     return getFirstCallWithState(DialerCall.State.DISCONNECTED);
465   }
466 
getDisconnectingCall()467   public DialerCall getDisconnectingCall() {
468     return getFirstCallWithState(DialerCall.State.DISCONNECTING);
469   }
470 
getSecondBackgroundCall()471   public DialerCall getSecondBackgroundCall() {
472     return getCallWithState(DialerCall.State.ONHOLD, 1);
473   }
474 
getActiveOrBackgroundCall()475   public DialerCall getActiveOrBackgroundCall() {
476     DialerCall call = getActiveCall();
477     if (call == null) {
478       call = getBackgroundCall();
479     }
480     return call;
481   }
482 
getIncomingCall()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 
getFirstCall()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 
hasLiveCall()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    */
getVideoUpgradeRequestCall()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 
getCallById(String callId)532   public DialerCall getCallById(String callId) {
533     return callById.get(callId);
534   }
535 
getAllCalls()536   public Collection<DialerCall> getAllCalls() {
537     return callById.values();
538   }
539 
540   /** Returns first call found in the call map with the specified state. */
getFirstCallWithState(int 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    */
getCallWithState(int state, int positionToFind)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    */
hasNonParentActiveOrBackgroundCall()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    */
clearOnDisconnect()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    */
onErrorDialogDismissed()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
onUpdateCall(DialerCall call)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    */
notifyGenericListeners()643   private void notifyGenericListeners() {
644     Trace.beginSection("CallList.notifyGenericListeners");
645     for (Listener listener : listeners) {
646       listener.onCallListChange(this);
647     }
648     Trace.endSection();
649   }
650 
notifyListenersOfDisconnect(DialerCall call)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    */
updateCallInMap(DialerCall call)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 
getDelayForDisconnect(DialerCall call)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 
isCallDead(DialerCall call)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. */
finishDisconnectedCall(DialerCall call)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    */
notifyCallsOfDeviceRotation(int rotation)746   public void notifyCallsOfDeviceRotation(int rotation) {
747     for (DialerCall call : callById.values()) {
748       call.getVideoTech().setDeviceOrientation(rotation);
749     }
750   }
751 
onInCallUiShown(boolean forFullScreenIntent)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      */
onIncomingCall(DialerCall call)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      */
onUpgradeToVideo(DialerCall call)776     void onUpgradeToVideo(DialerCall call);
777 
778     /** Called when the session modification state of a call changes. */
onSessionModificationStateChange(DialerCall call)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      */
onCallListChange(CallList callList)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      */
onDisconnect(DialerCall call)793     void onDisconnect(DialerCall call);
794 
onWiFiToLteHandover(DialerCall call)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      */
onHandoverToWifiFailed(DialerCall call)801     void onHandoverToWifiFailed(DialerCall call);
802 
803     /** Called when the user initiates a call to an international number while on WiFi. */
onInternationalCallOnWifi(@onNull DialerCall call)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 */
onCallAdded()811     void onCallAdded();
812 
813     /** Called in the end of onResume method of IncallActivityCommon. */
onInCallUiShown()814     void onInCallUiShown();
815   }
816 
817   private class DialerCallListenerImpl implements DialerCallListener {
818 
819     @NonNull private final DialerCall call;
820 
DialerCallListenerImpl(@onNull DialerCall call)821     DialerCallListenerImpl(@NonNull DialerCall call) {
822       this.call = Assert.isNotNull(call);
823     }
824 
825     @Override
onDialerCallDisconnect()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
onDialerCallUpdate()835     public void onDialerCallUpdate() {
836       Trace.beginSection("CallList.onDialerCallUpdate");
837       onUpdateCall(call);
838       notifyGenericListeners();
839       Trace.endSection();
840     }
841 
842     @Override
onDialerCallChildNumberChange()843     public void onDialerCallChildNumberChange() {}
844 
845     @Override
onDialerCallLastForwardedNumberChange()846     public void onDialerCallLastForwardedNumberChange() {}
847 
848     @Override
onDialerCallUpgradeToVideo()849     public void onDialerCallUpgradeToVideo() {
850       for (Listener listener : listeners) {
851         listener.onUpgradeToVideo(call);
852       }
853     }
854 
855     @Override
onWiFiToLteHandover()856     public void onWiFiToLteHandover() {
857       for (Listener listener : listeners) {
858         listener.onWiFiToLteHandover(call);
859       }
860     }
861 
862     @Override
onHandoverToWifiFailure()863     public void onHandoverToWifiFailure() {
864       for (Listener listener : listeners) {
865         listener.onHandoverToWifiFailed(call);
866       }
867     }
868 
869     @Override
onInternationalCallOnWifi()870     public void onInternationalCallOnWifi() {
871       LogUtil.enterBlock("DialerCallListenerImpl.onInternationalCallOnWifi");
872       for (Listener listener : listeners) {
873         listener.onInternationalCallOnWifi(call);
874       }
875     }
876 
877     @Override
onEnrichedCallSessionUpdate()878     public void onEnrichedCallSessionUpdate() {}
879 
880     @Override
onDialerCallSessionModificationStateChange()881     public void onDialerCallSessionModificationStateChange() {
882       for (Listener listener : listeners) {
883         listener.onSessionModificationStateChange(call);
884       }
885     }
886   }
887 }
888