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