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