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