ImsConference.java revision 0d3f7fa783846a6c10728bd4e3081685a887d422
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     */
240    public ImsConference(TelephonyConnectionService telephonyConnectionService,
241            TelephonyConnection conferenceHost) {
242
243        super((conferenceHost != null && conferenceHost.getCall() != null &&
244                        conferenceHost.getCall().getPhone() != null) ?
245                PhoneUtils.makePstnPhoneAccountHandle(
246                        conferenceHost.getCall().getPhone()) : null);
247
248        // Specify the connection time of the conference to be the connection time of the original
249        // connection.
250        long connectTime = conferenceHost.getOriginalConnection().getConnectTime();
251        setConnectTimeMillis(connectTime);
252        // Set the connectTime in the connection as well.
253        conferenceHost.setConnectTimeMillis(connectTime);
254
255        mTelephonyConnectionService = telephonyConnectionService;
256        setConferenceHost(conferenceHost);
257
258        int capabilities = Connection.CAPABILITY_SUPPORT_HOLD | Connection.CAPABILITY_HOLD |
259                Connection.CAPABILITY_MUTE | Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN;
260        capabilities = applyHostCapabilities(capabilities,
261                mConferenceHost.getConnectionCapabilities());
262        setConnectionCapabilities(capabilities);
263
264    }
265
266    /**
267     * Transfers capabilities from the conference host to the conference itself.
268     *
269     * @param conferenceCapabilities The current conference capabilities.
270     * @param capabilities The new conference host capabilities.
271     * @return The merged capabilities to be applied to the conference.
272     */
273    private int applyHostCapabilities(int conferenceCapabilities, int capabilities) {
274        if (can(capabilities, Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL)) {
275            conferenceCapabilities = applyCapability(conferenceCapabilities,
276                    Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL);
277        } else {
278            conferenceCapabilities = removeCapability(conferenceCapabilities,
279                    Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL);
280        }
281
282        if (can(capabilities, Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL)) {
283            conferenceCapabilities = applyCapability(conferenceCapabilities,
284                    Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
285        } else {
286            conferenceCapabilities = removeCapability(conferenceCapabilities,
287                    Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
288        }
289
290        if (can(capabilities, Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO)) {
291            conferenceCapabilities = applyCapability(conferenceCapabilities,
292                    Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO);
293        } else {
294            conferenceCapabilities = removeCapability(conferenceCapabilities,
295                    Connection.CAPABILITY_CAN_UPGRADE_TO_VIDEO);
296        }
297
298        if (can(capabilities, Connection.CAPABILITY_HIGH_DEF_AUDIO)) {
299            conferenceCapabilities = applyCapability(conferenceCapabilities,
300                    Connection.CAPABILITY_HIGH_DEF_AUDIO);
301        } else {
302            conferenceCapabilities = removeCapability(conferenceCapabilities,
303                    Connection.CAPABILITY_HIGH_DEF_AUDIO);
304        }
305        return conferenceCapabilities;
306    }
307
308    /**
309     * Not used by the IMS conference controller.
310     *
311     * @return {@code Null}.
312     */
313    @Override
314    public android.telecom.Connection getPrimaryConnection() {
315        return null;
316    }
317
318    /**
319     * Returns VideoProvider of the conference. This can be null.
320     *
321     * @hide
322     */
323    @Override
324    public VideoProvider getVideoProvider() {
325        if (mConferenceHost != null) {
326            return mConferenceHost.getVideoProvider();
327        }
328        return null;
329    }
330
331    /**
332     * Returns video state of conference
333     *
334     * @hide
335     */
336    @Override
337    public int getVideoState() {
338        if (mConferenceHost != null) {
339            return mConferenceHost.getVideoState();
340        }
341        return VideoProfile.STATE_AUDIO_ONLY;
342    }
343
344    /**
345     * Invoked when the Conference and all its {@link Connection}s should be disconnected.
346     * <p>
347     * Hangs up the call via the conference host connection.  When the host connection has been
348     * successfully disconnected, the {@link #mConferenceHostListener} listener receives an
349     * {@code onDestroyed} event, which triggers the conference participant connections to be
350     * disconnected.
351     */
352    @Override
353    public void onDisconnect() {
354        Log.v(this, "onDisconnect: hanging up conference host.");
355        if (mConferenceHost == null) {
356            return;
357        }
358
359        Call call = mConferenceHost.getCall();
360        if (call != null) {
361            try {
362                call.hangup();
363            } catch (CallStateException e) {
364                Log.e(this, e, "Exception thrown trying to hangup conference");
365            }
366        }
367    }
368
369    /**
370     * Invoked when the specified {@link android.telecom.Connection} should be separated from the
371     * conference call.
372     * <p>
373     * IMS does not support separating connections from the conference.
374     *
375     * @param connection The connection to separate.
376     */
377    @Override
378    public void onSeparate(android.telecom.Connection connection) {
379        Log.wtf(this, "Cannot separate connections from an IMS conference.");
380    }
381
382    /**
383     * Invoked when the specified {@link android.telecom.Connection} should be merged into the
384     * conference call.
385     *
386     * @param connection The {@code Connection} to merge.
387     */
388    @Override
389    public void onMerge(android.telecom.Connection connection) {
390        try {
391            Phone phone = ((TelephonyConnection) connection).getPhone();
392            if (phone != null) {
393                phone.conference();
394            }
395        } catch (CallStateException e) {
396            Log.e(this, e, "Exception thrown trying to merge call into a conference");
397        }
398    }
399
400    /**
401     * Invoked when the conference should be put on hold.
402     */
403    @Override
404    public void onHold() {
405        if (mConferenceHost == null) {
406            return;
407        }
408        mConferenceHost.performHold();
409    }
410
411    /**
412     * Invoked when the conference should be moved from hold to active.
413     */
414    @Override
415    public void onUnhold() {
416        if (mConferenceHost == null) {
417            return;
418        }
419        mConferenceHost.performUnhold();
420    }
421
422    /**
423     * Invoked to play a DTMF tone.
424     *
425     * @param c A DTMF character.
426     */
427    @Override
428    public void onPlayDtmfTone(char c) {
429        if (mConferenceHost == null) {
430            return;
431        }
432        mConferenceHost.onPlayDtmfTone(c);
433    }
434
435    /**
436     * Invoked to stop playing a DTMF tone.
437     */
438    @Override
439    public void onStopDtmfTone() {
440        if (mConferenceHost == null) {
441            return;
442        }
443        mConferenceHost.onStopDtmfTone();
444    }
445
446    /**
447     * Handles the addition of connections to the {@link ImsConference}.  The
448     * {@link ImsConferenceController} does not add connections to the conference.
449     *
450     * @param connection The newly added connection.
451     */
452    @Override
453    public void onConnectionAdded(android.telecom.Connection connection) {
454        // No-op
455    }
456
457    private int applyCapability(int capabilities, int capability) {
458        int newCapabilities = capabilities | capability;
459        return newCapabilities;
460    }
461
462    private int removeCapability(int capabilities, int capability) {
463        int newCapabilities = capabilities & ~capability;
464        return newCapabilities;
465    }
466
467    /**
468     * Determines if this conference is hosted on the current device or the peer device.
469     *
470     * @return {@code true} if this conference is hosted on the current device, {@code false} if it
471     *      is hosted on the peer device.
472     */
473    public boolean isConferenceHost() {
474        if (mConferenceHost == null) {
475            return false;
476        }
477        com.android.internal.telephony.Connection originalConnection =
478                mConferenceHost.getOriginalConnection();
479
480        return originalConnection.isMultiparty() && originalConnection.isConferenceHost();
481    }
482
483    /**
484     * Updates the manage conference capability of the conference.  Where there are one or more
485     * conference event package participants, the conference management is permitted.  Where there
486     * are no conference event package participants, conference management is not permitted.
487     * <p>
488     * Note: We add and remove {@link Connection#CAPABILITY_CONFERENCE_HAS_NO_CHILDREN} to ensure
489     * that the conference is represented appropriately on Bluetooth devices.
490     */
491    private void updateManageConference() {
492        boolean couldManageConference = can(Connection.CAPABILITY_MANAGE_CONFERENCE);
493        boolean canManageConference = !mConferenceParticipantConnections.isEmpty();
494        Log.v(this, "updateManageConference was :%s is:%s", couldManageConference ? "Y" : "N",
495                canManageConference ? "Y" : "N");
496
497        if (couldManageConference != canManageConference) {
498            int capabilities = getConnectionCapabilities();
499
500            if (canManageConference) {
501                capabilities |= Connection.CAPABILITY_MANAGE_CONFERENCE;
502                capabilities &= ~Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN;
503            } else {
504                capabilities &= ~Connection.CAPABILITY_MANAGE_CONFERENCE;
505                capabilities |= Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN;
506            }
507
508            setConnectionCapabilities(capabilities);
509        }
510    }
511
512    /**
513     * Sets the connection hosting the conference and registers for callbacks.
514     *
515     * @param conferenceHost The connection hosting the conference.
516     */
517    private void setConferenceHost(TelephonyConnection conferenceHost) {
518        if (Log.VERBOSE) {
519            Log.v(this, "setConferenceHost " + conferenceHost);
520        }
521
522        mConferenceHost = conferenceHost;
523
524        // Attempt to get the conference host's address (e.g. the host's own phone number).
525        // We need to look at the default phone for the ImsPhone when creating the phone account
526        // for the
527        if (mConferenceHost.getPhone() != null &&
528                mConferenceHost.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
529            // Look up the conference host's address; we need this later for filtering out the
530            // conference host in conference event package data.
531            Phone imsPhone = mConferenceHost.getPhone();
532            mConferenceHostPhoneAccountHandle =
533                    PhoneUtils.makePstnPhoneAccountHandle(imsPhone.getDefaultPhone());
534            mConferenceHostAddress = TelecomAccountRegistry.getInstance(mTelephonyConnectionService)
535                    .getAddress(mConferenceHostPhoneAccountHandle);
536        }
537
538        mConferenceHost.addConnectionListener(mConferenceHostListener);
539        mConferenceHost.addTelephonyConnectionListener(mTelephonyConnectionListener);
540        setState(mConferenceHost.getState());
541
542        updateStatusHints();
543    }
544
545    /**
546     * Handles state changes for conference participant(s).  The participants data passed in
547     *
548     * @param parent The connection which was notified of the conference participant.
549     * @param participants The conference participant information.
550     */
551    private void handleConferenceParticipantsUpdate(
552            TelephonyConnection parent, List<ConferenceParticipant> participants) {
553
554        if (participants == null) {
555            return;
556        }
557
558        // Perform the update in a synchronized manner.  It is possible for the IMS framework to
559        // trigger two onConferenceParticipantsChanged callbacks in quick succession.  If the first
560        // update adds new participants, and the second does something like update the status of one
561        // of the participants, we can get into a situation where the participant is added twice.
562        synchronized (mUpdateSyncRoot) {
563            boolean newParticipantsAdded = false;
564            boolean oldParticipantsRemoved = false;
565            ArrayList<ConferenceParticipant> newParticipants = new ArrayList<>(participants.size());
566            HashSet<Uri> participantUserEntities = new HashSet<>(participants.size());
567
568            // Add any new participants and update existing.
569            for (ConferenceParticipant participant : participants) {
570                Uri userEntity = participant.getHandle();
571
572                participantUserEntities.add(userEntity);
573                if (!mConferenceParticipantConnections.containsKey(userEntity)) {
574                    // Some carriers will also include the conference host in the CEP.  We will
575                    // filter that out here.
576                    if (!isParticipantHost(mConferenceHostAddress, userEntity)) {
577                        createConferenceParticipantConnection(parent, participant);
578                        newParticipants.add(participant);
579                        newParticipantsAdded = true;
580                    }
581                } else {
582                    ConferenceParticipantConnection connection =
583                            mConferenceParticipantConnections.get(userEntity);
584                    connection.updateState(participant.getState());
585                }
586            }
587
588            // Set state of new participants.
589            if (newParticipantsAdded) {
590                // Set the state of the new participants at once and add to the conference
591                for (ConferenceParticipant newParticipant : newParticipants) {
592                    ConferenceParticipantConnection connection =
593                            mConferenceParticipantConnections.get(newParticipant.getHandle());
594                    connection.updateState(newParticipant.getState());
595                }
596            }
597
598            // Finally, remove any participants from the conference that no longer exist in the
599            // conference event package data.
600            Iterator<Map.Entry<Uri, ConferenceParticipantConnection>> entryIterator =
601                    mConferenceParticipantConnections.entrySet().iterator();
602            while (entryIterator.hasNext()) {
603                Map.Entry<Uri, ConferenceParticipantConnection> entry = entryIterator.next();
604
605                if (!participantUserEntities.contains(entry.getKey())) {
606                    ConferenceParticipantConnection participant = entry.getValue();
607                    participant.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
608                    participant.removeConnectionListener(mParticipantListener);
609                    mTelephonyConnectionService.removeConnection(participant);
610                    removeConnection(participant);
611                    entryIterator.remove();
612                    oldParticipantsRemoved = true;
613                }
614            }
615
616            // If new participants were added or old ones were removed, we need to ensure the state
617            // of the manage conference capability is updated.
618            if (newParticipantsAdded || oldParticipantsRemoved) {
619                updateManageConference();
620            }
621        }
622    }
623
624    /**
625     * Creates a new {@link ConferenceParticipantConnection} to represent a
626     * {@link ConferenceParticipant}.
627     * <p>
628     * The new connection is added to the conference controller and connection service.
629     *
630     * @param parent The connection which was notified of the participant change (e.g. the
631     *                         parent connection).
632     * @param participant The conference participant information.
633     */
634    private void createConferenceParticipantConnection(
635            TelephonyConnection parent, ConferenceParticipant participant) {
636
637        // Create and add the new connection in holding state so that it does not become the
638        // active call.
639        ConferenceParticipantConnection connection = new ConferenceParticipantConnection(
640                parent.getOriginalConnection(), participant);
641        connection.addConnectionListener(mParticipantListener);
642        connection.setConnectTimeMillis(parent.getConnectTimeMillis());
643
644        if (Log.VERBOSE) {
645            Log.v(this, "createConferenceParticipantConnection: %s", connection);
646        }
647
648        synchronized(mUpdateSyncRoot) {
649            mConferenceParticipantConnections.put(participant.getHandle(), connection);
650        }
651        mTelephonyConnectionService.addExistingConnection(mConferenceHostPhoneAccountHandle,
652                connection);
653        addConnection(connection);
654    }
655
656    /**
657     * Removes a conference participant from the conference.
658     *
659     * @param participant The participant to remove.
660     */
661    private void removeConferenceParticipant(ConferenceParticipantConnection participant) {
662        Log.d(this, "removeConferenceParticipant: %s", participant);
663
664        participant.removeConnectionListener(mParticipantListener);
665        synchronized(mUpdateSyncRoot) {
666            mConferenceParticipantConnections.remove(participant.getUserEntity());
667        }
668        mTelephonyConnectionService.removeConnection(participant);
669    }
670
671    /**
672     * Disconnects all conference participants from the conference.
673     */
674    private void disconnectConferenceParticipants() {
675        Log.v(this, "disconnectConferenceParticipants");
676
677        synchronized(mUpdateSyncRoot) {
678            for (ConferenceParticipantConnection connection :
679                    mConferenceParticipantConnections.values()) {
680
681                connection.removeConnectionListener(mParticipantListener);
682                // Mark disconnect cause as cancelled to ensure that the call is not logged in the
683                // call log.
684                connection.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
685                mTelephonyConnectionService.removeConnection(connection);
686                connection.destroy();
687            }
688            mConferenceParticipantConnections.clear();
689        }
690    }
691
692    /**
693     * Determines if the passed in participant handle is the same as the conference host's handle.
694     * Starts with a simple equality check.  However, the handles from a conference event package
695     * will be a SIP uri, so we need to pull that apart to look for the participant's phone number.
696     *
697     * @param hostHandle The handle of the connection hosting the conference.
698     * @param handle The handle of the conference participant.
699     * @return {@code true} if the host's handle matches the participant's handle, {@code false}
700     *      otherwise.
701     */
702    private boolean isParticipantHost(Uri hostHandle, Uri handle) {
703        // If host and participant handles are the same, bail early.
704        if (Objects.equals(hostHandle, handle)) {
705            Log.v(this, "isParticipantHost(Y) : uris equal");
706            return true;
707        }
708
709        // If there is no host handle or not participant handle, bail early.
710        if (hostHandle == null || handle == null) {
711            Log.v(this, "isParticipantHost(N) : host or participant uri null");
712            return false;
713        }
714
715        // Conference event package participants are identified using SIP URIs (see RFC3261).
716        // A valid SIP uri has the format: sip:user:password@host:port;uri-parameters?headers
717        // Per RFC3261, the "user" can be a telephone number.
718        // For example: sip:1650555121;phone-context=blah.com@host.com
719        // In this case, the phone number is in the user field of the URI, and the parameters can be
720        // ignored.
721        //
722        // A SIP URI can also specify a phone number in a format similar to:
723        // sip:+1-212-555-1212@something.com;user=phone
724        // In this case, the phone number is again in user field and the parameters can be ignored.
725        // We can get the user field in these instances by splitting the string on the @, ;, or :
726        // and looking at the first found item.
727
728        String number = handle.getSchemeSpecificPart();
729        String numberParts[] = number.split("[@;:]");
730
731        if (numberParts.length == 0) {
732            Log.v(this, "isParticipantHost(N) : no number in participant handle");
733            return false;
734        }
735        number = numberParts[0];
736
737        // The host number will be a tel: uri.  Per RFC3966, the part after tel: is the phone
738        // number.
739        String hostNumber = hostHandle.getSchemeSpecificPart();
740
741        // Use a loose comparison of the phone numbers.  This ensures that numbers that differ by
742        // special characters are counted as equal.
743        // E.g. +16505551212 would be the same as 16505551212
744        boolean isHost = PhoneNumberUtils.compare(hostNumber, number);
745
746        Log.v(this, "isParticipantHost(%s) : host: %s, participant %s", (isHost ? "Y" : "N"),
747                Log.pii(hostNumber), Log.pii(number));
748        return isHost;
749    }
750
751    /**
752     * Handles a change in the original connection backing the conference host connection.  This can
753     * happen if an SRVCC event occurs on the original IMS connection, requiring a fallback to
754     * GSM or CDMA.
755     * <p>
756     * If this happens, we will add the conference host connection to telecom and tear down the
757     * conference.
758     */
759    private void handleOriginalConnectionChange() {
760        if (mConferenceHost == null) {
761            Log.w(this, "handleOriginalConnectionChange; conference host missing.");
762            return;
763        }
764
765        com.android.internal.telephony.Connection originalConnection =
766                mConferenceHost.getOriginalConnection();
767
768        if (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 =
776                    PhoneUtils.makePstnPhoneAccountHandle(mConferenceHost.getPhone());
777            if (mConferenceHost.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) {
778                GsmConnection c = new GsmConnection(originalConnection, getTelecomCallId());
779                c.updateState();
780                // Copy the connect time from the conferenceHost
781                c.setConnectTimeMillis(mConferenceHost.getConnectTimeMillis());
782                mTelephonyConnectionService.addExistingConnection(phoneAccountHandle, c);
783                mTelephonyConnectionService.addConnectionToConferenceController(c);
784            } // CDMA case not applicable for SRVCC
785            mConferenceHost.removeConnectionListener(mConferenceHostListener);
786            mConferenceHost.removeTelephonyConnectionListener(mTelephonyConnectionListener);
787            mConferenceHost = null;
788            setDisconnected(new DisconnectCause(DisconnectCause.OTHER));
789            disconnectConferenceParticipants();
790            destroy();
791        }
792
793        updateStatusHints();
794    }
795
796    /**
797     * Changes the state of the Ims conference.
798     *
799     * @param state the new state.
800     */
801    public void setState(int state) {
802        Log.v(this, "setState %s", Connection.stateToString(state));
803
804        switch (state) {
805            case Connection.STATE_INITIALIZING:
806            case Connection.STATE_NEW:
807            case Connection.STATE_RINGING:
808                // No-op -- not applicable.
809                break;
810            case Connection.STATE_DIALING:
811                setDialing();
812                break;
813            case Connection.STATE_DISCONNECTED:
814                DisconnectCause disconnectCause;
815                if (mConferenceHost == null) {
816                    disconnectCause = new DisconnectCause(DisconnectCause.CANCELED);
817                } else {
818                    disconnectCause = DisconnectCauseUtil.toTelecomDisconnectCause(
819                            mConferenceHost.getOriginalConnection().getDisconnectCause());
820                }
821                setDisconnected(disconnectCause);
822                disconnectConferenceParticipants();
823                destroy();
824                break;
825            case Connection.STATE_ACTIVE:
826                setActive();
827                break;
828            case Connection.STATE_HOLDING:
829                setOnHold();
830                break;
831        }
832    }
833
834    private void updateStatusHints() {
835        if (mConferenceHost == null) {
836            setStatusHints(null);
837            return;
838        }
839
840        if (mConferenceHost.isWifi()) {
841            Phone phone = mConferenceHost.getPhone();
842            if (phone != null) {
843                Context context = phone.getContext();
844                setStatusHints(new StatusHints(
845                        context.getString(R.string.status_hint_label_wifi_call),
846                        Icon.createWithResource(
847                                context.getResources(),
848                                R.drawable.ic_signal_wifi_4_bar_24dp),
849                        null /* extras */));
850            }
851        } else {
852            setStatusHints(null);
853        }
854    }
855
856    /**
857     * Builds a string representation of the {@link ImsConference}.
858     *
859     * @return String representing the conference.
860     */
861    public String toString() {
862        StringBuilder sb = new StringBuilder();
863        sb.append("[ImsConference objId:");
864        sb.append(System.identityHashCode(this));
865        sb.append(" telecomCallID:");
866        sb.append(getTelecomCallId());
867        sb.append(" state:");
868        sb.append(Connection.stateToString(getState()));
869        sb.append(" hostConnection:");
870        sb.append(mConferenceHost);
871        sb.append(" participants:");
872        sb.append(mConferenceParticipantConnections.size());
873        sb.append("]");
874        return sb.toString();
875    }
876}
877