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