Home | History | Annotate | Download | only in imsphone
      1 /*
      2  * Copyright (C) 2016 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.internal.telephony.imsphone;
     18 
     19 import android.telephony.ims.ImsCallProfile;
     20 import android.telephony.ims.ImsExternalCallState;
     21 import com.android.ims.ImsExternalCallStateListener;
     22 import com.android.internal.annotations.VisibleForTesting;
     23 import com.android.internal.telephony.Call;
     24 import com.android.internal.telephony.Connection;
     25 import com.android.internal.telephony.Phone;
     26 import com.android.internal.telephony.PhoneConstants;
     27 
     28 import android.os.AsyncResult;
     29 import android.os.Bundle;
     30 import android.os.Handler;
     31 import android.os.Message;
     32 import android.telecom.PhoneAccountHandle;
     33 import android.telecom.VideoProfile;
     34 import android.util.ArrayMap;
     35 import android.util.Log;
     36 
     37 import java.util.Iterator;
     38 import java.util.List;
     39 import java.util.Map;
     40 
     41 /**
     42  * Responsible for tracking external calls known to the system.
     43  */
     44 public class ImsExternalCallTracker implements ImsPhoneCallTracker.PhoneStateListener {
     45 
     46     /**
     47      * Interface implemented by modules which are capable of notifying interested parties of new
     48      * unknown connections, and changes to call state.
     49      * This is used to break the dependency between {@link ImsExternalCallTracker} and
     50      * {@link ImsPhone}.
     51      *
     52      * @hide
     53      */
     54     public static interface ImsCallNotify {
     55         /**
     56          * Notifies that an unknown connection has been added.
     57          * @param c The new unknown connection.
     58          */
     59         void notifyUnknownConnection(Connection c);
     60 
     61         /**
     62          * Notifies of a change to call state.
     63          */
     64         void notifyPreciseCallStateChanged();
     65     }
     66 
     67 
     68     /**
     69      * Implements the {@link ImsExternalCallStateListener}, which is responsible for receiving
     70      * external call state updates from the IMS framework.
     71      */
     72     public class ExternalCallStateListener extends ImsExternalCallStateListener {
     73         @Override
     74         public void onImsExternalCallStateUpdate(List<ImsExternalCallState> externalCallState) {
     75             refreshExternalCallState(externalCallState);
     76         }
     77     }
     78 
     79     /**
     80      * Receives callbacks from {@link ImsExternalConnection}s when a call pull has been initiated.
     81      */
     82     public class ExternalConnectionListener implements ImsExternalConnection.Listener {
     83         @Override
     84         public void onPullExternalCall(ImsExternalConnection connection) {
     85             Log.d(TAG, "onPullExternalCall: connection = " + connection);
     86             if (mCallPuller == null) {
     87                 Log.e(TAG, "onPullExternalCall : No call puller defined");
     88                 return;
     89             }
     90             mCallPuller.pullExternalCall(connection.getAddress(), connection.getVideoState(),
     91                     connection.getCallId());
     92         }
     93     }
     94 
     95     public final static String TAG = "ImsExternalCallTracker";
     96 
     97     private static final int EVENT_VIDEO_CAPABILITIES_CHANGED = 1;
     98 
     99     /**
    100      * Extra key used when informing telecom of a new external call using the
    101      * {@link android.telecom.TelecomManager#addNewUnknownCall(PhoneAccountHandle, Bundle)} API.
    102      * Used to ensure that when Telecom requests the {@link android.telecom.ConnectionService} to
    103      * create the connection for the unknown call that we can determine which
    104      * {@link ImsExternalConnection} in {@link #mExternalConnections} is the one being requested.
    105      */
    106     public final static String EXTRA_IMS_EXTERNAL_CALL_ID =
    107             "android.telephony.ImsExternalCallTracker.extra.EXTERNAL_CALL_ID";
    108 
    109     /**
    110      * Contains a list of the external connections known by the ImsExternalCallTracker.  These are
    111      * connections which originated from a dialog event package and reside on another device.
    112      * Used in multi-endpoint (VoLTE for internet connected endpoints) scenarios.
    113      */
    114     private Map<Integer, ImsExternalConnection> mExternalConnections =
    115             new ArrayMap<>();
    116 
    117     /**
    118      * Tracks whether each external connection tracked in
    119      * {@link #mExternalConnections} can be pulled, as reported by the latest dialog event package
    120      * received from the network.  We need to know this because the pull state of a call can be
    121      * overridden based on the following factors:
    122      * 1) An external video call cannot be pulled if the current device does not have video
    123      *    capability.
    124      * 2) If the device has any active or held calls locally, no external calls may be pulled to
    125      *    the local device.
    126      */
    127     private Map<Integer, Boolean> mExternalCallPullableState = new ArrayMap<>();
    128     private final ImsPhone mPhone;
    129     private final ImsCallNotify mCallStateNotifier;
    130     private final ExternalCallStateListener mExternalCallStateListener;
    131     private final ExternalConnectionListener mExternalConnectionListener =
    132             new ExternalConnectionListener();
    133     private ImsPullCall mCallPuller;
    134     private boolean mIsVideoCapable;
    135     private boolean mHasActiveCalls;
    136 
    137     private final Handler mHandler = new Handler() {
    138         @Override
    139         public void handleMessage(Message msg) {
    140             switch (msg.what) {
    141                 case EVENT_VIDEO_CAPABILITIES_CHANGED:
    142                     handleVideoCapabilitiesChanged((AsyncResult) msg.obj);
    143                     break;
    144                 default:
    145                     break;
    146             }
    147         }
    148     };
    149 
    150     @VisibleForTesting
    151     public ImsExternalCallTracker(ImsPhone phone, ImsPullCall callPuller,
    152             ImsCallNotify callNotifier) {
    153 
    154         mPhone = phone;
    155         mCallStateNotifier = callNotifier;
    156         mExternalCallStateListener = new ExternalCallStateListener();
    157         mCallPuller = callPuller;
    158     }
    159 
    160     public ImsExternalCallTracker(ImsPhone phone) {
    161         mPhone = phone;
    162         mCallStateNotifier = new ImsCallNotify() {
    163             @Override
    164             public void notifyUnknownConnection(Connection c) {
    165                 mPhone.notifyUnknownConnection(c);
    166             }
    167 
    168             @Override
    169             public void notifyPreciseCallStateChanged() {
    170                 mPhone.notifyPreciseCallStateChanged();
    171             }
    172         };
    173         mExternalCallStateListener = new ExternalCallStateListener();
    174         registerForNotifications();
    175     }
    176 
    177     /**
    178      * Performs any cleanup required before the ImsExternalCallTracker is destroyed.
    179      */
    180     public void tearDown() {
    181         unregisterForNotifications();
    182     }
    183 
    184     /**
    185      * Sets the implementation of {@link ImsPullCall} which is responsible for pulling calls.
    186      *
    187      * @param callPuller The pull call implementation.
    188      */
    189     public void setCallPuller(ImsPullCall callPuller) {
    190        mCallPuller = callPuller;
    191     }
    192 
    193     public ExternalCallStateListener getExternalCallStateListener() {
    194         return mExternalCallStateListener;
    195     }
    196 
    197     /**
    198      * Handles changes to the phone state as notified by the {@link ImsPhoneCallTracker}.
    199      *
    200      * @param oldState The previous phone state.
    201      * @param newState The new phone state.
    202      */
    203     @Override
    204     public void onPhoneStateChanged(PhoneConstants.State oldState, PhoneConstants.State newState) {
    205         mHasActiveCalls = newState != PhoneConstants.State.IDLE;
    206         Log.i(TAG, "onPhoneStateChanged : hasActiveCalls = " + mHasActiveCalls);
    207 
    208         refreshCallPullState();
    209     }
    210 
    211     /**
    212      * Registers for video capability changes.
    213      */
    214     private void registerForNotifications() {
    215         if (mPhone != null) {
    216             Log.d(TAG, "Registering: " + mPhone);
    217             mPhone.getDefaultPhone().registerForVideoCapabilityChanged(mHandler,
    218                     EVENT_VIDEO_CAPABILITIES_CHANGED, null);
    219         }
    220     }
    221 
    222     /**
    223      * Unregisters for video capability changes.
    224      */
    225     private void unregisterForNotifications() {
    226         if (mPhone != null) {
    227             Log.d(TAG, "Unregistering: " + mPhone);
    228             mPhone.unregisterForVideoCapabilityChanged(mHandler);
    229         }
    230     }
    231 
    232 
    233     /**
    234      * Called when the IMS stack receives a new dialog event package.  Triggers the creation and
    235      * update of {@link ImsExternalConnection}s to represent the dialogs in the dialog event
    236      * package data.
    237      *
    238      * @param externalCallStates the {@link ImsExternalCallState} information for the dialog event
    239      *                           package.
    240      */
    241     public void refreshExternalCallState(List<ImsExternalCallState> externalCallStates) {
    242         Log.d(TAG, "refreshExternalCallState");
    243 
    244         // Check to see if any call Ids are no longer present in the external call state.  If they
    245         // are, the calls are terminated and should be removed.
    246         Iterator<Map.Entry<Integer, ImsExternalConnection>> connectionIterator =
    247                 mExternalConnections.entrySet().iterator();
    248         boolean wasCallRemoved = false;
    249         while (connectionIterator.hasNext()) {
    250             Map.Entry<Integer, ImsExternalConnection> entry = connectionIterator.next();
    251             int callId = entry.getKey().intValue();
    252 
    253             if (!containsCallId(externalCallStates, callId)) {
    254                 ImsExternalConnection externalConnection = entry.getValue();
    255                 externalConnection.setTerminated();
    256                 externalConnection.removeListener(mExternalConnectionListener);
    257                 connectionIterator.remove();
    258                 wasCallRemoved = true;
    259             }
    260         }
    261         // If one or more calls were removed, trigger a notification that will cause the
    262         // TelephonyConnection instancse to refresh their state with Telecom.
    263         if (wasCallRemoved) {
    264             mCallStateNotifier.notifyPreciseCallStateChanged();
    265         }
    266 
    267         // Check for new calls, and updates to existing ones.
    268         if (externalCallStates != null && !externalCallStates.isEmpty()) {
    269             for (ImsExternalCallState callState : externalCallStates) {
    270                 if (!mExternalConnections.containsKey(callState.getCallId())) {
    271                     Log.d(TAG, "refreshExternalCallState: got = " + callState);
    272                     // If there is a new entry and it is already terminated, don't bother adding it to
    273                     // telecom.
    274                     if (callState.getCallState() != ImsExternalCallState.CALL_STATE_CONFIRMED) {
    275                         continue;
    276                     }
    277                     createExternalConnection(callState);
    278                 } else {
    279                     updateExistingConnection(mExternalConnections.get(callState.getCallId()),
    280                             callState);
    281                 }
    282             }
    283         }
    284     }
    285 
    286     /**
    287      * Finds an external connection given a call Id.
    288      *
    289      * @param callId The call Id.
    290      * @return The {@link Connection}, or {@code null} if no match found.
    291      */
    292     public Connection getConnectionById(int callId) {
    293         return mExternalConnections.get(callId);
    294     }
    295 
    296     /**
    297      * Given an {@link ImsExternalCallState} instance obtained from a dialog event package,
    298      * creates a new instance of {@link ImsExternalConnection} to represent the connection, and
    299      * initiates the addition of the new call to Telecom as an unknown call.
    300      *
    301      * @param state External call state from a dialog event package.
    302      */
    303     private void createExternalConnection(ImsExternalCallState state) {
    304         Log.i(TAG, "createExternalConnection : state = " + state);
    305 
    306         int videoState = ImsCallProfile.getVideoStateFromCallType(state.getCallType());
    307 
    308         boolean isCallPullPermitted = isCallPullPermitted(state.isCallPullable(), videoState);
    309         ImsExternalConnection connection = new ImsExternalConnection(mPhone,
    310                 state.getCallId(), /* Dialog event package call id */
    311                 state.getAddress() /* phone number */,
    312                 isCallPullPermitted);
    313         connection.setVideoState(videoState);
    314         connection.addListener(mExternalConnectionListener);
    315 
    316         Log.d(TAG,
    317                 "createExternalConnection - pullable state : externalCallId = "
    318                         + connection.getCallId()
    319                         + " ; isPullable = " + isCallPullPermitted
    320                         + " ; networkPullable = " + state.isCallPullable()
    321                         + " ; isVideo = " + VideoProfile.isVideo(videoState)
    322                         + " ; videoEnabled = " + mIsVideoCapable
    323                         + " ; hasActiveCalls = " + mHasActiveCalls);
    324 
    325         // Add to list of tracked connections.
    326         mExternalConnections.put(connection.getCallId(), connection);
    327         mExternalCallPullableState.put(connection.getCallId(), state.isCallPullable());
    328 
    329         // Note: The notification of unknown connection is ultimately handled by
    330         // PstnIncomingCallNotifier#addNewUnknownCall.  That method will ensure that an extra is set
    331         // containing the ImsExternalConnection#mCallId so that we have a means of reconciling which
    332         // unknown call was added.
    333         mCallStateNotifier.notifyUnknownConnection(connection);
    334     }
    335 
    336     /**
    337      * Given an existing {@link ImsExternalConnection}, applies any changes found found in a
    338      * {@link ImsExternalCallState} instance received from a dialog event package to the connection.
    339      *
    340      * @param connection The connection to apply changes to.
    341      * @param state The new dialog state for the connection.
    342      */
    343     private void updateExistingConnection(ImsExternalConnection connection,
    344             ImsExternalCallState state) {
    345 
    346         Log.i(TAG, "updateExistingConnection : state = " + state);
    347         Call.State existingState = connection.getState();
    348         Call.State newState = state.getCallState() == ImsExternalCallState.CALL_STATE_CONFIRMED ?
    349                 Call.State.ACTIVE : Call.State.DISCONNECTED;
    350 
    351         if (existingState != newState) {
    352             if (newState == Call.State.ACTIVE) {
    353                 connection.setActive();
    354             } else {
    355                 connection.setTerminated();
    356                 connection.removeListener(mExternalConnectionListener);
    357                 mExternalConnections.remove(connection.getCallId());
    358                 mExternalCallPullableState.remove(connection.getCallId());
    359                 mCallStateNotifier.notifyPreciseCallStateChanged();
    360             }
    361         }
    362 
    363         int newVideoState = ImsCallProfile.getVideoStateFromCallType(state.getCallType());
    364         if (newVideoState != connection.getVideoState()) {
    365             connection.setVideoState(newVideoState);
    366         }
    367 
    368         mExternalCallPullableState.put(state.getCallId(), state.isCallPullable());
    369         boolean isCallPullPermitted = isCallPullPermitted(state.isCallPullable(), newVideoState);
    370         Log.d(TAG,
    371                 "updateExistingConnection - pullable state : externalCallId = " + connection
    372                         .getCallId()
    373                         + " ; isPullable = " + isCallPullPermitted
    374                         + " ; networkPullable = " + state.isCallPullable()
    375                         + " ; isVideo = "
    376                         + VideoProfile.isVideo(connection.getVideoState())
    377                         + " ; videoEnabled = " + mIsVideoCapable
    378                         + " ; hasActiveCalls = " + mHasActiveCalls);
    379 
    380         connection.setIsPullable(isCallPullPermitted);
    381     }
    382 
    383     /**
    384      * Update whether the external calls known can be pulled.  Combines the last known network
    385      * pullable state with local device conditions to determine if each call can be pulled.
    386      */
    387     private void refreshCallPullState() {
    388         Log.d(TAG, "refreshCallPullState");
    389 
    390         for (ImsExternalConnection imsExternalConnection : mExternalConnections.values()) {
    391             boolean isNetworkPullable =
    392                     mExternalCallPullableState.get(imsExternalConnection.getCallId())
    393                             .booleanValue();
    394             boolean isCallPullPermitted =
    395                     isCallPullPermitted(isNetworkPullable, imsExternalConnection.getVideoState());
    396             Log.d(TAG,
    397                     "refreshCallPullState : externalCallId = " + imsExternalConnection.getCallId()
    398                             + " ; isPullable = " + isCallPullPermitted
    399                             + " ; networkPullable = " + isNetworkPullable
    400                             + " ; isVideo = "
    401                             + VideoProfile.isVideo(imsExternalConnection.getVideoState())
    402                             + " ; videoEnabled = " + mIsVideoCapable
    403                             + " ; hasActiveCalls = " + mHasActiveCalls);
    404             imsExternalConnection.setIsPullable(isCallPullPermitted);
    405         }
    406     }
    407 
    408     /**
    409      * Determines if a list of call states obtained from a dialog event package contacts an existing
    410      * call Id.
    411      *
    412      * @param externalCallStates The dialog event package state information.
    413      * @param callId The call Id.
    414      * @return {@code true} if the state information contains the call Id, {@code false} otherwise.
    415      */
    416     private boolean containsCallId(List<ImsExternalCallState> externalCallStates, int callId) {
    417         if (externalCallStates == null) {
    418             return false;
    419         }
    420 
    421         for (ImsExternalCallState state : externalCallStates) {
    422             if (state.getCallId() == callId) {
    423                 return true;
    424             }
    425         }
    426 
    427         return false;
    428     }
    429 
    430     /**
    431      * Handles a change to the video capabilities reported by
    432      * {@link Phone#notifyForVideoCapabilityChanged(boolean)}.
    433      *
    434      * @param ar The AsyncResult containing the new video capability of the device.
    435      */
    436     private void handleVideoCapabilitiesChanged(AsyncResult ar) {
    437         mIsVideoCapable = (Boolean) ar.result;
    438         Log.i(TAG, "handleVideoCapabilitiesChanged : isVideoCapable = " + mIsVideoCapable);
    439 
    440         // Refresh pullable state if video capability changed.
    441         refreshCallPullState();
    442     }
    443 
    444     /**
    445      * Determines whether an external call can be pulled based on the pullability state enforced
    446      * by the network, as well as local device rules.
    447      *
    448      * @param isNetworkPullable {@code true} if the network indicates the call can be pulled,
    449      *      {@code false} otherwise.
    450      * @param videoState the VideoState of the external call.
    451      * @return {@code true} if the external call can be pulled, {@code false} otherwise.
    452      */
    453     private boolean isCallPullPermitted(boolean isNetworkPullable, int videoState) {
    454         if (VideoProfile.isVideo(videoState) && !mIsVideoCapable) {
    455             // If the external call is a video call and the local device does not have video
    456             // capability at this time, it cannot be pulled.
    457             return false;
    458         }
    459 
    460         if (mHasActiveCalls) {
    461             // If there are active calls on the local device, the call cannot be pulled.
    462             return false;
    463         }
    464 
    465         return isNetworkPullable;
    466     }
    467 }
    468