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