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.android.collect.Lists;
     20 import com.google.android.collect.Maps;
     21 import com.google.android.collect.Sets;
     22 import com.google.common.base.Preconditions;
     23 
     24 import android.os.Handler;
     25 import android.os.Message;
     26 
     27 import com.android.services.telephony.common.Call;
     28 import com.android.services.telephony.common.Call.DisconnectCause;
     29 
     30 import java.util.ArrayList;
     31 import java.util.HashMap;
     32 import java.util.List;
     33 import java.util.Set;
     34 
     35 /**
     36  * Maintains the list of active calls received from CallHandlerService and notifies interested
     37  * classes of changes to the call list as they are received from the telephony stack.
     38  * Primary lister of changes to this class is InCallPresenter.
     39  */
     40 public class CallList {
     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<Integer, Call> mCallMap = Maps.newHashMap();
     51     private final HashMap<Integer, ArrayList<String>> mCallTextReponsesMap =
     52             Maps.newHashMap();
     53     private final Set<Listener> mListeners = Sets.newArraySet();
     54     private final HashMap<Integer, List<CallUpdateListener>> mCallUpdateListenerMap = Maps
     55             .newHashMap();
     56 
     57 
     58     /**
     59      * Static singleton accessor method.
     60      */
     61     public static CallList getInstance() {
     62         return sInstance;
     63     }
     64 
     65     /**
     66      * Private constructor.  Instance should only be acquired through getInstance().
     67      */
     68     private CallList() {
     69     }
     70 
     71     /**
     72      * Called when a single call has changed.
     73      */
     74     public void onUpdate(Call call) {
     75         Log.d(this, "onUpdate - ", call);
     76 
     77         updateCallInMap(call);
     78         notifyListenersOfChange();
     79     }
     80 
     81     /**
     82      * Called when a single call disconnects.
     83      */
     84     public void onDisconnect(Call call) {
     85         Log.d(this, "onDisconnect: ", call);
     86 
     87         boolean updated = updateCallInMap(call);
     88 
     89         if (updated) {
     90             // notify those listening for changes on this specific change
     91             notifyCallUpdateListeners(call);
     92 
     93             // notify those listening for all disconnects
     94             notifyListenersOfDisconnect(call);
     95         }
     96     }
     97 
     98     /**
     99      * Called when a single call has changed.
    100      */
    101     public void onIncoming(Call call, List<String> textMessages) {
    102         Log.d(this, "onIncoming - " + call);
    103 
    104         updateCallInMap(call);
    105         updateCallTextMap(call, textMessages);
    106 
    107         for (Listener listener : mListeners) {
    108             listener.onIncomingCall(call);
    109         }
    110     }
    111 
    112     /**
    113      * Called when multiple calls have changed.
    114      */
    115     public void onUpdate(List<Call> callsToUpdate) {
    116         Log.d(this, "onUpdate(...)");
    117 
    118         Preconditions.checkNotNull(callsToUpdate);
    119         for (Call call : callsToUpdate) {
    120             Log.d(this, "\t" + call);
    121 
    122             updateCallInMap(call);
    123             updateCallTextMap(call, null);
    124 
    125             notifyCallUpdateListeners(call);
    126         }
    127 
    128         notifyListenersOfChange();
    129     }
    130 
    131     public void notifyCallUpdateListeners(Call call) {
    132         final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getCallId());
    133         if (listeners != null) {
    134             for (CallUpdateListener listener : listeners) {
    135                 listener.onCallStateChanged(call);
    136             }
    137         }
    138     }
    139 
    140     /**
    141      * Add a call update listener for a call id.
    142      *
    143      * @param callId The call id to get updates for.
    144      * @param listener The listener to add.
    145      */
    146     public void addCallUpdateListener(int callId, CallUpdateListener listener) {
    147         List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(callId);
    148         if (listeners == null) {
    149             listeners = Lists.newArrayList();
    150             mCallUpdateListenerMap.put(callId, listeners);
    151         }
    152         listeners.add(listener);
    153     }
    154 
    155     /**
    156      * Remove a call update listener for a call id.
    157      *
    158      * @param callId The call id to remove the listener for.
    159      * @param listener The listener to remove.
    160      */
    161     public void removeCallUpdateListener(int callId, CallUpdateListener listener) {
    162         List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(callId);
    163         if (listeners != null) {
    164             listeners.remove(listener);
    165         }
    166     }
    167 
    168     public void addListener(Listener listener) {
    169         Preconditions.checkNotNull(listener);
    170 
    171         mListeners.add(listener);
    172 
    173         // Let the listener know about the active calls immediately.
    174         listener.onCallListChange(this);
    175     }
    176 
    177     public void removeListener(Listener listener) {
    178         Preconditions.checkNotNull(listener);
    179         mListeners.remove(listener);
    180     }
    181 
    182     /**
    183      * TODO: Change so that this function is not needed. Instead of assuming there is an active
    184      * call, the code should rely on the status of a specific Call and allow the presenters to
    185      * update the Call object when the active call changes.
    186      */
    187     public Call getIncomingOrActive() {
    188         Call retval = getIncomingCall();
    189         if (retval == null) {
    190             retval = getActiveCall();
    191         }
    192         return retval;
    193     }
    194 
    195     public Call getOutgoingCall() {
    196         Call call = getFirstCallWithState(Call.State.DIALING);
    197         if (call == null) {
    198             call = getFirstCallWithState(Call.State.REDIALING);
    199         }
    200         return call;
    201     }
    202 
    203     public Call getActiveCall() {
    204         return getFirstCallWithState(Call.State.ACTIVE);
    205     }
    206 
    207     public Call getBackgroundCall() {
    208         return getFirstCallWithState(Call.State.ONHOLD);
    209     }
    210 
    211     public Call getDisconnectedCall() {
    212         return getFirstCallWithState(Call.State.DISCONNECTED);
    213     }
    214 
    215     public Call getDisconnectingCall() {
    216         return getFirstCallWithState(Call.State.DISCONNECTING);
    217     }
    218 
    219     public Call getSecondBackgroundCall() {
    220         return getCallWithState(Call.State.ONHOLD, 1);
    221     }
    222 
    223     public Call getActiveOrBackgroundCall() {
    224         Call call = getActiveCall();
    225         if (call == null) {
    226             call = getBackgroundCall();
    227         }
    228         return call;
    229     }
    230 
    231     public Call getIncomingCall() {
    232         Call call = getFirstCallWithState(Call.State.INCOMING);
    233         if (call == null) {
    234             call = getFirstCallWithState(Call.State.CALL_WAITING);
    235         }
    236 
    237         return call;
    238     }
    239 
    240     public Call getFirstCall() {
    241         Call result = getIncomingCall();
    242         if (result == null) {
    243             result = getOutgoingCall();
    244         }
    245         if (result == null) {
    246             result = getFirstCallWithState(Call.State.ACTIVE);
    247         }
    248         if (result == null) {
    249             result = getDisconnectingCall();
    250         }
    251         if (result == null) {
    252             result = getDisconnectedCall();
    253         }
    254         return result;
    255     }
    256 
    257     public Call getCall(int callId) {
    258         return mCallMap.get(callId);
    259     }
    260 
    261     public boolean existsLiveCall() {
    262         for (Call call : mCallMap.values()) {
    263             if (!isCallDead(call)) {
    264                 return true;
    265             }
    266         }
    267         return false;
    268     }
    269 
    270     public ArrayList<String> getTextResponses(int callId) {
    271         return mCallTextReponsesMap.get(callId);
    272     }
    273 
    274     /**
    275      * Returns first call found in the call map with the specified state.
    276      */
    277     public Call getFirstCallWithState(int state) {
    278         return getCallWithState(state, 0);
    279     }
    280 
    281     /**
    282      * Returns the [position]th call found in the call map with the specified state.
    283      * TODO: Improve this logic to sort by call time.
    284      */
    285     public Call getCallWithState(int state, int positionToFind) {
    286         Call retval = null;
    287         int position = 0;
    288         for (Call call : mCallMap.values()) {
    289             if (call.getState() == state) {
    290                 if (position >= positionToFind) {
    291                     retval = call;
    292                     break;
    293                 } else {
    294                     position++;
    295                 }
    296             }
    297         }
    298 
    299         return retval;
    300     }
    301 
    302     /**
    303      * This is called when the service disconnects, either expectedly or unexpectedly.
    304      * For the expected case, it's because we have no calls left.  For the unexpected case,
    305      * it is likely a crash of phone and we need to clean up our calls manually.  Without phone,
    306      * there can be no active calls, so this is relatively safe thing to do.
    307      */
    308     public void clearOnDisconnect() {
    309         for (Call call : mCallMap.values()) {
    310             final int state = call.getState();
    311             if (state != Call.State.IDLE &&
    312                     state != Call.State.INVALID &&
    313                     state != Call.State.DISCONNECTED) {
    314 
    315                 call.setState(Call.State.DISCONNECTED);
    316                 call.setDisconnectCause(DisconnectCause.UNKNOWN);
    317                 updateCallInMap(call);
    318             }
    319         }
    320         notifyListenersOfChange();
    321     }
    322 
    323     /**
    324      * Sends a generic notification to all listeners that something has changed.
    325      * It is up to the listeners to call back to determine what changed.
    326      */
    327     private void notifyListenersOfChange() {
    328         for (Listener listener : mListeners) {
    329             listener.onCallListChange(this);
    330         }
    331     }
    332 
    333     private void notifyListenersOfDisconnect(Call call) {
    334         for (Listener listener : mListeners) {
    335             listener.onDisconnect(call);
    336         }
    337     }
    338 
    339     /**
    340      * Updates the call entry in the local map.
    341      * @return false if no call previously existed and no call was added, otherwise true.
    342      */
    343     private boolean updateCallInMap(Call call) {
    344         Preconditions.checkNotNull(call);
    345 
    346         boolean updated = false;
    347 
    348         final Integer id = new Integer(call.getCallId());
    349 
    350         if (call.getState() == Call.State.DISCONNECTED) {
    351             // update existing (but do not add!!) disconnected calls
    352             if (mCallMap.containsKey(id)) {
    353 
    354                 // For disconnected calls, we want to keep them alive for a few seconds so that the
    355                 // UI has a chance to display anything it needs when a call is disconnected.
    356 
    357                 // Set up a timer to destroy the call after X seconds.
    358                 final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call);
    359                 mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call));
    360 
    361                 mCallMap.put(id, call);
    362                 updated = true;
    363             }
    364         } else if (!isCallDead(call)) {
    365             mCallMap.put(id, call);
    366             updated = true;
    367         } else if (mCallMap.containsKey(id)) {
    368             mCallMap.remove(id);
    369             updated = true;
    370         }
    371 
    372         return updated;
    373     }
    374 
    375     private int getDelayForDisconnect(Call call) {
    376         Preconditions.checkState(call.getState() == Call.State.DISCONNECTED);
    377 
    378 
    379         final Call.DisconnectCause cause = call.getDisconnectCause();
    380         final int delay;
    381         switch (cause) {
    382             case LOCAL:
    383                 delay = DISCONNECTED_CALL_SHORT_TIMEOUT_MS;
    384                 break;
    385             case NORMAL:
    386             case UNKNOWN:
    387                 delay = DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS;
    388                 break;
    389             case INCOMING_REJECTED:
    390             case INCOMING_MISSED:
    391                 // no delay for missed/rejected incoming calls
    392                 delay = 0;
    393                 break;
    394             default:
    395                 delay = DISCONNECTED_CALL_LONG_TIMEOUT_MS;
    396                 break;
    397         }
    398 
    399         return delay;
    400     }
    401 
    402     private void updateCallTextMap(Call call, List<String> textResponses) {
    403         Preconditions.checkNotNull(call);
    404 
    405         final Integer id = new Integer(call.getCallId());
    406 
    407         if (!isCallDead(call)) {
    408             if (textResponses != null) {
    409                 mCallTextReponsesMap.put(id, (ArrayList<String>) textResponses);
    410             }
    411         } else if (mCallMap.containsKey(id)) {
    412             mCallTextReponsesMap.remove(id);
    413         }
    414     }
    415 
    416     private boolean isCallDead(Call call) {
    417         final int state = call.getState();
    418         return Call.State.IDLE == state || Call.State.INVALID == state;
    419     }
    420 
    421     /**
    422      * Sets up a call for deletion and notifies listeners of change.
    423      */
    424     private void finishDisconnectedCall(Call call) {
    425         call.setState(Call.State.IDLE);
    426         updateCallInMap(call);
    427         notifyListenersOfChange();
    428     }
    429 
    430     /**
    431      * Handles the timeout for destroying disconnected calls.
    432      */
    433     private Handler mHandler = new Handler() {
    434         @Override
    435         public void handleMessage(Message msg) {
    436             switch (msg.what) {
    437                 case EVENT_DISCONNECTED_TIMEOUT:
    438                     Log.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj);
    439                     finishDisconnectedCall((Call) msg.obj);
    440                     break;
    441                 default:
    442                     Log.wtf(this, "Message not expected: " + msg.what);
    443                     break;
    444             }
    445         }
    446     };
    447 
    448     /**
    449      * Listener interface for any class that wants to be notified of changes
    450      * to the call list.
    451      */
    452     public interface Listener {
    453         /**
    454          * Called when a new incoming call comes in.
    455          * This is the only method that gets called for incoming calls. Listeners
    456          * that want to perform an action on incoming call should respond in this method
    457          * because {@link #onCallListChange} does not automatically get called for
    458          * incoming calls.
    459          */
    460         public void onIncomingCall(Call call);
    461 
    462         /**
    463          * Called anytime there are changes to the call list.  The change can be switching call
    464          * states, updating information, etc. This method will NOT be called for new incoming
    465          * calls and for calls that switch to disconnected state. Listeners must add actions
    466          * to those method implementations if they want to deal with those actions.
    467          */
    468         public void onCallListChange(CallList callList);
    469 
    470         /**
    471          * Called when a call switches to the disconnected state.  This is the only method
    472          * that will get called upon disconnection.
    473          */
    474         public void onDisconnect(Call call);
    475     }
    476 
    477     public interface CallUpdateListener {
    478         // TODO: refactor and limit arg to be call state.  Caller info is not needed.
    479         public void onCallStateChanged(Call call);
    480     }
    481 }
    482