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