1 /*
2  * Copyright (C) 2013 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;
18 
19 import android.os.Handler;
20 import android.os.Message;
21 import android.os.Trace;
22 import android.telecom.DisconnectCause;
23 import android.telecom.PhoneAccount;
24 
25 import com.android.contacts.common.testing.NeededForTesting;
26 import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
27 import com.android.dialer.logging.Logger;
28 import com.android.incallui.util.TelecomCallUtil;
29 
30 import com.google.common.base.Preconditions;
31 import com.google.common.collect.Maps;
32 
33 import java.util.Collections;
34 import java.util.HashMap;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.Set;
38 import java.util.concurrent.ConcurrentHashMap;
39 import java.util.concurrent.CopyOnWriteArrayList;
40 import java.util.concurrent.atomic.AtomicBoolean;
41 
42 /**
43  * Maintains the list of active calls and notifies interested classes of changes to the call list
44  * as they are received from the telephony stack. Primary listener of changes to this class is
45  * InCallPresenter.
46  */
47 public class CallList {
48 
49     private static final int DISCONNECTED_CALL_SHORT_TIMEOUT_MS = 200;
50     private static final int DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS = 2000;
51     private static final int DISCONNECTED_CALL_LONG_TIMEOUT_MS = 5000;
52 
53     private static final int EVENT_DISCONNECTED_TIMEOUT = 1;
54     private static final long BLOCK_QUERY_TIMEOUT_MS = 1000;
55 
56     private static CallList sInstance = new CallList();
57 
58     private final HashMap<String, Call> mCallById = new HashMap<>();
59     private final HashMap<android.telecom.Call, Call> mCallByTelecomCall = new HashMap<>();
60     private final HashMap<String, List<String>> mCallTextReponsesMap = Maps.newHashMap();
61     /**
62      * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
63      * load factor before resizing, 1 means we only expect a single thread to
64      * access the map so make only a single shard
65      */
66     private final Set<Listener> mListeners = Collections.newSetFromMap(
67             new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
68     private final HashMap<String, List<CallUpdateListener>> mCallUpdateListenerMap = Maps
69             .newHashMap();
70     private final Set<Call> mPendingDisconnectCalls = Collections.newSetFromMap(
71             new ConcurrentHashMap<Call, Boolean>(8, 0.9f, 1));
72     private FilteredNumberAsyncQueryHandler mFilteredQueryHandler;
73 
74     /**
75      * Static singleton accessor method.
76      */
getInstance()77     public static CallList getInstance() {
78         return sInstance;
79     }
80 
81     /**
82      * USED ONLY FOR TESTING
83      * Testing-only constructor.  Instance should only be acquired through getInstance().
84      */
85     @NeededForTesting
CallList()86     CallList() {
87     }
88 
onCallAdded(final android.telecom.Call telecomCall)89     public void onCallAdded(final android.telecom.Call telecomCall) {
90         Trace.beginSection("onCallAdded");
91         final Call call = new Call(telecomCall);
92         Log.d(this, "onCallAdded: callState=" + call.getState());
93 
94         if (call.getState() == Call.State.INCOMING ||
95                 call.getState() == Call.State.CALL_WAITING) {
96             onIncoming(call, call.getCannedSmsResponses());
97         } else {
98             onUpdate(call);
99         }
100 
101         call.logCallInitiationType();
102         Trace.endSection();
103     }
104 
onCallRemoved(android.telecom.Call telecomCall)105     public void onCallRemoved(android.telecom.Call telecomCall) {
106         if (mCallByTelecomCall.containsKey(telecomCall)) {
107             Call call = mCallByTelecomCall.get(telecomCall);
108             Logger.logCall(call);
109             if (updateCallInMap(call)) {
110                 Log.w(this, "Removing call not previously disconnected " + call.getId());
111             }
112             updateCallTextMap(call, null);
113         }
114     }
115 
116     /**
117      * Called when a single call disconnects.
118      */
onDisconnect(Call call)119     public void onDisconnect(Call call) {
120         if (updateCallInMap(call)) {
121             Log.i(this, "onDisconnect: " + call);
122             // notify those listening for changes on this specific change
123             notifyCallUpdateListeners(call);
124             // notify those listening for all disconnects
125             notifyListenersOfDisconnect(call);
126         }
127     }
128 
129     /**
130      * Called when a single call has changed.
131      */
onIncoming(Call call, List<String> textMessages)132     public void onIncoming(Call call, List<String> textMessages) {
133         if (updateCallInMap(call)) {
134             Log.i(this, "onIncoming - " + call);
135         }
136         updateCallTextMap(call, textMessages);
137 
138         for (Listener listener : mListeners) {
139             listener.onIncomingCall(call);
140         }
141     }
142 
onUpgradeToVideo(Call call)143     public void onUpgradeToVideo(Call call){
144         Log.d(this, "onUpgradeToVideo call=" + call);
145         for (Listener listener : mListeners) {
146             listener.onUpgradeToVideo(call);
147         }
148     }
149     /**
150      * Called when a single call has changed.
151      */
onUpdate(Call call)152     public void onUpdate(Call call) {
153         Trace.beginSection("onUpdate");
154         onUpdateCall(call);
155         notifyGenericListeners();
156         Trace.endSection();
157     }
158 
159     /**
160      * Called when a single call has changed session modification state.
161      *
162      * @param call The call.
163      * @param sessionModificationState The new session modification state.
164      */
onSessionModificationStateChange(Call call, int sessionModificationState)165     public void onSessionModificationStateChange(Call call, int sessionModificationState) {
166         final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId());
167         if (listeners != null) {
168             for (CallUpdateListener listener : listeners) {
169                 listener.onSessionModificationStateChange(sessionModificationState);
170             }
171         }
172     }
173 
174     /**
175      * Called when the last forwarded number changes for a call.  With IMS, the last forwarded
176      * number changes due to a supplemental service notification, so it is not pressent at the
177      * start of the call.
178      *
179      * @param call The call.
180      */
onLastForwardedNumberChange(Call call)181     public void onLastForwardedNumberChange(Call call) {
182         final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId());
183         if (listeners != null) {
184             for (CallUpdateListener listener : listeners) {
185                 listener.onLastForwardedNumberChange();
186             }
187         }
188     }
189 
190     /**
191      * Called when the child number changes for a call.  The child number can be received after a
192      * call is initially set up, so we need to be able to inform listeners of the change.
193      *
194      * @param call The call.
195      */
onChildNumberChange(Call call)196     public void onChildNumberChange(Call call) {
197         final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId());
198         if (listeners != null) {
199             for (CallUpdateListener listener : listeners) {
200                 listener.onChildNumberChange();
201             }
202         }
203     }
204 
notifyCallUpdateListeners(Call call)205     public void notifyCallUpdateListeners(Call call) {
206         final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId());
207         if (listeners != null) {
208             for (CallUpdateListener listener : listeners) {
209                 listener.onCallChanged(call);
210             }
211         }
212     }
213 
214     /**
215      * Add a call update listener for a call id.
216      *
217      * @param callId The call id to get updates for.
218      * @param listener The listener to add.
219      */
addCallUpdateListener(String callId, CallUpdateListener listener)220     public void addCallUpdateListener(String callId, CallUpdateListener listener) {
221         List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(callId);
222         if (listeners == null) {
223             listeners = new CopyOnWriteArrayList<CallUpdateListener>();
224             mCallUpdateListenerMap.put(callId, listeners);
225         }
226         listeners.add(listener);
227     }
228 
229     /**
230      * Remove a call update listener for a call id.
231      *
232      * @param callId The call id to remove the listener for.
233      * @param listener The listener to remove.
234      */
removeCallUpdateListener(String callId, CallUpdateListener listener)235     public void removeCallUpdateListener(String callId, CallUpdateListener listener) {
236         List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(callId);
237         if (listeners != null) {
238             listeners.remove(listener);
239         }
240     }
241 
addListener(Listener listener)242     public void addListener(Listener listener) {
243         Preconditions.checkNotNull(listener);
244 
245         mListeners.add(listener);
246 
247         // Let the listener know about the active calls immediately.
248         listener.onCallListChange(this);
249     }
250 
removeListener(Listener listener)251     public void removeListener(Listener listener) {
252         if (listener != null) {
253             mListeners.remove(listener);
254         }
255     }
256 
257     /**
258      * TODO: Change so that this function is not needed. Instead of assuming there is an active
259      * call, the code should rely on the status of a specific Call and allow the presenters to
260      * update the Call object when the active call changes.
261      */
getIncomingOrActive()262     public Call getIncomingOrActive() {
263         Call retval = getIncomingCall();
264         if (retval == null) {
265             retval = getActiveCall();
266         }
267         return retval;
268     }
269 
getOutgoingOrActive()270     public Call getOutgoingOrActive() {
271         Call retval = getOutgoingCall();
272         if (retval == null) {
273             retval = getActiveCall();
274         }
275         return retval;
276     }
277 
278     /**
279      * A call that is waiting for {@link PhoneAccount} selection
280      */
getWaitingForAccountCall()281     public Call getWaitingForAccountCall() {
282         return getFirstCallWithState(Call.State.SELECT_PHONE_ACCOUNT);
283     }
284 
getPendingOutgoingCall()285     public Call getPendingOutgoingCall() {
286         return getFirstCallWithState(Call.State.CONNECTING);
287     }
288 
getOutgoingCall()289     public Call getOutgoingCall() {
290         Call call = getFirstCallWithState(Call.State.DIALING);
291         if (call == null) {
292             call = getFirstCallWithState(Call.State.REDIALING);
293         }
294         return call;
295     }
296 
getActiveCall()297     public Call getActiveCall() {
298         return getFirstCallWithState(Call.State.ACTIVE);
299     }
300 
getSecondActiveCall()301     public Call getSecondActiveCall() {
302         return getCallWithState(Call.State.ACTIVE, 1);
303     }
304 
getBackgroundCall()305     public Call getBackgroundCall() {
306         return getFirstCallWithState(Call.State.ONHOLD);
307     }
308 
getDisconnectedCall()309     public Call getDisconnectedCall() {
310         return getFirstCallWithState(Call.State.DISCONNECTED);
311     }
312 
getDisconnectingCall()313     public Call getDisconnectingCall() {
314         return getFirstCallWithState(Call.State.DISCONNECTING);
315     }
316 
getSecondBackgroundCall()317     public Call getSecondBackgroundCall() {
318         return getCallWithState(Call.State.ONHOLD, 1);
319     }
320 
getActiveOrBackgroundCall()321     public Call getActiveOrBackgroundCall() {
322         Call call = getActiveCall();
323         if (call == null) {
324             call = getBackgroundCall();
325         }
326         return call;
327     }
328 
getIncomingCall()329     public Call getIncomingCall() {
330         Call call = getFirstCallWithState(Call.State.INCOMING);
331         if (call == null) {
332             call = getFirstCallWithState(Call.State.CALL_WAITING);
333         }
334 
335         return call;
336     }
337 
getFirstCall()338     public Call getFirstCall() {
339         Call result = getIncomingCall();
340         if (result == null) {
341             result = getPendingOutgoingCall();
342         }
343         if (result == null) {
344             result = getOutgoingCall();
345         }
346         if (result == null) {
347             result = getFirstCallWithState(Call.State.ACTIVE);
348         }
349         if (result == null) {
350             result = getDisconnectingCall();
351         }
352         if (result == null) {
353             result = getDisconnectedCall();
354         }
355         return result;
356     }
357 
hasLiveCall()358     public boolean hasLiveCall() {
359         Call call = getFirstCall();
360         if (call == null) {
361             return false;
362         }
363         return call != getDisconnectingCall() && call != getDisconnectedCall();
364     }
365 
366     /**
367      * Returns the first call found in the call map with the specified call modification state.
368      * @param state The session modification state to search for.
369      * @return The first call with the specified state.
370      */
getVideoUpgradeRequestCall()371     public Call getVideoUpgradeRequestCall() {
372         for(Call call : mCallById.values()) {
373             if (call.getSessionModificationState() ==
374                     Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
375                 return call;
376             }
377         }
378         return null;
379     }
380 
getCallById(String callId)381     public Call getCallById(String callId) {
382         return mCallById.get(callId);
383     }
384 
getCallByTelecomCall(android.telecom.Call telecomCall)385     public Call getCallByTelecomCall(android.telecom.Call telecomCall) {
386         return mCallByTelecomCall.get(telecomCall);
387     }
388 
getTextResponses(String callId)389     public List<String> getTextResponses(String callId) {
390         return mCallTextReponsesMap.get(callId);
391     }
392 
393     /**
394      * Returns first call found in the call map with the specified state.
395      */
getFirstCallWithState(int state)396     public Call getFirstCallWithState(int state) {
397         return getCallWithState(state, 0);
398     }
399 
400     /**
401      * Returns the [position]th call found in the call map with the specified state.
402      * TODO: Improve this logic to sort by call time.
403      */
getCallWithState(int state, int positionToFind)404     public Call getCallWithState(int state, int positionToFind) {
405         Call retval = null;
406         int position = 0;
407         for (Call call : mCallById.values()) {
408             if (call.getState() == state) {
409                 if (position >= positionToFind) {
410                     retval = call;
411                     break;
412                 } else {
413                     position++;
414                 }
415             }
416         }
417 
418         return retval;
419     }
420 
421     /**
422      * This is called when the service disconnects, either expectedly or unexpectedly.
423      * For the expected case, it's because we have no calls left.  For the unexpected case,
424      * it is likely a crash of phone and we need to clean up our calls manually.  Without phone,
425      * there can be no active calls, so this is relatively safe thing to do.
426      */
clearOnDisconnect()427     public void clearOnDisconnect() {
428         for (Call call : mCallById.values()) {
429             final int state = call.getState();
430             if (state != Call.State.IDLE &&
431                     state != Call.State.INVALID &&
432                     state != Call.State.DISCONNECTED) {
433 
434                 call.setState(Call.State.DISCONNECTED);
435                 call.setDisconnectCause(new DisconnectCause(DisconnectCause.UNKNOWN));
436                 updateCallInMap(call);
437             }
438         }
439         notifyGenericListeners();
440     }
441 
442     /**
443      * Called when the user has dismissed an error dialog. This indicates acknowledgement of
444      * the disconnect cause, and that any pending disconnects should immediately occur.
445      */
onErrorDialogDismissed()446     public void onErrorDialogDismissed() {
447         final Iterator<Call> iterator = mPendingDisconnectCalls.iterator();
448         while (iterator.hasNext()) {
449             Call call = iterator.next();
450             iterator.remove();
451             finishDisconnectedCall(call);
452         }
453     }
454 
455     /**
456      * Processes an update for a single call.
457      *
458      * @param call The call to update.
459      */
onUpdateCall(Call call)460     private void onUpdateCall(Call call) {
461         Log.d(this, "\t" + call);
462         if (updateCallInMap(call)) {
463             Log.i(this, "onUpdate - " + call);
464         }
465         updateCallTextMap(call, call.getCannedSmsResponses());
466         notifyCallUpdateListeners(call);
467     }
468 
469     /**
470      * Sends a generic notification to all listeners that something has changed.
471      * It is up to the listeners to call back to determine what changed.
472      */
notifyGenericListeners()473     private void notifyGenericListeners() {
474         for (Listener listener : mListeners) {
475             listener.onCallListChange(this);
476         }
477     }
478 
notifyListenersOfDisconnect(Call call)479     private void notifyListenersOfDisconnect(Call call) {
480         for (Listener listener : mListeners) {
481             listener.onDisconnect(call);
482         }
483     }
484 
485     /**
486      * Updates the call entry in the local map.
487      * @return false if no call previously existed and no call was added, otherwise true.
488      */
updateCallInMap(Call call)489     private boolean updateCallInMap(Call call) {
490         Preconditions.checkNotNull(call);
491 
492         boolean updated = false;
493 
494         if (call.getState() == Call.State.DISCONNECTED) {
495             // update existing (but do not add!!) disconnected calls
496             if (mCallById.containsKey(call.getId())) {
497                 // For disconnected calls, we want to keep them alive for a few seconds so that the
498                 // UI has a chance to display anything it needs when a call is disconnected.
499 
500                 // Set up a timer to destroy the call after X seconds.
501                 final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call);
502                 mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call));
503                 mPendingDisconnectCalls.add(call);
504 
505                 mCallById.put(call.getId(), call);
506                 mCallByTelecomCall.put(call.getTelecomCall(), call);
507                 updated = true;
508             }
509         } else if (!isCallDead(call)) {
510             mCallById.put(call.getId(), call);
511             mCallByTelecomCall.put(call.getTelecomCall(), call);
512             updated = true;
513         } else if (mCallById.containsKey(call.getId())) {
514             mCallById.remove(call.getId());
515             mCallByTelecomCall.remove(call.getTelecomCall());
516             updated = true;
517         }
518 
519         return updated;
520     }
521 
getDelayForDisconnect(Call call)522     private int getDelayForDisconnect(Call call) {
523         Preconditions.checkState(call.getState() == Call.State.DISCONNECTED);
524 
525 
526         final int cause = call.getDisconnectCause().getCode();
527         final int delay;
528         switch (cause) {
529             case DisconnectCause.LOCAL:
530                 delay = DISCONNECTED_CALL_SHORT_TIMEOUT_MS;
531                 break;
532             case DisconnectCause.REMOTE:
533             case DisconnectCause.ERROR:
534                 delay = DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS;
535                 break;
536             case DisconnectCause.REJECTED:
537             case DisconnectCause.MISSED:
538             case DisconnectCause.CANCELED:
539                 // no delay for missed/rejected incoming calls and canceled outgoing calls.
540                 delay = 0;
541                 break;
542             default:
543                 delay = DISCONNECTED_CALL_LONG_TIMEOUT_MS;
544                 break;
545         }
546 
547         return delay;
548     }
549 
updateCallTextMap(Call call, List<String> textResponses)550     private void updateCallTextMap(Call call, List<String> textResponses) {
551         Preconditions.checkNotNull(call);
552 
553         if (!isCallDead(call)) {
554             if (textResponses != null) {
555                 mCallTextReponsesMap.put(call.getId(), textResponses);
556             }
557         } else if (mCallById.containsKey(call.getId())) {
558             mCallTextReponsesMap.remove(call.getId());
559         }
560     }
561 
isCallDead(Call call)562     private boolean isCallDead(Call call) {
563         final int state = call.getState();
564         return Call.State.IDLE == state || Call.State.INVALID == state;
565     }
566 
567     /**
568      * Sets up a call for deletion and notifies listeners of change.
569      */
finishDisconnectedCall(Call call)570     private void finishDisconnectedCall(Call call) {
571         if (mPendingDisconnectCalls.contains(call)) {
572             mPendingDisconnectCalls.remove(call);
573         }
574         call.setState(Call.State.IDLE);
575         updateCallInMap(call);
576         notifyGenericListeners();
577     }
578 
579     /**
580      * Notifies all video calls of a change in device orientation.
581      *
582      * @param rotation The new rotation angle (in degrees).
583      */
notifyCallsOfDeviceRotation(int rotation)584     public void notifyCallsOfDeviceRotation(int rotation) {
585         for (Call call : mCallById.values()) {
586             // First, ensure that the call videoState has video enabled (there is no need to set
587             // device orientation on a voice call which has not yet been upgraded to video).
588             // Second, ensure a VideoCall is set on the call so that the change can be sent to the
589             // provider (a VideoCall can be present for a call that does not currently have video,
590             // but can be upgraded to video).
591 
592             // NOTE: is it necessary to use this order because getVideoCall references the class
593             // VideoProfile which is not available on APIs <23 (M).
594             if (VideoUtils.isVideoCall(call) && call.getVideoCall() != null) {
595                 call.getVideoCall().setDeviceOrientation(rotation);
596             }
597         }
598     }
599 
600     /**
601      * Handles the timeout for destroying disconnected calls.
602      */
603     private Handler mHandler = new Handler() {
604         @Override
605         public void handleMessage(Message msg) {
606             switch (msg.what) {
607                 case EVENT_DISCONNECTED_TIMEOUT:
608                     Log.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj);
609                     finishDisconnectedCall((Call) msg.obj);
610                     break;
611                 default:
612                     Log.wtf(this, "Message not expected: " + msg.what);
613                     break;
614             }
615         }
616     };
617 
setFilteredNumberQueryHandler(FilteredNumberAsyncQueryHandler handler)618     public void setFilteredNumberQueryHandler(FilteredNumberAsyncQueryHandler handler) {
619         mFilteredQueryHandler = handler;
620     }
621 
622     /**
623      * Listener interface for any class that wants to be notified of changes
624      * to the call list.
625      */
626     public interface Listener {
627         /**
628          * Called when a new incoming call comes in.
629          * This is the only method that gets called for incoming calls. Listeners
630          * that want to perform an action on incoming call should respond in this method
631          * because {@link #onCallListChange} does not automatically get called for
632          * incoming calls.
633          */
onIncomingCall(Call call)634         public void onIncomingCall(Call call);
635         /**
636          * Called when a new modify call request comes in
637          * This is the only method that gets called for modify requests.
638          */
onUpgradeToVideo(Call call)639         public void onUpgradeToVideo(Call call);
640         /**
641          * Called anytime there are changes to the call list.  The change can be switching call
642          * states, updating information, etc. This method will NOT be called for new incoming
643          * calls and for calls that switch to disconnected state. Listeners must add actions
644          * to those method implementations if they want to deal with those actions.
645          */
onCallListChange(CallList callList)646         public void onCallListChange(CallList callList);
647 
648         /**
649          * Called when a call switches to the disconnected state.  This is the only method
650          * that will get called upon disconnection.
651          */
onDisconnect(Call call)652         public void onDisconnect(Call call);
653 
654 
655     }
656 
657     public interface CallUpdateListener {
658         // TODO: refactor and limit arg to be call state.  Caller info is not needed.
onCallChanged(Call call)659         public void onCallChanged(Call call);
660 
661         /**
662          * Notifies of a change to the session modification state for a call.
663          *
664          * @param sessionModificationState The new session modification state.
665          */
onSessionModificationStateChange(int sessionModificationState)666         public void onSessionModificationStateChange(int sessionModificationState);
667 
668         /**
669          * Notifies of a change to the last forwarded number for a call.
670          */
onLastForwardedNumberChange()671         public void onLastForwardedNumberChange();
672 
673         /**
674          * Notifies of a change to the child number for a call.
675          */
onChildNumberChange()676         public void onChildNumberChange();
677     }
678 }
679