/* * 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.Handler; import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.Registrant; import android.os.SystemClock; import android.telecom.Log; import android.telephony.DisconnectCause; import android.telephony.PhoneNumberUtils; import android.telephony.Rlog; import com.android.ims.ImsException; import com.android.ims.ImsStreamMediaProfile; 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 com.android.ims.ImsCall; import com.android.ims.ImsCallProfile; /** * {@hide} */ public class ImsPhoneConnection extends Connection { 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 String mPostDialString; // outgoing calls only 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 int mNextPostDialChar; // index into postDialString private int mCause = DisconnectCause.NOT_DISCONNECTED; private PostDialState mPostDialState = PostDialState.NOT_STARTED; private UUSInfo mUusInfo; private Handler mHandler; private PowerManager.WakeLock mPartialWakeLock; // The cached connect time of the connection when it turns into a conference. private long mConferenceConnectTime = 0; //***** 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; //***** 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_DONE: case EVENT_PAUSE_DONE: processNextPostDialChar(); break; case EVENT_WAKE_LOCK_TIMEOUT: releaseWakeLock(); break; } } } //***** Constructors /** This is probably an MT call */ /*package*/ ImsPhoneConnection(Context context, ImsCall imsCall, ImsPhoneCallTracker ct, ImsPhoneCall parent) { createWakeLock(context); acquireWakeLock(); mOwner = ct; mHandler = new MyHandler(mOwner.getLooper()); 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 = true; mCreateTime = System.currentTimeMillis(); mUusInfo = null; //mIndex = index; mParent = parent; mParent.attach(this, ImsPhoneCall.State.INCOMING); } /** This is an MO call, created when dialing */ /*package*/ ImsPhoneConnection(Context context, String dialString, ImsPhoneCallTracker ct, ImsPhoneCall parent) { createWakeLock(context); 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); } public void dispose() { } static boolean equalsHandlesNulls (Object a, Object b) { return (a == null) ? (b == null) : a.equals (b); } @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; } } @Override public int getDisconnectCause() { return mCause; } public void setDisconnectCause(int cause) { mCause = cause; } public ImsPhoneCallTracker getOwner () { return mOwner; } @Override public ImsPhoneCall.State getState() { if (mDisconnected) { return ImsPhoneCall.State.DISCONNECTED; } else { return super.getState(); } } @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 PostDialState getPostDialState() { return mPostDialState; } @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 */ public boolean onDisconnect(int cause) { Rlog.d(LOG_TAG, "onDisconnect: cause=" + cause); if (mCause != DisconnectCause.LOCAL) mCause = cause; return onDisconnect(); } /*package*/ boolean onDisconnect() { boolean changed = false; if (!mDisconnected) { //mIndex = -1; mDisconnectTime = System.currentTimeMillis(); mDuration = SystemClock.elapsedRealtime() - mConnectTimeReal; mDisconnected = true; mOwner.mPhone.notifyDisconnect(this); if (mParent != null) { changed = mParent.connectionDisconnected(this); } else { Rlog.d(LOG_TAG, "onDisconnect: no parent"); } 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)) { mOwner.sendDtmf(c, mHandler.obtainMessage(EVENT_DTMF_DONE)); } 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 public String getRemainingPostDialString() { if (mPostDialState == PostDialState.CANCELLED || mPostDialState == PostDialState.COMPLETE || mPostDialString == null || mPostDialString.length() <= mNextPostDialChar ) { return ""; } return mPostDialString.substring(mNextPostDialChar); } @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.mPostDialHandler; 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() { synchronized(mPartialWakeLock) { if (mPartialWakeLock.isHeld()) { Rlog.d(LOG_TAG, "releaseWakeLock"); mPartialWakeLock.release(); } } } @Override public int getNumberPresentation() { return mNumberPresentation; } @Override public UUSInfo getUUSInfo() { return mUusInfo; } @Override public Connection getOrigConnection() { return null; } @Override public boolean isMultiparty() { return mImsCall != null && mImsCall.isMultiparty(); } /*package*/ ImsCall getImsCall() { return mImsCall; } /*package*/ void setImsCall(ImsCall imsCall) { mImsCall = imsCall; } /*package*/ void changeParent(ImsPhoneCall parent) { mParent = parent; } /** * @return {@code true} if the {@link ImsPhoneConnection} or its media capabilities have been * changed, and {@code false} otherwise. */ /*package*/ boolean update(ImsCall imsCall, ImsPhoneCall.State state) { if (state == ImsPhoneCall.State.ACTIVE) { 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 updateMediaCapabilities = updateMediaCapabilities(imsCall); return updateParent || updateMediaCapabilities; } @Override public int getPreciseDisconnectCause() { return 0; } /** * 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 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. */ private 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(); // The capabilities of the local device. ImsCallProfile localCallProfile = imsCall.getLocalCallProfile(); // The capabilities of the peer device. ImsCallProfile remoteCallProfile = imsCall.getRemoteCallProfile(); if (negotiatedCallProfile != null) { int callType = negotiatedCallProfile.mCallType; int newVideoState = ImsCallProfile.getVideoStateFromCallType(callType); if (getVideoState() != newVideoState) { setVideoState(newVideoState); changed = true; } } if (localCallProfile != null) { int callType = localCallProfile.mCallType; boolean newLocalVideoCapable = callType == ImsCallProfile.CALL_TYPE_VT; if (isLocalVideoCapable() != newLocalVideoCapable) { setLocalVideoCapable(newLocalVideoCapable); 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; } /** * Determines the {@link ImsPhoneConnection} audio quality based on the local and remote * {@link ImsCallProfile}. If indicate a HQ audio call if the local stream profile * indicates AMR_WB or EVRC_WB 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; } boolean isHighDef = (localCallProfile.mMediaProfile.mAudioQuality == ImsStreamMediaProfile.AUDIO_QUALITY_AMR_WB || localCallProfile.mMediaProfile.mAudioQuality == ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_WB) && 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(" address:"); sb.append(Log.pii(getAddress())); sb.append(" ImsCall:"); if (mImsCall == null) { sb.append("null"); } else { sb.append(mImsCall); } sb.append("]"); return sb.toString(); } }