1/*
2 * Copyright (C) 2014 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.services.telephony;
18
19import com.android.internal.telephony.Phone;
20import com.android.internal.telephony.PhoneConstants;
21
22import android.net.Uri;
23import android.telecom.Connection;
24import android.telecom.ConferenceParticipant;
25import android.telecom.DisconnectCause;
26import android.telecom.PhoneAccount;
27import android.telephony.PhoneNumberUtils;
28import android.telephony.SubscriptionInfo;
29import android.text.TextUtils;
30
31/**
32 * Represents a participant in a conference call.
33 */
34public class ConferenceParticipantConnection extends Connection {
35    /**
36     * RFC5767 states that a SIP URI with an unknown number should use an address of
37     * {@code anonymous@anonymous.invalid}.  E.g. the host name is anonymous.invalid.
38     */
39    private static final String ANONYMOUS_INVALID_HOST = "anonymous.invalid";
40
41    /**
42     * The user entity URI For the conference participant.
43     */
44    private final Uri mUserEntity;
45
46    /**
47     * The endpoint URI For the conference participant.
48     */
49    private final Uri mEndpoint;
50
51    /**
52     * The connection which owns this participant.
53     */
54    private final com.android.internal.telephony.Connection mParentConnection;
55
56    /**
57     * Creates a new instance.
58     *
59     * @param participant The conference participant to create the instance for.
60     */
61    public ConferenceParticipantConnection(
62            com.android.internal.telephony.Connection parentConnection,
63            ConferenceParticipant participant) {
64
65        mParentConnection = parentConnection;
66
67        int presentation = getParticipantPresentation(participant);
68        Uri address;
69        if (presentation != PhoneConstants.PRESENTATION_ALLOWED) {
70            address = null;
71        } else {
72            String countryIso = getCountryIso(parentConnection.getCall().getPhone());
73            address = getParticipantAddress(participant, countryIso);
74        }
75        setAddress(address, presentation);
76        setCallerDisplayName(participant.getDisplayName(), presentation);
77
78        mUserEntity = participant.getHandle();
79        mEndpoint = participant.getEndpoint();
80
81        setCapabilities();
82    }
83
84    /**
85     * Changes the state of the conference participant.
86     *
87     * @param newState The new state.
88     */
89    public void updateState(int newState) {
90        Log.v(this, "updateState endPoint: %s state: %s", Log.pii(mEndpoint),
91                Connection.stateToString(newState));
92        if (newState == getState()) {
93            return;
94        }
95
96        switch (newState) {
97            case STATE_INITIALIZING:
98                setInitializing();
99                break;
100            case STATE_RINGING:
101                setRinging();
102                break;
103            case STATE_DIALING:
104                setDialing();
105                break;
106            case STATE_HOLDING:
107                setOnHold();
108                break;
109            case STATE_ACTIVE:
110                setActive();
111                break;
112            case STATE_DISCONNECTED:
113                setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
114                destroy();
115                break;
116            default:
117                setActive();
118        }
119    }
120
121    /**
122     * Disconnects the current {@code ConferenceParticipantConnection} from the conference.
123     * <p>
124     * Sends a participant disconnect signal to the associated parent connection.  The participant
125     * connection is not disconnected and cleaned up here.  On successful disconnection of the
126     * participant, the conference server will send an update to the conference controller
127     * indicating the disconnection was successful.
128     */
129    @Override
130    public void onDisconnect() {
131        mParentConnection.onDisconnectConferenceParticipant(mUserEntity);
132    }
133
134    /**
135     * Retrieves the user handle for this connection.
136     *
137     * @return The userEntity.
138     */
139    public Uri getUserEntity() {
140        return mUserEntity;
141    }
142
143    /**
144     * Retrieves the endpoint for this connection.
145     *
146     * @return The endpoint.
147     */
148    public Uri getEndpoint() {
149        return mEndpoint;
150    }
151
152    /**
153     * Configures the capabilities applicable to this connection.  A
154     * conference participant can only be disconnected from a conference since there is not
155     * actual connection to the participant which could be split from the conference.
156     */
157    private void setCapabilities() {
158        int capabilities = CAPABILITY_DISCONNECT_FROM_CONFERENCE;
159        setConnectionCapabilities(capabilities);
160    }
161
162    /**
163     * Determines the number presentation for a conference participant.  Per RFC5767, if the host
164     * name contains {@code anonymous.invalid} we can assume that there is no valid caller ID
165     * information for the caller, otherwise we'll assume that the URI can be shown.
166     *
167     * @param participant The conference participant.
168     * @return The number presentation.
169     */
170    private int getParticipantPresentation(ConferenceParticipant participant) {
171        Uri address = participant.getHandle();
172        if (address == null) {
173            return PhoneConstants.PRESENTATION_RESTRICTED;
174        }
175
176        String number = address.getSchemeSpecificPart();
177        // If no number, bail early and set restricted presentation.
178        if (TextUtils.isEmpty(number)) {
179            return PhoneConstants.PRESENTATION_RESTRICTED;
180        }
181        // Per RFC3261, the host name portion can also potentially include extra information:
182        // E.g. sip:anonymous1@anonymous.invalid;legid=1
183        // In this case, hostName will be anonymous.invalid and there is an extra parameter for
184        // legid=1.
185        // Parameters are optional, and the address (e.g. test@test.com) will always be the first
186        // part, with any parameters coming afterwards.
187        String hostParts[] = number.split("[;]");
188        String addressPart = hostParts[0];
189
190        // Get the number portion from the address part.
191        // This will typically be formatted similar to: 6505551212@test.com
192        String numberParts[] = addressPart.split("[@]");
193
194        // If we can't parse the host name out of the URI, then there is probably other data
195        // present, and is likely a valid SIP URI.
196        if (numberParts.length != 2) {
197            return PhoneConstants.PRESENTATION_ALLOWED;
198        }
199        String hostName = numberParts[1];
200
201        // If the hostname portion of the SIP URI is the invalid host string, presentation is
202        // restricted.
203        if (hostName.equals(ANONYMOUS_INVALID_HOST)) {
204            return PhoneConstants.PRESENTATION_RESTRICTED;
205        }
206
207        return PhoneConstants.PRESENTATION_ALLOWED;
208    }
209
210    /**
211     * Attempts to build a tel: style URI from a conference participant.
212     * Conference event package data contains SIP URIs, so we try to extract the phone number and
213     * format into a typical tel: style URI.
214     *
215     * @param participant The conference participant.
216     * @param countryIso The country ISO of the current subscription; used when formatting the
217     *                   participant phone number to E.164 format.
218     * @return The participant's address URI.
219     */
220    private Uri getParticipantAddress(ConferenceParticipant participant, String countryIso) {
221        Uri address = participant.getHandle();
222        if (address == null) {
223            return address;
224        }
225
226        // If the participant's address is already a TEL scheme, just return it as is.
227        if (PhoneAccount.SCHEME_TEL.equals(address.getScheme())) {
228            return address;
229        }
230
231        // Conference event package participants are identified using SIP URIs (see RFC3261).
232        // A valid SIP uri has the format: sip:user:password@host:port;uri-parameters?headers
233        // Per RFC3261, the "user" can be a telephone number.
234        // For example: sip:1650555121;phone-context=blah.com@host.com
235        // In this case, the phone number is in the user field of the URI, and the parameters can be
236        // ignored.
237        //
238        // A SIP URI can also specify a phone number in a format similar to:
239        // sip:+1-212-555-1212@something.com;user=phone
240        // In this case, the phone number is again in user field and the parameters can be ignored.
241        // We can get the user field in these instances by splitting the string on the @, ;, or :
242        // and looking at the first found item.
243        String number = address.getSchemeSpecificPart();
244        if (TextUtils.isEmpty(number)) {
245            return address;
246        }
247
248        String numberParts[] = number.split("[@;:]");
249        if (numberParts.length == 0) {
250            return address;
251        }
252        number = numberParts[0];
253
254        // Attempt to format the number in E.164 format and use that as part of the TEL URI.
255        // RFC2806 recommends to format telephone numbers using E.164 since it is independent of
256        // how the dialing of said numbers takes place.
257        // If conversion to E.164 fails, the returned value is null.  In that case, fallback to the
258        // number which was in the CEP data.
259        String formattedNumber = null;
260        if (!TextUtils.isEmpty(countryIso)) {
261            formattedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
262        }
263
264        return Uri.fromParts(PhoneAccount.SCHEME_TEL,
265                formattedNumber != null ? formattedNumber : number, null);
266    }
267
268    /**
269     * Given a {@link Phone} instance, determines the country ISO associated with the phone's
270     * subscription.
271     *
272     * @param phone The phone instance.
273     * @return The country ISO.
274     */
275    private String getCountryIso(Phone phone) {
276        if (phone == null) {
277            return null;
278        }
279
280        int subId = phone.getSubId();
281
282        SubscriptionInfo subInfo = TelecomAccountRegistry.getInstance(null).
283                getSubscriptionManager().getActiveSubscriptionInfo(subId);
284
285        if (subInfo == null) {
286            return null;
287        }
288        // The SubscriptionInfo reports ISO country codes in lower case.  Convert to upper case,
289        // since ultimately we use this ISO when formatting the CEP phone number, and the phone
290        // number formatting library expects uppercase ISO country codes.
291        return subInfo.getCountryIso().toUpperCase();
292    }
293
294    /**
295     * Builds a string representation of this conference participant connection.
296     *
297     * @return String representation of connection.
298     */
299    @Override
300    public String toString() {
301        StringBuilder sb = new StringBuilder();
302        sb.append("[ConferenceParticipantConnection objId:");
303        sb.append(System.identityHashCode(this));
304        sb.append(" endPoint:");
305        sb.append(Log.pii(mEndpoint));
306        sb.append(" address:");
307        sb.append(Log.pii(getAddress()));
308        sb.append(" addressPresentation:");
309        sb.append(getAddressPresentation());
310        sb.append(" parentConnection:");
311        sb.append(Log.pii(mParentConnection.getAddress()));
312        sb.append(" state:");
313        sb.append(Connection.stateToString(getState()));
314        sb.append("]");
315
316        return sb.toString();
317    }
318}
319