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