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.os.AsyncResult;
20 import android.os.Bundle;
21 import android.os.Handler;
22 import android.os.Message;
23 import android.telecom.PhoneAccountHandle;
24 import android.telecom.VideoProfile;
25 import android.telephony.ims.ImsCallProfile;
26 import android.telephony.ims.ImsExternalCallState;
27 import android.util.ArrayMap;
28 import android.util.Log;
29 
30 import com.android.ims.ImsExternalCallStateListener;
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.internal.telephony.Call;
33 import com.android.internal.telephony.Connection;
34 import com.android.internal.telephony.Phone;
35 import com.android.internal.telephony.PhoneConstants;
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          */
notifyUnknownConnection(Connection c)59         void notifyUnknownConnection(Connection c);
60 
61         /**
62          * Notifies of a change to call state.
63          */
notifyPreciseCallStateChanged()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
onImsExternalCallStateUpdate(List<ImsExternalCallState> externalCallState)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
onPullExternalCall(ImsExternalConnection connection)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
ImsExternalCallTracker(ImsPhone phone, ImsPullCall callPuller, ImsCallNotify callNotifier)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 
ImsExternalCallTracker(ImsPhone phone)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      */
tearDown()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      */
setCallPuller(ImsPullCall callPuller)189     public void setCallPuller(ImsPullCall callPuller) {
190        mCallPuller = callPuller;
191     }
192 
getExternalCallStateListener()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
onPhoneStateChanged(PhoneConstants.State oldState, PhoneConstants.State newState)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      */
registerForNotifications()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      */
unregisterForNotifications()225     private void unregisterForNotifications() {
226         if (mPhone != null) {
227             Log.d(TAG, "Unregistering: " + mPhone);
228             mPhone.getDefaultPhone().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      */
refreshExternalCallState(List<ImsExternalCallState> externalCallStates)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      */
getConnectionById(int callId)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      */
createExternalConnection(ImsExternalCallState state)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      */
updateExistingConnection(ImsExternalConnection connection, ImsExternalCallState state)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      */
refreshCallPullState()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      */
containsCallId(List<ImsExternalCallState> externalCallStates, int callId)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      */
handleVideoCapabilitiesChanged(AsyncResult ar)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      */
isCallPullPermitted(boolean isNetworkPullable, int videoState)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