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