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