ImsExternalCallTracker.java revision 67f3535dbfa1e4ca1ed846dd649015e8bd1f7328
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;
26
27import android.os.Bundle;
28import android.telecom.PhoneAccountHandle;
29import android.util.ArrayMap;
30import android.util.Log;
31
32import java.util.ArrayList;
33import java.util.Iterator;
34import java.util.List;
35import java.util.Map;
36
37/**
38 * Responsible for tracking external calls known to the system.
39 */
40public class ImsExternalCallTracker {
41
42    /**
43     * Interface implemented by modules which are capable of notifying interested parties of new
44     * unknown connections, and changes to call state.
45     * This is used to break the dependency between {@link ImsExternalCallTracker} and
46     * {@link ImsPhone}.
47     *
48     * @hide
49     */
50
51    public static interface ImsCallNotify {
52        /**
53         * Notifies that an unknown connection has been added.
54         * @param c The new unknown connection.
55         */
56        void notifyUnknownConnection(Connection c);
57
58        /**
59         * Notifies of a change to call state.
60         */
61        void notifyPreciseCallStateChanged();
62    }
63
64
65    /**
66     * Implements the {@link ImsExternalCallStateListener}, which is responsible for receiving
67     * external call state updates from the IMS framework.
68     */
69    public class ExternalCallStateListener extends ImsExternalCallStateListener {
70        @Override
71        public void onImsExternalCallStateUpdate(List<ImsExternalCallState> externalCallState) {
72            refreshExternalCallState(externalCallState);
73        }
74    }
75
76    /**
77     * Receives callbacks from {@link ImsExternalConnection}s when a call pull has been initiated.
78     */
79    public class ExternalConnectionListener implements ImsExternalConnection.Listener {
80        @Override
81        public void onPullExternalCall(ImsExternalConnection connection) {
82            Log.d(TAG, "onPullExternalCall: connection = " + connection);
83            mCallPuller.pullExternalCall(connection.getAddress(), connection.getVideoState());
84        }
85    }
86
87    public final static String TAG = "ImsExternalCallTracker";
88
89    /**
90     * Extra key used when informing telecom of a new external call using the
91     * {@link android.telecom.TelecomManager#addNewUnknownCall(PhoneAccountHandle, Bundle)} API.
92     * Used to ensure that when Telecom requests the {@link android.telecom.ConnectionService} to
93     * create the connection for the unknown call that we can determine which
94     * {@link ImsExternalConnection} in {@link #mExternalConnections} is the one being requested.
95     */
96    public final static String EXTRA_IMS_EXTERNAL_CALL_ID =
97            "android.telephony.ImsExternalCallTracker.extra.EXTERNAL_CALL_ID";
98
99    /**
100     * Contains a list of the external connections known by the ImsPhoneCallTracker.  These are
101     * connections which originated from a dialog event package and reside on another device.
102     * Used in multi-endpoint (VoLTE for internet connected endpoints) scenarios.
103     */
104    private Map<Integer, ImsExternalConnection> mExternalConnections =
105            new ArrayMap<>();
106    private final ImsPhone mPhone;
107    private final ImsCallNotify mCallStateNotifier;
108    private final ExternalCallStateListener mExternalCallStateListener;
109    private final ExternalConnectionListener mExternalConnectionListener =
110            new ExternalConnectionListener();
111    private final ImsPullCall mCallPuller;
112
113    @VisibleForTesting
114    public ImsExternalCallTracker(ImsPhone phone, ImsPullCall callPuller,
115            ImsCallNotify callNotifier) {
116
117        mPhone = phone;
118        mCallStateNotifier = callNotifier;
119        mExternalCallStateListener = new ExternalCallStateListener();
120        mCallPuller = callPuller;
121
122    }
123    public ImsExternalCallTracker(ImsPhone phone, ImsPullCall callPuller) {
124        mPhone = phone;
125        mCallStateNotifier = new ImsCallNotify() {
126            @Override
127            public void notifyUnknownConnection(Connection c) {
128                mPhone.notifyUnknownConnection(c);
129            }
130
131            @Override
132            public void notifyPreciseCallStateChanged() {
133                mPhone.notifyPreciseCallStateChanged();
134            }
135        };
136        mExternalCallStateListener = new ExternalCallStateListener();
137        mCallPuller = callPuller;
138    }
139
140    public ExternalCallStateListener getExternalCallStateListener() {
141        return mExternalCallStateListener;
142    }
143
144    /**
145     * Called when the IMS stack receives a new dialog event package.  Triggers the creation and
146     * update of {@link ImsExternalConnection}s to represent the dialogs in the dialog event
147     * package data.
148     *
149     * @param externalCallStates the {@link ImsExternalCallState} information for the dialog event
150     *                           package.
151     */
152    public void refreshExternalCallState(List<ImsExternalCallState> externalCallStates) {
153        Log.d(TAG, "refreshExternalCallState");
154
155        // Check to see if any call Ids are no longer present in the external call state.  If they
156        // are, the calls are terminated and should be removed.
157        Iterator<Map.Entry<Integer, ImsExternalConnection>> connectionIterator =
158                mExternalConnections.entrySet().iterator();
159        boolean wasCallRemoved = false;
160        while (connectionIterator.hasNext()) {
161            Map.Entry<Integer, ImsExternalConnection> entry = connectionIterator.next();
162            int callId = entry.getKey().intValue();
163
164            if (!containsCallId(externalCallStates, callId)) {
165                ImsExternalConnection externalConnection = entry.getValue();
166                externalConnection.setTerminated();
167                externalConnection.removeListener(mExternalConnectionListener);
168                connectionIterator.remove();
169                wasCallRemoved = true;
170            }
171        }
172        // If one or more calls were removed, trigger a notification that will cause the
173        // TelephonyConnection instancse to refresh their state with Telecom.
174        if (wasCallRemoved) {
175            mCallStateNotifier.notifyPreciseCallStateChanged();
176        }
177
178        // Check for new calls, and updates to existing ones.
179        if (externalCallStates != null && !externalCallStates.isEmpty()) {
180            for (ImsExternalCallState callState : externalCallStates) {
181                if (!mExternalConnections.containsKey(callState.getCallId())) {
182                    Log.d(TAG, "refreshExternalCallState: got = " + callState);
183                    // If there is a new entry and it is already terminated, don't bother adding it to
184                    // telecom.
185                    if (callState.getCallState() != ImsExternalCallState.CALL_STATE_CONFIRMED) {
186                        continue;
187                    }
188                    createExternalConnection(callState);
189                } else {
190                    updateExistingConnection(mExternalConnections.get(callState.getCallId()),
191                            callState);
192                }
193            }
194        }
195    }
196
197    /**
198     * Finds an external connection given a call Id.
199     *
200     * @param callId The call Id.
201     * @return The {@link Connection}, or {@code null} if no match found.
202     */
203    public Connection getConnectionById(int callId) {
204        return mExternalConnections.get(callId);
205    }
206
207    /**
208     * Given an {@link ImsExternalCallState} instance obtained from a dialog event package,
209     * creates a new instance of {@link ImsExternalConnection} to represent the connection, and
210     * initiates the addition of the new call to Telecom as an unknown call.
211     *
212     * @param state External call state from a dialog event package.
213     */
214    private void createExternalConnection(ImsExternalCallState state) {
215        Log.i(TAG, "createExternalConnection");
216
217        ImsExternalConnection connection = new ImsExternalConnection(mPhone,
218                state.getCallId(), /* Dialog event package call id */
219                state.getAddress() /* phone number */,
220                state.isCallPullable());
221        connection.setVideoState(ImsCallProfile.getVideoStateFromCallType(state.getCallType()));
222        connection.addListener(mExternalConnectionListener);
223
224        // Add to list of tracked connections.
225        mExternalConnections.put(connection.getCallId(), connection);
226
227        // Note: The notification of unknown connection is ultimately handled by
228        // PstnIncomingCallNotifier#addNewUnknownCall.  That method will ensure that an extra is set
229        // containing the ImsExternalConnection#mCallId so that we have a means of reconciling which
230        // unknown call was added.
231        mCallStateNotifier.notifyUnknownConnection(connection);
232    }
233
234    /**
235     * Given an existing {@link ImsExternalConnection}, applies any changes found found in a
236     * {@link ImsExternalCallState} instance received from a dialog event package to the connection.
237     *
238     * @param connection The connection to apply changes to.
239     * @param state The new dialog state for the connection.
240     */
241    private void updateExistingConnection(ImsExternalConnection connection,
242            ImsExternalCallState state) {
243        Call.State existingState = connection.getState();
244        Call.State newState = state.getCallState() == ImsExternalCallState.CALL_STATE_CONFIRMED ?
245                Call.State.ACTIVE : Call.State.DISCONNECTED;
246
247        if (existingState != newState) {
248            if (newState == Call.State.ACTIVE) {
249                connection.setActive();
250            } else {
251                connection.setTerminated();
252                connection.removeListener(mExternalConnectionListener);
253                mExternalConnections.remove(connection);
254                mCallStateNotifier.notifyPreciseCallStateChanged();
255            }
256        }
257
258        connection.setIsPullable(state.isCallPullable());
259
260        int newVideoState = ImsCallProfile.getVideoStateFromCallType(state.getCallType());
261        if (newVideoState != connection.getVideoState()) {
262            connection.setVideoState(newVideoState);
263        }
264    }
265
266    /**
267     * Determines if a list of call states obtained from a dialog event package contacts an existing
268     * call Id.
269     *
270     * @param externalCallStates The dialog event package state information.
271     * @param callId The call Id.
272     * @return {@code true} if the state information contains the call Id, {@code false} otherwise.
273     */
274    private boolean containsCallId(List<ImsExternalCallState> externalCallStates, int callId) {
275        if (externalCallStates == null) {
276            return false;
277        }
278
279        for (ImsExternalCallState state : externalCallStates) {
280            if (state.getCallId() == callId) {
281                return true;
282            }
283        }
284
285        return false;
286    }
287}
288