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