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