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