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