/* * 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 java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.AsyncResult; import android.os.Handler; import android.os.Message; import android.os.Registrant; import android.os.RegistrantList; import android.os.RemoteException; import android.os.SystemProperties; import android.provider.Settings; import android.preference.PreferenceManager; import android.telecom.ConferenceParticipant; import android.telecom.VideoProfile; import android.telephony.DisconnectCause; import android.telephony.PhoneNumberUtils; import android.telephony.Rlog; import android.telephony.ServiceState; import com.android.ims.ImsCall; import com.android.ims.ImsCallProfile; import com.android.ims.ImsConfig; import com.android.ims.ImsConnectionStateListener; import com.android.ims.ImsEcbm; import com.android.ims.ImsException; import com.android.ims.ImsManager; import com.android.ims.ImsReasonInfo; import com.android.ims.ImsServiceClass; import com.android.ims.ImsUtInterface; import com.android.ims.internal.IImsVideoCallProvider; import com.android.ims.internal.ImsVideoCallProviderWrapper; import com.android.internal.telephony.Call; import com.android.internal.telephony.CallStateException; import com.android.internal.telephony.CallTracker; import com.android.internal.telephony.CommandException; import com.android.internal.telephony.CommandsInterface; import com.android.internal.telephony.Connection; import com.android.internal.telephony.Phone; import com.android.internal.telephony.PhoneBase; import com.android.internal.telephony.PhoneConstants; import com.android.internal.telephony.TelephonyProperties; /** * {@hide} */ public final class ImsPhoneCallTracker extends CallTracker { static final String LOG_TAG = "ImsPhoneCallTracker"; private static final boolean DBG = true; private boolean mIsVolteEnabled = false; private boolean mIsVtEnabled = false; private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(ImsManager.ACTION_IMS_INCOMING_CALL)) { if (DBG) log("onReceive : incoming call intent"); if (mImsManager == null) return; if (mServiceId < 0) return; try { // Network initiated USSD will be treated by mImsUssdListener boolean isUssd = intent.getBooleanExtra(ImsManager.EXTRA_USSD, false); if (isUssd) { if (DBG) log("onReceive : USSD"); mUssdSession = mImsManager.takeCall(mServiceId, intent, mImsUssdListener); if (mUssdSession != null) { mUssdSession.accept(ImsCallProfile.CALL_TYPE_VOICE); } return; } // Normal MT call ImsCall imsCall = mImsManager.takeCall(mServiceId, intent, mImsCallListener); ImsPhoneConnection conn = new ImsPhoneConnection(mPhone.getContext(), imsCall, ImsPhoneCallTracker.this, mRingingCall); addConnection(conn); IImsVideoCallProvider imsVideoCallProvider = imsCall.getCallSession().getVideoCallProvider(); if (imsVideoCallProvider != null) { ImsVideoCallProviderWrapper imsVideoCallProviderWrapper = new ImsVideoCallProviderWrapper(imsVideoCallProvider); conn.setVideoProvider(imsVideoCallProviderWrapper); } if ((mForegroundCall.getState() != ImsPhoneCall.State.IDLE) || (mBackgroundCall.getState() != ImsPhoneCall.State.IDLE)) { conn.update(imsCall, ImsPhoneCall.State.WAITING); } mPhone.notifyNewRingingConnection(conn); mPhone.notifyIncomingRing(); updatePhoneState(); mPhone.notifyPreciseCallStateChanged(); } catch (ImsException e) { loge("onReceive : exception " + e); } catch (RemoteException e) { } } } }; //***** Constants static final int MAX_CONNECTIONS = 7; static final int MAX_CONNECTIONS_PER_CALL = 5; private static final int EVENT_HANGUP_PENDINGMO = 18; private static final int EVENT_RESUME_BACKGROUND = 19; private static final int EVENT_DIAL_PENDINGMO = 20; private static final int TIMEOUT_HANGUP_PENDINGMO = 500; //***** Instance Variables private ArrayList mConnections = new ArrayList(); private RegistrantList mVoiceCallEndedRegistrants = new RegistrantList(); private RegistrantList mVoiceCallStartedRegistrants = new RegistrantList(); ImsPhoneCall mRingingCall = new ImsPhoneCall(this); ImsPhoneCall mForegroundCall = new ImsPhoneCall(this); ImsPhoneCall mBackgroundCall = new ImsPhoneCall(this); ImsPhoneCall mHandoverCall = new ImsPhoneCall(this); private ImsPhoneConnection mPendingMO; private int mClirMode = CommandsInterface.CLIR_DEFAULT; private Object mSyncHold = new Object(); private ImsCall mUssdSession = null; private Message mPendingUssd = null; ImsPhone mPhone; private boolean mDesiredMute = false; // false = mute off private boolean mOnHoldToneStarted = false; PhoneConstants.State mState = PhoneConstants.State.IDLE; private ImsManager mImsManager; private int mServiceId = -1; private Call.SrvccState mSrvccState = Call.SrvccState.NONE; private boolean mIsInEmergencyCall = false; private int pendingCallClirMode; private int pendingCallVideoState; private boolean pendingCallInEcm = false; private boolean mSwitchingFgAndBgCalls = false; private ImsCall mCallExpectedToResume = null; //***** Events //***** Constructors ImsPhoneCallTracker(ImsPhone phone) { this.mPhone = phone; IntentFilter intentfilter = new IntentFilter(); intentfilter.addAction(ImsManager.ACTION_IMS_INCOMING_CALL); mPhone.getContext().registerReceiver(mReceiver, intentfilter); Thread t = new Thread() { public void run() { getImsService(); } }; t.start(); } private PendingIntent createIncomingCallPendingIntent() { Intent intent = new Intent(ImsManager.ACTION_IMS_INCOMING_CALL); intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); return PendingIntent.getBroadcast(mPhone.getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } private void getImsService() { if (DBG) log("getImsService"); mImsManager = ImsManager.getInstance(mPhone.getContext(), mPhone.getPhoneId()); try { mServiceId = mImsManager.open(ImsServiceClass.MMTEL, createIncomingCallPendingIntent(), mImsConnectionStateListener); // Get the ECBM interface and set IMSPhone's listener object for notifications getEcbmInterface().setEcbmStateListener(mPhone.mImsEcbmStateListener); if (mPhone.isInEcm()) { // Call exit ECBM which will invoke onECBMExited mPhone.exitEmergencyCallbackMode(); } int mPreferredTtyMode = Settings.Secure.getInt( mPhone.getContext().getContentResolver(), Settings.Secure.PREFERRED_TTY_MODE, Phone.TTY_MODE_OFF); mImsManager.setUiTTYMode(mPhone.getContext(), mServiceId, mPreferredTtyMode, null); } catch (ImsException e) { loge("getImsService: " + e); //Leave mImsManager as null, then CallStateException will be thrown when dialing mImsManager = null; } } public void dispose() { if (DBG) log("dispose"); mRingingCall.dispose(); mBackgroundCall.dispose(); mForegroundCall.dispose(); mHandoverCall.dispose(); clearDisconnected(); mPhone.getContext().unregisterReceiver(mReceiver); } @Override protected void finalize() { log("ImsPhoneCallTracker finalized"); } //***** Instance Methods //***** Public Methods @Override public void registerForVoiceCallStarted(Handler h, int what, Object obj) { Registrant r = new Registrant(h, what, obj); mVoiceCallStartedRegistrants.add(r); } @Override public void unregisterForVoiceCallStarted(Handler h) { mVoiceCallStartedRegistrants.remove(h); } @Override public void registerForVoiceCallEnded(Handler h, int what, Object obj) { Registrant r = new Registrant(h, what, obj); mVoiceCallEndedRegistrants.add(r); } @Override public void unregisterForVoiceCallEnded(Handler h) { mVoiceCallEndedRegistrants.remove(h); } Connection dial(String dialString, int videoState) throws CallStateException { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mPhone.getContext()); int oirMode = sp.getInt(PhoneBase.CLIR_KEY, CommandsInterface.CLIR_DEFAULT); return dial(dialString, oirMode, videoState); } /** * oirMode is one of the CLIR_ constants */ synchronized Connection dial(String dialString, int clirMode, int videoState) throws CallStateException { boolean isPhoneInEcmMode = SystemProperties.getBoolean( TelephonyProperties.PROPERTY_INECM_MODE, false); boolean isEmergencyNumber = PhoneNumberUtils.isEmergencyNumber(dialString); if (DBG) log("dial clirMode=" + clirMode); // note that this triggers call state changed notif clearDisconnected(); if (mImsManager == null) { throw new CallStateException("service not available"); } if (!canDial()) { throw new CallStateException("cannot dial in current state"); } if (isPhoneInEcmMode && isEmergencyNumber) { handleEcmTimer(ImsPhone.CANCEL_ECM_TIMER); } boolean holdBeforeDial = false; // The new call must be assigned to the foreground call. // That call must be idle, so place anything that's // there on hold if (mForegroundCall.getState() == ImsPhoneCall.State.ACTIVE) { if (mBackgroundCall.getState() != ImsPhoneCall.State.IDLE) { //we should have failed in !canDial() above before we get here throw new CallStateException("cannot dial in current state"); } // foreground call is empty for the newly dialed connection holdBeforeDial = true; switchWaitingOrHoldingAndActive(); } ImsPhoneCall.State fgState = ImsPhoneCall.State.IDLE; ImsPhoneCall.State bgState = ImsPhoneCall.State.IDLE; mClirMode = clirMode; synchronized (mSyncHold) { if (holdBeforeDial) { fgState = mForegroundCall.getState(); bgState = mBackgroundCall.getState(); //holding foreground call failed if (fgState == ImsPhoneCall.State.ACTIVE) { throw new CallStateException("cannot dial in current state"); } //holding foreground call succeeded if (bgState == ImsPhoneCall.State.HOLDING) { holdBeforeDial = false; } } mPendingMO = new ImsPhoneConnection(mPhone.getContext(), checkForTestEmergencyNumber(dialString), this, mForegroundCall); } addConnection(mPendingMO); if (!holdBeforeDial) { if ((!isPhoneInEcmMode) || (isPhoneInEcmMode && isEmergencyNumber)) { dialInternal(mPendingMO, clirMode, videoState); } else { try { getEcbmInterface().exitEmergencyCallbackMode(); } catch (ImsException e) { e.printStackTrace(); throw new CallStateException("service not available"); } mPhone.setOnEcbModeExitResponse(this, EVENT_EXIT_ECM_RESPONSE_CDMA, null); pendingCallClirMode = clirMode; pendingCallVideoState = videoState; pendingCallInEcm = true; } } updatePhoneState(); mPhone.notifyPreciseCallStateChanged(); return mPendingMO; } private void handleEcmTimer(int action) { mPhone.handleTimerInEmergencyCallbackMode(action); switch (action) { case ImsPhone.CANCEL_ECM_TIMER: break; case ImsPhone.RESTART_ECM_TIMER: break; default: log("handleEcmTimer, unsupported action " + action); } } private void dialInternal(ImsPhoneConnection conn, int clirMode, int videoState) { if (conn == null) { return; } if (conn.getAddress()== null || conn.getAddress().length() == 0 || conn.getAddress().indexOf(PhoneNumberUtils.WILD) >= 0) { // Phone number is invalid conn.setDisconnectCause(DisconnectCause.INVALID_NUMBER); sendEmptyMessageDelayed(EVENT_HANGUP_PENDINGMO, TIMEOUT_HANGUP_PENDINGMO); return; } // Always unmute when initiating a new call setMute(false); int serviceType = PhoneNumberUtils.isEmergencyNumber(conn.getAddress()) ? ImsCallProfile.SERVICE_TYPE_EMERGENCY : ImsCallProfile.SERVICE_TYPE_NORMAL; int callType = ImsCallProfile.getCallTypeFromVideoState(videoState); //TODO(vt): Is this sufficient? At what point do we know the video state of the call? conn.setVideoState(videoState); try { String[] callees = new String[] { conn.getAddress() }; ImsCallProfile profile = mImsManager.createCallProfile(mServiceId, serviceType, callType); profile.setCallExtraInt(ImsCallProfile.EXTRA_OIR, clirMode); ImsCall imsCall = mImsManager.makeCall(mServiceId, profile, callees, mImsCallListener); conn.setImsCall(imsCall); IImsVideoCallProvider imsVideoCallProvider = imsCall.getCallSession().getVideoCallProvider(); if (imsVideoCallProvider != null) { ImsVideoCallProviderWrapper imsVideoCallProviderWrapper = new ImsVideoCallProviderWrapper(imsVideoCallProvider); conn.setVideoProvider(imsVideoCallProviderWrapper); } } catch (ImsException e) { loge("dialInternal : " + e); conn.setDisconnectCause(DisconnectCause.ERROR_UNSPECIFIED); sendEmptyMessageDelayed(EVENT_HANGUP_PENDINGMO, TIMEOUT_HANGUP_PENDINGMO); } catch (RemoteException e) { } } /** * Accepts a call with the specified video state. The video state is the video state that the * user has agreed upon in the InCall UI. * * @param videoState The video State * @throws CallStateException */ void acceptCall (int videoState) throws CallStateException { if (DBG) log("acceptCall"); if (mForegroundCall.getState().isAlive() && mBackgroundCall.getState().isAlive()) { throw new CallStateException("cannot accept call"); } if ((mRingingCall.getState() == ImsPhoneCall.State.WAITING) && mForegroundCall.getState().isAlive()) { setMute(false); switchWaitingOrHoldingAndActive(); } else if (mRingingCall.getState().isRinging()) { if (DBG) log("acceptCall: incoming..."); // Always unmute when answering a new call setMute(false); try { ImsCall imsCall = mRingingCall.getImsCall(); if (imsCall != null) { imsCall.accept(ImsCallProfile.getCallTypeFromVideoState(videoState)); } else { throw new CallStateException("no valid ims call"); } } catch (ImsException e) { throw new CallStateException("cannot accept call"); } } else { throw new CallStateException("phone not ringing"); } } void rejectCall () throws CallStateException { if (DBG) log("rejectCall"); if (mRingingCall.getState().isRinging()) { hangup(mRingingCall); } else { throw new CallStateException("phone not ringing"); } } void switchWaitingOrHoldingAndActive() throws CallStateException { if (DBG) log("switchWaitingOrHoldingAndActive"); if (mRingingCall.getState() == ImsPhoneCall.State.INCOMING) { throw new CallStateException("cannot be in the incoming state"); } if (mForegroundCall.getState() == ImsPhoneCall.State.ACTIVE) { ImsCall imsCall = mForegroundCall.getImsCall(); if (imsCall == null) { throw new CallStateException("no ims call"); } // Swap the ImsCalls pointed to by the foreground and background ImsPhoneCalls. // If hold or resume later fails, we will swap them back. mSwitchingFgAndBgCalls = true; mCallExpectedToResume = mBackgroundCall.getImsCall(); mForegroundCall.switchWith(mBackgroundCall); // Hold the foreground call; once the foreground call is held, the background call will // be resumed. try { imsCall.hold(); } catch (ImsException e) { mForegroundCall.switchWith(mBackgroundCall); throw new CallStateException(e.getMessage()); } } else if (mBackgroundCall.getState() == ImsPhoneCall.State.HOLDING) { resumeWaitingOrHolding(); } } void conference() { if (DBG) log("conference"); ImsCall fgImsCall = mForegroundCall.getImsCall(); if (fgImsCall == null) { log("conference no foreground ims call"); return; } ImsCall bgImsCall = mBackgroundCall.getImsCall(); if (bgImsCall == null) { log("conference no background ims call"); return; } // Keep track of the connect time of the earliest call so that it can be set on the // {@code ImsConference} when it is created. long conferenceConnectTime = Math.min(mForegroundCall.getEarliestConnectTime(), mBackgroundCall.getEarliestConnectTime()); ImsPhoneConnection foregroundConnection = mForegroundCall.getFirstConnection(); if (foregroundConnection != null) { foregroundConnection.setConferenceConnectTime(conferenceConnectTime); } try { fgImsCall.merge(bgImsCall); } catch (ImsException e) { log("conference " + e.getMessage()); } } void explicitCallTransfer() { //TODO : implement } void clearDisconnected() { if (DBG) log("clearDisconnected"); internalClearDisconnected(); updatePhoneState(); mPhone.notifyPreciseCallStateChanged(); } boolean canConference() { return mForegroundCall.getState() == ImsPhoneCall.State.ACTIVE && mBackgroundCall.getState() == ImsPhoneCall.State.HOLDING && !mBackgroundCall.isFull() && !mForegroundCall.isFull(); } boolean canDial() { boolean ret; int serviceState = mPhone.getServiceState().getState(); String disableCall = SystemProperties.get( TelephonyProperties.PROPERTY_DISABLE_CALL, "false"); ret = (serviceState != ServiceState.STATE_POWER_OFF) && mPendingMO == null && !mRingingCall.isRinging() && !disableCall.equals("true") && (!mForegroundCall.getState().isAlive() || !mBackgroundCall.getState().isAlive()); return ret; } boolean canTransfer() { return mForegroundCall.getState() == ImsPhoneCall.State.ACTIVE && mBackgroundCall.getState() == ImsPhoneCall.State.HOLDING; } //***** Private Instance Methods private void internalClearDisconnected() { mRingingCall.clearDisconnected(); mForegroundCall.clearDisconnected(); mBackgroundCall.clearDisconnected(); mHandoverCall.clearDisconnected(); } private void updatePhoneState() { PhoneConstants.State oldState = mState; if (mRingingCall.isRinging()) { mState = PhoneConstants.State.RINGING; } else if (mPendingMO != null || !(mForegroundCall.isIdle() && mBackgroundCall.isIdle())) { mState = PhoneConstants.State.OFFHOOK; } else { mState = PhoneConstants.State.IDLE; } if (mState == PhoneConstants.State.IDLE && oldState != mState) { mVoiceCallEndedRegistrants.notifyRegistrants( new AsyncResult(null, null, null)); } else if (oldState == PhoneConstants.State.IDLE && oldState != mState) { mVoiceCallStartedRegistrants.notifyRegistrants ( new AsyncResult(null, null, null)); } if (DBG) log("updatePhoneState oldState=" + oldState + ", newState=" + mState); if (mState != oldState) { mPhone.notifyPhoneStateChanged(); } } private void handleRadioNotAvailable() { // handlePollCalls will clear out its // call list when it gets the CommandException // error result from this pollCallsWhenSafe(); } private void dumpState() { List l; log("Phone State:" + mState); log("Ringing call: " + mRingingCall.toString()); l = mRingingCall.getConnections(); for (int i = 0, s = l.size(); i < s; i++) { log(l.get(i).toString()); } log("Foreground call: " + mForegroundCall.toString()); l = mForegroundCall.getConnections(); for (int i = 0, s = l.size(); i < s; i++) { log(l.get(i).toString()); } log("Background call: " + mBackgroundCall.toString()); l = mBackgroundCall.getConnections(); for (int i = 0, s = l.size(); i < s; i++) { log(l.get(i).toString()); } } //***** Called from ImsPhone void setUiTTYMode(int uiTtyMode, Message onComplete) { try { mImsManager.setUiTTYMode(mPhone.getContext(), mServiceId, uiTtyMode, onComplete); } catch (ImsException e) { loge("setTTYMode : " + e); mPhone.sendErrorResponse(onComplete, e); } } /*package*/ void setMute(boolean mute) { mDesiredMute = mute; mForegroundCall.setMute(mute); } /*package*/ boolean getMute() { return mDesiredMute; } /* package */ void sendDtmf(char c, Message result) { if (DBG) log("sendDtmf"); ImsCall imscall = mForegroundCall.getImsCall(); if (imscall != null) { imscall.sendDtmf(c, result); } } /*package*/ void startDtmf(char c) { if (DBG) log("startDtmf"); ImsCall imscall = mForegroundCall.getImsCall(); if (imscall != null) { imscall.startDtmf(c); } else { loge("startDtmf : no foreground call"); } } /*package*/ void stopDtmf() { if (DBG) log("stopDtmf"); ImsCall imscall = mForegroundCall.getImsCall(); if (imscall != null) { imscall.stopDtmf(); } else { loge("stopDtmf : no foreground call"); } } //***** Called from ImsPhoneConnection /*package*/ void hangup (ImsPhoneConnection conn) throws CallStateException { if (DBG) log("hangup connection"); if (conn.getOwner() != this) { throw new CallStateException ("ImsPhoneConnection " + conn + "does not belong to ImsPhoneCallTracker " + this); } hangup(conn.getCall()); } //***** Called from ImsPhoneCall /* package */ void hangup (ImsPhoneCall call) throws CallStateException { if (DBG) log("hangup call"); if (call.getConnections().size() == 0) { throw new CallStateException("no connections"); } ImsCall imsCall = call.getImsCall(); boolean rejectCall = false; if (call == mRingingCall) { if (Phone.DEBUG_PHONE) log("(ringing) hangup incoming"); rejectCall = true; } else if (call == mForegroundCall) { if (call.isDialingOrAlerting()) { if (Phone.DEBUG_PHONE) { log("(foregnd) hangup dialing or alerting..."); } } else { if (Phone.DEBUG_PHONE) { log("(foregnd) hangup foreground"); } //held call will be resumed by onCallTerminated } } else if (call == mBackgroundCall) { if (Phone.DEBUG_PHONE) { log("(backgnd) hangup waiting or background"); } } else { throw new CallStateException ("ImsPhoneCall " + call + "does not belong to ImsPhoneCallTracker " + this); } call.onHangupLocal(); try { if (imsCall != null) { if (rejectCall) imsCall.reject(ImsReasonInfo.CODE_USER_DECLINE); else imsCall.terminate(ImsReasonInfo.CODE_USER_TERMINATED); } else if (mPendingMO != null && call == mForegroundCall) { // is holding a foreground call mPendingMO.update(null, ImsPhoneCall.State.DISCONNECTED); mPendingMO.onDisconnect(); removeConnection(mPendingMO); mPendingMO = null; updatePhoneState(); removeMessages(EVENT_DIAL_PENDINGMO); } } catch (ImsException e) { throw new CallStateException(e.getMessage()); } mPhone.notifyPreciseCallStateChanged(); } void callEndCleanupHandOverCallIfAny() { if (mHandoverCall.mConnections.size() > 0) { if (DBG) log("callEndCleanupHandOverCallIfAny, mHandoverCall.mConnections=" + mHandoverCall.mConnections); mHandoverCall.mConnections.clear(); mState = PhoneConstants.State.IDLE; } } /* package */ void resumeWaitingOrHolding() throws CallStateException { if (DBG) log("resumeWaitingOrHolding"); try { if (mForegroundCall.getState().isAlive()) { //resume foreground call after holding background call //they were switched before holding ImsCall imsCall = mForegroundCall.getImsCall(); if (imsCall != null) imsCall.resume(); } else if (mRingingCall.getState() == ImsPhoneCall.State.WAITING) { //accept waiting call after holding background call ImsCall imsCall = mRingingCall.getImsCall(); if (imsCall != null) imsCall.accept(ImsCallProfile.CALL_TYPE_VOICE); } else { //Just resume background call. //To distinguish resuming call with swapping calls //we do not switch calls.here //ImsPhoneConnection.update will chnage the parent when completed ImsCall imsCall = mBackgroundCall.getImsCall(); if (imsCall != null) imsCall.resume(); } } catch (ImsException e) { throw new CallStateException(e.getMessage()); } } /* package */ void sendUSSD (String ussdString, Message response) { if (DBG) log("sendUSSD"); try { if (mUssdSession != null) { mUssdSession.sendUssd(ussdString); AsyncResult.forMessage(response, null, null); response.sendToTarget(); return; } String[] callees = new String[] { ussdString }; ImsCallProfile profile = mImsManager.createCallProfile(mServiceId, ImsCallProfile.SERVICE_TYPE_NORMAL, ImsCallProfile.CALL_TYPE_VOICE); profile.setCallExtraInt(ImsCallProfile.EXTRA_DIALSTRING, ImsCallProfile.DIALSTRING_USSD); mUssdSession = mImsManager.makeCall(mServiceId, profile, callees, mImsUssdListener); } catch (ImsException e) { loge("sendUSSD : " + e); mPhone.sendErrorResponse(response, e); } } /* package */ void cancelUSSD() { if (mUssdSession == null) return; try { mUssdSession.terminate(ImsReasonInfo.CODE_USER_TERMINATED); } catch (ImsException e) { } } private synchronized ImsPhoneConnection findConnection(ImsCall imsCall) { for (ImsPhoneConnection conn : mConnections) { if (conn.getImsCall() == imsCall) { return conn; } } return null; } private synchronized void removeConnection(ImsPhoneConnection conn) { mConnections.remove(conn); } private synchronized void addConnection(ImsPhoneConnection conn) { mConnections.add(conn); } private void processCallStateChange(ImsCall imsCall, ImsPhoneCall.State state, int cause) { if (DBG) log("processCallStateChange " + imsCall + " state=" + state + " cause=" + cause); if (imsCall == null) return; boolean changed = false; ImsPhoneConnection conn = findConnection(imsCall); if (conn == null) { // TODO : what should be done? return; } changed = conn.update(imsCall, state); if (state == ImsPhoneCall.State.DISCONNECTED) { changed = conn.onDisconnect(cause) || changed; //detach the disconnected connections conn.getCall().detach(conn); removeConnection(conn); } if (changed) { if (conn.getCall() == mHandoverCall) return; updatePhoneState(); mPhone.notifyPreciseCallStateChanged(); } } private int getDisconnectCauseFromReasonInfo(ImsReasonInfo reasonInfo) { int cause = DisconnectCause.ERROR_UNSPECIFIED; //int type = reasonInfo.getReasonType(); int code = reasonInfo.getCode(); switch (code) { case ImsReasonInfo.CODE_SIP_BAD_ADDRESS: case ImsReasonInfo.CODE_SIP_NOT_REACHABLE: return DisconnectCause.NUMBER_UNREACHABLE; case ImsReasonInfo.CODE_SIP_BUSY: return DisconnectCause.BUSY; case ImsReasonInfo.CODE_USER_TERMINATED: return DisconnectCause.LOCAL; case ImsReasonInfo.CODE_LOCAL_CALL_DECLINE: return DisconnectCause.INCOMING_REJECTED; case ImsReasonInfo.CODE_USER_TERMINATED_BY_REMOTE: return DisconnectCause.NORMAL; case ImsReasonInfo.CODE_SIP_REDIRECTED: case ImsReasonInfo.CODE_SIP_BAD_REQUEST: case ImsReasonInfo.CODE_SIP_FORBIDDEN: case ImsReasonInfo.CODE_SIP_NOT_ACCEPTABLE: case ImsReasonInfo.CODE_SIP_USER_REJECTED: case ImsReasonInfo.CODE_SIP_GLOBAL_ERROR: return DisconnectCause.SERVER_ERROR; case ImsReasonInfo.CODE_SIP_SERVICE_UNAVAILABLE: case ImsReasonInfo.CODE_SIP_NOT_FOUND: case ImsReasonInfo.CODE_SIP_SERVER_ERROR: return DisconnectCause.SERVER_UNREACHABLE; case ImsReasonInfo.CODE_LOCAL_NETWORK_ROAMING: case ImsReasonInfo.CODE_LOCAL_NETWORK_IP_CHANGED: case ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN: case ImsReasonInfo.CODE_LOCAL_SERVICE_UNAVAILABLE: case ImsReasonInfo.CODE_LOCAL_NOT_REGISTERED: case ImsReasonInfo.CODE_LOCAL_NETWORK_NO_LTE_COVERAGE: case ImsReasonInfo.CODE_LOCAL_NETWORK_NO_SERVICE: case ImsReasonInfo.CODE_LOCAL_CALL_VCC_ON_PROGRESSING: return DisconnectCause.OUT_OF_SERVICE; case ImsReasonInfo.CODE_SIP_REQUEST_TIMEOUT: case ImsReasonInfo.CODE_TIMEOUT_1XX_WAITING: case ImsReasonInfo.CODE_TIMEOUT_NO_ANSWER: case ImsReasonInfo.CODE_TIMEOUT_NO_ANSWER_CALL_UPDATE: return DisconnectCause.TIMED_OUT; case ImsReasonInfo.CODE_LOCAL_LOW_BATTERY: case ImsReasonInfo.CODE_LOCAL_POWER_OFF: return DisconnectCause.POWER_OFF; default: } return cause; } /** * Listen to the IMS call state change */ private ImsCall.Listener mImsCallListener = new ImsCall.Listener() { @Override public void onCallProgressing(ImsCall imsCall) { if (DBG) log("onCallProgressing"); mPendingMO = null; processCallStateChange(imsCall, ImsPhoneCall.State.ALERTING, DisconnectCause.NOT_DISCONNECTED); } @Override public void onCallStarted(ImsCall imsCall) { if (DBG) log("onCallStarted"); mPendingMO = null; processCallStateChange(imsCall, ImsPhoneCall.State.ACTIVE, DisconnectCause.NOT_DISCONNECTED); } /** * onCallStartFailed will be invoked when: * case 1) Dialing fails * case 2) Ringing call is disconnected by local or remote user */ @Override public void onCallStartFailed(ImsCall imsCall, ImsReasonInfo reasonInfo) { if (DBG) log("onCallStartFailed reasonCode=" + reasonInfo.getCode()); if (mPendingMO != null) { // To initiate dialing circuit-switched call if (reasonInfo.getCode() == ImsReasonInfo.CODE_LOCAL_CALL_CS_RETRY_REQUIRED && mBackgroundCall.getState() == ImsPhoneCall.State.IDLE && mRingingCall.getState() == ImsPhoneCall.State.IDLE) { mForegroundCall.detach(mPendingMO); removeConnection(mPendingMO); mPendingMO.finalize(); mPendingMO = null; mPhone.initiateSilentRedial(); return; } else { int cause = getDisconnectCauseFromReasonInfo(reasonInfo); processCallStateChange(imsCall, ImsPhoneCall.State.DISCONNECTED, cause); } mPendingMO = null; } } @Override public void onCallTerminated(ImsCall imsCall, ImsReasonInfo reasonInfo) { if (DBG) log("onCallTerminated reasonCode=" + reasonInfo.getCode()); ImsPhoneCall.State oldState = mForegroundCall.getState(); int cause = getDisconnectCauseFromReasonInfo(reasonInfo); ImsPhoneConnection conn = findConnection(imsCall); if (DBG) log("cause = " + cause + " conn = " + conn); if (conn != null && conn.isIncoming() && conn.getConnectTime() == 0) { // Missed if (cause == DisconnectCause.NORMAL) { cause = DisconnectCause.INCOMING_MISSED; } if (DBG) log("Incoming connection of 0 connect time detected - translated cause = " + cause); } if (cause == DisconnectCause.NORMAL && conn != null && conn.getImsCall().isMerged()) { // Call was terminated while it is merged instead of a remote disconnect. cause = DisconnectCause.IMS_MERGED_SUCCESSFULLY; } processCallStateChange(imsCall, ImsPhoneCall.State.DISCONNECTED, cause); } @Override public void onCallHeld(ImsCall imsCall) { if (DBG) log("onCallHeld"); synchronized (mSyncHold) { ImsPhoneCall.State oldState = mBackgroundCall.getState(); processCallStateChange(imsCall, ImsPhoneCall.State.HOLDING, DisconnectCause.NOT_DISCONNECTED); // Note: If we're performing a switchWaitingOrHoldingAndActive, the call to // processCallStateChange above may have caused the mBackgroundCall and // mForegroundCall references below to change meaning. Watch out for this if you // are reading through this code. if (oldState == ImsPhoneCall.State.ACTIVE) { // Note: This case comes up when we have just held a call in response to a // switchWaitingOrHoldingAndActive. We now need to resume the background call. // The EVENT_RESUME_BACKGROUND causes resumeWaitingOrHolding to be called. if ((mForegroundCall.getState() == ImsPhoneCall.State.HOLDING) || (mRingingCall.getState() == ImsPhoneCall.State.WAITING)) { sendEmptyMessage(EVENT_RESUME_BACKGROUND); } else { //when multiple connections belong to background call, //only the first callback reaches here //otherwise the oldState is already HOLDING if (mPendingMO != null) { sendEmptyMessage(EVENT_DIAL_PENDINGMO); } // In this case there will be no call resumed, so we can assume that we // are done switching fg and bg calls now. // This may happen if there is no BG call and we are holding a call so that // we can dial another one. mSwitchingFgAndBgCalls = false; } } } } @Override public void onCallHoldFailed(ImsCall imsCall, ImsReasonInfo reasonInfo) { if (DBG) log("onCallHoldFailed reasonCode=" + reasonInfo.getCode()); synchronized (mSyncHold) { ImsPhoneCall.State bgState = mBackgroundCall.getState(); if (reasonInfo.getCode() == ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED) { // disconnected while processing hold if (mPendingMO != null) { sendEmptyMessage(EVENT_DIAL_PENDINGMO); } } else if (bgState == ImsPhoneCall.State.ACTIVE) { mForegroundCall.switchWith(mBackgroundCall); if (mPendingMO != null) { mPendingMO.setDisconnectCause(DisconnectCause.ERROR_UNSPECIFIED); sendEmptyMessageDelayed(EVENT_HANGUP_PENDINGMO, TIMEOUT_HANGUP_PENDINGMO); } } } } @Override public void onCallResumed(ImsCall imsCall) { if (DBG) log("onCallResumed"); // If we are the in midst of swapping FG and BG calls and the call we end up resuming // is not the one we expected, we likely had a resume failure and we need to swap the // FG and BG calls back. if (mSwitchingFgAndBgCalls && imsCall != mCallExpectedToResume) { mForegroundCall.switchWith(mBackgroundCall); mSwitchingFgAndBgCalls = false; mCallExpectedToResume = null; } processCallStateChange(imsCall, ImsPhoneCall.State.ACTIVE, DisconnectCause.NOT_DISCONNECTED); } @Override public void onCallResumeFailed(ImsCall imsCall, ImsReasonInfo reasonInfo) { // TODO : What should be done? // If we are in the midst of swapping the FG and BG calls and we got a resume fail, we // need to swap back the FG and BG calls. if (mSwitchingFgAndBgCalls && imsCall == mCallExpectedToResume) { mForegroundCall.switchWith(mBackgroundCall); mCallExpectedToResume = null; mSwitchingFgAndBgCalls = false; } mPhone.notifySuppServiceFailed(Phone.SuppService.RESUME); } @Override public void onCallResumeReceived(ImsCall imsCall) { if (DBG) log("onCallResumeReceived"); if (mOnHoldToneStarted) { mPhone.stopOnHoldTone(); mOnHoldToneStarted = false; } } @Override public void onCallHoldReceived(ImsCall imsCall) { if (DBG) log("onCallHoldReceived"); ImsPhoneConnection conn = findConnection(imsCall); if (conn != null && conn.getState() == ImsPhoneCall.State.ACTIVE) { if (!mOnHoldToneStarted && ImsPhoneCall.isLocalTone(imsCall)) { mPhone.startOnHoldTone(); mOnHoldToneStarted = true; } } } @Override public void onCallMerged(ImsCall call, boolean swapCalls) { if (DBG) log("onCallMerged"); mForegroundCall.merge(mBackgroundCall, mForegroundCall.getState()); if (swapCalls) { try { switchWaitingOrHoldingAndActive(); } catch (CallStateException e) { if (Phone.DEBUG_PHONE) { loge("Failed swap fg and bg calls on merge exception=" + e); } } } updatePhoneState(); mPhone.notifyPreciseCallStateChanged(); } @Override public void onCallMergeFailed(ImsCall call, ImsReasonInfo reasonInfo) { if (DBG) log("onCallMergeFailed reasonInfo=" + reasonInfo); mPhone.notifySuppServiceFailed(Phone.SuppService.CONFERENCE); } /** * Called when the state of IMS conference participant(s) has changed. * * @param call the call object that carries out the IMS call. * @param participants the participant(s) and their new state information. */ @Override public void onConferenceParticipantsStateChanged(ImsCall call, List participants) { if (DBG) log("onConferenceParticipantsStateChanged"); ImsPhoneConnection conn = findConnection(call); if (conn != null) { conn.updateConferenceParticipants(participants); } } @Override public void onCallSessionTtyModeReceived(ImsCall call, int mode) { mPhone.onTtyModeReceived(mode); } }; /** * Listen to the IMS call state change */ private ImsCall.Listener mImsUssdListener = new ImsCall.Listener() { @Override public void onCallStarted(ImsCall imsCall) { if (DBG) log("mImsUssdListener onCallStarted"); if (imsCall == mUssdSession) { if (mPendingUssd != null) { AsyncResult.forMessage(mPendingUssd); mPendingUssd.sendToTarget(); mPendingUssd = null; } } } @Override public void onCallStartFailed(ImsCall imsCall, ImsReasonInfo reasonInfo) { if (DBG) log("mImsUssdListener onCallStartFailed reasonCode=" + reasonInfo.getCode()); onCallTerminated(imsCall, reasonInfo); } @Override public void onCallTerminated(ImsCall imsCall, ImsReasonInfo reasonInfo) { if (DBG) log("mImsUssdListener onCallTerminated reasonCode=" + reasonInfo.getCode()); if (imsCall == mUssdSession) { mUssdSession = null; if (mPendingUssd != null) { CommandException ex = new CommandException(CommandException.Error.GENERIC_FAILURE); AsyncResult.forMessage(mPendingUssd, null, ex); mPendingUssd.sendToTarget(); mPendingUssd = null; } } imsCall.close(); } @Override public void onCallUssdMessageReceived(ImsCall call, int mode, String ussdMessage) { if (DBG) log("mImsUssdListener onCallUssdMessageReceived mode=" + mode); int ussdMode = -1; switch(mode) { case ImsCall.USSD_MODE_REQUEST: ussdMode = CommandsInterface.USSD_MODE_REQUEST; break; case ImsCall.USSD_MODE_NOTIFY: ussdMode = CommandsInterface.USSD_MODE_NOTIFY; break; } mPhone.onIncomingUSSD(ussdMode, ussdMessage); } }; /** * Listen to the IMS service state change * */ private ImsConnectionStateListener mImsConnectionStateListener = new ImsConnectionStateListener() { @Override public void onImsConnected() { if (DBG) log("onImsConnected"); mPhone.setServiceState(ServiceState.STATE_IN_SERVICE); mPhone.setImsRegistered(true); } @Override public void onImsDisconnected() { if (DBG) log("onImsDisconnected"); mPhone.setServiceState(ServiceState.STATE_OUT_OF_SERVICE); mPhone.setImsRegistered(false); } @Override public void onImsResumed() { if (DBG) log("onImsResumed"); mPhone.setServiceState(ServiceState.STATE_IN_SERVICE); } @Override public void onImsSuspended() { if (DBG) log("onImsSuspended"); mPhone.setServiceState(ServiceState.STATE_OUT_OF_SERVICE); } @Override public void onFeatureCapabilityChanged(int serviceClass, int[] enabledFeatures, int[] disabledFeatures) { if (serviceClass == ImsServiceClass.MMTEL) { if (enabledFeatures[ImsConfig.FeatureConstants.FEATURE_TYPE_VOICE_OVER_LTE] == ImsConfig.FeatureConstants.FEATURE_TYPE_VOICE_OVER_LTE) { mIsVolteEnabled = true; } if (enabledFeatures[ImsConfig.FeatureConstants.FEATURE_TYPE_VIDEO_OVER_LTE] == ImsConfig.FeatureConstants.FEATURE_TYPE_VIDEO_OVER_LTE) { mIsVtEnabled = true; } if (disabledFeatures[ImsConfig.FeatureConstants.FEATURE_TYPE_VOICE_OVER_LTE] == ImsConfig.FeatureConstants.FEATURE_TYPE_VOICE_OVER_LTE) { mIsVolteEnabled = false; } if (disabledFeatures[ImsConfig.FeatureConstants.FEATURE_TYPE_VIDEO_OVER_LTE] == ImsConfig.FeatureConstants.FEATURE_TYPE_VIDEO_OVER_LTE) { mIsVtEnabled = false; } } if (DBG) log("onFeatureCapabilityChanged, mIsVolteEnabled = " + mIsVolteEnabled + " mIsVtEnabled = " + mIsVtEnabled); } }; /* package */ ImsUtInterface getUtInterface() throws ImsException { if (mImsManager == null) { throw new ImsException("no ims manager", ImsReasonInfo.CODE_UNSPECIFIED); } ImsUtInterface ut = mImsManager.getSupplementaryServiceConfiguration(mServiceId); return ut; } private void transferHandoverConnections(ImsPhoneCall call) { if (call.mConnections != null) { for (Connection c : call.mConnections) { c.mPreHandoverState = call.mState; log ("Connection state before handover is " + c.getStateBeforeHandover()); } } if (mHandoverCall.mConnections == null ) { mHandoverCall.mConnections = call.mConnections; } else { // Multi-call SRVCC mHandoverCall.mConnections.addAll(call.mConnections); } if (mHandoverCall.mConnections != null) { if (call.getImsCall() != null) { call.getImsCall().close(); } for (Connection c : mHandoverCall.mConnections) { ((ImsPhoneConnection)c).changeParent(mHandoverCall); ((ImsPhoneConnection)c).releaseWakeLock(); } } if (call.getState().isAlive()) { log ("Call is alive and state is " + call.mState); mHandoverCall.mState = call.mState; } call.mConnections.clear(); call.mState = ImsPhoneCall.State.IDLE; } /* package */ void notifySrvccState(Call.SrvccState state) { if (DBG) log("notifySrvccState state=" + state); mSrvccState = state; if (mSrvccState == Call.SrvccState.COMPLETED) { transferHandoverConnections(mForegroundCall); transferHandoverConnections(mBackgroundCall); transferHandoverConnections(mRingingCall); } } //****** Overridden from Handler @Override public void handleMessage (Message msg) { AsyncResult ar; if (DBG) log("handleMessage what=" + msg.what); switch (msg.what) { case EVENT_HANGUP_PENDINGMO: if (mPendingMO != null) { mPendingMO.onDisconnect(); removeConnection(mPendingMO); mPendingMO = null; } updatePhoneState(); mPhone.notifyPreciseCallStateChanged(); break; case EVENT_RESUME_BACKGROUND: try { resumeWaitingOrHolding(); } catch (CallStateException e) { if (Phone.DEBUG_PHONE) { loge("handleMessage EVENT_RESUME_BACKGROUND exception=" + e); } } break; case EVENT_DIAL_PENDINGMO: dialInternal(mPendingMO, mClirMode, VideoProfile.VideoState.AUDIO_ONLY); break; case EVENT_EXIT_ECM_RESPONSE_CDMA: // no matter the result, we still do the same here if (pendingCallInEcm) { dialInternal(mPendingMO, pendingCallClirMode, pendingCallVideoState); pendingCallInEcm = false; } mPhone.unsetOnEcbModeExitResponse(this); break; } } @Override protected void log(String msg) { Rlog.d(LOG_TAG, "[ImsPhoneCallTracker] " + msg); } protected void loge(String msg) { Rlog.e(LOG_TAG, "[ImsPhoneCallTracker] " + msg); } @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("ImsPhoneCallTracker extends:"); super.dump(fd, pw, args); pw.println(" mVoiceCallEndedRegistrants=" + mVoiceCallEndedRegistrants); pw.println(" mVoiceCallStartedRegistrants=" + mVoiceCallStartedRegistrants); pw.println(" mRingingCall=" + mRingingCall); pw.println(" mForegroundCall=" + mForegroundCall); pw.println(" mBackgroundCall=" + mBackgroundCall); pw.println(" mHandoverCall=" + mHandoverCall); pw.println(" mPendingMO=" + mPendingMO); //pw.println(" mHangupPendingMO=" + mHangupPendingMO); pw.println(" mPhone=" + mPhone); pw.println(" mDesiredMute=" + mDesiredMute); pw.println(" mState=" + mState); } @Override protected void handlePollCalls(AsyncResult ar) { } /* package */ ImsEcbm getEcbmInterface() throws ImsException { if (mImsManager == null) { throw new ImsException("no ims manager", ImsReasonInfo.CODE_UNSPECIFIED); } ImsEcbm ecbm = mImsManager.getEcbmInterface(mServiceId); return ecbm; } public boolean isInEmergencyCall() { return mIsInEmergencyCall; } public boolean isVolteEnabled() { return mIsVolteEnabled; } public boolean isVtEnabled() { return mIsVtEnabled; } @Override public PhoneConstants.State getState() { return mState; } }