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