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