CallList.java revision 3c5581a67ba89fd96fc19e64f6c4780d0a58f640
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;
23import com.google.common.collect.ImmutableList;
24import com.google.common.collect.ImmutableMap;
25
26import android.os.Handler;
27import android.os.Message;
28
29import com.android.services.telephony.common.Call;
30
31import java.util.AbstractMap;
32import java.util.ArrayList;
33import java.util.HashMap;
34import java.util.List;
35import java.util.Map;
36import java.util.Set;
37
38/**
39 * Maintains the list of active calls received from CallHandlerService and notifies interested
40 * classes of changes to the call list as they are received from the telephony stack.
41 * Primary lister of changes to this class is InCallPresenter.
42 */
43public class CallList {
44
45    private static final int DISCONNECTED_CALL_TIMEOUT_MS = 2000;
46
47    private static final int EVENT_DISCONNECTED_TIMEOUT = 1;
48
49    private static CallList sInstance;
50
51    private final HashMap<Integer, Call> mCallMap = Maps.newHashMap();
52    private final HashMap<Integer, ArrayList<String>> mCallTextReponsesMap =
53            Maps.newHashMap();
54    private final Set<Listener> mListeners = Sets.newArraySet();
55
56    /**
57     * Static singleton accessor method.
58     */
59    /*public static synchronized CallList getInstance() {
60        if (sInstance == null) {
61            sInstance = new CallList();
62        }
63        return sInstance;
64    }*/
65
66    /**
67     * Private constructor.  Instance should only be acquired through getInstance().
68     */
69    public CallList() {
70    }
71
72    /**
73     * Called when a single call has changed.
74     */
75    public void onUpdate(Call call) {
76        Logger.d(this, "onUpdate - ", call);
77
78        updateCallInMap(call);
79        notifyListenersOfChange();
80    }
81
82    /**
83     * Called when a single call disconnects.
84     */
85    public void onDisconnect(Call call) {
86        Logger.d(this, "onDisconnect: ", call);
87
88        updateCallInMap(call);
89
90        notifyListenersOfChange();
91    }
92
93    /**
94     * Called when a single call has changed.
95     */
96    public void onUpdate(AbstractMap.SimpleEntry<Call, List<String> > incomingCall) {
97        Logger.d(this, "onUpdate - " + incomingCall.getKey());
98
99        updateCallInMap(incomingCall.getKey());
100        updateCallTextMap(incomingCall.getKey(), incomingCall.getValue());
101
102        notifyListenersOfChange();
103    }
104
105    /**
106     * Called when multiple calls have changed.
107     */
108    public void onUpdate(List<Call> callsToUpdate) {
109        Logger.d(this, "onUpdate(...)");
110
111        Preconditions.checkNotNull(callsToUpdate);
112        for (Call call : callsToUpdate) {
113            Logger.d(this, "\t" + call);
114
115            updateCallInMap(call);
116            updateCallTextMap(call, null);
117        }
118
119        notifyListenersOfChange();
120    }
121
122    public void addListener(Listener listener) {
123        Preconditions.checkNotNull(listener);
124
125        mListeners.add(listener);
126
127        // Let the listener know about the active calls immediately.
128        listener.onCallListChange(this);
129    }
130
131    public void removeListener(Listener listener) {
132        Preconditions.checkNotNull(listener);
133        mListeners.remove(listener);
134    }
135
136    /**
137     * TODO: Change so that this function is not needed. Instead of assuming there is an active
138     * call, the code should rely on the status of a specific Call and allow the presenters to
139     * update the Call object when the active call changes.
140     */
141    public Call getIncomingOrActive() {
142        Call retval = getIncomingCall();
143        if (retval == null) {
144            retval = getActiveCall();
145        }
146        return retval;
147    }
148
149    public Call getOutgoingCall() {
150        return getFirstCallWithState(Call.State.DIALING);
151    }
152
153    public Call getActiveCall() {
154        return getFirstCallWithState(Call.State.ACTIVE);
155    }
156
157    public Call getBackgroundCall() {
158        return getFirstCallWithState(Call.State.ONHOLD);
159    }
160
161    public Call getDisconnectedCall() {
162        return getFirstCallWithState(Call.State.DISCONNECTED);
163    }
164
165    public Call getSecondBackgroundCall() {
166        return getCallWithState(Call.State.ONHOLD, 1);
167    }
168
169    public Call getActiveOrBackgroundCall() {
170        Call call = getActiveCall();
171        if (call == null) {
172            call = getBackgroundCall();
173        }
174        return call;
175    }
176
177    public Call getIncomingCall() {
178        Call call = getFirstCallWithState(Call.State.INCOMING);
179        if (call == null) {
180            call = getFirstCallWithState(Call.State.CALL_WAITING);
181        }
182
183        return call;
184    }
185
186    public boolean existsLiveCall() {
187        for (Call call : mCallMap.values()) {
188            if (!isCallDead(call)) {
189                return true;
190            }
191        }
192        return false;
193    }
194
195    public ArrayList<String> getTextResponses(Call call) {
196        return mCallTextReponsesMap.get(call.getCallId());
197    }
198
199    /**
200     * Returns first call found in the call map with the specified state.
201     */
202    public Call getFirstCallWithState(int state) {
203        return getCallWithState(state, 0);
204    }
205
206    /**
207     * Returns the [position]th call found in the call map with the specified state.
208     * TODO(klp): Improve this logic to sort by call time.
209     */
210    public Call getCallWithState(int state, int positionToFind) {
211        Call retval = null;
212        int position = 0;
213        for (Call call : mCallMap.values()) {
214            if (call.getState() == state) {
215                if (position >= positionToFind) {
216                    retval = call;
217                    break;
218                } else {
219                    position++;
220                }
221            }
222        }
223
224        return retval;
225    }
226
227    /**
228     * This is called when the service disconnects, either expectedly or unexpectedly.
229     * For the expected case, it's because we have no calls left.  For the unexpected case,
230     * it is likely a crash of phone and we need to clean up our calls manually.  Without phone,
231     * there can be no active calls, so this is relatively safe thing to do.
232     */
233    public void clearOnDisconnect() {
234        for (Call call : mCallMap.values()) {
235            final int state = call.getState();
236            if (state != Call.State.IDLE &&
237                    state != Call.State.INVALID &&
238                    state != Call.State.DISCONNECTED) {
239                call.setState(Call.State.DISCONNECTED);
240                updateCallInMap(call);
241            }
242        }
243        notifyListenersOfChange();
244    }
245
246    /**
247     * Sends a generic notification to all listeners that something has changed.
248     * It is up to the listeners to call back to determine what changed.
249     */
250    private void notifyListenersOfChange() {
251        for (Listener listener : mListeners) {
252            listener.onCallListChange(this);
253        }
254    }
255
256    private void updateCallInMap(Call call) {
257        Preconditions.checkNotNull(call);
258
259        final Integer id = new Integer(call.getCallId());
260
261        if (call.getState() == Call.State.DISCONNECTED) {
262
263            // update existing (but do not add!!) disconnected calls
264            if (mCallMap.containsKey(id)) {
265
266                // For disconnected calls, we want to keep them alive for a few seconds so that the
267                // UI has a chance to display anything it needs when a call is disconnected.
268
269                // Set up a timer to destroy the call after X seconds.
270                final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call);
271                mHandler.sendMessageDelayed(msg, DISCONNECTED_CALL_TIMEOUT_MS);
272
273                mCallMap.put(id, call);
274            }
275        } else if (!isCallDead(call)) {
276            mCallMap.put(id, call);
277        } else if (mCallMap.containsKey(id)) {
278            mCallMap.remove(id);
279        }
280    }
281
282    private void updateCallTextMap(Call call, List<String> textResponses) {
283        Preconditions.checkNotNull(call);
284
285        final Integer id = new Integer(call.getCallId());
286
287        if (!isCallDead(call)) {
288            if (textResponses != null) {
289                mCallTextReponsesMap.put(id, (ArrayList<String>) textResponses);
290            }
291        } else if (mCallMap.containsKey(id)) {
292            mCallTextReponsesMap.remove(id);
293        }
294    }
295
296    private boolean isCallDead(Call call) {
297        final int state = call.getState();
298        return Call.State.IDLE == state || Call.State.INVALID == state;
299    }
300
301    /**
302     * Sets up a call for deletion and notifies listeners of change.
303     */
304    private void finishDisconnectedCall(Call call) {
305        call.setState(Call.State.IDLE);
306        updateCallInMap(call);
307        notifyListenersOfChange();
308    }
309
310    /**
311     * Handles the timeout for destroying disconnected calls.
312     */
313    private Handler mHandler = new Handler() {
314        @Override
315        public void handleMessage(Message msg) {
316            switch (msg.what) {
317                case EVENT_DISCONNECTED_TIMEOUT:
318                    Logger.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj);
319                    finishDisconnectedCall((Call) msg.obj);
320                    break;
321                default:
322                    Logger.wtf(this, "Message not expected: " + msg.what);
323                    break;
324            }
325        }
326    };
327
328    /**
329     * Listener interface for any class that wants to be notified of changes
330     * to the call list.
331     */
332    public interface Listener {
333        public void onCallListChange(CallList callList);
334    }
335}
336