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