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