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