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