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