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