/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.telephony.imsphone; import android.content.Context; import android.net.Uri; import android.os.AsyncResult; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Messenger; import android.os.PersistableBundle; import android.os.PowerManager; import android.os.Registrant; import android.os.SystemClock; import android.telecom.VideoProfile; import android.telephony.CarrierConfigManager; import android.telephony.DisconnectCause; import android.telephony.PhoneNumberUtils; import android.telephony.Rlog; import android.telephony.ServiceState; import android.telephony.ims.ImsCallProfile; import android.telephony.ims.ImsStreamMediaProfile; import android.text.TextUtils; import com.android.ims.ImsCall; import com.android.ims.ImsException; import com.android.ims.internal.ImsVideoCallProviderWrapper; import com.android.internal.telephony.CallStateException; import com.android.internal.telephony.Connection; import com.android.internal.telephony.Phone; import com.android.internal.telephony.PhoneConstants; import com.android.internal.telephony.UUSInfo; import java.util.Objects; /** * {@hide} */ public class ImsPhoneConnection extends Connection implements ImsVideoCallProviderWrapper.ImsVideoProviderWrapperCallback { private static final String LOG_TAG = "ImsPhoneConnection"; private static final boolean DBG = true; //***** Instance Variables private ImsPhoneCallTracker mOwner; private ImsPhoneCall mParent; private ImsCall mImsCall; private Bundle mExtras = new Bundle(); private boolean mDisconnected; /* int mIndex; // index in ImsPhoneCallTracker.connections[], -1 if unassigned // The GSM index is 1 + this */ /* * These time/timespan values are based on System.currentTimeMillis(), * i.e., "wall clock" time. */ private long mDisconnectTime; private UUSInfo mUusInfo; private Handler mHandler; private Messenger mHandlerMessenger; private PowerManager.WakeLock mPartialWakeLock; // The cached connect time of the connection when it turns into a conference. private long mConferenceConnectTime = 0; // The cached delay to be used between DTMF tones fetched from carrier config. private int mDtmfToneDelay = 0; private boolean mIsEmergency = false; /** * Used to indicate that video state changes detected by * {@link #updateMediaCapabilities(ImsCall)} should be ignored. When a video state change from * unpaused to paused occurs, we set this flag and then update the existing video state when * new {@link #onReceiveSessionModifyResponse(int, VideoProfile, VideoProfile)} callbacks come * in. When the video un-pauses we continue receiving the video state updates. */ private boolean mShouldIgnoreVideoStateChanges = false; private ImsVideoCallProviderWrapper mImsVideoCallProviderWrapper; private int mPreciseDisconnectCause = 0; private ImsRttTextHandler mRttTextHandler; private android.telecom.Connection.RttTextStream mRttTextStream; // This reflects the RTT status as reported to us by the IMS stack via the media profile. private boolean mIsRttEnabledForCall = false; /** * Used to indicate that this call is in the midst of being merged into a conference. */ private boolean mIsMergeInProcess = false; /** * Used as an override to determine whether video is locally available for this call. * This allows video availability to be overridden in the case that the modem says video is * currently available, but mobile data is off and the carrier is metering data for video * calls. */ private boolean mIsVideoEnabled = true; //***** Event Constants private static final int EVENT_DTMF_DONE = 1; private static final int EVENT_PAUSE_DONE = 2; private static final int EVENT_NEXT_POST_DIAL = 3; private static final int EVENT_WAKE_LOCK_TIMEOUT = 4; private static final int EVENT_DTMF_DELAY_DONE = 5; //***** Constants private static final int PAUSE_DELAY_MILLIS = 3 * 1000; private static final int WAKE_LOCK_TIMEOUT_MILLIS = 60*1000; //***** Inner Classes class MyHandler extends Handler { MyHandler(Looper l) {super(l);} @Override public void handleMessage(Message msg) { switch (msg.what) { case EVENT_NEXT_POST_DIAL: case EVENT_DTMF_DELAY_DONE: case EVENT_PAUSE_DONE: processNextPostDialChar(); break; case EVENT_WAKE_LOCK_TIMEOUT: releaseWakeLock(); break; case EVENT_DTMF_DONE: // We may need to add a delay specified by carrier between DTMF tones that are // sent out. mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_DTMF_DELAY_DONE), mDtmfToneDelay); break; } } } //***** Constructors /** This is probably an MT call */ public ImsPhoneConnection(Phone phone, ImsCall imsCall, ImsPhoneCallTracker ct, ImsPhoneCall parent, boolean isUnknown) { super(PhoneConstants.PHONE_TYPE_IMS); createWakeLock(phone.getContext()); acquireWakeLock(); mOwner = ct; mHandler = new MyHandler(mOwner.getLooper()); mHandlerMessenger = new Messenger(mHandler); mImsCall = imsCall; if ((imsCall != null) && (imsCall.getCallProfile() != null)) { mAddress = imsCall.getCallProfile().getCallExtra(ImsCallProfile.EXTRA_OI); mCnapName = imsCall.getCallProfile().getCallExtra(ImsCallProfile.EXTRA_CNA); mNumberPresentation = ImsCallProfile.OIRToPresentation( imsCall.getCallProfile().getCallExtraInt(ImsCallProfile.EXTRA_OIR)); mCnapNamePresentation = ImsCallProfile.OIRToPresentation( imsCall.getCallProfile().getCallExtraInt(ImsCallProfile.EXTRA_CNAP)); updateMediaCapabilities(imsCall); } else { mNumberPresentation = PhoneConstants.PRESENTATION_UNKNOWN; mCnapNamePresentation = PhoneConstants.PRESENTATION_UNKNOWN; } mIsIncoming = !isUnknown; mCreateTime = System.currentTimeMillis(); mUusInfo = null; // Ensure any extras set on the ImsCallProfile at the start of the call are cached locally // in the ImsPhoneConnection. This isn't going to inform any listeners (since the original // connection is not likely to be associated with a TelephonyConnection yet). updateExtras(imsCall); mParent = parent; mParent.attach(this, (mIsIncoming? ImsPhoneCall.State.INCOMING: ImsPhoneCall.State.DIALING)); fetchDtmfToneDelay(phone); if (phone.getContext().getResources().getBoolean( com.android.internal.R.bool.config_use_voip_mode_for_ims)) { setAudioModeIsVoip(true); } } /** This is an MO call, created when dialing */ public ImsPhoneConnection(Phone phone, String dialString, ImsPhoneCallTracker ct, ImsPhoneCall parent, boolean isEmergency) { super(PhoneConstants.PHONE_TYPE_IMS); createWakeLock(phone.getContext()); acquireWakeLock(); mOwner = ct; mHandler = new MyHandler(mOwner.getLooper()); mDialString = dialString; mAddress = PhoneNumberUtils.extractNetworkPortionAlt(dialString); mPostDialString = PhoneNumberUtils.extractPostDialPortion(dialString); //mIndex = -1; mIsIncoming = false; mCnapName = null; mCnapNamePresentation = PhoneConstants.PRESENTATION_ALLOWED; mNumberPresentation = PhoneConstants.PRESENTATION_ALLOWED; mCreateTime = System.currentTimeMillis(); mParent = parent; parent.attachFake(this, ImsPhoneCall.State.DIALING); mIsEmergency = isEmergency; fetchDtmfToneDelay(phone); if (phone.getContext().getResources().getBoolean( com.android.internal.R.bool.config_use_voip_mode_for_ims)) { setAudioModeIsVoip(true); } } public void dispose() { } static boolean equalsHandlesNulls (Object a, Object b) { return (a == null) ? (b == null) : a.equals (b); } static boolean equalsBaseDialString (String a, String b) { return (a == null) ? (b == null) : (b != null && a.startsWith (b)); } private int applyLocalCallCapabilities(ImsCallProfile localProfile, int capabilities) { Rlog.i(LOG_TAG, "applyLocalCallCapabilities - localProfile = " + localProfile); capabilities = removeCapability(capabilities, Connection.Capability.SUPPORTS_VT_LOCAL_BIDIRECTIONAL); if (!mIsVideoEnabled) { Rlog.i(LOG_TAG, "applyLocalCallCapabilities - disabling video (overidden)"); return capabilities; } switch (localProfile.mCallType) { case ImsCallProfile.CALL_TYPE_VT: // Fall-through case ImsCallProfile.CALL_TYPE_VIDEO_N_VOICE: capabilities = addCapability(capabilities, Connection.Capability.SUPPORTS_VT_LOCAL_BIDIRECTIONAL); break; } return capabilities; } private static int applyRemoteCallCapabilities(ImsCallProfile remoteProfile, int capabilities) { Rlog.w(LOG_TAG, "applyRemoteCallCapabilities - remoteProfile = "+remoteProfile); capabilities = removeCapability(capabilities, Connection.Capability.SUPPORTS_VT_REMOTE_BIDIRECTIONAL); switch (remoteProfile.mCallType) { case ImsCallProfile.CALL_TYPE_VT: // fall-through case ImsCallProfile.CALL_TYPE_VIDEO_N_VOICE: capabilities = addCapability(capabilities, Connection.Capability.SUPPORTS_VT_REMOTE_BIDIRECTIONAL); break; } return capabilities; } @Override public String getOrigDialString(){ return mDialString; } @Override public ImsPhoneCall getCall() { return mParent; } @Override public long getDisconnectTime() { return mDisconnectTime; } @Override public long getHoldingStartTime() { return mHoldingStartTime; } @Override public long getHoldDurationMillis() { if (getState() != ImsPhoneCall.State.HOLDING) { // If not holding, return 0 return 0; } else { return SystemClock.elapsedRealtime() - mHoldingStartTime; } } public void setDisconnectCause(int cause) { mCause = cause; } @Override public String getVendorDisconnectCause() { return null; } public ImsPhoneCallTracker getOwner () { return mOwner; } @Override public ImsPhoneCall.State getState() { if (mDisconnected) { return ImsPhoneCall.State.DISCONNECTED; } else { return super.getState(); } } @Override public void deflect(String number) throws CallStateException { if (mParent.getState().isRinging()) { try { if (mImsCall != null) { mImsCall.deflect(number); } else { throw new CallStateException("no valid ims call to deflect"); } } catch (ImsException e) { throw new CallStateException("cannot deflect call"); } } else { throw new CallStateException("phone not ringing"); } } @Override public void hangup() throws CallStateException { if (!mDisconnected) { mOwner.hangup(this); } else { throw new CallStateException ("disconnected"); } } @Override public void separate() throws CallStateException { throw new CallStateException ("not supported"); } @Override public void proceedAfterWaitChar() { if (mPostDialState != PostDialState.WAIT) { Rlog.w(LOG_TAG, "ImsPhoneConnection.proceedAfterWaitChar(): Expected " + "getPostDialState() to be WAIT but was " + mPostDialState); return; } setPostDialState(PostDialState.STARTED); processNextPostDialChar(); } @Override public void proceedAfterWildChar(String str) { if (mPostDialState != PostDialState.WILD) { Rlog.w(LOG_TAG, "ImsPhoneConnection.proceedAfterWaitChar(): Expected " + "getPostDialState() to be WILD but was " + mPostDialState); return; } setPostDialState(PostDialState.STARTED); // make a new postDialString, with the wild char replacement string // at the beginning, followed by the remaining postDialString. StringBuilder buf = new StringBuilder(str); buf.append(mPostDialString.substring(mNextPostDialChar)); mPostDialString = buf.toString(); mNextPostDialChar = 0; if (Phone.DEBUG_PHONE) { Rlog.d(LOG_TAG, "proceedAfterWildChar: new postDialString is " + mPostDialString); } processNextPostDialChar(); } @Override public void cancelPostDial() { setPostDialState(PostDialState.CANCELLED); } /** * Called when this Connection is being hung up locally (eg, user pressed "end") */ void onHangupLocal() { mCause = DisconnectCause.LOCAL; } /** Called when the connection has been disconnected */ @Override public boolean onDisconnect(int cause) { Rlog.d(LOG_TAG, "onDisconnect: cause=" + cause); if (mCause != DisconnectCause.LOCAL || cause == DisconnectCause.INCOMING_REJECTED) { mCause = cause; } return onDisconnect(); } public boolean onDisconnect() { boolean changed = false; if (!mDisconnected) { //mIndex = -1; mDisconnectTime = System.currentTimeMillis(); mDuration = SystemClock.elapsedRealtime() - mConnectTimeReal; mDisconnected = true; mOwner.mPhone.notifyDisconnect(this); notifyDisconnect(mCause); if (mParent != null) { changed = mParent.connectionDisconnected(this); } else { Rlog.d(LOG_TAG, "onDisconnect: no parent"); } synchronized (this) { if (mImsCall != null) mImsCall.close(); mImsCall = null; } } releaseWakeLock(); return changed; } /** * An incoming or outgoing call has connected */ void onConnectedInOrOut() { mConnectTime = System.currentTimeMillis(); mConnectTimeReal = SystemClock.elapsedRealtime(); mDuration = 0; if (Phone.DEBUG_PHONE) { Rlog.d(LOG_TAG, "onConnectedInOrOut: connectTime=" + mConnectTime); } if (!mIsIncoming) { // outgoing calls only processNextPostDialChar(); } releaseWakeLock(); } /*package*/ void onStartedHolding() { mHoldingStartTime = SystemClock.elapsedRealtime(); } /** * Performs the appropriate action for a post-dial char, but does not * notify application. returns false if the character is invalid and * should be ignored */ private boolean processPostDialChar(char c) { if (PhoneNumberUtils.is12Key(c)) { Message dtmfComplete = mHandler.obtainMessage(EVENT_DTMF_DONE); dtmfComplete.replyTo = mHandlerMessenger; mOwner.sendDtmf(c, dtmfComplete); } else if (c == PhoneNumberUtils.PAUSE) { // From TS 22.101: // It continues... // Upon the called party answering the UE shall send the DTMF digits // automatically to the network after a delay of 3 seconds( 20 ). // The digits shall be sent according to the procedures and timing // specified in 3GPP TS 24.008 [13]. The first occurrence of the // "DTMF Control Digits Separator" shall be used by the ME to // distinguish between the addressing digits (i.e. the phone number) // and the DTMF digits. Upon subsequent occurrences of the // separator, // the UE shall pause again for 3 seconds ( 20 ) before sending // any further DTMF digits. mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_PAUSE_DONE), PAUSE_DELAY_MILLIS); } else if (c == PhoneNumberUtils.WAIT) { setPostDialState(PostDialState.WAIT); } else if (c == PhoneNumberUtils.WILD) { setPostDialState(PostDialState.WILD); } else { return false; } return true; } @Override protected void finalize() { releaseWakeLock(); } private void processNextPostDialChar() { char c = 0; Registrant postDialHandler; if (mPostDialState == PostDialState.CANCELLED) { //Rlog.d(LOG_TAG, "##### processNextPostDialChar: postDialState == CANCELLED, bail"); return; } if (mPostDialString == null || mPostDialString.length() <= mNextPostDialChar) { setPostDialState(PostDialState.COMPLETE); // notifyMessage.arg1 is 0 on complete c = 0; } else { boolean isValid; setPostDialState(PostDialState.STARTED); c = mPostDialString.charAt(mNextPostDialChar++); isValid = processPostDialChar(c); if (!isValid) { // Will call processNextPostDialChar mHandler.obtainMessage(EVENT_NEXT_POST_DIAL).sendToTarget(); // Don't notify application Rlog.e(LOG_TAG, "processNextPostDialChar: c=" + c + " isn't valid!"); return; } } notifyPostDialListenersNextChar(c); // TODO: remove the following code since the handler no longer executes anything. postDialHandler = mOwner.mPhone.getPostDialHandler(); Message notifyMessage; if (postDialHandler != null && (notifyMessage = postDialHandler.messageForRegistrant()) != null) { // The AsyncResult.result is the Connection object PostDialState state = mPostDialState; AsyncResult ar = AsyncResult.forMessage(notifyMessage); ar.result = this; ar.userObj = state; // arg1 is the character that was/is being processed notifyMessage.arg1 = c; //Rlog.v(LOG_TAG, // "##### processNextPostDialChar: send msg to postDialHandler, arg1=" + c); notifyMessage.sendToTarget(); } } /** * Set post dial state and acquire wake lock while switching to "started" * state, the wake lock will be released if state switches out of "started" * state or after WAKE_LOCK_TIMEOUT_MILLIS. * @param s new PostDialState */ private void setPostDialState(PostDialState s) { if (mPostDialState != PostDialState.STARTED && s == PostDialState.STARTED) { acquireWakeLock(); Message msg = mHandler.obtainMessage(EVENT_WAKE_LOCK_TIMEOUT); mHandler.sendMessageDelayed(msg, WAKE_LOCK_TIMEOUT_MILLIS); } else if (mPostDialState == PostDialState.STARTED && s != PostDialState.STARTED) { mHandler.removeMessages(EVENT_WAKE_LOCK_TIMEOUT); releaseWakeLock(); } mPostDialState = s; notifyPostDialListeners(); } private void createWakeLock(Context context) { PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG); } private void acquireWakeLock() { Rlog.d(LOG_TAG, "acquireWakeLock"); mPartialWakeLock.acquire(); } void releaseWakeLock() { if (mPartialWakeLock != null) { synchronized (mPartialWakeLock) { if (mPartialWakeLock.isHeld()) { Rlog.d(LOG_TAG, "releaseWakeLock"); mPartialWakeLock.release(); } } } } private void fetchDtmfToneDelay(Phone phone) { CarrierConfigManager configMgr = (CarrierConfigManager) phone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE); PersistableBundle b = configMgr.getConfigForSubId(phone.getSubId()); if (b != null) { mDtmfToneDelay = b.getInt(CarrierConfigManager.KEY_IMS_DTMF_TONE_DELAY_INT); } } @Override public int getNumberPresentation() { return mNumberPresentation; } @Override public UUSInfo getUUSInfo() { return mUusInfo; } @Override public Connection getOrigConnection() { return null; } @Override public synchronized boolean isMultiparty() { return mImsCall != null && mImsCall.isMultiparty(); } /** * Where {@link #isMultiparty()} is {@code true}, determines if this {@link ImsCall} is the * origin of the conference call (i.e. {@code #isConferenceHost()} is {@code true}), or if this * {@link ImsCall} is a member of a conference hosted on another device. * * @return {@code true} if this call is the origin of the conference call it is a member of, * {@code false} otherwise. */ @Override public synchronized boolean isConferenceHost() { return mImsCall != null && mImsCall.isConferenceHost(); } @Override public boolean isMemberOfPeerConference() { return !isConferenceHost(); } public synchronized ImsCall getImsCall() { return mImsCall; } public synchronized void setImsCall(ImsCall imsCall) { mImsCall = imsCall; } public void changeParent(ImsPhoneCall parent) { mParent = parent; } /** * @return {@code true} if the {@link ImsPhoneConnection} or its media capabilities have been * changed, and {@code false} otherwise. */ public boolean update(ImsCall imsCall, ImsPhoneCall.State state) { if (state == ImsPhoneCall.State.ACTIVE) { // If the state of the call is active, but there is a pending request to the RIL to hold // the call, we will skip this update. This is really a signalling delay or failure // from the RIL, but we will prevent it from going through as we will end up erroneously // making this call active when really it should be on hold. if (imsCall.isPendingHold()) { Rlog.w(LOG_TAG, "update : state is ACTIVE, but call is pending hold, skipping"); return false; } if (mParent.getState().isRinging() || mParent.getState().isDialing()) { onConnectedInOrOut(); } if (mParent.getState().isRinging() || mParent == mOwner.mBackgroundCall) { //mForegroundCall should be IDLE //when accepting WAITING call //before accept WAITING call, //the ACTIVE call should be held ahead mParent.detach(this); mParent = mOwner.mForegroundCall; mParent.attach(this); } } else if (state == ImsPhoneCall.State.HOLDING) { onStartedHolding(); } boolean updateParent = mParent.update(this, imsCall, state); boolean updateAddressDisplay = updateAddressDisplay(imsCall); boolean updateMediaCapabilities = updateMediaCapabilities(imsCall); boolean updateExtras = updateExtras(imsCall); return updateParent || updateAddressDisplay || updateMediaCapabilities || updateExtras; } @Override public int getPreciseDisconnectCause() { return mPreciseDisconnectCause; } public void setPreciseDisconnectCause(int cause) { mPreciseDisconnectCause = cause; } /** * Notifies this Connection of a request to disconnect a participant of the conference managed * by the connection. * * @param endpoint the {@link android.net.Uri} of the participant to disconnect. */ @Override public void onDisconnectConferenceParticipant(Uri endpoint) { ImsCall imsCall = getImsCall(); if (imsCall == null) { return; } try { imsCall.removeParticipants(new String[]{endpoint.toString()}); } catch (ImsException e) { // No session in place -- no change Rlog.e(LOG_TAG, "onDisconnectConferenceParticipant: no session in place. "+ "Failed to disconnect endpoint = " + endpoint); } } /** * Sets the conference connect time. Used when an {@code ImsConference} is created to out of * this phone connection. * * @param conferenceConnectTime The conference connect time. */ public void setConferenceConnectTime(long conferenceConnectTime) { mConferenceConnectTime = conferenceConnectTime; } /** * @return The conference connect time. */ public long getConferenceConnectTime() { return mConferenceConnectTime; } /** * Check for a change in the address display related fields for the {@link ImsCall}, and * update the {@link ImsPhoneConnection} with this information. * * @param imsCall The call to check for changes in address display fields. * @return Whether the address display fields have been changed. */ public boolean updateAddressDisplay(ImsCall imsCall) { if (imsCall == null) { return false; } boolean changed = false; ImsCallProfile callProfile = imsCall.getCallProfile(); if (callProfile != null && isIncoming()) { // Only look for changes to the address for incoming calls. The originating identity // can change for outgoing calls due to, for example, a call being forwarded to // voicemail. This address change does not need to be presented to the user. String address = callProfile.getCallExtra(ImsCallProfile.EXTRA_OI); String name = callProfile.getCallExtra(ImsCallProfile.EXTRA_CNA); int nump = ImsCallProfile.OIRToPresentation( callProfile.getCallExtraInt(ImsCallProfile.EXTRA_OIR)); int namep = ImsCallProfile.OIRToPresentation( callProfile.getCallExtraInt(ImsCallProfile.EXTRA_CNAP)); if (Phone.DEBUG_PHONE) { Rlog.d(LOG_TAG, "updateAddressDisplay: callId = " + getTelecomCallId() + " address = " + Rlog.pii(LOG_TAG, address) + " name = " + Rlog.pii(LOG_TAG, name) + " nump = " + nump + " namep = " + namep); } if (!mIsMergeInProcess) { // Only process changes to the name and address when a merge is not in process. // When call A initiated a merge with call B to form a conference C, there is a // point in time when the ImsCall transfers the conference call session into A, // at which point the ImsConferenceController creates the conference in Telecom. // For some carriers C will have a unique conference URI address. Swapping the // conference session into A, which is about to be disconnected, to be logged to // the call log using the conference address. To prevent this we suppress updates // to the call address while a merge is in process. if (!equalsBaseDialString(mAddress, address)) { mAddress = address; changed = true; } if (TextUtils.isEmpty(name)) { if (!TextUtils.isEmpty(mCnapName)) { mCnapName = ""; changed = true; } } else if (!name.equals(mCnapName)) { mCnapName = name; changed = true; } if (mNumberPresentation != nump) { mNumberPresentation = nump; changed = true; } if (mCnapNamePresentation != namep) { mCnapNamePresentation = namep; changed = true; } } } return changed; } /** * Check for a change in the video capabilities and audio quality for the {@link ImsCall}, and * update the {@link ImsPhoneConnection} with this information. * * @param imsCall The call to check for changes in media capabilities. * @return Whether the media capabilities have been changed. */ public boolean updateMediaCapabilities(ImsCall imsCall) { if (imsCall == null) { return false; } boolean changed = false; try { // The actual call profile (negotiated between local and peer). ImsCallProfile negotiatedCallProfile = imsCall.getCallProfile(); if (negotiatedCallProfile != null) { int oldVideoState = getVideoState(); int newVideoState = ImsCallProfile .getVideoStateFromImsCallProfile(negotiatedCallProfile); if (oldVideoState != newVideoState) { // The video state has changed. See also code in onReceiveSessionModifyResponse // below. When the video enters a paused state, subsequent changes to the video // state will not be reported by the modem. In onReceiveSessionModifyResponse // we will be updating the current video state while paused to include any // changes the modem reports via the video provider. When the video enters an // unpaused state, we will resume passing the video states from the modem as is. if (VideoProfile.isPaused(oldVideoState) && !VideoProfile.isPaused(newVideoState)) { // Video entered un-paused state; recognize updates from now on; we want to // ensure that the new un-paused state is propagated to Telecom, so change // this now. mShouldIgnoreVideoStateChanges = false; } if (!mShouldIgnoreVideoStateChanges) { updateVideoState(newVideoState); changed = true; } else { Rlog.d(LOG_TAG, "updateMediaCapabilities - ignoring video state change " + "due to paused state."); } if (!VideoProfile.isPaused(oldVideoState) && VideoProfile.isPaused(newVideoState)) { // Video entered pause state; ignore updates until un-paused. We do this // after setVideoState is called above to ensure Telecom is notified that // the device has entered paused state. mShouldIgnoreVideoStateChanges = true; } } if (negotiatedCallProfile.mMediaProfile != null) { mIsRttEnabledForCall = negotiatedCallProfile.mMediaProfile.isRttCall(); if (mIsRttEnabledForCall && mRttTextHandler == null) { Rlog.d(LOG_TAG, "updateMediaCapabilities -- turning RTT on, profile=" + negotiatedCallProfile); startRttTextProcessing(); onRttInitiated(); changed = true; } else if (!mIsRttEnabledForCall && mRttTextHandler != null) { Rlog.d(LOG_TAG, "updateMediaCapabilities -- turning RTT off, profile=" + negotiatedCallProfile); mRttTextHandler.tearDown(); mRttTextHandler = null; onRttTerminated(); changed = true; } } } // Check for a change in the capabilities for the call and update // {@link ImsPhoneConnection} with this information. int capabilities = getConnectionCapabilities(); // Use carrier config to determine if downgrading directly to audio-only is supported. if (mOwner.isCarrierDowngradeOfVtCallSupported()) { capabilities = addCapability(capabilities, Connection.Capability.SUPPORTS_DOWNGRADE_TO_VOICE_REMOTE | Capability.SUPPORTS_DOWNGRADE_TO_VOICE_LOCAL); } else { capabilities = removeCapability(capabilities, Connection.Capability.SUPPORTS_DOWNGRADE_TO_VOICE_REMOTE | Capability.SUPPORTS_DOWNGRADE_TO_VOICE_LOCAL); } // Get the current local call capabilities which might be voice or video or both. ImsCallProfile localCallProfile = imsCall.getLocalCallProfile(); Rlog.v(LOG_TAG, "update localCallProfile=" + localCallProfile); if (localCallProfile != null) { capabilities = applyLocalCallCapabilities(localCallProfile, capabilities); } // Get the current remote call capabilities which might be voice or video or both. ImsCallProfile remoteCallProfile = imsCall.getRemoteCallProfile(); Rlog.v(LOG_TAG, "update remoteCallProfile=" + remoteCallProfile); if (remoteCallProfile != null) { capabilities = applyRemoteCallCapabilities(remoteCallProfile, capabilities); } if (getConnectionCapabilities() != capabilities) { setConnectionCapabilities(capabilities); changed = true; } int newAudioQuality = getAudioQualityFromCallProfile(localCallProfile, remoteCallProfile); if (getAudioQuality() != newAudioQuality) { setAudioQuality(newAudioQuality); changed = true; } } catch (ImsException e) { // No session in place -- no change } return changed; } private void updateVideoState(int newVideoState) { if (mImsVideoCallProviderWrapper != null) { mImsVideoCallProviderWrapper.onVideoStateChanged(newVideoState); } setVideoState(newVideoState); } public void sendRttModifyRequest(android.telecom.Connection.RttTextStream textStream) { getImsCall().sendRttModifyRequest(); setCurrentRttTextStream(textStream); } /** * Sends the user's response to a remotely-issued RTT upgrade request * * @param textStream A valid {@link android.telecom.Connection.RttTextStream} if the user * accepts, {@code null} if not. */ public void sendRttModifyResponse(android.telecom.Connection.RttTextStream textStream) { boolean accept = textStream != null; ImsCall imsCall = getImsCall(); imsCall.sendRttModifyResponse(accept); if (accept) { setCurrentRttTextStream(textStream); } else { Rlog.e(LOG_TAG, "sendRttModifyResponse: foreground call has no connections"); } } public void onRttMessageReceived(String message) { synchronized (this) { if (mRttTextHandler == null) { Rlog.w(LOG_TAG, "onRttMessageReceived: RTT text handler not available." + " Attempting to create one."); if (mRttTextStream == null) { Rlog.e(LOG_TAG, "onRttMessageReceived:" + " Unable to process incoming message. No textstream available"); return; } createRttTextHandler(); } } mRttTextHandler.sendToInCall(message); } public void setCurrentRttTextStream(android.telecom.Connection.RttTextStream rttTextStream) { synchronized (this) { mRttTextStream = rttTextStream; if (mRttTextHandler == null && mIsRttEnabledForCall) { Rlog.i(LOG_TAG, "setCurrentRttTextStream: Creating a text handler"); createRttTextHandler(); } } } public boolean hasRttTextStream() { return mRttTextStream != null; } public boolean isRttEnabledForCall() { return mIsRttEnabledForCall; } public void startRttTextProcessing() { synchronized (this) { if (mRttTextStream == null) { Rlog.w(LOG_TAG, "startRttTextProcessing: no RTT text stream. Ignoring."); return; } if (mRttTextHandler != null) { Rlog.w(LOG_TAG, "startRttTextProcessing: RTT text handler already exists"); return; } createRttTextHandler(); } } // Make sure to synchronize on ImsPhoneConnection.this before calling. private void createRttTextHandler() { mRttTextHandler = new ImsRttTextHandler(Looper.getMainLooper(), (message) -> getImsCall().sendRttMessage(message)); mRttTextHandler.initialize(mRttTextStream); } /** * Updates the wifi state based on the {@link ImsCallProfile#EXTRA_CALL_RAT_TYPE}. * The call is considered to be a WIFI call if the extra value is * {@link ServiceState#RIL_RADIO_TECHNOLOGY_IWLAN}. * * @param extras The ImsCallProfile extras. */ private void updateWifiStateFromExtras(Bundle extras) { if (extras.containsKey(ImsCallProfile.EXTRA_CALL_RAT_TYPE) || extras.containsKey(ImsCallProfile.EXTRA_CALL_RAT_TYPE_ALT)) { ImsCall call = getImsCall(); boolean isWifi = false; if (call != null) { isWifi = call.isWifiCall(); } // Report any changes if (isWifi() != isWifi) { setWifi(isWifi); } } } /** * Check for a change in call extras of {@link ImsCall}, and * update the {@link ImsPhoneConnection} accordingly. * * @param imsCall The call to check for changes in extras. * @return Whether the extras fields have been changed. */ boolean updateExtras(ImsCall imsCall) { if (imsCall == null) { return false; } final ImsCallProfile callProfile = imsCall.getCallProfile(); final Bundle extras = callProfile != null ? callProfile.mCallExtras : null; if (extras == null && DBG) { Rlog.d(LOG_TAG, "Call profile extras are null."); } final boolean changed = !areBundlesEqual(extras, mExtras); if (changed) { updateWifiStateFromExtras(extras); mExtras.clear(); mExtras.putAll(extras); setConnectionExtras(mExtras); } return changed; } private static boolean areBundlesEqual(Bundle extras, Bundle newExtras) { if (extras == null || newExtras == null) { return extras == newExtras; } if (extras.size() != newExtras.size()) { return false; } for(String key : extras.keySet()) { if (key != null) { final Object value = extras.get(key); final Object newValue = newExtras.get(key); if (!Objects.equals(value, newValue)) { return false; } } } return true; } /** * Determines the {@link ImsPhoneConnection} audio quality based on the local and remote * {@link ImsCallProfile}. Indicate a HD audio call if the local stream profile * is AMR_WB, EVRC_WB, EVS_WB, EVS_SWB, EVS_FB and * there is no remote restrict cause. * * @param localCallProfile The local call profile. * @param remoteCallProfile The remote call profile. * @return The audio quality. */ private int getAudioQualityFromCallProfile( ImsCallProfile localCallProfile, ImsCallProfile remoteCallProfile) { if (localCallProfile == null || remoteCallProfile == null || localCallProfile.mMediaProfile == null) { return AUDIO_QUALITY_STANDARD; } final boolean isEvsCodecHighDef = (localCallProfile.mMediaProfile.mAudioQuality == ImsStreamMediaProfile.AUDIO_QUALITY_EVS_WB || localCallProfile.mMediaProfile.mAudioQuality == ImsStreamMediaProfile.AUDIO_QUALITY_EVS_SWB || localCallProfile.mMediaProfile.mAudioQuality == ImsStreamMediaProfile.AUDIO_QUALITY_EVS_FB); final boolean isHighDef = (localCallProfile.mMediaProfile.mAudioQuality == ImsStreamMediaProfile.AUDIO_QUALITY_AMR_WB || localCallProfile.mMediaProfile.mAudioQuality == ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_WB || isEvsCodecHighDef) && remoteCallProfile.mRestrictCause == ImsCallProfile.CALL_RESTRICT_CAUSE_NONE; return isHighDef ? AUDIO_QUALITY_HIGH_DEFINITION : AUDIO_QUALITY_STANDARD; } /** * Provides a string representation of the {@link ImsPhoneConnection}. Primarily intended for * use in log statements. * * @return String representation of call. */ @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("[ImsPhoneConnection objId: "); sb.append(System.identityHashCode(this)); sb.append(" telecomCallID: "); sb.append(getTelecomCallId()); sb.append(" address: "); sb.append(Rlog.pii(LOG_TAG, getAddress())); sb.append(" ImsCall: "); synchronized (this) { if (mImsCall == null) { sb.append("null"); } else { sb.append(mImsCall); } } sb.append("]"); return sb.toString(); } @Override public void setVideoProvider(android.telecom.Connection.VideoProvider videoProvider) { super.setVideoProvider(videoProvider); if (videoProvider instanceof ImsVideoCallProviderWrapper) { mImsVideoCallProviderWrapper = (ImsVideoCallProviderWrapper) videoProvider; } } /** * Indicates whether current phone connection is emergency or not * @return boolean: true if emergency, false otherwise */ protected boolean isEmergency() { return mIsEmergency; } /** * Handles notifications from the {@link ImsVideoCallProviderWrapper} of session modification * responses received. * * @param status The status of the original request. * @param requestProfile The requested video profile. * @param responseProfile The response upon video profile. */ @Override public void onReceiveSessionModifyResponse(int status, VideoProfile requestProfile, VideoProfile responseProfile) { if (status == android.telecom.Connection.VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS && mShouldIgnoreVideoStateChanges) { int currentVideoState = getVideoState(); int newVideoState = responseProfile.getVideoState(); // If the current video state is paused, the modem will not send us any changes to // the TX and RX bits of the video state. Until the video is un-paused we will // "fake out" the video state by applying the changes that the modem reports via a // response. // First, find out whether there was a change to the TX or RX bits: int changedBits = currentVideoState ^ newVideoState; changedBits &= VideoProfile.STATE_BIDIRECTIONAL; if (changedBits == 0) { // No applicable change, bail out. return; } // Turn off any existing bits that changed. currentVideoState &= ~(changedBits & currentVideoState); // Turn on any new bits that turned on. currentVideoState |= changedBits & newVideoState; Rlog.d(LOG_TAG, "onReceiveSessionModifyResponse : received " + VideoProfile.videoStateToString(requestProfile.getVideoState()) + " / " + VideoProfile.videoStateToString(responseProfile.getVideoState()) + " while paused ; sending new videoState = " + VideoProfile.videoStateToString(currentVideoState)); setVideoState(currentVideoState); } } /** * Issues a request to pause the video using {@link VideoProfile#STATE_PAUSED} from a source * other than the InCall UI. * * @param source The source of the pause request. */ public void pauseVideo(int source) { if (mImsVideoCallProviderWrapper == null) { return; } mImsVideoCallProviderWrapper.pauseVideo(getVideoState(), source); } /** * Issues a request to resume the video using {@link VideoProfile#STATE_PAUSED} from a source * other than the InCall UI. * * @param source The source of the resume request. */ public void resumeVideo(int source) { if (mImsVideoCallProviderWrapper == null) { return; } mImsVideoCallProviderWrapper.resumeVideo(getVideoState(), source); } /** * Determines if a specified source has issued a pause request. * * @param source The source. * @return {@code true} if the source issued a pause request, {@code false} otherwise. */ public boolean wasVideoPausedFromSource(int source) { if (mImsVideoCallProviderWrapper == null) { return false; } return mImsVideoCallProviderWrapper.wasVideoPausedFromSource(source); } /** * Mark the call as in the process of being merged and inform the UI of the merge start. */ public void handleMergeStart() { mIsMergeInProcess = true; onConnectionEvent(android.telecom.Connection.EVENT_MERGE_START, null); } /** * Mark the call as done merging and inform the UI of the merge start. */ public void handleMergeComplete() { mIsMergeInProcess = false; onConnectionEvent(android.telecom.Connection.EVENT_MERGE_COMPLETE, null); } public void changeToPausedState() { int newVideoState = getVideoState() | VideoProfile.STATE_PAUSED; Rlog.i(LOG_TAG, "ImsPhoneConnection: changeToPausedState - setting paused bit; " + "newVideoState=" + VideoProfile.videoStateToString(newVideoState)); updateVideoState(newVideoState); mShouldIgnoreVideoStateChanges = true; } public void changeToUnPausedState() { int newVideoState = getVideoState() & ~VideoProfile.STATE_PAUSED; Rlog.i(LOG_TAG, "ImsPhoneConnection: changeToUnPausedState - unsetting paused bit; " + "newVideoState=" + VideoProfile.videoStateToString(newVideoState)); updateVideoState(newVideoState); mShouldIgnoreVideoStateChanges = false; } public void handleDataEnabledChange(boolean isDataEnabled) { mIsVideoEnabled = isDataEnabled; Rlog.i(LOG_TAG, "handleDataEnabledChange: isDataEnabled=" + isDataEnabled + "; updating local video availability."); updateMediaCapabilities(getImsCall()); if (mImsVideoCallProviderWrapper != null) { mImsVideoCallProviderWrapper.setIsVideoEnabled( hasCapabilities(Connection.Capability.SUPPORTS_VT_LOCAL_BIDIRECTIONAL)); } } }