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