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