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     /**
    215      * A call that is waiting for {@link PhoneAccount} selection
    216      */
    217     public Call getWaitingForAccountCall() {
    218         return getFirstCallWithState(Call.State.PRE_DIAL_WAIT);
    219     }
    220 
    221     public Call getPendingOutgoingCall() {
    222         return getFirstCallWithState(Call.State.CONNECTING);
    223     }
    224 
    225     public Call getOutgoingCall() {
    226         Call call = getFirstCallWithState(Call.State.DIALING);
    227         if (call == null) {
    228             call = getFirstCallWithState(Call.State.REDIALING);
    229         }
    230         return call;
    231     }
    232 
    233     public Call getActiveCall() {
    234         return getFirstCallWithState(Call.State.ACTIVE);
    235     }
    236 
    237     public Call getBackgroundCall() {
    238         return getFirstCallWithState(Call.State.ONHOLD);
    239     }
    240 
    241     public Call getDisconnectedCall() {
    242         return getFirstCallWithState(Call.State.DISCONNECTED);
    243     }
    244 
    245     public Call getDisconnectingCall() {
    246         return getFirstCallWithState(Call.State.DISCONNECTING);
    247     }
    248 
    249     public Call getSecondBackgroundCall() {
    250         return getCallWithState(Call.State.ONHOLD, 1);
    251     }
    252 
    253     public Call getActiveOrBackgroundCall() {
    254         Call call = getActiveCall();
    255         if (call == null) {
    256             call = getBackgroundCall();
    257         }
    258         return call;
    259     }
    260 
    261     public Call getIncomingCall() {
    262         Call call = getFirstCallWithState(Call.State.INCOMING);
    263         if (call == null) {
    264             call = getFirstCallWithState(Call.State.CALL_WAITING);
    265         }
    266 
    267         return call;
    268     }
    269 
    270     public Call getFirstCall() {
    271         Call result = getIncomingCall();
    272         if (result == null) {
    273             result = getPendingOutgoingCall();
    274         }
    275         if (result == null) {
    276             result = getOutgoingCall();
    277         }
    278         if (result == null) {
    279             result = getFirstCallWithState(Call.State.ACTIVE);
    280         }
    281         if (result == null) {
    282             result = getDisconnectingCall();
    283         }
    284         if (result == null) {
    285             result = getDisconnectedCall();
    286         }
    287         return result;
    288     }
    289 
    290     public boolean hasLiveCall() {
    291         Call call = getFirstCall();
    292         if (call == null) {
    293             return false;
    294         }
    295         return call != getDisconnectingCall() && call != getDisconnectedCall();
    296     }
    297 
    298     /**
    299      * Returns the first call found in the call map with the specified call modification state.
    300      * @param state The session modification state to search for.
    301      * @return The first call with the specified state.
    302      */
    303     public Call getVideoUpgradeRequestCall() {
    304         for(Call call : mCallById.values()) {
    305             if (call.getSessionModificationState() ==
    306                     Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
    307                 return call;
    308             }
    309         }
    310         return null;
    311     }
    312 
    313     public Call getCallById(String callId) {
    314         return mCallById.get(callId);
    315     }
    316 
    317     public Call getCallByTelecommCall(android.telecom.Call telecommCall) {
    318         return mCallByTelecommCall.get(telecommCall);
    319     }
    320 
    321     public List<String> getTextResponses(String callId) {
    322         return mCallTextReponsesMap.get(callId);
    323     }
    324 
    325     /**
    326      * Returns first call found in the call map with the specified state.
    327      */
    328     public Call getFirstCallWithState(int state) {
    329         return getCallWithState(state, 0);
    330     }
    331 
    332     /**
    333      * Returns the [position]th call found in the call map with the specified state.
    334      * TODO: Improve this logic to sort by call time.
    335      */
    336     public Call getCallWithState(int state, int positionToFind) {
    337         Call retval = null;
    338         int position = 0;
    339         for (Call call : mCallById.values()) {
    340             if (call.getState() == state) {
    341                 if (position >= positionToFind) {
    342                     retval = call;
    343                     break;
    344                 } else {
    345                     position++;
    346                 }
    347             }
    348         }
    349 
    350         return retval;
    351     }
    352 
    353     /**
    354      * This is called when the service disconnects, either expectedly or unexpectedly.
    355      * For the expected case, it's because we have no calls left.  For the unexpected case,
    356      * it is likely a crash of phone and we need to clean up our calls manually.  Without phone,
    357      * there can be no active calls, so this is relatively safe thing to do.
    358      */
    359     public void clearOnDisconnect() {
    360         for (Call call : mCallById.values()) {
    361             final int state = call.getState();
    362             if (state != Call.State.IDLE &&
    363                     state != Call.State.INVALID &&
    364                     state != Call.State.DISCONNECTED) {
    365 
    366                 call.setState(Call.State.DISCONNECTED);
    367                 call.setDisconnectCause(new DisconnectCause(DisconnectCause.UNKNOWN));
    368                 updateCallInMap(call);
    369             }
    370         }
    371         notifyGenericListeners();
    372     }
    373 
    374     /**
    375      * Processes an update for a single call.
    376      *
    377      * @param call The call to update.
    378      */
    379     private void onUpdateCall(Call call) {
    380         Log.d(this, "\t" + call);
    381         if (updateCallInMap(call)) {
    382             Log.i(this, "onUpdate - " + call);
    383         }
    384         updateCallTextMap(call, call.getCannedSmsResponses());
    385         notifyCallUpdateListeners(call);
    386     }
    387 
    388     /**
    389      * Sends a generic notification to all listeners that something has changed.
    390      * It is up to the listeners to call back to determine what changed.
    391      */
    392     private void notifyGenericListeners() {
    393         for (Listener listener : mListeners) {
    394             listener.onCallListChange(this);
    395         }
    396     }
    397 
    398     private void notifyListenersOfDisconnect(Call call) {
    399         for (Listener listener : mListeners) {
    400             listener.onDisconnect(call);
    401         }
    402     }
    403 
    404     /**
    405      * Updates the call entry in the local map.
    406      * @return false if no call previously existed and no call was added, otherwise true.
    407      */
    408     private boolean updateCallInMap(Call call) {
    409         Preconditions.checkNotNull(call);
    410 
    411         boolean updated = false;
    412 
    413         if (call.getState() == Call.State.DISCONNECTED) {
    414             // update existing (but do not add!!) disconnected calls
    415             if (mCallById.containsKey(call.getId())) {
    416 
    417                 // For disconnected calls, we want to keep them alive for a few seconds so that the
    418                 // UI has a chance to display anything it needs when a call is disconnected.
    419 
    420                 // Set up a timer to destroy the call after X seconds.
    421                 final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call);
    422                 mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call));
    423 
    424                 mCallById.put(call.getId(), call);
    425                 mCallByTelecommCall.put(call.getTelecommCall(), call);
    426                 updated = true;
    427             }
    428         } else if (!isCallDead(call)) {
    429             mCallById.put(call.getId(), call);
    430             mCallByTelecommCall.put(call.getTelecommCall(), call);
    431             updated = true;
    432         } else if (mCallById.containsKey(call.getId())) {
    433             mCallById.remove(call.getId());
    434             mCallByTelecommCall.remove(call.getTelecommCall());
    435             updated = true;
    436         }
    437 
    438         return updated;
    439     }
    440 
    441     private int getDelayForDisconnect(Call call) {
    442         Preconditions.checkState(call.getState() == Call.State.DISCONNECTED);
    443 
    444 
    445         final int cause = call.getDisconnectCause().getCode();
    446         final int delay;
    447         switch (cause) {
    448             case DisconnectCause.LOCAL:
    449                 delay = DISCONNECTED_CALL_SHORT_TIMEOUT_MS;
    450                 break;
    451             case DisconnectCause.REMOTE:
    452                 delay = DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS;
    453                 break;
    454             case DisconnectCause.REJECTED:
    455             case DisconnectCause.MISSED:
    456             case DisconnectCause.CANCELED:
    457                 // no delay for missed/rejected incoming calls and canceled outgoing calls.
    458                 delay = 0;
    459                 break;
    460             default:
    461                 delay = DISCONNECTED_CALL_LONG_TIMEOUT_MS;
    462                 break;
    463         }
    464 
    465         return delay;
    466     }
    467 
    468     private void updateCallTextMap(Call call, List<String> textResponses) {
    469         Preconditions.checkNotNull(call);
    470 
    471         if (!isCallDead(call)) {
    472             if (textResponses != null) {
    473                 mCallTextReponsesMap.put(call.getId(), textResponses);
    474             }
    475         } else if (mCallById.containsKey(call.getId())) {
    476             mCallTextReponsesMap.remove(call.getId());
    477         }
    478     }
    479 
    480     private boolean isCallDead(Call call) {
    481         final int state = call.getState();
    482         return Call.State.IDLE == state || Call.State.INVALID == state;
    483     }
    484 
    485     /**
    486      * Sets up a call for deletion and notifies listeners of change.
    487      */
    488     private void finishDisconnectedCall(Call call) {
    489         call.setState(Call.State.IDLE);
    490         updateCallInMap(call);
    491         notifyGenericListeners();
    492     }
    493 
    494     /**
    495      * Notifies all video calls of a change in device orientation.
    496      *
    497      * @param rotation The new rotation angle (in degrees).
    498      */
    499     public void notifyCallsOfDeviceRotation(int rotation) {
    500         for (Call call : mCallById.values()) {
    501             if (call.getVideoCall() != null) {
    502                 call.getVideoCall().setDeviceOrientation(rotation);
    503             }
    504         }
    505     }
    506 
    507     /**
    508      * Handles the timeout for destroying disconnected calls.
    509      */
    510     private Handler mHandler = new Handler() {
    511         @Override
    512         public void handleMessage(Message msg) {
    513             switch (msg.what) {
    514                 case EVENT_DISCONNECTED_TIMEOUT:
    515                     Log.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj);
    516                     finishDisconnectedCall((Call) msg.obj);
    517                     break;
    518                 default:
    519                     Log.wtf(this, "Message not expected: " + msg.what);
    520                     break;
    521             }
    522         }
    523     };
    524 
    525     /**
    526      * Listener interface for any class that wants to be notified of changes
    527      * to the call list.
    528      */
    529     public interface Listener {
    530         /**
    531          * Called when a new incoming call comes in.
    532          * This is the only method that gets called for incoming calls. Listeners
    533          * that want to perform an action on incoming call should respond in this method
    534          * because {@link #onCallListChange} does not automatically get called for
    535          * incoming calls.
    536          */
    537         public void onIncomingCall(Call call);
    538 
    539         /**
    540          * Called anytime there are changes to the call list.  The change can be switching call
    541          * states, updating information, etc. This method will NOT be called for new incoming
    542          * calls and for calls that switch to disconnected state. Listeners must add actions
    543          * to those method implementations if they want to deal with those actions.
    544          */
    545         public void onCallListChange(CallList callList);
    546 
    547         /**
    548          * Called when a call switches to the disconnected state.  This is the only method
    549          * that will get called upon disconnection.
    550          */
    551         public void onDisconnect(Call call);
    552     }
    553 
    554     public interface CallUpdateListener {
    555         // TODO: refactor and limit arg to be call state.  Caller info is not needed.
    556         public void onCallChanged(Call call);
    557     }
    558 }
    559