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