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