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