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