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