/* * Copyright (C) 2014 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.services.telephony; import android.net.Uri; import android.os.AsyncResult; import android.os.Handler; import android.os.Message; import android.telecom.AudioState; import android.telecom.Conference; import android.telecom.ConferenceParticipant; import android.telecom.Connection; import android.telecom.PhoneAccount; import android.telecom.PhoneCapabilities; import com.android.internal.telephony.Call; import com.android.internal.telephony.CallStateException; import com.android.internal.telephony.Connection.PostDialListener; import com.android.internal.telephony.Phone; import com.android.internal.telephony.imsphone.ImsPhoneConnection; import java.lang.Override; import java.util.Objects; /** * Base class for CDMA and GSM connections. */ abstract class TelephonyConnection extends Connection { private static final int MSG_PRECISE_CALL_STATE_CHANGED = 1; private static final int MSG_RINGBACK_TONE = 2; private static final int MSG_HANDOVER_STATE_CHANGED = 3; private static final int MSG_DISCONNECT = 4; private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_PRECISE_CALL_STATE_CHANGED: Log.v(TelephonyConnection.this, "MSG_PRECISE_CALL_STATE_CHANGED"); updateState(); break; case MSG_HANDOVER_STATE_CHANGED: Log.v(TelephonyConnection.this, "MSG_HANDOVER_STATE_CHANGED"); AsyncResult ar = (AsyncResult) msg.obj; com.android.internal.telephony.Connection connection = (com.android.internal.telephony.Connection) ar.result; setOriginalConnection(connection); break; case MSG_RINGBACK_TONE: Log.v(TelephonyConnection.this, "MSG_RINGBACK_TONE"); // TODO: This code assumes that there is only one connection in the foreground // call, in other words, it punts on network-mediated conference calling. if (getOriginalConnection() != getForegroundConnection()) { Log.v(TelephonyConnection.this, "handleMessage, original connection is " + "not foreground connection, skipping"); return; } setRingbackRequested((Boolean) ((AsyncResult) msg.obj).result); break; case MSG_DISCONNECT: updateState(); break; } } }; private final PostDialListener mPostDialListener = new PostDialListener() { @Override public void onPostDialWait() { Log.v(TelephonyConnection.this, "onPostDialWait"); if (mOriginalConnection != null) { setPostDialWait(mOriginalConnection.getRemainingPostDialString()); } } }; /** * Listener for listening to events in the {@link com.android.internal.telephony.Connection}. */ private final com.android.internal.telephony.Connection.Listener mOriginalConnectionListener = new com.android.internal.telephony.Connection.ListenerBase() { @Override public void onVideoStateChanged(int videoState) { setVideoState(videoState); } /** * The {@link com.android.internal.telephony.Connection} has reported a change in local * video capability. * * @param capable True if capable. */ @Override public void onLocalVideoCapabilityChanged(boolean capable) { setLocalVideoCapable(capable); } /** * The {@link com.android.internal.telephony.Connection} has reported a change in remote * video capability. * * @param capable True if capable. */ @Override public void onRemoteVideoCapabilityChanged(boolean capable) { setRemoteVideoCapable(capable); } /** * The {@link com.android.internal.telephony.Connection} has reported a change in the * video call provider. * * @param videoProvider The video call provider. */ @Override public void onVideoProviderChanged(VideoProvider videoProvider) { setVideoProvider(videoProvider); } /** * Used by the {@link com.android.internal.telephony.Connection} to report a change in the * audio quality for the current call. * * @param audioQuality The audio quality. */ @Override public void onAudioQualityChanged(int audioQuality) { setAudioQuality(audioQuality); } /** * Handles a change in the state of a conference participant, as reported by the * {@link com.android.internal.telephony.Connection}. * * @param participant The participant which changed. */ @Override public void onConferenceParticipantChanged(ConferenceParticipant participant) { updateConferenceParticipant(participant); } }; private com.android.internal.telephony.Connection mOriginalConnection; private Call.State mOriginalConnectionState = Call.State.IDLE; private boolean mWasImsConnection; /** * Determines if the {@link TelephonyConnection} has local video capabilities. * This is used when {@link TelephonyConnection#updateCallCapabilities()}} is called, * ensuring the appropriate {@link PhoneCapabilities} are set. Since {@link PhoneCapabilities} * can be rebuilt at any time it is necessary to track the video capabilities between rebuild. * The {@link PhoneCapabilities} (including video capabilities) are communicated to the telecom * layer. */ private boolean mLocalVideoCapable; /** * Determines if the {@link TelephonyConnection} has remote video capabilities. * This is used when {@link TelephonyConnection#updateCallCapabilities()}} is called, * ensuring the appropriate {@link PhoneCapabilities} are set. Since {@link PhoneCapabilities} * can be rebuilt at any time it is necessary to track the video capabilities between rebuild. * The {@link PhoneCapabilities} (including video capabilities) are communicated to the telecom * layer. */ private boolean mRemoteVideoCapable; /** * Determines the current audio quality for the {@link TelephonyConnection}. * This is used when {@link TelephonyConnection#updateCallCapabilities}} is called to indicate * whether a call has the {@link android.telecom.CallCapabilities#VoLTE} capability. */ private int mAudioQuality; protected TelephonyConnection(com.android.internal.telephony.Connection originalConnection) { if (originalConnection != null) { setOriginalConnection(originalConnection); } } @Override public void onAudioStateChanged(AudioState audioState) { // TODO: update TTY mode. if (getPhone() != null) { getPhone().setEchoSuppressionEnabled(); } } @Override public void onStateChanged(int state) { Log.v(this, "onStateChanged, state: " + Connection.stateToString(state)); } @Override public void onDisconnect() { Log.v(this, "onDisconnect"); hangup(android.telephony.DisconnectCause.LOCAL); } /** * Notifies this Connection of a request to disconnect a participant of the conference managed * by the connection. * * @param endpoint the {@link Uri} of the participant to disconnect. */ @Override public void onDisconnectConferenceParticipant(Uri endpoint) { Log.v(this, "onDisconnectConferenceParticipant %s", endpoint); if (mOriginalConnection == null) { return; } mOriginalConnection.onDisconnectConferenceParticipant(endpoint); } @Override public void onSeparate() { Log.v(this, "onSeparate"); if (mOriginalConnection != null) { try { mOriginalConnection.separate(); } catch (CallStateException e) { Log.e(this, e, "Call to Connection.separate failed with exception"); } } } @Override public void onAbort() { Log.v(this, "onAbort"); hangup(android.telephony.DisconnectCause.LOCAL); } @Override public void onHold() { performHold(); } @Override public void onUnhold() { performUnhold(); } @Override public void onAnswer(int videoState) { Log.v(this, "onAnswer"); if (isValidRingingCall() && getPhone() != null) { try { getPhone().acceptCall(videoState); } catch (CallStateException e) { Log.e(this, e, "Failed to accept call."); } } } @Override public void onReject() { Log.v(this, "onReject"); if (isValidRingingCall()) { hangup(android.telephony.DisconnectCause.INCOMING_REJECTED); } super.onReject(); } @Override public void onPostDialContinue(boolean proceed) { Log.v(this, "onPostDialContinue, proceed: " + proceed); if (mOriginalConnection != null) { if (proceed) { mOriginalConnection.proceedAfterWaitChar(); } else { mOriginalConnection.cancelPostDial(); } } } @Override public void onConferenceChanged() { Conference conference = getConference(); if (conference == null) { return; } // If the conference was an IMS connection currently or before, disable MANAGE_CONFERENCE // as the default behavior. If there is a conference event package, this may be overridden. if (mWasImsConnection) { int capabilities = conference.getCapabilities(); if (PhoneCapabilities.can(capabilities, PhoneCapabilities.MANAGE_CONFERENCE)) { int newCapabilities = PhoneCapabilities.remove(capabilities, PhoneCapabilities.MANAGE_CONFERENCE); conference.setCapabilities(newCapabilities); } } } public void performHold() { Log.v(this, "performHold"); // TODO: Can dialing calls be put on hold as well since they take up the // foreground call slot? if (Call.State.ACTIVE == mOriginalConnectionState) { Log.v(this, "Holding active call"); try { Phone phone = mOriginalConnection.getCall().getPhone(); Call ringingCall = phone.getRingingCall(); // Although the method says switchHoldingAndActive, it eventually calls a RIL method // called switchWaitingOrHoldingAndActive. What this means is that if we try to put // a call on hold while a call-waiting call exists, it'll end up accepting the // call-waiting call, which is bad if that was not the user's intention. We are // cheating here and simply skipping it because we know any attempt to hold a call // while a call-waiting call is happening is likely a request from Telecom prior to // accepting the call-waiting call. // TODO: Investigate a better solution. It would be great here if we // could "fake" hold by silencing the audio and microphone streams for this call // instead of actually putting it on hold. if (ringingCall.getState() != Call.State.WAITING) { phone.switchHoldingAndActive(); } // TODO: Cdma calls are slightly different. } catch (CallStateException e) { Log.e(this, e, "Exception occurred while trying to put call on hold."); } } else { Log.w(this, "Cannot put a call that is not currently active on hold."); } } public void performUnhold() { Log.v(this, "performUnhold"); if (Call.State.HOLDING == mOriginalConnectionState) { try { // Here's the deal--Telephony hold/unhold is weird because whenever there exists // more than one call, one of them must always be active. In other words, if you // have an active call and holding call, and you put the active call on hold, it // will automatically activate the holding call. This is weird with how Telecom // sends its commands. When a user opts to "unhold" a background call, telecom // issues hold commands to all active calls, and then the unhold command to the // background call. This means that we get two commands...each of which reduces to // switchHoldingAndActive(). The result is that they simply cancel each other out. // To fix this so that it works well with telecom we add a minor hack. If we // have one telephony call, everything works as normally expected. But if we have // two or more calls, we will ignore all requests to "unhold" knowing that the hold // requests already do what we want. If you've read up to this point, I'm very sorry // that we are doing this. I didn't think of a better solution that wouldn't also // make the Telecom APIs very ugly. if (!hasMultipleTopLevelCalls()) { mOriginalConnection.getCall().getPhone().switchHoldingAndActive(); } else { Log.i(this, "Skipping unhold command for %s", this); } } catch (CallStateException e) { Log.e(this, e, "Exception occurred while trying to release call from hold."); } } else { Log.w(this, "Cannot release a call that is not already on hold from hold."); } } public void performConference(TelephonyConnection otherConnection) {} /** * Builds call capabilities common to all TelephonyConnections. Namely, apply IMS-based * capabilities. */ protected int buildCallCapabilities() { int callCapabilities = 0; if (isImsConnection()) { callCapabilities |= PhoneCapabilities.ADD_CALL; callCapabilities |= PhoneCapabilities.SUPPORT_HOLD; if (getState() == STATE_ACTIVE || getState() == STATE_HOLDING) { callCapabilities |= PhoneCapabilities.HOLD; } } return callCapabilities; } protected final void updateCallCapabilities() { int newCallCapabilities = buildCallCapabilities(); newCallCapabilities = applyVideoCapabilities(newCallCapabilities); newCallCapabilities = applyAudioQualityCapabilities(newCallCapabilities); newCallCapabilities = applyConferenceTerminationCapabilities(newCallCapabilities); if (getCallCapabilities() != newCallCapabilities) { setCallCapabilities(newCallCapabilities); } } protected final void updateAddress() { updateCallCapabilities(); if (mOriginalConnection != null) { Uri address = getAddressFromNumber(mOriginalConnection.getAddress()); int presentation = mOriginalConnection.getNumberPresentation(); if (!Objects.equals(address, getAddress()) || presentation != getAddressPresentation()) { Log.v(this, "updateAddress, address changed"); setAddress(address, presentation); } String name = mOriginalConnection.getCnapName(); int namePresentation = mOriginalConnection.getCnapNamePresentation(); if (!Objects.equals(name, getCallerDisplayName()) || namePresentation != getCallerDisplayNamePresentation()) { Log.v(this, "updateAddress, caller display name changed"); setCallerDisplayName(name, namePresentation); } } } void onRemovedFromCallService() { // Subclass can override this to do cleanup. } void setOriginalConnection(com.android.internal.telephony.Connection originalConnection) { Log.v(this, "new TelephonyConnection, originalConnection: " + originalConnection); if (mOriginalConnection != null) { getPhone().unregisterForPreciseCallStateChanged(mHandler); getPhone().unregisterForRingbackTone(mHandler); getPhone().unregisterForHandoverStateChanged(mHandler); getPhone().unregisterForDisconnect(mHandler); } mOriginalConnection = originalConnection; getPhone().registerForPreciseCallStateChanged( mHandler, MSG_PRECISE_CALL_STATE_CHANGED, null); getPhone().registerForHandoverStateChanged( mHandler, MSG_HANDOVER_STATE_CHANGED, null); getPhone().registerForRingbackTone(mHandler, MSG_RINGBACK_TONE, null); getPhone().registerForDisconnect(mHandler, MSG_DISCONNECT, null); mOriginalConnection.addPostDialListener(mPostDialListener); mOriginalConnection.addListener(mOriginalConnectionListener); // Set video state and capabilities setVideoState(mOriginalConnection.getVideoState()); setLocalVideoCapable(mOriginalConnection.isLocalVideoCapable()); setRemoteVideoCapable(mOriginalConnection.isRemoteVideoCapable()); setVideoProvider(mOriginalConnection.getVideoProvider()); setAudioQuality(mOriginalConnection.getAudioQuality()); if (isImsConnection()) { mWasImsConnection = true; } updateAddress(); } protected void hangup(int telephonyDisconnectCode) { if (mOriginalConnection != null) { try { // Hanging up a ringing call requires that we invoke call.hangup() as opposed to // connection.hangup(). Without this change, the party originating the call will not // get sent to voicemail if the user opts to reject the call. if (isValidRingingCall()) { Call call = getCall(); if (call != null) { call.hangup(); } else { Log.w(this, "Attempting to hangup a connection without backing call."); } } else { // We still prefer to call connection.hangup() for non-ringing calls in order // to support hanging-up specific calls within a conference call. If we invoked // call.hangup() while in a conference, we would end up hanging up the entire // conference call instead of the specific connection. mOriginalConnection.hangup(); } } catch (CallStateException e) { Log.e(this, e, "Call to Connection.hangup failed with exception"); } } } com.android.internal.telephony.Connection getOriginalConnection() { return mOriginalConnection; } protected Call getCall() { if (mOriginalConnection != null) { return mOriginalConnection.getCall(); } return null; } Phone getPhone() { Call call = getCall(); if (call != null) { return call.getPhone(); } return null; } private boolean hasMultipleTopLevelCalls() { int numCalls = 0; Phone phone = getPhone(); if (phone != null) { if (!phone.getRingingCall().isIdle()) { numCalls++; } if (!phone.getForegroundCall().isIdle()) { numCalls++; } if (!phone.getBackgroundCall().isIdle()) { numCalls++; } } return numCalls > 1; } private com.android.internal.telephony.Connection getForegroundConnection() { if (getPhone() != null) { return getPhone().getForegroundCall().getEarliestConnection(); } return null; } /** * Checks to see the original connection corresponds to an active incoming call. Returns false * if there is no such actual call, or if the associated call is not incoming (See * {@link Call.State#isRinging}). */ private boolean isValidRingingCall() { if (getPhone() == null) { Log.v(this, "isValidRingingCall, phone is null"); return false; } Call ringingCall = getPhone().getRingingCall(); if (!ringingCall.getState().isRinging()) { Log.v(this, "isValidRingingCall, ringing call is not in ringing state"); return false; } if (ringingCall.getEarliestConnection() != mOriginalConnection) { Log.v(this, "isValidRingingCall, ringing call connection does not match"); return false; } Log.v(this, "isValidRingingCall, returning true"); return true; } void updateState() { if (mOriginalConnection == null) { return; } Call.State newState = mOriginalConnection.getState(); Log.v(this, "Update state from %s to %s for %s", mOriginalConnectionState, newState, this); if (mOriginalConnectionState != newState) { mOriginalConnectionState = newState; switch (newState) { case IDLE: break; case ACTIVE: setActiveInternal(); break; case HOLDING: setOnHold(); break; case DIALING: case ALERTING: setDialing(); break; case INCOMING: case WAITING: setRinging(); break; case DISCONNECTED: setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause( mOriginalConnection.getDisconnectCause())); close(); break; case DISCONNECTING: break; } } updateCallCapabilities(); updateAddress(); } private void setActiveInternal() { if (getState() == STATE_ACTIVE) { Log.w(this, "Should not be called if this is already ACTIVE"); return; } // When we set a call to active, we need to make sure that there are no other active // calls. However, the ordering of state updates to connections can be non-deterministic // since all connections register for state changes on the phone independently. // To "optimize", we check here to see if there already exists any active calls. If so, // we issue an update for those calls first to make sure we only have one top-level // active call. if (getConnectionService() != null) { for (Connection current : getConnectionService().getAllConnections()) { if (current != this && current instanceof TelephonyConnection) { TelephonyConnection other = (TelephonyConnection) current; if (other.getState() == STATE_ACTIVE) { other.updateState(); } } } } setActive(); } private void close() { Log.v(this, "close"); if (getPhone() != null) { getPhone().unregisterForPreciseCallStateChanged(mHandler); getPhone().unregisterForRingbackTone(mHandler); getPhone().unregisterForHandoverStateChanged(mHandler); } mOriginalConnection = null; destroy(); } /** * Applies the video capability states to the CallCapabilities bit-mask. * * @param capabilities The CallCapabilities bit-mask. * @return The capabilities with video capabilities applied. */ private int applyVideoCapabilities(int capabilities) { int currentCapabilities = capabilities; if (mRemoteVideoCapable) { currentCapabilities = applyCapability(currentCapabilities, PhoneCapabilities.SUPPORTS_VT_REMOTE); } else { currentCapabilities = removeCapability(currentCapabilities, PhoneCapabilities.SUPPORTS_VT_REMOTE); } if (mLocalVideoCapable) { currentCapabilities = applyCapability(currentCapabilities, PhoneCapabilities.SUPPORTS_VT_LOCAL); } else { currentCapabilities = removeCapability(currentCapabilities, PhoneCapabilities.SUPPORTS_VT_LOCAL); } return currentCapabilities; } /** * Applies the audio capabilities to the {@code CallCapabilities} bit-mask. A call with high * definition audio is considered to have the {@code VoLTE} call capability as VoLTE uses high * definition audio. * * @param callCapabilities The {@code CallCapabilities} bit-mask. * @return The capabilities with the audio capabilities applied. */ private int applyAudioQualityCapabilities(int callCapabilities) { int currentCapabilities = callCapabilities; if (mAudioQuality == com.android.internal.telephony.Connection.AUDIO_QUALITY_HIGH_DEFINITION) { currentCapabilities = applyCapability(currentCapabilities, PhoneCapabilities.VoLTE); } else { currentCapabilities = removeCapability(currentCapabilities, PhoneCapabilities.VoLTE); } return currentCapabilities; } /** * Applies capabilities specific to conferences termination to the * {@code CallCapabilities} bit-mask. * * @param callCapabilities The {@code CallCapabilities} bit-mask. * @return The capabilities with the IMS conference capabilities applied. */ private int applyConferenceTerminationCapabilities(int callCapabilities) { int currentCapabilities = callCapabilities; // An IMS call cannot be individually disconnected or separated from its parent conference. // If the call was IMS, even if it hands over to GMS, these capabilities are not supported. if (!mWasImsConnection) { currentCapabilities |= PhoneCapabilities.DISCONNECT_FROM_CONFERENCE; currentCapabilities |= PhoneCapabilities.SEPARATE_FROM_CONFERENCE; } return currentCapabilities; } /** * Returns the local video capability state for the connection. * * @return {@code True} if the connection has local video capabilities. */ public boolean isLocalVideoCapable() { return mLocalVideoCapable; } /** * Returns the remote video capability state for the connection. * * @return {@code True} if the connection has remote video capabilities. */ public boolean isRemoteVideoCapable() { return mRemoteVideoCapable; } /** * Sets whether video capability is present locally. Used during rebuild of the * {@link PhoneCapabilities} to set the video call capabilities. * * @param capable {@code True} if video capable. */ public void setLocalVideoCapable(boolean capable) { mLocalVideoCapable = capable; updateCallCapabilities(); } /** * Sets whether video capability is present remotely. Used during rebuild of the * {@link PhoneCapabilities} to set the video call capabilities. * * @param capable {@code True} if video capable. */ public void setRemoteVideoCapable(boolean capable) { mRemoteVideoCapable = capable; updateCallCapabilities(); } /** * Sets the current call audio quality. Used during rebuild of the * {@link PhoneCapabilities} to set or unset the {@link PhoneCapabilities#VoLTE} capability. * * @param audioQuality The audio quality. */ public void setAudioQuality(int audioQuality) { mAudioQuality = audioQuality; updateCallCapabilities(); } /** * Obtains the current call audio quality. */ public int getAudioQuality() { return mAudioQuality; } void resetStateForConference() { if (getState() == Connection.STATE_HOLDING) { if (mOriginalConnection.getState() == Call.State.ACTIVE) { setActive(); } } } boolean setHoldingForConference() { if (getState() == Connection.STATE_ACTIVE) { setOnHold(); return true; } return false; } /** * Whether the original connection is an IMS connection. * @return {@code True} if the original connection is an IMS connection, {@code false} * otherwise. */ protected boolean isImsConnection() { return getOriginalConnection() instanceof ImsPhoneConnection; } private static Uri getAddressFromNumber(String number) { // Address can be null for blocked calls. if (number == null) { number = ""; } return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null); } /** * Applies a capability to a capabilities bit-mask. * * @param capabilities The capabilities bit-mask. * @param capability The capability to apply. * @return The capabilities bit-mask with the capability applied. */ private int applyCapability(int capabilities, int capability) { int newCapabilities = capabilities | capability; return newCapabilities; } /** * Removes a capability from a capabilities bit-mask. * * @param capabilities The capabilities bit-mask. * @param capability The capability to remove. * @return The capabilities bit-mask with the capability removed. */ private int removeCapability(int capabilities, int capability) { int newCapabilities = capabilities & ~capability; return newCapabilities; } }