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