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