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