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