CallList.java revision 42373eb59cbef15ec61ebb5c919031f293291a53
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_TIMEOUT_MS = 2000;
42
43    private static final int EVENT_DISCONNECTED_TIMEOUT = 1;
44
45    private static CallList sInstance = new CallList();
46
47    private final HashMap<Integer, Call> mCallMap = Maps.newHashMap();
48    private final HashMap<Integer, ArrayList<String>> mCallTextReponsesMap =
49            Maps.newHashMap();
50    private final Set<Listener> mListeners = Sets.newArraySet();
51    private final HashMap<Integer, List<CallUpdateListener>> mCallUpdateListenerMap = Maps
52            .newHashMap();
53
54
55    /**
56     * Static singleton accessor method.
57     */
58    public static CallList getInstance() {
59        return sInstance;
60    }
61
62    /**
63     * Private constructor.  Instance should only be acquired through getInstance().
64     */
65    private CallList() {
66    }
67
68    /**
69     * Called when a single call has changed.
70     */
71    public void onUpdate(Call call) {
72        Log.d(this, "onUpdate - ", call);
73
74        updateCallInMap(call);
75        notifyListenersOfChange();
76    }
77
78    /**
79     * Called when a single call disconnects.
80     */
81    public void onDisconnect(Call call) {
82        Log.d(this, "onDisconnect: ", call);
83
84        updateCallInMap(call);
85
86        notifyListenersOfChange();
87    }
88
89    /**
90     * Called when a single call has changed.
91     */
92    public void onIncoming(Call call, List<String> textMessages) {
93        Log.d(this, "onIncoming - " + call);
94
95        updateCallInMap(call);
96        updateCallTextMap(call, textMessages);
97
98        for (Listener listener : mListeners) {
99            listener.onIncomingCall(call);
100        }
101    }
102
103    /**
104     * Called when multiple calls have changed.
105     */
106    public void onUpdate(List<Call> callsToUpdate) {
107        Log.d(this, "onUpdate(...)");
108
109        Preconditions.checkNotNull(callsToUpdate);
110        for (Call call : callsToUpdate) {
111            Log.d(this, "\t" + call);
112
113            updateCallInMap(call);
114            updateCallTextMap(call, null);
115
116            notifyCallUpdateListeners(call);
117        }
118
119        notifyListenersOfChange();
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(int 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(int 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        return getFirstCallWithState(Call.State.DIALING);
188    }
189
190    public Call getActiveCall() {
191        return getFirstCallWithState(Call.State.ACTIVE);
192    }
193
194    public Call getBackgroundCall() {
195        return getFirstCallWithState(Call.State.ONHOLD);
196    }
197
198    public Call getDisconnectedCall() {
199        return getFirstCallWithState(Call.State.DISCONNECTED);
200    }
201
202    public Call getSecondBackgroundCall() {
203        return getCallWithState(Call.State.ONHOLD, 1);
204    }
205
206    public Call getActiveOrBackgroundCall() {
207        Call call = getActiveCall();
208        if (call == null) {
209            call = getBackgroundCall();
210        }
211        return call;
212    }
213
214    public Call getIncomingCall() {
215        Call call = getFirstCallWithState(Call.State.INCOMING);
216        if (call == null) {
217            call = getFirstCallWithState(Call.State.CALL_WAITING);
218        }
219
220        return call;
221    }
222
223
224    public Call getFirstCall() {
225        // TODO: should we switch to a simple list and pull the first one?
226        Call result = getIncomingCall();
227        if (result == null) {
228            result = getFirstCallWithState(Call.State.DIALING);
229        }
230        if (result == null) {
231            result = getFirstCallWithState(Call.State.ACTIVE);
232        }
233        return result;
234    }
235
236    public boolean existsLiveCall() {
237        for (Call call : mCallMap.values()) {
238            if (!isCallDead(call)) {
239                return true;
240            }
241        }
242        return false;
243    }
244
245    public ArrayList<String> getTextResponses(int callId) {
246        return mCallTextReponsesMap.get(callId);
247    }
248
249    /**
250     * Returns first call found in the call map with the specified state.
251     */
252    public Call getFirstCallWithState(int state) {
253        return getCallWithState(state, 0);
254    }
255
256    /**
257     * Returns the [position]th call found in the call map with the specified state.
258     * TODO(klp): Improve this logic to sort by call time.
259     */
260    public Call getCallWithState(int state, int positionToFind) {
261        Call retval = null;
262        int position = 0;
263        for (Call call : mCallMap.values()) {
264            if (call.getState() == state) {
265                if (position >= positionToFind) {
266                    retval = call;
267                    break;
268                } else {
269                    position++;
270                }
271            }
272        }
273
274        return retval;
275    }
276
277    /**
278     * This is called when the service disconnects, either expectedly or unexpectedly.
279     * For the expected case, it's because we have no calls left.  For the unexpected case,
280     * it is likely a crash of phone and we need to clean up our calls manually.  Without phone,
281     * there can be no active calls, so this is relatively safe thing to do.
282     */
283    public void clearOnDisconnect() {
284        for (Call call : mCallMap.values()) {
285            final int state = call.getState();
286            if (state != Call.State.IDLE &&
287                    state != Call.State.INVALID &&
288                    state != Call.State.DISCONNECTED) {
289                call.setState(Call.State.DISCONNECTED);
290                updateCallInMap(call);
291            }
292        }
293        notifyListenersOfChange();
294    }
295
296    /**
297     * Sends a generic notification to all listeners that something has changed.
298     * It is up to the listeners to call back to determine what changed.
299     */
300    private void notifyListenersOfChange() {
301        for (Listener listener : mListeners) {
302            listener.onCallListChange(this);
303        }
304    }
305
306    private void updateCallInMap(Call call) {
307        Preconditions.checkNotNull(call);
308
309        final Integer id = new Integer(call.getCallId());
310
311        if (call.getState() == Call.State.DISCONNECTED) {
312
313            // update existing (but do not add!!) disconnected calls
314            if (mCallMap.containsKey(id)) {
315
316                // For disconnected calls, we want to keep them alive for a few seconds so that the
317                // UI has a chance to display anything it needs when a call is disconnected.
318
319                // Set up a timer to destroy the call after X seconds.
320                final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call);
321                mHandler.sendMessageDelayed(msg, DISCONNECTED_CALL_TIMEOUT_MS);
322
323                mCallMap.put(id, call);
324            }
325        } else if (!isCallDead(call)) {
326            mCallMap.put(id, call);
327        } else if (mCallMap.containsKey(id)) {
328            mCallMap.remove(id);
329        }
330    }
331
332    private void updateCallTextMap(Call call, List<String> textResponses) {
333        Preconditions.checkNotNull(call);
334
335        final Integer id = new Integer(call.getCallId());
336
337        if (!isCallDead(call)) {
338            if (textResponses != null) {
339                mCallTextReponsesMap.put(id, (ArrayList<String>) textResponses);
340            }
341        } else if (mCallMap.containsKey(id)) {
342            mCallTextReponsesMap.remove(id);
343        }
344    }
345
346    private boolean isCallDead(Call call) {
347        final int state = call.getState();
348        return Call.State.IDLE == state || Call.State.INVALID == state;
349    }
350
351    /**
352     * Sets up a call for deletion and notifies listeners of change.
353     */
354    private void finishDisconnectedCall(Call call) {
355        call.setState(Call.State.IDLE);
356        updateCallInMap(call);
357        notifyListenersOfChange();
358    }
359
360    /**
361     * Handles the timeout for destroying disconnected calls.
362     */
363    private Handler mHandler = new Handler() {
364        @Override
365        public void handleMessage(Message msg) {
366            switch (msg.what) {
367                case EVENT_DISCONNECTED_TIMEOUT:
368                    Log.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj);
369                    finishDisconnectedCall((Call) msg.obj);
370                    break;
371                default:
372                    Log.wtf(this, "Message not expected: " + msg.what);
373                    break;
374            }
375        }
376    };
377
378    /**
379     * Listener interface for any class that wants to be notified of changes
380     * to the call list.
381     */
382    public interface Listener {
383        public void onCallListChange(CallList callList);
384        public void onIncomingCall(Call call);
385    }
386
387    public interface CallUpdateListener {
388        // TODO: refactor and limit arg to be call state.  Caller info is not needed.
389        public void onCallStateChanged(Call call);
390    }
391}
392