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