CallList.java revision ecabb6923e6587e0f0ca98cc895be7d99e694015
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 getDisconnectingCall() {
206        return getFirstCallWithState(Call.State.DISCONNECTING);
207    }
208
209    public Call getSecondBackgroundCall() {
210        return getCallWithState(Call.State.ONHOLD, 1);
211    }
212
213    public Call getActiveOrBackgroundCall() {
214        Call call = getActiveCall();
215        if (call == null) {
216            call = getBackgroundCall();
217        }
218        return call;
219    }
220
221    public Call getIncomingCall() {
222        Call call = getFirstCallWithState(Call.State.INCOMING);
223        if (call == null) {
224            call = getFirstCallWithState(Call.State.CALL_WAITING);
225        }
226
227        return call;
228    }
229
230    public Call getFirstCall() {
231        // TODO: should we switch to a simple list and pull the first one?
232        Call result = getIncomingCall();
233        if (result == null) {
234            result = getFirstCallWithState(Call.State.DIALING);
235        }
236        if (result == null) {
237            result = getFirstCallWithState(Call.State.ACTIVE);
238        }
239        return result;
240    }
241
242    public Call getCall(int callId) {
243        return mCallMap.get(callId);
244    }
245
246    public boolean existsLiveCall() {
247        for (Call call : mCallMap.values()) {
248            if (!isCallDead(call)) {
249                return true;
250            }
251        }
252        return false;
253    }
254
255    public ArrayList<String> getTextResponses(int callId) {
256        return mCallTextReponsesMap.get(callId);
257    }
258
259    /**
260     * Returns first call found in the call map with the specified state.
261     */
262    public Call getFirstCallWithState(int state) {
263        return getCallWithState(state, 0);
264    }
265
266    /**
267     * Returns the [position]th call found in the call map with the specified state.
268     * TODO: Improve this logic to sort by call time.
269     */
270    public Call getCallWithState(int state, int positionToFind) {
271        Call retval = null;
272        int position = 0;
273        for (Call call : mCallMap.values()) {
274            if (call.getState() == state) {
275                if (position >= positionToFind) {
276                    retval = call;
277                    break;
278                } else {
279                    position++;
280                }
281            }
282        }
283
284        return retval;
285    }
286
287    /**
288     * This is called when the service disconnects, either expectedly or unexpectedly.
289     * For the expected case, it's because we have no calls left.  For the unexpected case,
290     * it is likely a crash of phone and we need to clean up our calls manually.  Without phone,
291     * there can be no active calls, so this is relatively safe thing to do.
292     */
293    public void clearOnDisconnect() {
294        for (Call call : mCallMap.values()) {
295            final int state = call.getState();
296            if (state != Call.State.IDLE &&
297                    state != Call.State.INVALID &&
298                    state != Call.State.DISCONNECTED) {
299                call.setState(Call.State.DISCONNECTED);
300                updateCallInMap(call);
301            }
302        }
303        notifyListenersOfChange();
304    }
305
306    /**
307     * Sends a generic notification to all listeners that something has changed.
308     * It is up to the listeners to call back to determine what changed.
309     */
310    private void notifyListenersOfChange() {
311        for (Listener listener : mListeners) {
312            listener.onCallListChange(this);
313        }
314    }
315
316    private void updateCallInMap(Call call) {
317        Preconditions.checkNotNull(call);
318
319        final Integer id = new Integer(call.getCallId());
320
321        if (call.getState() == Call.State.DISCONNECTED) {
322            // update existing (but do not add!!) disconnected calls
323            if (mCallMap.containsKey(id)) {
324
325                // For disconnected calls, we want to keep them alive for a few seconds so that the
326                // UI has a chance to display anything it needs when a call is disconnected.
327
328                // Set up a timer to destroy the call after X seconds.
329                final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call);
330                mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call));
331
332                mCallMap.put(id, call);
333            }
334        } else if (!isCallDead(call)) {
335            mCallMap.put(id, call);
336        } else if (mCallMap.containsKey(id)) {
337            mCallMap.remove(id);
338        }
339    }
340
341    private int getDelayForDisconnect(Call call) {
342        Preconditions.checkState(call.getState() == Call.State.DISCONNECTED);
343
344
345        final Call.DisconnectCause cause = call.getDisconnectCause();
346        final int delay;
347        switch (cause) {
348            case LOCAL:
349                delay = DISCONNECTED_CALL_SHORT_TIMEOUT_MS;
350                break;
351            case NORMAL:
352                delay = DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS;
353                break;
354            case INCOMING_REJECTED:
355            case INCOMING_MISSED:
356                // no delay for missed/rejected incoming calls
357                delay = 0;
358                break;
359            default:
360                delay = DISCONNECTED_CALL_LONG_TIMEOUT_MS;
361                break;
362        }
363
364        return delay;
365    }
366
367    private void updateCallTextMap(Call call, List<String> textResponses) {
368        Preconditions.checkNotNull(call);
369
370        final Integer id = new Integer(call.getCallId());
371
372        if (!isCallDead(call)) {
373            if (textResponses != null) {
374                mCallTextReponsesMap.put(id, (ArrayList<String>) textResponses);
375            }
376        } else if (mCallMap.containsKey(id)) {
377            mCallTextReponsesMap.remove(id);
378        }
379    }
380
381    private boolean isCallDead(Call call) {
382        final int state = call.getState();
383        return Call.State.IDLE == state || Call.State.INVALID == state;
384    }
385
386    /**
387     * Sets up a call for deletion and notifies listeners of change.
388     */
389    private void finishDisconnectedCall(Call call) {
390        call.setState(Call.State.IDLE);
391        updateCallInMap(call);
392        notifyListenersOfChange();
393    }
394
395    /**
396     * Handles the timeout for destroying disconnected calls.
397     */
398    private Handler mHandler = new Handler() {
399        @Override
400        public void handleMessage(Message msg) {
401            switch (msg.what) {
402                case EVENT_DISCONNECTED_TIMEOUT:
403                    Log.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj);
404                    finishDisconnectedCall((Call) msg.obj);
405                    break;
406                default:
407                    Log.wtf(this, "Message not expected: " + msg.what);
408                    break;
409            }
410        }
411    };
412
413    /**
414     * Listener interface for any class that wants to be notified of changes
415     * to the call list.
416     */
417    public interface Listener {
418        public void onCallListChange(CallList callList);
419        public void onIncomingCall(Call call);
420    }
421
422    public interface CallUpdateListener {
423        // TODO: refactor and limit arg to be call state.  Caller info is not needed.
424        public void onCallStateChanged(Call call);
425    }
426}
427