ImsConference.java revision 80e5718cf33451905408956838a98835e3e2441f
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 android.content.Context;
20import android.graphics.drawable.Icon;
21import android.net.Uri;
22import android.telecom.Conference;
23import android.telecom.ConferenceParticipant;
24import android.telecom.Connection.VideoProvider;
25import android.telecom.Connection;
26import android.telecom.DisconnectCause;
27import android.telecom.Log;
28import android.telecom.PhoneAccountHandle;
29import android.telecom.StatusHints;
30import android.telecom.VideoProfile;
31import android.telephony.PhoneNumberUtils;
32
33import com.android.internal.telephony.Call;
34import com.android.internal.telephony.CallStateException;
35import com.android.internal.telephony.Phone;
36import com.android.internal.telephony.PhoneConstants;
37import com.android.phone.PhoneUtils;
38import com.android.phone.R;
39
40import java.util.ArrayList;
41import java.util.HashMap;
42import java.util.HashSet;
43import java.util.Iterator;
44import java.util.List;
45import java.util.Map;
46import java.util.Objects;
47
48/**
49 * Represents an IMS conference call.
50 * <p>
51 * An IMS conference call consists of a conference host connection and potentially a list of
52 * conference participants.  The conference host connection represents the radio connection to the
53 * IMS conference server.  Since it is not a connection to any one individual, it is not represented
54 * in Telecom/InCall as a call.  The conference participant information is received via the host
55 * connection via a conference event package.  Conference participant connections do not represent
56 * actual radio connections to the participants; they act as a virtual representation of the
57 * participant, keyed by a unique endpoint {@link android.net.Uri}.
58 * <p>
59 * The {@link ImsConference} listens for conference event package data received via the host
60 * connection and is responsible for managing the conference participant connections which represent
61 * the participants.
62 */
63public class ImsConference extends Conference {
64
65    /**
66     * Listener used to respond to changes to conference participants.  At the conference level we
67     * are most concerned with handling destruction of a conference participant.
68     */
69    private final Connection.Listener mParticipantListener = new Connection.Listener() {
70        /**
71         * Participant has been destroyed.  Remove it from the conference.
72         *
73         * @param connection The participant which was destroyed.
74         */
75        @Override
76        public void onDestroyed(Connection connection) {
77            ConferenceParticipantConnection participant =
78                    (ConferenceParticipantConnection) connection;
79            removeConferenceParticipant(participant);
80            updateManageConference();
81        }
82
83    };
84
85    /**
86     * Listener used to respond to changes to the underlying radio connection for the conference
87     * host connection.  Used to respond to SRVCC changes.
88     */
89    private final TelephonyConnection.TelephonyConnectionListener mTelephonyConnectionListener =
90            new TelephonyConnection.TelephonyConnectionListener() {
91
92        @Override
93        public void onOriginalConnectionConfigured(TelephonyConnection c) {
94            if (c == mConferenceHost) {
95               handleOriginalConnectionChange();
96            }
97        }
98    };
99
100    /**
101     * Listener used to respond to changes to the connection to the IMS conference server.
102     */
103    private final android.telecom.Connection.Listener mConferenceHostListener =
104            new android.telecom.Connection.Listener() {
105
106        /**
107         * Updates the state of the conference based on the new state of the host.
108         *
109         * @param c The host connection.
110         * @param state The new state
111         */
112        @Override
113        public void onStateChanged(android.telecom.Connection c, int state) {
114            setState(state);
115        }
116
117        /**
118         * Disconnects the conference when its host connection disconnects.
119         *
120         * @param c The host connection.
121         * @param disconnectCause The host connection disconnect cause.
122         */
123        @Override
124        public void onDisconnected(android.telecom.Connection c, DisconnectCause disconnectCause) {
125            setDisconnected(disconnectCause);
126        }
127
128        /**
129         * Handles destruction of the host connection; once the host connection has been
130         * destroyed, cleans up the conference participant connection.
131         *
132         * @param connection The host connection.
133         */
134        @Override
135        public void onDestroyed(android.telecom.Connection connection) {
136            disconnectConferenceParticipants();
137        }
138
139        /**
140         * Handles changes to conference participant data as reported by the conference host
141         * connection.
142         *
143         * @param c The connection.
144         * @param participants The participant information.
145         */
146        @Override
147        public void onConferenceParticipantsChanged(android.telecom.Connection c,
148                List<ConferenceParticipant> participants) {
149
150            if (c == null || participants == null) {
151                return;
152            }
153            Log.v(this, "onConferenceParticipantsChanged: %d participants", participants.size());
154            TelephonyConnection telephonyConnection = (TelephonyConnection) c;
155            handleConferenceParticipantsUpdate(telephonyConnection, participants);
156        }
157
158        @Override
159        public void onVideoStateChanged(android.telecom.Connection c, int videoState) {
160            Log.d(this, "onVideoStateChanged video state %d", videoState);
161            setVideoState(c, videoState);
162        }
163
164        @Override
165        public void onVideoProviderChanged(android.telecom.Connection c,
166                Connection.VideoProvider videoProvider) {
167            Log.d(this, "onVideoProviderChanged: Connection: %s, VideoProvider: %s", c,
168                    videoProvider);
169            setVideoProvider(c, videoProvider);
170        }
171
172        @Override
173        public void onConnectionCapabilitiesChanged(Connection c, int connectionCapabilities) {
174            Log.d(this, "onCallCapabilitiesChanged: Connection: %s, callCapabilities: %s", c,
175                    connectionCapabilities);
176            int capabilites = ImsConference.this.getConnectionCapabilities();
177            setConnectionCapabilities(applyHostCapabilities(capabilites, connectionCapabilities));
178        }
179
180        @Override
181        public void onStatusHintsChanged(Connection c, StatusHints statusHints) {
182            Log.v(this, "onStatusHintsChanged");
183            updateStatusHints();
184        }
185    };
186
187    /**
188     * The telephony connection service; used to add new participant connections to Telecom.
189     */
190    private TelephonyConnectionService mTelephonyConnectionService;
191
192    /**
193     * The connection to the conference server which is hosting the conference.
194     */
195    private TelephonyConnection mConferenceHost;
196
197    /**
198     * The PhoneAccountHandle of the conference host.
199     */
200    private PhoneAccountHandle mConferenceHostPhoneAccountHandle;
201
202    /**
203     * The address of the conference host.
204     */
205    private Uri mConferenceHostAddress;
206
207    /**
208     * The known conference participant connections.  The HashMap is keyed by endpoint Uri.
209     * Access to the hashmap is protected by the {@link #mUpdateSyncRoot}.
210     */
211    private final HashMap<Uri, ConferenceParticipantConnection>
212            mConferenceParticipantConnections = new HashMap<Uri, ConferenceParticipantConnection>();
213
214    /**
215     * Sychronization root used to ensure that updates to the
216     * {@link #mConferenceParticipantConnections} happen atomically are are not interleaved across
217     * threads.  There are some instances where the network will send conference event package
218     * data closely spaced.  If that happens, it is possible that the interleaving of the update
219     * will cause duplicate participant info to be added.
220     */
221    private final Object mUpdateSyncRoot = new Object();
222
223    public void updateConferenceParticipantsAfterCreation() {
224        if (mConferenceHost != null) {
225            Log.v(this, "updateConferenceStateAfterCreation :: process participant update");
226            handleConferenceParticipantsUpdate(mConferenceHost,
227                    mConferenceHost.getConferenceParticipants());
228        } else {
229            Log.v(this, "updateConferenceStateAfterCreation :: null mConferenceHost");
230        }
231    }
232
233    /**
234     * Initializes a new {@link ImsConference}.
235     *
236     * @param telephonyConnectionService The connection service responsible for adding new
237     *                                   conferene participants.
238     * @param conferenceHost The telephony connection hosting the conference.
239     * @param phoneAccountHandle The phone account handle associated with the conference.
240     */
241    public ImsConference(TelephonyConnectionService telephonyConnectionService,
242            TelephonyConnection conferenceHost, PhoneAccountHandle phoneAccountHandle) {
243
244        super(phoneAccountHandle);
245
246        // Specify the connection time of the conference to be the connection time of the original
247        // connection.
248        long connectTime = conferenceHost.getOriginalConnection().getConnectTime();
249        setConnectTimeMillis(connectTime);
250        // Set the connectTime in the connection as well.
251        conferenceHost.setConnectTimeMillis(connectTime);
252
253        mTelephonyConnectionService = telephonyConnectionService;
254        setConferenceHost(conferenceHost);
255
256        int capabilities = Connection.CAPABILITY_SUPPORT_HOLD | Connection.CAPABILITY_HOLD |
257                Connection.CAPABILITY_MUTE | Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN;
258        capabilities = applyHostCapabilities(capabilities,
259                mConferenceHost.getConnectionCapabilities());
260        setConnectionCapabilities(capabilities);
261
262    }
263
264    /**
265     * Transfers capabilities from the conference host to the conference itself.
266     *
267     * @param conferenceCapabilities The current conference capabilities.
268     * @param capabilities The new conference host capabilities.
269     * @return The merged capabilities to be applied to the conference.
270     */
271    private int applyHostCapabilities(int conferenceCapabilities, int capabilities) {
272        if (can(capabilities, Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL)) {
273            conferenceCapabilities = applyCapability(conferenceCapabilities,
274                    Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL);
275        } else {
276            conferenceCapabilities = removeCapability(conferenceCapabilities,
277                    Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL);
278        }
279
280        if (can(capabilities, Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL)) {
281            conferenceCapabilities = applyCapability(conferenceCapabilities,
282                    Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
283        } else {
284            conferenceCapabilities = removeCapability(conferenceCapabilities,
285                    Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
286        }
287
288        if (can(capabilities, Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO)) {
289            conferenceCapabilities = applyCapability(conferenceCapabilities,
290                    Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO);
291        } else {
292            conferenceCapabilities = removeCapability(conferenceCapabilities,
293                    Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO);
294        }
295
296        if (can(capabilities, Connection.CAPABILITY_HIGH_DEF_AUDIO)) {
297            conferenceCapabilities = applyCapability(conferenceCapabilities,
298                    Connection.CAPABILITY_HIGH_DEF_AUDIO);
299        } else {
300            conferenceCapabilities = removeCapability(conferenceCapabilities,
301                    Connection.CAPABILITY_HIGH_DEF_AUDIO);
302        }
303        return conferenceCapabilities;
304    }
305
306    /**
307     * Not used by the IMS conference controller.
308     *
309     * @return {@code Null}.
310     */
311    @Override
312    public android.telecom.Connection getPrimaryConnection() {
313        return null;
314    }
315
316    /**
317     * Returns VideoProvider of the conference. This can be null.
318     *
319     * @hide
320     */
321    @Override
322    public VideoProvider getVideoProvider() {
323        if (mConferenceHost != null) {
324            return mConferenceHost.getVideoProvider();
325        }
326        return null;
327    }
328
329    /**
330     * Returns video state of conference
331     *
332     * @hide
333     */
334    @Override
335    public int getVideoState() {
336        if (mConferenceHost != null) {
337            return mConferenceHost.getVideoState();
338        }
339        return VideoProfile.STATE_AUDIO_ONLY;
340    }
341
342    /**
343     * Invoked when the Conference and all its {@link Connection}s should be disconnected.
344     * <p>
345     * Hangs up the call via the conference host connection.  When the host connection has been
346     * successfully disconnected, the {@link #mConferenceHostListener} listener receives an
347     * {@code onDestroyed} event, which triggers the conference participant connections to be
348     * disconnected.
349     */
350    @Override
351    public void onDisconnect() {
352        Log.v(this, "onDisconnect: hanging up conference host.");
353        if (mConferenceHost == null) {
354            return;
355        }
356
357        Call call = mConferenceHost.getCall();
358        if (call != null) {
359            try {
360                call.hangup();
361            } catch (CallStateException e) {
362                Log.e(this, e, "Exception thrown trying to hangup conference");
363            }
364        }
365    }
366
367    /**
368     * Invoked when the specified {@link android.telecom.Connection} should be separated from the
369     * conference call.
370     * <p>
371     * IMS does not support separating connections from the conference.
372     *
373     * @param connection The connection to separate.
374     */
375    @Override
376    public void onSeparate(android.telecom.Connection connection) {
377        Log.wtf(this, "Cannot separate connections from an IMS conference.");
378    }
379
380    /**
381     * Invoked when the specified {@link android.telecom.Connection} should be merged into the
382     * conference call.
383     *
384     * @param connection The {@code Connection} to merge.
385     */
386    @Override
387    public void onMerge(android.telecom.Connection connection) {
388        try {
389            Phone phone = ((TelephonyConnection) connection).getPhone();
390            if (phone != null) {
391                phone.conference();
392            }
393        } catch (CallStateException e) {
394            Log.e(this, e, "Exception thrown trying to merge call into a conference");
395        }
396    }
397
398    /**
399     * Invoked when the conference should be put on hold.
400     */
401    @Override
402    public void onHold() {
403        if (mConferenceHost == null) {
404            return;
405        }
406        mConferenceHost.performHold();
407    }
408
409    /**
410     * Invoked when the conference should be moved from hold to active.
411     */
412    @Override
413    public void onUnhold() {
414        if (mConferenceHost == null) {
415            return;
416        }
417        mConferenceHost.performUnhold();
418    }
419
420    /**
421     * Invoked to play a DTMF tone.
422     *
423     * @param c A DTMF character.
424     */
425    @Override
426    public void onPlayDtmfTone(char c) {
427        if (mConferenceHost == null) {
428            return;
429        }
430        mConferenceHost.onPlayDtmfTone(c);
431    }
432
433    /**
434     * Invoked to stop playing a DTMF tone.
435     */
436    @Override
437    public void onStopDtmfTone() {
438        if (mConferenceHost == null) {
439            return;
440        }
441        mConferenceHost.onStopDtmfTone();
442    }
443
444    /**
445     * Handles the addition of connections to the {@link ImsConference}.  The
446     * {@link ImsConferenceController} does not add connections to the conference.
447     *
448     * @param connection The newly added connection.
449     */
450    @Override
451    public void onConnectionAdded(android.telecom.Connection connection) {
452        // No-op
453    }
454
455    private int applyCapability(int capabilities, int capability) {
456        int newCapabilities = capabilities | capability;
457        return newCapabilities;
458    }
459
460    private int removeCapability(int capabilities, int capability) {
461        int newCapabilities = capabilities & ~capability;
462        return newCapabilities;
463    }
464
465    /**
466     * Determines if this conference is hosted on the current device or the peer device.
467     *
468     * @return {@code true} if this conference is hosted on the current device, {@code false} if it
469     *      is hosted on the peer device.
470     */
471    public boolean isConferenceHost() {
472        if (mConferenceHost == null) {
473            return false;
474        }
475        com.android.internal.telephony.Connection originalConnection =
476                mConferenceHost.getOriginalConnection();
477
478        return originalConnection != null && originalConnection.isMultiparty() &&
479                originalConnection.isConferenceHost();
480    }
481
482    /**
483     * Updates the manage conference capability of the conference.  Where there are one or more
484     * conference event package participants, the conference management is permitted.  Where there
485     * are no conference event package participants, conference management is not permitted.
486     * <p>
487     * Note: We add and remove {@link Connection#CAPABILITY_CONFERENCE_HAS_NO_CHILDREN} to ensure
488     * that the conference is represented appropriately on Bluetooth devices.
489     */
490    private void updateManageConference() {
491        boolean couldManageConference = can(Connection.CAPABILITY_MANAGE_CONFERENCE);
492        boolean canManageConference = !mConferenceParticipantConnections.isEmpty();
493        Log.v(this, "updateManageConference was :%s is:%s", couldManageConference ? "Y" : "N",
494                canManageConference ? "Y" : "N");
495
496        if (couldManageConference != canManageConference) {
497            int capabilities = getConnectionCapabilities();
498
499            if (canManageConference) {
500                capabilities |= Connection.CAPABILITY_MANAGE_CONFERENCE;
501                capabilities &= ~Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN;
502            } else {
503                capabilities &= ~Connection.CAPABILITY_MANAGE_CONFERENCE;
504                capabilities |= Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN;
505            }
506
507            setConnectionCapabilities(capabilities);
508        }
509    }
510
511    /**
512     * Sets the connection hosting the conference and registers for callbacks.
513     *
514     * @param conferenceHost The connection hosting the conference.
515     */
516    private void setConferenceHost(TelephonyConnection conferenceHost) {
517        if (Log.VERBOSE) {
518            Log.v(this, "setConferenceHost " + conferenceHost);
519        }
520
521        mConferenceHost = conferenceHost;
522
523        // Attempt to get the conference host's address (e.g. the host's own phone number).
524        // We need to look at the default phone for the ImsPhone when creating the phone account
525        // for the
526        if (mConferenceHost.getPhone() != null &&
527                mConferenceHost.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
528            // Look up the conference host's address; we need this later for filtering out the
529            // conference host in conference event package data.
530            Phone imsPhone = mConferenceHost.getPhone();
531            mConferenceHostPhoneAccountHandle =
532                    PhoneUtils.makePstnPhoneAccountHandle(imsPhone.getDefaultPhone());
533            mConferenceHostAddress = TelecomAccountRegistry.getInstance(mTelephonyConnectionService)
534                    .getAddress(mConferenceHostPhoneAccountHandle);
535        }
536
537        mConferenceHost.addConnectionListener(mConferenceHostListener);
538        mConferenceHost.addTelephonyConnectionListener(mTelephonyConnectionListener);
539        setState(mConferenceHost.getState());
540
541        updateStatusHints();
542    }
543
544    /**
545     * Handles state changes for conference participant(s).  The participants data passed in
546     *
547     * @param parent The connection which was notified of the conference participant.
548     * @param participants The conference participant information.
549     */
550    private void handleConferenceParticipantsUpdate(
551            TelephonyConnection parent, List<ConferenceParticipant> participants) {
552
553        if (participants == null) {
554            return;
555        }
556
557        // Perform the update in a synchronized manner.  It is possible for the IMS framework to
558        // trigger two onConferenceParticipantsChanged callbacks in quick succession.  If the first
559        // update adds new participants, and the second does something like update the status of one
560        // of the participants, we can get into a situation where the participant is added twice.
561        synchronized (mUpdateSyncRoot) {
562            boolean newParticipantsAdded = false;
563            boolean oldParticipantsRemoved = false;
564            ArrayList<ConferenceParticipant> newParticipants = new ArrayList<>(participants.size());
565            HashSet<Uri> participantUserEntities = new HashSet<>(participants.size());
566
567            // Add any new participants and update existing.
568            for (ConferenceParticipant participant : participants) {
569                Uri userEntity = participant.getHandle();
570
571                participantUserEntities.add(userEntity);
572                if (!mConferenceParticipantConnections.containsKey(userEntity)) {
573                    // Some carriers will also include the conference host in the CEP.  We will
574                    // filter that out here.
575                    if (!isParticipantHost(mConferenceHostAddress, userEntity)) {
576                        createConferenceParticipantConnection(parent, participant);
577                        newParticipants.add(participant);
578                        newParticipantsAdded = true;
579                    }
580                } else {
581                    ConferenceParticipantConnection connection =
582                            mConferenceParticipantConnections.get(userEntity);
583                    connection.updateState(participant.getState());
584                }
585            }
586
587            // Set state of new participants.
588            if (newParticipantsAdded) {
589                // Set the state of the new participants at once and add to the conference
590                for (ConferenceParticipant newParticipant : newParticipants) {
591                    ConferenceParticipantConnection connection =
592                            mConferenceParticipantConnections.get(newParticipant.getHandle());
593                    connection.updateState(newParticipant.getState());
594                }
595            }
596
597            // Finally, remove any participants from the conference that no longer exist in the
598            // conference event package data.
599            Iterator<Map.Entry<Uri, ConferenceParticipantConnection>> entryIterator =
600                    mConferenceParticipantConnections.entrySet().iterator();
601            while (entryIterator.hasNext()) {
602                Map.Entry<Uri, ConferenceParticipantConnection> entry = entryIterator.next();
603
604                if (!participantUserEntities.contains(entry.getKey())) {
605                    ConferenceParticipantConnection participant = entry.getValue();
606                    participant.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
607                    participant.removeConnectionListener(mParticipantListener);
608                    mTelephonyConnectionService.removeConnection(participant);
609                    removeConnection(participant);
610                    entryIterator.remove();
611                    oldParticipantsRemoved = true;
612                }
613            }
614
615            // If new participants were added or old ones were removed, we need to ensure the state
616            // of the manage conference capability is updated.
617            if (newParticipantsAdded || oldParticipantsRemoved) {
618                updateManageConference();
619            }
620        }
621    }
622
623    /**
624     * Creates a new {@link ConferenceParticipantConnection} to represent a
625     * {@link ConferenceParticipant}.
626     * <p>
627     * The new connection is added to the conference controller and connection service.
628     *
629     * @param parent The connection which was notified of the participant change (e.g. the
630     *                         parent connection).
631     * @param participant The conference participant information.
632     */
633    private void createConferenceParticipantConnection(
634            TelephonyConnection parent, ConferenceParticipant participant) {
635
636        // Create and add the new connection in holding state so that it does not become the
637        // active call.
638        ConferenceParticipantConnection connection = new ConferenceParticipantConnection(
639                parent.getOriginalConnection(), participant);
640        connection.addConnectionListener(mParticipantListener);
641        connection.setConnectTimeMillis(parent.getConnectTimeMillis());
642
643        if (Log.VERBOSE) {
644            Log.v(this, "createConferenceParticipantConnection: %s", connection);
645        }
646
647        synchronized(mUpdateSyncRoot) {
648            mConferenceParticipantConnections.put(participant.getHandle(), connection);
649        }
650        mTelephonyConnectionService.addExistingConnection(mConferenceHostPhoneAccountHandle,
651                connection);
652        addConnection(connection);
653    }
654
655    /**
656     * Removes a conference participant from the conference.
657     *
658     * @param participant The participant to remove.
659     */
660    private void removeConferenceParticipant(ConferenceParticipantConnection participant) {
661        Log.d(this, "removeConferenceParticipant: %s", participant);
662
663        participant.removeConnectionListener(mParticipantListener);
664        synchronized(mUpdateSyncRoot) {
665            mConferenceParticipantConnections.remove(participant.getUserEntity());
666        }
667        mTelephonyConnectionService.removeConnection(participant);
668    }
669
670    /**
671     * Disconnects all conference participants from the conference.
672     */
673    private void disconnectConferenceParticipants() {
674        Log.v(this, "disconnectConferenceParticipants");
675
676        synchronized(mUpdateSyncRoot) {
677            for (ConferenceParticipantConnection connection :
678                    mConferenceParticipantConnections.values()) {
679
680                connection.removeConnectionListener(mParticipantListener);
681                // Mark disconnect cause as cancelled to ensure that the call is not logged in the
682                // call log.
683                connection.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
684                mTelephonyConnectionService.removeConnection(connection);
685                connection.destroy();
686            }
687            mConferenceParticipantConnections.clear();
688        }
689    }
690
691    /**
692     * Determines if the passed in participant handle is the same as the conference host's handle.
693     * Starts with a simple equality check.  However, the handles from a conference event package
694     * will be a SIP uri, so we need to pull that apart to look for the participant's phone number.
695     *
696     * @param hostHandle The handle of the connection hosting the conference.
697     * @param handle The handle of the conference participant.
698     * @return {@code true} if the host's handle matches the participant's handle, {@code false}
699     *      otherwise.
700     */
701    private boolean isParticipantHost(Uri hostHandle, Uri handle) {
702        // If host and participant handles are the same, bail early.
703        if (Objects.equals(hostHandle, handle)) {
704            Log.v(this, "isParticipantHost(Y) : uris equal");
705            return true;
706        }
707
708        // If there is no host handle or not participant handle, bail early.
709        if (hostHandle == null || handle == null) {
710            Log.v(this, "isParticipantHost(N) : host or participant uri null");
711            return false;
712        }
713
714        // Conference event package participants are identified using SIP URIs (see RFC3261).
715        // A valid SIP uri has the format: sip:user:password@host:port;uri-parameters?headers
716        // Per RFC3261, the "user" can be a telephone number.
717        // For example: sip:1650555121;phone-context=blah.com@host.com
718        // In this case, the phone number is in the user field of the URI, and the parameters can be
719        // ignored.
720        //
721        // A SIP URI can also specify a phone number in a format similar to:
722        // sip:+1-212-555-1212@something.com;user=phone
723        // In this case, the phone number is again in user field and the parameters can be ignored.
724        // We can get the user field in these instances by splitting the string on the @, ;, or :
725        // and looking at the first found item.
726
727        String number = handle.getSchemeSpecificPart();
728        String numberParts[] = number.split("[@;:]");
729
730        if (numberParts.length == 0) {
731            Log.v(this, "isParticipantHost(N) : no number in participant handle");
732            return false;
733        }
734        number = numberParts[0];
735
736        // The host number will be a tel: uri.  Per RFC3966, the part after tel: is the phone
737        // number.
738        String hostNumber = hostHandle.getSchemeSpecificPart();
739
740        // Use a loose comparison of the phone numbers.  This ensures that numbers that differ by
741        // special characters are counted as equal.
742        // E.g. +16505551212 would be the same as 16505551212
743        boolean isHost = PhoneNumberUtils.compare(hostNumber, number);
744
745        Log.v(this, "isParticipantHost(%s) : host: %s, participant %s", (isHost ? "Y" : "N"),
746                Log.pii(hostNumber), Log.pii(number));
747        return isHost;
748    }
749
750    /**
751     * Handles a change in the original connection backing the conference host connection.  This can
752     * happen if an SRVCC event occurs on the original IMS connection, requiring a fallback to
753     * GSM or CDMA.
754     * <p>
755     * If this happens, we will add the conference host connection to telecom and tear down the
756     * conference.
757     */
758    private void handleOriginalConnectionChange() {
759        if (mConferenceHost == null) {
760            Log.w(this, "handleOriginalConnectionChange; conference host missing.");
761            return;
762        }
763
764        com.android.internal.telephony.Connection originalConnection =
765                mConferenceHost.getOriginalConnection();
766
767        if (originalConnection != null &&
768                originalConnection.getPhoneType() != PhoneConstants.PHONE_TYPE_IMS) {
769            if (Log.VERBOSE) {
770                Log.v(this,
771                        "Original connection for conference host is no longer an IMS connection; " +
772                                "new connection: %s", originalConnection);
773            }
774
775            PhoneAccountHandle phoneAccountHandle = null;
776            if (mConferenceHost.getPhone() != null &&
777                    mConferenceHost.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
778                Phone imsPhone = mConferenceHost.getPhone();
779                // The phone account handle for an ImsPhone is based on the default phone (ie the
780                // base GSM or CDMA phone, not on the ImsPhone itself).
781                phoneAccountHandle =
782                        PhoneUtils.makePstnPhoneAccountHandle(imsPhone.getDefaultPhone());
783            }
784
785            if (mConferenceHost.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) {
786                GsmConnection c = new GsmConnection(originalConnection, getTelecomCallId());
787                c.updateState();
788                // Copy the connect time from the conferenceHost
789                c.setConnectTimeMillis(mConferenceHost.getConnectTimeMillis());
790                mTelephonyConnectionService.addExistingConnection(phoneAccountHandle, c);
791                mTelephonyConnectionService.addConnectionToConferenceController(c);
792            } // CDMA case not applicable for SRVCC
793            mConferenceHost.removeConnectionListener(mConferenceHostListener);
794            mConferenceHost.removeTelephonyConnectionListener(mTelephonyConnectionListener);
795            mConferenceHost = null;
796            setDisconnected(new DisconnectCause(DisconnectCause.OTHER));
797            disconnectConferenceParticipants();
798            destroy();
799        }
800
801        updateStatusHints();
802    }
803
804    /**
805     * Changes the state of the Ims conference.
806     *
807     * @param state the new state.
808     */
809    public void setState(int state) {
810        Log.v(this, "setState %s", Connection.stateToString(state));
811
812        switch (state) {
813            case Connection.STATE_INITIALIZING:
814            case Connection.STATE_NEW:
815            case Connection.STATE_RINGING:
816                // No-op -- not applicable.
817                break;
818            case Connection.STATE_DIALING:
819                setDialing();
820                break;
821            case Connection.STATE_DISCONNECTED:
822                DisconnectCause disconnectCause;
823                if (mConferenceHost == null) {
824                    disconnectCause = new DisconnectCause(DisconnectCause.CANCELED);
825                } else {
826                    disconnectCause = DisconnectCauseUtil.toTelecomDisconnectCause(
827                            mConferenceHost.getOriginalConnection().getDisconnectCause());
828                }
829                setDisconnected(disconnectCause);
830                disconnectConferenceParticipants();
831                destroy();
832                break;
833            case Connection.STATE_ACTIVE:
834                setActive();
835                break;
836            case Connection.STATE_HOLDING:
837                setOnHold();
838                break;
839        }
840    }
841
842    private void updateStatusHints() {
843        if (mConferenceHost == null) {
844            setStatusHints(null);
845            return;
846        }
847
848        if (mConferenceHost.isWifi()) {
849            Phone phone = mConferenceHost.getPhone();
850            if (phone != null) {
851                Context context = phone.getContext();
852                setStatusHints(new StatusHints(
853                        context.getString(R.string.status_hint_label_wifi_call),
854                        Icon.createWithResource(
855                                context.getResources(),
856                                R.drawable.ic_signal_wifi_4_bar_24dp),
857                        null /* extras */));
858            }
859        } else {
860            setStatusHints(null);
861        }
862    }
863
864    /**
865     * Builds a string representation of the {@link ImsConference}.
866     *
867     * @return String representing the conference.
868     */
869    public String toString() {
870        StringBuilder sb = new StringBuilder();
871        sb.append("[ImsConference objId:");
872        sb.append(System.identityHashCode(this));
873        sb.append(" telecomCallID:");
874        sb.append(getTelecomCallId());
875        sb.append(" state:");
876        sb.append(Connection.stateToString(getState()));
877        sb.append(" hostConnection:");
878        sb.append(mConferenceHost);
879        sb.append(" participants:");
880        sb.append(mConferenceParticipantConnections.size());
881        sb.append("]");
882        return sb.toString();
883    }
884}
885