ImsPhoneConnection.java revision 99ea18f0323a53cacf6824db5ec4dd460f9b3798
1/*
2 * Copyright (C) 2013 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.internal.telephony.imsphone;
18
19import android.content.Context;
20import android.net.Uri;
21import android.os.AsyncResult;
22import android.os.Bundle;
23import android.os.Handler;
24import android.os.Looper;
25import android.os.Message;
26import android.os.PersistableBundle;
27import android.os.PowerManager;
28import android.os.Registrant;
29import android.os.SystemClock;
30import android.telecom.VideoProfile;
31import android.telephony.CarrierConfigManager;
32import android.telephony.DisconnectCause;
33import android.telephony.PhoneNumberUtils;
34import android.telephony.Rlog;
35import android.telephony.ServiceState;
36import android.text.TextUtils;
37
38import com.android.ims.ImsException;
39import com.android.ims.ImsStreamMediaProfile;
40import com.android.ims.internal.ImsVideoCallProviderWrapper;
41import com.android.internal.telephony.CallStateException;
42import com.android.internal.telephony.Connection;
43import com.android.internal.telephony.Phone;
44import com.android.internal.telephony.PhoneConstants;
45import com.android.internal.telephony.UUSInfo;
46
47import com.android.ims.ImsCall;
48import com.android.ims.ImsCallProfile;
49
50import java.util.Objects;
51
52/**
53 * {@hide}
54 */
55public class ImsPhoneConnection extends Connection implements
56        ImsVideoCallProviderWrapper.ImsVideoProviderWrapperCallback {
57
58    private static final String LOG_TAG = "ImsPhoneConnection";
59    private static final boolean DBG = true;
60
61    //***** Instance Variables
62
63    private ImsPhoneCallTracker mOwner;
64    private ImsPhoneCall mParent;
65    private ImsCall mImsCall;
66    private Bundle mExtras = new Bundle();
67
68    private boolean mDisconnected;
69
70    /*
71    int mIndex;          // index in ImsPhoneCallTracker.connections[], -1 if unassigned
72                        // The GSM index is 1 + this
73    */
74
75    /*
76     * These time/timespan values are based on System.currentTimeMillis(),
77     * i.e., "wall clock" time.
78     */
79    private long mDisconnectTime;
80
81    private UUSInfo mUusInfo;
82    private Handler mHandler;
83
84    private PowerManager.WakeLock mPartialWakeLock;
85
86    // The cached connect time of the connection when it turns into a conference.
87    private long mConferenceConnectTime = 0;
88
89    // The cached delay to be used between DTMF tones fetched from carrier config.
90    private int mDtmfToneDelay = 0;
91
92    private boolean mIsEmergency = false;
93
94    /**
95     * Used to indicate that video state changes detected by
96     * {@link #updateMediaCapabilities(ImsCall)} should be ignored.  When a video state change from
97     * unpaused to paused occurs, we set this flag and then update the existing video state when
98     * new {@link #onReceiveSessionModifyResponse(int, VideoProfile, VideoProfile)} callbacks come
99     * in.  When the video un-pauses we continue receiving the video state updates.
100     */
101    private boolean mShouldIgnoreVideoStateChanges = false;
102
103    private ImsVideoCallProviderWrapper mImsVideoCallProviderWrapper;
104
105    private int mPreciseDisconnectCause = 0;
106
107    //***** Event Constants
108    private static final int EVENT_DTMF_DONE = 1;
109    private static final int EVENT_PAUSE_DONE = 2;
110    private static final int EVENT_NEXT_POST_DIAL = 3;
111    private static final int EVENT_WAKE_LOCK_TIMEOUT = 4;
112    private static final int EVENT_DTMF_DELAY_DONE = 5;
113
114    //***** Constants
115    private static final int PAUSE_DELAY_MILLIS = 3 * 1000;
116    private static final int WAKE_LOCK_TIMEOUT_MILLIS = 60*1000;
117
118    //***** Inner Classes
119
120    class MyHandler extends Handler {
121        MyHandler(Looper l) {super(l);}
122
123        @Override
124        public void
125        handleMessage(Message msg) {
126
127            switch (msg.what) {
128                case EVENT_NEXT_POST_DIAL:
129                case EVENT_DTMF_DELAY_DONE:
130                case EVENT_PAUSE_DONE:
131                    processNextPostDialChar();
132                    break;
133                case EVENT_WAKE_LOCK_TIMEOUT:
134                    releaseWakeLock();
135                    break;
136                case EVENT_DTMF_DONE:
137                    // We may need to add a delay specified by carrier between DTMF tones that are
138                    // sent out.
139                    mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_DTMF_DELAY_DONE),
140                            mDtmfToneDelay);
141                    break;
142            }
143        }
144    }
145
146    //***** Constructors
147
148    /** This is probably an MT call */
149    public ImsPhoneConnection(Phone phone, ImsCall imsCall, ImsPhoneCallTracker ct,
150           ImsPhoneCall parent, boolean isUnknown) {
151        super(PhoneConstants.PHONE_TYPE_IMS);
152        createWakeLock(phone.getContext());
153        acquireWakeLock();
154
155        mOwner = ct;
156        mHandler = new MyHandler(mOwner.getLooper());
157        mImsCall = imsCall;
158
159        if ((imsCall != null) && (imsCall.getCallProfile() != null)) {
160            mAddress = imsCall.getCallProfile().getCallExtra(ImsCallProfile.EXTRA_OI);
161            mCnapName = imsCall.getCallProfile().getCallExtra(ImsCallProfile.EXTRA_CNA);
162            mNumberPresentation = ImsCallProfile.OIRToPresentation(
163                    imsCall.getCallProfile().getCallExtraInt(ImsCallProfile.EXTRA_OIR));
164            mCnapNamePresentation = ImsCallProfile.OIRToPresentation(
165                    imsCall.getCallProfile().getCallExtraInt(ImsCallProfile.EXTRA_CNAP));
166            updateMediaCapabilities(imsCall);
167        } else {
168            mNumberPresentation = PhoneConstants.PRESENTATION_UNKNOWN;
169            mCnapNamePresentation = PhoneConstants.PRESENTATION_UNKNOWN;
170        }
171
172        mIsIncoming = !isUnknown;
173        mCreateTime = System.currentTimeMillis();
174        mUusInfo = null;
175
176        // Ensure any extras set on the ImsCallProfile at the start of the call are cached locally
177        // in the ImsPhoneConnection.  This isn't going to inform any listeners (since the original
178        // connection is not likely to be associated with a TelephonyConnection yet).
179        updateExtras(imsCall);
180
181        mParent = parent;
182        mParent.attach(this,
183                (mIsIncoming? ImsPhoneCall.State.INCOMING: ImsPhoneCall.State.DIALING));
184
185        fetchDtmfToneDelay(phone);
186
187        if (phone.getContext().getResources().getBoolean(
188                com.android.internal.R.bool.config_use_voip_mode_for_ims)) {
189            setAudioModeIsVoip(true);
190        }
191    }
192
193    /** This is an MO call, created when dialing */
194    public ImsPhoneConnection(Phone phone, String dialString, ImsPhoneCallTracker ct,
195            ImsPhoneCall parent, boolean isEmergency) {
196        super(PhoneConstants.PHONE_TYPE_IMS);
197        createWakeLock(phone.getContext());
198        acquireWakeLock();
199
200        mOwner = ct;
201        mHandler = new MyHandler(mOwner.getLooper());
202
203        mDialString = dialString;
204
205        mAddress = PhoneNumberUtils.extractNetworkPortionAlt(dialString);
206        mPostDialString = PhoneNumberUtils.extractPostDialPortion(dialString);
207
208        //mIndex = -1;
209
210        mIsIncoming = false;
211        mCnapName = null;
212        mCnapNamePresentation = PhoneConstants.PRESENTATION_ALLOWED;
213        mNumberPresentation = PhoneConstants.PRESENTATION_ALLOWED;
214        mCreateTime = System.currentTimeMillis();
215
216        mParent = parent;
217        parent.attachFake(this, ImsPhoneCall.State.DIALING);
218
219        mIsEmergency = isEmergency;
220
221        fetchDtmfToneDelay(phone);
222
223        if (phone.getContext().getResources().getBoolean(
224                com.android.internal.R.bool.config_use_voip_mode_for_ims)) {
225            setAudioModeIsVoip(true);
226        }
227    }
228
229    public void dispose() {
230    }
231
232    static boolean
233    equalsHandlesNulls (Object a, Object b) {
234        return (a == null) ? (b == null) : a.equals (b);
235    }
236
237    private static int applyLocalCallCapabilities(ImsCallProfile localProfile, int capabilities) {
238        Rlog.w(LOG_TAG, "applyLocalCallCapabilities - localProfile = "+localProfile);
239        capabilities = removeCapability(capabilities,
240                Connection.Capability.SUPPORTS_VT_LOCAL_BIDIRECTIONAL);
241
242        switch (localProfile.mCallType) {
243            case ImsCallProfile.CALL_TYPE_VT:
244                // Fall-through
245            case ImsCallProfile.CALL_TYPE_VIDEO_N_VOICE:
246                capabilities = addCapability(capabilities,
247                        Connection.Capability.SUPPORTS_VT_LOCAL_BIDIRECTIONAL);
248                break;
249        }
250        return capabilities;
251    }
252
253    private static int applyRemoteCallCapabilities(ImsCallProfile remoteProfile, int capabilities) {
254        Rlog.w(LOG_TAG, "applyRemoteCallCapabilities - remoteProfile = "+remoteProfile);
255        capabilities = removeCapability(capabilities,
256                Connection.Capability.SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
257
258        switch (remoteProfile.mCallType) {
259            case ImsCallProfile.CALL_TYPE_VT:
260                // fall-through
261            case ImsCallProfile.CALL_TYPE_VIDEO_N_VOICE:
262                capabilities = addCapability(capabilities,
263                        Connection.Capability.SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
264                break;
265        }
266        return capabilities;
267    }
268
269    @Override
270    public String getOrigDialString(){
271        return mDialString;
272    }
273
274    @Override
275    public ImsPhoneCall getCall() {
276        return mParent;
277    }
278
279    @Override
280    public long getDisconnectTime() {
281        return mDisconnectTime;
282    }
283
284    @Override
285    public long getHoldingStartTime() {
286        return mHoldingStartTime;
287    }
288
289    @Override
290    public long getHoldDurationMillis() {
291        if (getState() != ImsPhoneCall.State.HOLDING) {
292            // If not holding, return 0
293            return 0;
294        } else {
295            return SystemClock.elapsedRealtime() - mHoldingStartTime;
296        }
297    }
298
299    public void setDisconnectCause(int cause) {
300        mCause = cause;
301    }
302
303    @Override
304    public String getVendorDisconnectCause() {
305      return null;
306    }
307
308    public ImsPhoneCallTracker getOwner () {
309        return mOwner;
310    }
311
312    @Override
313    public ImsPhoneCall.State getState() {
314        if (mDisconnected) {
315            return ImsPhoneCall.State.DISCONNECTED;
316        } else {
317            return super.getState();
318        }
319    }
320
321    @Override
322    public void hangup() throws CallStateException {
323        if (!mDisconnected) {
324            mOwner.hangup(this);
325        } else {
326            throw new CallStateException ("disconnected");
327        }
328    }
329
330    @Override
331    public void separate() throws CallStateException {
332        throw new CallStateException ("not supported");
333    }
334
335    @Override
336    public void proceedAfterWaitChar() {
337        if (mPostDialState != PostDialState.WAIT) {
338            Rlog.w(LOG_TAG, "ImsPhoneConnection.proceedAfterWaitChar(): Expected "
339                    + "getPostDialState() to be WAIT but was " + mPostDialState);
340            return;
341        }
342
343        setPostDialState(PostDialState.STARTED);
344
345        processNextPostDialChar();
346    }
347
348    @Override
349    public void proceedAfterWildChar(String str) {
350        if (mPostDialState != PostDialState.WILD) {
351            Rlog.w(LOG_TAG, "ImsPhoneConnection.proceedAfterWaitChar(): Expected "
352                    + "getPostDialState() to be WILD but was " + mPostDialState);
353            return;
354        }
355
356        setPostDialState(PostDialState.STARTED);
357
358        // make a new postDialString, with the wild char replacement string
359        // at the beginning, followed by the remaining postDialString.
360
361        StringBuilder buf = new StringBuilder(str);
362        buf.append(mPostDialString.substring(mNextPostDialChar));
363        mPostDialString = buf.toString();
364        mNextPostDialChar = 0;
365        if (Phone.DEBUG_PHONE) {
366            Rlog.d(LOG_TAG, "proceedAfterWildChar: new postDialString is " +
367                    mPostDialString);
368        }
369
370        processNextPostDialChar();
371    }
372
373    @Override
374    public void cancelPostDial() {
375        setPostDialState(PostDialState.CANCELLED);
376    }
377
378    /**
379     * Called when this Connection is being hung up locally (eg, user pressed "end")
380     */
381    void
382    onHangupLocal() {
383        mCause = DisconnectCause.LOCAL;
384    }
385
386    /** Called when the connection has been disconnected */
387    @Override
388    public boolean onDisconnect(int cause) {
389        Rlog.d(LOG_TAG, "onDisconnect: cause=" + cause);
390        if (mCause != DisconnectCause.LOCAL || cause == DisconnectCause.INCOMING_REJECTED) {
391            mCause = cause;
392        }
393        return onDisconnect();
394    }
395
396    public boolean onDisconnect() {
397        boolean changed = false;
398
399        if (!mDisconnected) {
400            //mIndex = -1;
401
402            mDisconnectTime = System.currentTimeMillis();
403            mDuration = SystemClock.elapsedRealtime() - mConnectTimeReal;
404            mDisconnected = true;
405
406            mOwner.mPhone.notifyDisconnect(this);
407
408            if (mParent != null) {
409                changed = mParent.connectionDisconnected(this);
410            } else {
411                Rlog.d(LOG_TAG, "onDisconnect: no parent");
412            }
413            synchronized (this) {
414                if (mImsCall != null) mImsCall.close();
415                mImsCall = null;
416            }
417        }
418        releaseWakeLock();
419        return changed;
420    }
421
422    /**
423     * An incoming or outgoing call has connected
424     */
425    void
426    onConnectedInOrOut() {
427        mConnectTime = System.currentTimeMillis();
428        mConnectTimeReal = SystemClock.elapsedRealtime();
429        mDuration = 0;
430
431        if (Phone.DEBUG_PHONE) {
432            Rlog.d(LOG_TAG, "onConnectedInOrOut: connectTime=" + mConnectTime);
433        }
434
435        if (!mIsIncoming) {
436            // outgoing calls only
437            processNextPostDialChar();
438        }
439        releaseWakeLock();
440    }
441
442    /*package*/ void
443    onStartedHolding() {
444        mHoldingStartTime = SystemClock.elapsedRealtime();
445    }
446    /**
447     * Performs the appropriate action for a post-dial char, but does not
448     * notify application. returns false if the character is invalid and
449     * should be ignored
450     */
451    private boolean
452    processPostDialChar(char c) {
453        if (PhoneNumberUtils.is12Key(c)) {
454            mOwner.sendDtmf(c, mHandler.obtainMessage(EVENT_DTMF_DONE));
455        } else if (c == PhoneNumberUtils.PAUSE) {
456            // From TS 22.101:
457            // It continues...
458            // Upon the called party answering the UE shall send the DTMF digits
459            // automatically to the network after a delay of 3 seconds( 20 ).
460            // The digits shall be sent according to the procedures and timing
461            // specified in 3GPP TS 24.008 [13]. The first occurrence of the
462            // "DTMF Control Digits Separator" shall be used by the ME to
463            // distinguish between the addressing digits (i.e. the phone number)
464            // and the DTMF digits. Upon subsequent occurrences of the
465            // separator,
466            // the UE shall pause again for 3 seconds ( 20 ) before sending
467            // any further DTMF digits.
468            mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_PAUSE_DONE),
469                    PAUSE_DELAY_MILLIS);
470        } else if (c == PhoneNumberUtils.WAIT) {
471            setPostDialState(PostDialState.WAIT);
472        } else if (c == PhoneNumberUtils.WILD) {
473            setPostDialState(PostDialState.WILD);
474        } else {
475            return false;
476        }
477
478        return true;
479    }
480
481    @Override
482    protected void finalize() {
483        releaseWakeLock();
484    }
485
486    private void
487    processNextPostDialChar() {
488        char c = 0;
489        Registrant postDialHandler;
490
491        if (mPostDialState == PostDialState.CANCELLED) {
492            //Rlog.d(LOG_TAG, "##### processNextPostDialChar: postDialState == CANCELLED, bail");
493            return;
494        }
495
496        if (mPostDialString == null || mPostDialString.length() <= mNextPostDialChar) {
497            setPostDialState(PostDialState.COMPLETE);
498
499            // notifyMessage.arg1 is 0 on complete
500            c = 0;
501        } else {
502            boolean isValid;
503
504            setPostDialState(PostDialState.STARTED);
505
506            c = mPostDialString.charAt(mNextPostDialChar++);
507
508            isValid = processPostDialChar(c);
509
510            if (!isValid) {
511                // Will call processNextPostDialChar
512                mHandler.obtainMessage(EVENT_NEXT_POST_DIAL).sendToTarget();
513                // Don't notify application
514                Rlog.e(LOG_TAG, "processNextPostDialChar: c=" + c + " isn't valid!");
515                return;
516            }
517        }
518
519        notifyPostDialListenersNextChar(c);
520
521        // TODO: remove the following code since the handler no longer executes anything.
522        postDialHandler = mOwner.mPhone.getPostDialHandler();
523
524        Message notifyMessage;
525
526        if (postDialHandler != null
527                && (notifyMessage = postDialHandler.messageForRegistrant()) != null) {
528            // The AsyncResult.result is the Connection object
529            PostDialState state = mPostDialState;
530            AsyncResult ar = AsyncResult.forMessage(notifyMessage);
531            ar.result = this;
532            ar.userObj = state;
533
534            // arg1 is the character that was/is being processed
535            notifyMessage.arg1 = c;
536
537            //Rlog.v(LOG_TAG,
538            //      "##### processNextPostDialChar: send msg to postDialHandler, arg1=" + c);
539            notifyMessage.sendToTarget();
540        }
541    }
542
543    /**
544     * Set post dial state and acquire wake lock while switching to "started"
545     * state, the wake lock will be released if state switches out of "started"
546     * state or after WAKE_LOCK_TIMEOUT_MILLIS.
547     * @param s new PostDialState
548     */
549    private void setPostDialState(PostDialState s) {
550        if (mPostDialState != PostDialState.STARTED
551                && s == PostDialState.STARTED) {
552            acquireWakeLock();
553            Message msg = mHandler.obtainMessage(EVENT_WAKE_LOCK_TIMEOUT);
554            mHandler.sendMessageDelayed(msg, WAKE_LOCK_TIMEOUT_MILLIS);
555        } else if (mPostDialState == PostDialState.STARTED
556                && s != PostDialState.STARTED) {
557            mHandler.removeMessages(EVENT_WAKE_LOCK_TIMEOUT);
558            releaseWakeLock();
559        }
560        mPostDialState = s;
561        notifyPostDialListeners();
562    }
563
564    private void
565    createWakeLock(Context context) {
566        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
567        mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG);
568    }
569
570    private void
571    acquireWakeLock() {
572        Rlog.d(LOG_TAG, "acquireWakeLock");
573        mPartialWakeLock.acquire();
574    }
575
576    void
577    releaseWakeLock() {
578        if (mPartialWakeLock != null) {
579            synchronized (mPartialWakeLock) {
580                if (mPartialWakeLock.isHeld()) {
581                    Rlog.d(LOG_TAG, "releaseWakeLock");
582                    mPartialWakeLock.release();
583                }
584            }
585        }
586    }
587
588    private void fetchDtmfToneDelay(Phone phone) {
589        CarrierConfigManager configMgr = (CarrierConfigManager)
590                phone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
591        PersistableBundle b = configMgr.getConfigForSubId(phone.getSubId());
592        if (b != null) {
593            mDtmfToneDelay = b.getInt(CarrierConfigManager.KEY_IMS_DTMF_TONE_DELAY_INT);
594        }
595    }
596
597    @Override
598    public int getNumberPresentation() {
599        return mNumberPresentation;
600    }
601
602    @Override
603    public UUSInfo getUUSInfo() {
604        return mUusInfo;
605    }
606
607    @Override
608    public Connection getOrigConnection() {
609        return null;
610    }
611
612    @Override
613    public synchronized boolean isMultiparty() {
614        return mImsCall != null && mImsCall.isMultiparty();
615    }
616
617    /**
618     * Where {@link #isMultiparty()} is {@code true}, determines if this {@link ImsCall} is the
619     * origin of the conference call (i.e. {@code #isConferenceHost()} is {@code true}), or if this
620     * {@link ImsCall} is a member of a conference hosted on another device.
621     *
622     * @return {@code true} if this call is the origin of the conference call it is a member of,
623     *      {@code false} otherwise.
624     */
625    @Override
626    public synchronized boolean isConferenceHost() {
627        return mImsCall != null && mImsCall.isConferenceHost();
628    }
629
630    @Override
631    public boolean isMemberOfPeerConference() {
632        return !isConferenceHost();
633    }
634
635    public synchronized ImsCall getImsCall() {
636        return mImsCall;
637    }
638
639    public synchronized void setImsCall(ImsCall imsCall) {
640        mImsCall = imsCall;
641    }
642
643    public void changeParent(ImsPhoneCall parent) {
644        mParent = parent;
645    }
646
647    /**
648     * @return {@code true} if the {@link ImsPhoneConnection} or its media capabilities have been
649     *     changed, and {@code false} otherwise.
650     */
651    public boolean update(ImsCall imsCall, ImsPhoneCall.State state) {
652        if (state == ImsPhoneCall.State.ACTIVE) {
653            // If the state of the call is active, but there is a pending request to the RIL to hold
654            // the call, we will skip this update.  This is really a signalling delay or failure
655            // from the RIL, but we will prevent it from going through as we will end up erroneously
656            // making this call active when really it should be on hold.
657            if (imsCall.isPendingHold()) {
658                Rlog.w(LOG_TAG, "update : state is ACTIVE, but call is pending hold, skipping");
659                return false;
660            }
661
662            if (mParent.getState().isRinging() || mParent.getState().isDialing()) {
663                onConnectedInOrOut();
664            }
665
666            if (mParent.getState().isRinging() || mParent == mOwner.mBackgroundCall) {
667                //mForegroundCall should be IDLE
668                //when accepting WAITING call
669                //before accept WAITING call,
670                //the ACTIVE call should be held ahead
671                mParent.detach(this);
672                mParent = mOwner.mForegroundCall;
673                mParent.attach(this);
674            }
675        } else if (state == ImsPhoneCall.State.HOLDING) {
676            onStartedHolding();
677        }
678
679        boolean updateParent = mParent.update(this, imsCall, state);
680        boolean updateAddressDisplay = updateAddressDisplay(imsCall);
681        boolean updateMediaCapabilities = updateMediaCapabilities(imsCall);
682        boolean updateExtras = updateExtras(imsCall);
683
684        return updateParent || updateAddressDisplay || updateMediaCapabilities || updateExtras;
685    }
686
687    @Override
688    public int getPreciseDisconnectCause() {
689        return mPreciseDisconnectCause;
690    }
691
692    public void setPreciseDisconnectCause(int cause) {
693        mPreciseDisconnectCause = cause;
694    }
695
696    /**
697     * Notifies this Connection of a request to disconnect a participant of the conference managed
698     * by the connection.
699     *
700     * @param endpoint the {@link android.net.Uri} of the participant to disconnect.
701     */
702    @Override
703    public void onDisconnectConferenceParticipant(Uri endpoint) {
704        ImsCall imsCall = getImsCall();
705        if (imsCall == null) {
706            return;
707        }
708        try {
709            imsCall.removeParticipants(new String[]{endpoint.toString()});
710        } catch (ImsException e) {
711            // No session in place -- no change
712            Rlog.e(LOG_TAG, "onDisconnectConferenceParticipant: no session in place. "+
713                    "Failed to disconnect endpoint = " + endpoint);
714        }
715    }
716
717    /**
718     * Sets the conference connect time.  Used when an {@code ImsConference} is created to out of
719     * this phone connection.
720     *
721     * @param conferenceConnectTime The conference connect time.
722     */
723    public void setConferenceConnectTime(long conferenceConnectTime) {
724        mConferenceConnectTime = conferenceConnectTime;
725    }
726
727    /**
728     * @return The conference connect time.
729     */
730    public long getConferenceConnectTime() {
731        return mConferenceConnectTime;
732    }
733
734    /**
735     * Check for a change in the address display related fields for the {@link ImsCall}, and
736     * update the {@link ImsPhoneConnection} with this information.
737     *
738     * @param imsCall The call to check for changes in address display fields.
739     * @return Whether the address display fields have been changed.
740     */
741    public boolean updateAddressDisplay(ImsCall imsCall) {
742        if (imsCall == null) {
743            return false;
744        }
745
746        boolean changed = false;
747        ImsCallProfile callProfile = imsCall.getCallProfile();
748        if (callProfile != null) {
749            String address = callProfile.getCallExtra(ImsCallProfile.EXTRA_OI);
750            String name = callProfile.getCallExtra(ImsCallProfile.EXTRA_CNA);
751            int nump = ImsCallProfile.OIRToPresentation(
752                    callProfile.getCallExtraInt(ImsCallProfile.EXTRA_OIR));
753            int namep = ImsCallProfile.OIRToPresentation(
754                    callProfile.getCallExtraInt(ImsCallProfile.EXTRA_CNAP));
755            if (Phone.DEBUG_PHONE) {
756                Rlog.d(LOG_TAG, "address = " + Rlog.pii(LOG_TAG, address) + " name = " + name +
757                        " nump = " + nump + " namep = " + namep);
758            }
759            if(equalsHandlesNulls(mAddress, address)) {
760                mAddress = address;
761                changed = true;
762            }
763            if (TextUtils.isEmpty(name)) {
764                if (!TextUtils.isEmpty(mCnapName)) {
765                    mCnapName = "";
766                    changed = true;
767                }
768            } else if (!name.equals(mCnapName)) {
769                mCnapName = name;
770                changed = true;
771            }
772            if (mNumberPresentation != nump) {
773                mNumberPresentation = nump;
774                changed = true;
775            }
776            if (mCnapNamePresentation != namep) {
777                mCnapNamePresentation = namep;
778                changed = true;
779            }
780        }
781        return changed;
782    }
783
784    /**
785     * Check for a change in the video capabilities and audio quality for the {@link ImsCall}, and
786     * update the {@link ImsPhoneConnection} with this information.
787     *
788     * @param imsCall The call to check for changes in media capabilities.
789     * @return Whether the media capabilities have been changed.
790     */
791    public boolean updateMediaCapabilities(ImsCall imsCall) {
792        if (imsCall == null) {
793            return false;
794        }
795
796        boolean changed = false;
797
798        try {
799            // The actual call profile (negotiated between local and peer).
800            ImsCallProfile negotiatedCallProfile = imsCall.getCallProfile();
801
802            if (negotiatedCallProfile != null) {
803                int oldVideoState = getVideoState();
804                int newVideoState = ImsCallProfile
805                        .getVideoStateFromImsCallProfile(negotiatedCallProfile);
806
807                if (oldVideoState != newVideoState) {
808                    // The video state has changed.  See also code in onReceiveSessionModifyResponse
809                    // below.  When the video enters a paused state, subsequent changes to the video
810                    // state will not be reported by the modem.  In onReceiveSessionModifyResponse
811                    // we will be updating the current video state while paused to include any
812                    // changes the modem reports via the video provider.  When the video enters an
813                    // unpaused state, we will resume passing the video states from the modem as is.
814                    if (VideoProfile.isPaused(oldVideoState) &&
815                            !VideoProfile.isPaused(newVideoState)) {
816                        // Video entered un-paused state; recognize updates from now on; we want to
817                        // ensure that the new un-paused state is propagated to Telecom, so change
818                        // this now.
819                        mShouldIgnoreVideoStateChanges = false;
820                    }
821
822                    if (!mShouldIgnoreVideoStateChanges) {
823                        if (mImsVideoCallProviderWrapper != null) {
824                            mImsVideoCallProviderWrapper.onVideoStateChanged(newVideoState);
825                        }
826                        setVideoState(newVideoState);
827                        changed = true;
828                    } else {
829                        Rlog.d(LOG_TAG, "updateMediaCapabilities - ignoring video state change " +
830                                "due to paused state.");
831                    }
832
833                    if (!VideoProfile.isPaused(oldVideoState) &&
834                            VideoProfile.isPaused(newVideoState)) {
835                        // Video entered pause state; ignore updates until un-paused.  We do this
836                        // after setVideoState is called above to ensure Telecom is notified that
837                        // the device has entered paused state.
838                        mShouldIgnoreVideoStateChanges = true;
839                    }
840                }
841            }
842
843            // Check for a change in the capabilities for the call and update
844            // {@link ImsPhoneConnection} with this information.
845            int capabilities = getConnectionCapabilities();
846
847            // Use carrier config to determine if downgrading directly to audio-only is supported.
848            if (mOwner.isCarrierDowngradeOfVtCallSupported()) {
849                capabilities = addCapability(capabilities,
850                        Connection.Capability.SUPPORTS_DOWNGRADE_TO_VOICE_REMOTE |
851                                Capability.SUPPORTS_DOWNGRADE_TO_VOICE_LOCAL);
852            } else {
853                capabilities = removeCapability(capabilities,
854                        Connection.Capability.SUPPORTS_DOWNGRADE_TO_VOICE_REMOTE |
855                                Capability.SUPPORTS_DOWNGRADE_TO_VOICE_LOCAL);
856            }
857
858            // Get the current local call capabilities which might be voice or video or both.
859            ImsCallProfile localCallProfile = imsCall.getLocalCallProfile();
860            Rlog.v(LOG_TAG, "update localCallProfile=" + localCallProfile);
861            if (localCallProfile != null) {
862                capabilities = applyLocalCallCapabilities(localCallProfile, capabilities);
863            }
864
865            // Get the current remote call capabilities which might be voice or video or both.
866            ImsCallProfile remoteCallProfile = imsCall.getRemoteCallProfile();
867            Rlog.v(LOG_TAG, "update remoteCallProfile=" + remoteCallProfile);
868            if (remoteCallProfile != null) {
869                capabilities = applyRemoteCallCapabilities(remoteCallProfile, capabilities);
870            }
871            if (getConnectionCapabilities() != capabilities) {
872                setConnectionCapabilities(capabilities);
873                changed = true;
874            }
875
876            int newAudioQuality =
877                    getAudioQualityFromCallProfile(localCallProfile, remoteCallProfile);
878            if (getAudioQuality() != newAudioQuality) {
879                setAudioQuality(newAudioQuality);
880                changed = true;
881            }
882        } catch (ImsException e) {
883            // No session in place -- no change
884        }
885
886        return changed;
887    }
888
889    /**
890     * Updates the wifi state based on the {@link ImsCallProfile#EXTRA_CALL_RAT_TYPE}.
891     * The call is considered to be a WIFI call if the extra value is
892     * {@link ServiceState#RIL_RADIO_TECHNOLOGY_IWLAN}.
893     *
894     * @param extras The ImsCallProfile extras.
895     */
896    private void updateWifiStateFromExtras(Bundle extras) {
897        if (extras.containsKey(ImsCallProfile.EXTRA_CALL_RAT_TYPE) ||
898                extras.containsKey(ImsCallProfile.EXTRA_CALL_RAT_TYPE_ALT)) {
899
900            ImsCall call = getImsCall();
901            boolean isWifi = false;
902            if (call != null) {
903                isWifi = call.isWifiCall();
904            }
905
906            // Report any changes
907            if (isWifi() != isWifi) {
908                setWifi(isWifi);
909            }
910        }
911    }
912
913    /**
914     * Check for a change in call extras of {@link ImsCall}, and
915     * update the {@link ImsPhoneConnection} accordingly.
916     *
917     * @param imsCall The call to check for changes in extras.
918     * @return Whether the extras fields have been changed.
919     */
920     boolean updateExtras(ImsCall imsCall) {
921        if (imsCall == null) {
922            return false;
923        }
924
925        final ImsCallProfile callProfile = imsCall.getCallProfile();
926        final Bundle extras = callProfile != null ? callProfile.mCallExtras : null;
927        if (extras == null && DBG) {
928            Rlog.d(LOG_TAG, "Call profile extras are null.");
929        }
930
931        final boolean changed = !areBundlesEqual(extras, mExtras);
932        if (changed) {
933            updateWifiStateFromExtras(extras);
934
935            mExtras.clear();
936            mExtras.putAll(extras);
937            setConnectionExtras(mExtras);
938        }
939        return changed;
940    }
941
942    private static boolean areBundlesEqual(Bundle extras, Bundle newExtras) {
943        if (extras == null || newExtras == null) {
944            return extras == newExtras;
945        }
946
947        if (extras.size() != newExtras.size()) {
948            return false;
949        }
950
951        for(String key : extras.keySet()) {
952            if (key != null) {
953                final Object value = extras.get(key);
954                final Object newValue = newExtras.get(key);
955                if (!Objects.equals(value, newValue)) {
956                    return false;
957                }
958            }
959        }
960        return true;
961    }
962
963    /**
964     * Determines the {@link ImsPhoneConnection} audio quality based on the local and remote
965     * {@link ImsCallProfile}. Indicate a HD audio call if the local stream profile
966     * is AMR_WB, EVRC_WB, EVS_WB, EVS_SWB, EVS_FB and
967     * there is no remote restrict cause.
968     *
969     * @param localCallProfile The local call profile.
970     * @param remoteCallProfile The remote call profile.
971     * @return The audio quality.
972     */
973    private int getAudioQualityFromCallProfile(
974            ImsCallProfile localCallProfile, ImsCallProfile remoteCallProfile) {
975        if (localCallProfile == null || remoteCallProfile == null
976                || localCallProfile.mMediaProfile == null) {
977            return AUDIO_QUALITY_STANDARD;
978        }
979
980        final boolean isEvsCodecHighDef = (localCallProfile.mMediaProfile.mAudioQuality
981                        == ImsStreamMediaProfile.AUDIO_QUALITY_EVS_WB
982                || localCallProfile.mMediaProfile.mAudioQuality
983                        == ImsStreamMediaProfile.AUDIO_QUALITY_EVS_SWB
984                || localCallProfile.mMediaProfile.mAudioQuality
985                        == ImsStreamMediaProfile.AUDIO_QUALITY_EVS_FB);
986
987        final boolean isHighDef = (localCallProfile.mMediaProfile.mAudioQuality
988                        == ImsStreamMediaProfile.AUDIO_QUALITY_AMR_WB
989                || localCallProfile.mMediaProfile.mAudioQuality
990                        == ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_WB
991                || isEvsCodecHighDef)
992                && remoteCallProfile.mRestrictCause == ImsCallProfile.CALL_RESTRICT_CAUSE_NONE;
993        return isHighDef ? AUDIO_QUALITY_HIGH_DEFINITION : AUDIO_QUALITY_STANDARD;
994    }
995
996    /**
997     * Provides a string representation of the {@link ImsPhoneConnection}.  Primarily intended for
998     * use in log statements.
999     *
1000     * @return String representation of call.
1001     */
1002    @Override
1003    public String toString() {
1004        StringBuilder sb = new StringBuilder();
1005        sb.append("[ImsPhoneConnection objId: ");
1006        sb.append(System.identityHashCode(this));
1007        sb.append(" telecomCallID: ");
1008        sb.append(getTelecomCallId());
1009        sb.append(" address: ");
1010        sb.append(Rlog.pii(LOG_TAG, getAddress()));
1011        sb.append(" ImsCall: ");
1012        synchronized (this) {
1013            if (mImsCall == null) {
1014                sb.append("null");
1015            } else {
1016                sb.append(mImsCall);
1017            }
1018        }
1019        sb.append("]");
1020        return sb.toString();
1021    }
1022
1023    @Override
1024    public void setVideoProvider(android.telecom.Connection.VideoProvider videoProvider) {
1025        super.setVideoProvider(videoProvider);
1026
1027        if (videoProvider instanceof ImsVideoCallProviderWrapper) {
1028            mImsVideoCallProviderWrapper = (ImsVideoCallProviderWrapper) videoProvider;
1029        }
1030    }
1031
1032    /**
1033     * Indicates whether current phone connection is emergency or not
1034     * @return boolean: true if emergency, false otherwise
1035     */
1036    protected boolean isEmergency() {
1037        return mIsEmergency;
1038    }
1039
1040    /**
1041     * Handles notifications from the {@link ImsVideoCallProviderWrapper} of session modification
1042     * responses received.
1043     *
1044     * @param status The status of the original request.
1045     * @param requestProfile The requested video profile.
1046     * @param responseProfile The response upon video profile.
1047     */
1048    @Override
1049    public void onReceiveSessionModifyResponse(int status, VideoProfile requestProfile,
1050            VideoProfile responseProfile) {
1051        if (status == android.telecom.Connection.VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS &&
1052                mShouldIgnoreVideoStateChanges) {
1053            int currentVideoState = getVideoState();
1054            int newVideoState = responseProfile.getVideoState();
1055
1056            // If the current video state is paused, the modem will not send us any changes to
1057            // the TX and RX bits of the video state.  Until the video is un-paused we will
1058            // "fake out" the video state by applying the changes that the modem reports via a
1059            // response.
1060
1061            // First, find out whether there was a change to the TX or RX bits:
1062            int changedBits = currentVideoState ^ newVideoState;
1063            changedBits &= VideoProfile.STATE_BIDIRECTIONAL;
1064            if (changedBits == 0) {
1065                // No applicable change, bail out.
1066                return;
1067            }
1068
1069            // Turn off any existing bits that changed.
1070            currentVideoState &= ~(changedBits & currentVideoState);
1071            // Turn on any new bits that turned on.
1072            currentVideoState |= changedBits & newVideoState;
1073
1074            Rlog.d(LOG_TAG, "onReceiveSessionModifyResponse : received " +
1075                    VideoProfile.videoStateToString(requestProfile.getVideoState()) +
1076                    " / " +
1077                    VideoProfile.videoStateToString(responseProfile.getVideoState()) +
1078                    " while paused ; sending new videoState = " +
1079                    VideoProfile.videoStateToString(currentVideoState));
1080            setVideoState(currentVideoState);
1081        }
1082    }
1083
1084    /**
1085     * Issues a request to pause the video using {@link VideoProfile#STATE_PAUSED} from a source
1086     * other than the InCall UI.
1087     *
1088     * @param source The source of the pause request.
1089     */
1090    public void pauseVideo(int source) {
1091        if (mImsVideoCallProviderWrapper == null) {
1092            return;
1093        }
1094
1095        mImsVideoCallProviderWrapper.pauseVideo(getVideoState(), source);
1096    }
1097
1098    /**
1099     * Issues a request to resume the video using {@link VideoProfile#STATE_PAUSED} from a source
1100     * other than the InCall UI.
1101     *
1102     * @param source The source of the resume request.
1103     */
1104    public void resumeVideo(int source) {
1105        if (mImsVideoCallProviderWrapper == null) {
1106            return;
1107        }
1108
1109        mImsVideoCallProviderWrapper.resumeVideo(getVideoState(), source);
1110    }
1111
1112    /**
1113     * Determines if a specified source has issued a pause request.
1114     *
1115     * @param source The source.
1116     * @return {@code true} if the source issued a pause request, {@code false} otherwise.
1117     */
1118    public boolean wasVideoPausedFromSource(int source) {
1119        if (mImsVideoCallProviderWrapper == null) {
1120            return false;
1121        }
1122
1123        return mImsVideoCallProviderWrapper.wasVideoPausedFromSource(source);
1124    }
1125}
1126