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