ImsConference.java revision 6d83e5eb90ee8b4551d61c4789bb8f124c4c12d2
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.isMultiparty() && originalConnection.isConferenceHost();
479    }
480
481    /**
482     * Updates the manage conference capability of the conference.  Where there are one or more
483     * conference event package participants, the conference management is permitted.  Where there
484     * are no conference event package participants, conference management is not permitted.
485     * <p>
486     * Note: We add and remove {@link Connection#CAPABILITY_CONFERENCE_HAS_NO_CHILDREN} to ensure
487     * that the conference is represented appropriately on Bluetooth devices.
488     */
489    private void updateManageConference() {
490        boolean couldManageConference = can(Connection.CAPABILITY_MANAGE_CONFERENCE);
491        boolean canManageConference = !mConferenceParticipantConnections.isEmpty();
492        Log.v(this, "updateManageConference was :%s is:%s", couldManageConference ? "Y" : "N",
493                canManageConference ? "Y" : "N");
494
495        if (couldManageConference != canManageConference) {
496            int capabilities = getConnectionCapabilities();
497
498            if (canManageConference) {
499                capabilities |= Connection.CAPABILITY_MANAGE_CONFERENCE;
500                capabilities &= ~Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN;
501            } else {
502                capabilities &= ~Connection.CAPABILITY_MANAGE_CONFERENCE;
503                capabilities |= Connection.CAPABILITY_CONFERENCE_HAS_NO_CHILDREN;
504            }
505
506            setConnectionCapabilities(capabilities);
507        }
508    }
509
510    /**
511     * Sets the connection hosting the conference and registers for callbacks.
512     *
513     * @param conferenceHost The connection hosting the conference.
514     */
515    private void setConferenceHost(TelephonyConnection conferenceHost) {
516        if (Log.VERBOSE) {
517            Log.v(this, "setConferenceHost " + conferenceHost);
518        }
519
520        mConferenceHost = conferenceHost;
521
522        // Attempt to get the conference host's address (e.g. the host's own phone number).
523        // We need to look at the default phone for the ImsPhone when creating the phone account
524        // for the
525        if (mConferenceHost.getPhone() != null &&
526                mConferenceHost.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
527            // Look up the conference host's address; we need this later for filtering out the
528            // conference host in conference event package data.
529            Phone imsPhone = mConferenceHost.getPhone();
530            mConferenceHostPhoneAccountHandle =
531                    PhoneUtils.makePstnPhoneAccountHandle(imsPhone.getDefaultPhone());
532            mConferenceHostAddress = TelecomAccountRegistry.getInstance(mTelephonyConnectionService)
533                    .getAddress(mConferenceHostPhoneAccountHandle);
534        }
535
536        mConferenceHost.addConnectionListener(mConferenceHostListener);
537        mConferenceHost.addTelephonyConnectionListener(mTelephonyConnectionListener);
538        setState(mConferenceHost.getState());
539
540        updateStatusHints();
541    }
542
543    /**
544     * Handles state changes for conference participant(s).  The participants data passed in
545     *
546     * @param parent The connection which was notified of the conference participant.
547     * @param participants The conference participant information.
548     */
549    private void handleConferenceParticipantsUpdate(
550            TelephonyConnection parent, List<ConferenceParticipant> participants) {
551
552        if (participants == null) {
553            return;
554        }
555
556        // Perform the update in a synchronized manner.  It is possible for the IMS framework to
557        // trigger two onConferenceParticipantsChanged callbacks in quick succession.  If the first
558        // update adds new participants, and the second does something like update the status of one
559        // of the participants, we can get into a situation where the participant is added twice.
560        synchronized (mUpdateSyncRoot) {
561            boolean newParticipantsAdded = false;
562            boolean oldParticipantsRemoved = false;
563            ArrayList<ConferenceParticipant> newParticipants = new ArrayList<>(participants.size());
564            HashSet<Uri> participantUserEntities = new HashSet<>(participants.size());
565
566            // Add any new participants and update existing.
567            for (ConferenceParticipant participant : participants) {
568                Uri userEntity = participant.getHandle();
569
570                participantUserEntities.add(userEntity);
571                if (!mConferenceParticipantConnections.containsKey(userEntity)) {
572                    // Some carriers will also include the conference host in the CEP.  We will
573                    // filter that out here.
574                    if (!isParticipantHost(mConferenceHostAddress, userEntity)) {
575                        createConferenceParticipantConnection(parent, participant);
576                        newParticipants.add(participant);
577                        newParticipantsAdded = true;
578                    }
579                } else {
580                    ConferenceParticipantConnection connection =
581                            mConferenceParticipantConnections.get(userEntity);
582                    connection.updateState(participant.getState());
583                }
584            }
585
586            // Set state of new participants.
587            if (newParticipantsAdded) {
588                // Set the state of the new participants at once and add to the conference
589                for (ConferenceParticipant newParticipant : newParticipants) {
590                    ConferenceParticipantConnection connection =
591                            mConferenceParticipantConnections.get(newParticipant.getHandle());
592                    connection.updateState(newParticipant.getState());
593                }
594            }
595
596            // Finally, remove any participants from the conference that no longer exist in the
597            // conference event package data.
598            Iterator<Map.Entry<Uri, ConferenceParticipantConnection>> entryIterator =
599                    mConferenceParticipantConnections.entrySet().iterator();
600            while (entryIterator.hasNext()) {
601                Map.Entry<Uri, ConferenceParticipantConnection> entry = entryIterator.next();
602
603                if (!participantUserEntities.contains(entry.getKey())) {
604                    ConferenceParticipantConnection participant = entry.getValue();
605                    participant.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
606                    participant.removeConnectionListener(mParticipantListener);
607                    mTelephonyConnectionService.removeConnection(participant);
608                    removeConnection(participant);
609                    entryIterator.remove();
610                    oldParticipantsRemoved = true;
611                }
612            }
613
614            // If new participants were added or old ones were removed, we need to ensure the state
615            // of the manage conference capability is updated.
616            if (newParticipantsAdded || oldParticipantsRemoved) {
617                updateManageConference();
618            }
619        }
620    }
621
622    /**
623     * Creates a new {@link ConferenceParticipantConnection} to represent a
624     * {@link ConferenceParticipant}.
625     * <p>
626     * The new connection is added to the conference controller and connection service.
627     *
628     * @param parent The connection which was notified of the participant change (e.g. the
629     *                         parent connection).
630     * @param participant The conference participant information.
631     */
632    private void createConferenceParticipantConnection(
633            TelephonyConnection parent, ConferenceParticipant participant) {
634
635        // Create and add the new connection in holding state so that it does not become the
636        // active call.
637        ConferenceParticipantConnection connection = new ConferenceParticipantConnection(
638                parent.getOriginalConnection(), participant);
639        connection.addConnectionListener(mParticipantListener);
640        connection.setConnectTimeMillis(parent.getConnectTimeMillis());
641
642        if (Log.VERBOSE) {
643            Log.v(this, "createConferenceParticipantConnection: %s", connection);
644        }
645
646        synchronized(mUpdateSyncRoot) {
647            mConferenceParticipantConnections.put(participant.getHandle(), connection);
648        }
649        mTelephonyConnectionService.addExistingConnection(mConferenceHostPhoneAccountHandle,
650                connection);
651        addConnection(connection);
652    }
653
654    /**
655     * Removes a conference participant from the conference.
656     *
657     * @param participant The participant to remove.
658     */
659    private void removeConferenceParticipant(ConferenceParticipantConnection participant) {
660        Log.d(this, "removeConferenceParticipant: %s", participant);
661
662        participant.removeConnectionListener(mParticipantListener);
663        synchronized(mUpdateSyncRoot) {
664            mConferenceParticipantConnections.remove(participant.getUserEntity());
665        }
666        mTelephonyConnectionService.removeConnection(participant);
667    }
668
669    /**
670     * Disconnects all conference participants from the conference.
671     */
672    private void disconnectConferenceParticipants() {
673        Log.v(this, "disconnectConferenceParticipants");
674
675        synchronized(mUpdateSyncRoot) {
676            for (ConferenceParticipantConnection connection :
677                    mConferenceParticipantConnections.values()) {
678
679                connection.removeConnectionListener(mParticipantListener);
680                // Mark disconnect cause as cancelled to ensure that the call is not logged in the
681                // call log.
682                connection.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
683                mTelephonyConnectionService.removeConnection(connection);
684                connection.destroy();
685            }
686            mConferenceParticipantConnections.clear();
687        }
688    }
689
690    /**
691     * Determines if the passed in participant handle is the same as the conference host's handle.
692     * Starts with a simple equality check.  However, the handles from a conference event package
693     * will be a SIP uri, so we need to pull that apart to look for the participant's phone number.
694     *
695     * @param hostHandle The handle of the connection hosting the conference.
696     * @param handle The handle of the conference participant.
697     * @return {@code true} if the host's handle matches the participant's handle, {@code false}
698     *      otherwise.
699     */
700    private boolean isParticipantHost(Uri hostHandle, Uri handle) {
701        // If host and participant handles are the same, bail early.
702        if (Objects.equals(hostHandle, handle)) {
703            Log.v(this, "isParticipantHost(Y) : uris equal");
704            return true;
705        }
706
707        // If there is no host handle or not participant handle, bail early.
708        if (hostHandle == null || handle == null) {
709            Log.v(this, "isParticipantHost(N) : host or participant uri null");
710            return false;
711        }
712
713        // Conference event package participants are identified using SIP URIs (see RFC3261).
714        // A valid SIP uri has the format: sip:user:password@host:port;uri-parameters?headers
715        // Per RFC3261, the "user" can be a telephone number.
716        // For example: sip:1650555121;phone-context=blah.com@host.com
717        // In this case, the phone number is in the user field of the URI, and the parameters can be
718        // ignored.
719        //
720        // A SIP URI can also specify a phone number in a format similar to:
721        // sip:+1-212-555-1212@something.com;user=phone
722        // In this case, the phone number is again in user field and the parameters can be ignored.
723        // We can get the user field in these instances by splitting the string on the @, ;, or :
724        // and looking at the first found item.
725
726        String number = handle.getSchemeSpecificPart();
727        String numberParts[] = number.split("[@;:]");
728
729        if (numberParts.length == 0) {
730            Log.v(this, "isParticipantHost(N) : no number in participant handle");
731            return false;
732        }
733        number = numberParts[0];
734
735        // The host number will be a tel: uri.  Per RFC3966, the part after tel: is the phone
736        // number.
737        String hostNumber = hostHandle.getSchemeSpecificPart();
738
739        // Use a loose comparison of the phone numbers.  This ensures that numbers that differ by
740        // special characters are counted as equal.
741        // E.g. +16505551212 would be the same as 16505551212
742        boolean isHost = PhoneNumberUtils.compare(hostNumber, number);
743
744        Log.v(this, "isParticipantHost(%s) : host: %s, participant %s", (isHost ? "Y" : "N"),
745                Log.pii(hostNumber), Log.pii(number));
746        return isHost;
747    }
748
749    /**
750     * Handles a change in the original connection backing the conference host connection.  This can
751     * happen if an SRVCC event occurs on the original IMS connection, requiring a fallback to
752     * GSM or CDMA.
753     * <p>
754     * If this happens, we will add the conference host connection to telecom and tear down the
755     * conference.
756     */
757    private void handleOriginalConnectionChange() {
758        if (mConferenceHost == null) {
759            Log.w(this, "handleOriginalConnectionChange; conference host missing.");
760            return;
761        }
762
763        com.android.internal.telephony.Connection originalConnection =
764                mConferenceHost.getOriginalConnection();
765
766        if (originalConnection.getPhoneType() != PhoneConstants.PHONE_TYPE_IMS) {
767            if (Log.VERBOSE) {
768                Log.v(this,
769                        "Original connection for conference host is no longer an IMS connection; " +
770                                "new connection: %s", originalConnection);
771            }
772
773            PhoneAccountHandle phoneAccountHandle = null;
774            if (mConferenceHost.getPhone() != null &&
775                    mConferenceHost.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {
776                Phone imsPhone = mConferenceHost.getPhone();
777                // The phone account handle for an ImsPhone is based on the default phone (ie the
778                // base GSM or CDMA phone, not on the ImsPhone itself).
779                phoneAccountHandle =
780                        PhoneUtils.makePstnPhoneAccountHandle(imsPhone.getDefaultPhone());
781            }
782
783            if (mConferenceHost.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) {
784                GsmConnection c = new GsmConnection(originalConnection, getTelecomCallId());
785                c.updateState();
786                // Copy the connect time from the conferenceHost
787                c.setConnectTimeMillis(mConferenceHost.getConnectTimeMillis());
788                mTelephonyConnectionService.addExistingConnection(phoneAccountHandle, c);
789                mTelephonyConnectionService.addConnectionToConferenceController(c);
790            } // CDMA case not applicable for SRVCC
791            mConferenceHost.removeConnectionListener(mConferenceHostListener);
792            mConferenceHost.removeTelephonyConnectionListener(mTelephonyConnectionListener);
793            mConferenceHost = null;
794            setDisconnected(new DisconnectCause(DisconnectCause.OTHER));
795            disconnectConferenceParticipants();
796            destroy();
797        }
798
799        updateStatusHints();
800    }
801
802    /**
803     * Changes the state of the Ims conference.
804     *
805     * @param state the new state.
806     */
807    public void setState(int state) {
808        Log.v(this, "setState %s", Connection.stateToString(state));
809
810        switch (state) {
811            case Connection.STATE_INITIALIZING:
812            case Connection.STATE_NEW:
813            case Connection.STATE_RINGING:
814                // No-op -- not applicable.
815                break;
816            case Connection.STATE_DIALING:
817                setDialing();
818                break;
819            case Connection.STATE_DISCONNECTED:
820                DisconnectCause disconnectCause;
821                if (mConferenceHost == null) {
822                    disconnectCause = new DisconnectCause(DisconnectCause.CANCELED);
823                } else {
824                    disconnectCause = DisconnectCauseUtil.toTelecomDisconnectCause(
825                            mConferenceHost.getOriginalConnection().getDisconnectCause());
826                }
827                setDisconnected(disconnectCause);
828                disconnectConferenceParticipants();
829                destroy();
830                break;
831            case Connection.STATE_ACTIVE:
832                setActive();
833                break;
834            case Connection.STATE_HOLDING:
835                setOnHold();
836                break;
837        }
838    }
839
840    private void updateStatusHints() {
841        if (mConferenceHost == null) {
842            setStatusHints(null);
843            return;
844        }
845
846        if (mConferenceHost.isWifi()) {
847            Phone phone = mConferenceHost.getPhone();
848            if (phone != null) {
849                Context context = phone.getContext();
850                setStatusHints(new StatusHints(
851                        context.getString(R.string.status_hint_label_wifi_call),
852                        Icon.createWithResource(
853                                context.getResources(),
854                                R.drawable.ic_signal_wifi_4_bar_24dp),
855                        null /* extras */));
856            }
857        } else {
858            setStatusHints(null);
859        }
860    }
861
862    /**
863     * Builds a string representation of the {@link ImsConference}.
864     *
865     * @return String representing the conference.
866     */
867    public String toString() {
868        StringBuilder sb = new StringBuilder();
869        sb.append("[ImsConference objId:");
870        sb.append(System.identityHashCode(this));
871        sb.append(" telecomCallID:");
872        sb.append(getTelecomCallId());
873        sb.append(" state:");
874        sb.append(Connection.stateToString(getState()));
875        sb.append(" hostConnection:");
876        sb.append(mConferenceHost);
877        sb.append(" participants:");
878        sb.append(mConferenceParticipantConnections.size());
879        sb.append("]");
880        return sb.toString();
881    }
882}
883