/* * Copyright (C) 2015 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.server.telecom; import android.media.AudioAttributes; import android.media.AudioFocusRequest; import android.media.AudioManager; import android.os.Looper; import android.os.Message; import android.os.Trace; import android.telecom.Log; import android.telecom.Logging.Runnable; import android.telecom.Logging.Session; import android.util.LocalLog; import android.util.SparseArray; import com.android.internal.util.IState; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.server.telecom.flags.FeatureFlags; public class CallAudioModeStateMachine extends StateMachine { /** * Captures the most recent CallAudioModeStateMachine state transitions and the corresponding * changes to the {@link AudioManager#setMode}. */ private LocalLog mLocalLog = new LocalLog(20); public static class Factory { public CallAudioModeStateMachine create(SystemStateHelper systemStateHelper, AudioManager am, FeatureFlags featureFlags, CallAudioCommunicationDeviceTracker callAudioCommunicationDeviceTracker) { return new CallAudioModeStateMachine(systemStateHelper, am, featureFlags, callAudioCommunicationDeviceTracker); } } private static final AudioAttributes RING_AUDIO_ATTRIBUTES = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .setLegacyStreamType(AudioManager.STREAM_RING) .build(); public static final AudioFocusRequest RING_AUDIO_FOCUS_REQUEST = new AudioFocusRequest .Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) .setAudioAttributes(RING_AUDIO_ATTRIBUTES).build(); private static final AudioAttributes CALL_AUDIO_ATTRIBUTES = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) .setLegacyStreamType(AudioManager.STREAM_VOICE_CALL) .build(); public static final AudioFocusRequest CALL_AUDIO_FOCUS_REQUEST = new AudioFocusRequest .Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) .setAudioAttributes(CALL_AUDIO_ATTRIBUTES).build(); public static class MessageArgs { public boolean hasActiveOrDialingCalls; public boolean hasRingingCalls; public boolean hasHoldingCalls; public boolean hasAudioProcessingCalls; public boolean isTonePlaying; public boolean foregroundCallIsVoip; public boolean isStreaming; public Session session; private MessageArgs(boolean hasActiveOrDialingCalls, boolean hasRingingCalls, boolean hasHoldingCalls, boolean hasAudioProcessingCalls, boolean isTonePlaying, boolean foregroundCallIsVoip, boolean isStreaming, Session session) { this.hasActiveOrDialingCalls = hasActiveOrDialingCalls; this.hasRingingCalls = hasRingingCalls; this.hasHoldingCalls = hasHoldingCalls; this.hasAudioProcessingCalls = hasAudioProcessingCalls; this.isTonePlaying = isTonePlaying; this.foregroundCallIsVoip = foregroundCallIsVoip; this.isStreaming = isStreaming; this.session = session; } @Override public String toString() { return "MessageArgs{" + "hasActiveCalls=" + hasActiveOrDialingCalls + ", hasRingingCalls=" + hasRingingCalls + ", hasHoldingCalls=" + hasHoldingCalls + ", hasAudioProcessingCalls=" + hasAudioProcessingCalls + ", isTonePlaying=" + isTonePlaying + ", foregroundCallIsVoip=" + foregroundCallIsVoip + ", isStreaming=" + isStreaming + ", session=" + session + '}'; } public static class Builder { private boolean mHasActiveOrDialingCalls; private boolean mHasRingingCalls; private boolean mHasHoldingCalls; private boolean mHasAudioProcessingCalls; private boolean mIsTonePlaying; private boolean mForegroundCallIsVoip; private boolean mIsStreaming; private Session mSession; public Builder setHasActiveOrDialingCalls(boolean hasActiveOrDialingCalls) { mHasActiveOrDialingCalls = hasActiveOrDialingCalls; return this; } public Builder setHasRingingCalls(boolean hasRingingCalls) { mHasRingingCalls = hasRingingCalls; return this; } public Builder setHasHoldingCalls(boolean hasHoldingCalls) { mHasHoldingCalls = hasHoldingCalls; return this; } public Builder setHasAudioProcessingCalls(boolean hasAudioProcessingCalls) { mHasAudioProcessingCalls = hasAudioProcessingCalls; return this; } public Builder setIsTonePlaying(boolean isTonePlaying) { mIsTonePlaying = isTonePlaying; return this; } public Builder setForegroundCallIsVoip(boolean foregroundCallIsVoip) { mForegroundCallIsVoip = foregroundCallIsVoip; return this; } public Builder setSession(Session session) { mSession = session; return this; } public Builder setIsStreaming(boolean isStraeming) { mIsStreaming = isStraeming; return this; } public MessageArgs build() { return new MessageArgs(mHasActiveOrDialingCalls, mHasRingingCalls, mHasHoldingCalls, mHasAudioProcessingCalls, mIsTonePlaying, mForegroundCallIsVoip, mIsStreaming, mSession); } } } // TODO: remove this and replace when the new audio mode gets pushed to AOSP. public static final int NEW_AUDIO_MODE_FOR_AUDIO_PROCESSING = 4; public static final int INITIALIZE = 1; // These ENTER_*_FOCUS commands are for testing. public static final int ENTER_CALL_FOCUS_FOR_TESTING = 2; public static final int ENTER_COMMS_FOCUS_FOR_TESTING = 3; public static final int ENTER_RING_FOCUS_FOR_TESTING = 4; public static final int ENTER_TONE_OR_HOLD_FOCUS_FOR_TESTING = 5; public static final int ENTER_AUDIO_PROCESSING_FOCUS_FOR_TESTING = 6; public static final int ENTER_STREAMING_FOCUS_FOR_TESTING = 7; public static final int ABANDON_FOCUS_FOR_TESTING = 8; public static final int NO_MORE_ACTIVE_OR_DIALING_CALLS = 1001; public static final int NO_MORE_RINGING_CALLS = 1002; public static final int NO_MORE_HOLDING_CALLS = 1003; public static final int NO_MORE_AUDIO_PROCESSING_CALLS = 1004; public static final int NEW_ACTIVE_OR_DIALING_CALL = 2001; public static final int NEW_RINGING_CALL = 2002; public static final int NEW_HOLDING_CALL = 2003; public static final int NEW_AUDIO_PROCESSING_CALL = 2004; public static final int TONE_STARTED_PLAYING = 3001; public static final int TONE_STOPPED_PLAYING = 3002; public static final int FOREGROUND_VOIP_MODE_CHANGE = 4001; public static final int RINGER_MODE_CHANGE = 5001; // Used to indicate that Telecom is done doing things to the AudioManager and that it's safe // to release focus for other apps to take over. public static final int AUDIO_OPERATIONS_COMPLETE = 6001; public static final int START_CALL_STREAMING = 7001; public static final int STOP_CALL_STREAMING = 7002; public static final int RUN_RUNNABLE = 9001; private static final SparseArray MESSAGE_CODE_TO_NAME = new SparseArray() {{ put(ENTER_CALL_FOCUS_FOR_TESTING, "ENTER_CALL_FOCUS_FOR_TESTING"); put(ENTER_COMMS_FOCUS_FOR_TESTING, "ENTER_COMMS_FOCUS_FOR_TESTING"); put(ENTER_RING_FOCUS_FOR_TESTING, "ENTER_RING_FOCUS_FOR_TESTING"); put(ENTER_AUDIO_PROCESSING_FOCUS_FOR_TESTING, "ENTER_AUDIO_PROCESSING_FOCUS_FOR_TESTING"); put(ENTER_TONE_OR_HOLD_FOCUS_FOR_TESTING, "ENTER_TONE_OR_HOLD_FOCUS_FOR_TESTING"); put(ABANDON_FOCUS_FOR_TESTING, "ABANDON_FOCUS_FOR_TESTING"); put(NO_MORE_ACTIVE_OR_DIALING_CALLS, "NO_MORE_ACTIVE_OR_DIALING_CALLS"); put(NO_MORE_RINGING_CALLS, "NO_MORE_RINGING_CALLS"); put(NO_MORE_HOLDING_CALLS, "NO_MORE_HOLDING_CALLS"); put(NO_MORE_AUDIO_PROCESSING_CALLS, "NO_MORE_AUDIO_PROCESSING_CALLS"); put(NEW_ACTIVE_OR_DIALING_CALL, "NEW_ACTIVE_OR_DIALING_CALL"); put(NEW_RINGING_CALL, "NEW_RINGING_CALL"); put(NEW_HOLDING_CALL, "NEW_HOLDING_CALL"); put(NEW_AUDIO_PROCESSING_CALL, "NEW_AUDIO_PROCESSING_CALL"); put(TONE_STARTED_PLAYING, "TONE_STARTED_PLAYING"); put(TONE_STOPPED_PLAYING, "TONE_STOPPED_PLAYING"); put(FOREGROUND_VOIP_MODE_CHANGE, "FOREGROUND_VOIP_MODE_CHANGE"); put(RINGER_MODE_CHANGE, "RINGER_MODE_CHANGE"); put(AUDIO_OPERATIONS_COMPLETE, "AUDIO_OPERATIONS_COMPLETE"); put(START_CALL_STREAMING, "START_CALL_STREAMING"); put(STOP_CALL_STREAMING, "STOP_CALL_STREAMING"); put(RUN_RUNNABLE, "RUN_RUNNABLE"); }}; public static final String TONE_HOLD_STATE_NAME = OtherFocusState.class.getSimpleName(); public static final String UNFOCUSED_STATE_NAME = UnfocusedState.class.getSimpleName(); public static final String AUDIO_PROCESSING_STATE_NAME = AudioProcessingFocusState.class.getSimpleName(); public static final String CALL_STATE_NAME = SimCallFocusState.class.getSimpleName(); public static final String RING_STATE_NAME = RingingFocusState.class.getSimpleName(); public static final String STREAMING_STATE_NAME = StreamingFocusState.class.getSimpleName(); public static final String COMMS_STATE_NAME = VoipCallFocusState.class.getSimpleName(); private AudioFocusRequest mCurrentAudioFocusRequest = null; private class BaseState extends State { @Override public boolean processMessage(Message msg) { switch (msg.what) { case ENTER_CALL_FOCUS_FOR_TESTING: transitionTo(mSimCallFocusState); return HANDLED; case ENTER_COMMS_FOCUS_FOR_TESTING: transitionTo(mVoipCallFocusState); return HANDLED; case ENTER_RING_FOCUS_FOR_TESTING: transitionTo(mRingingFocusState); return HANDLED; case ENTER_TONE_OR_HOLD_FOCUS_FOR_TESTING: transitionTo(mOtherFocusState); return HANDLED; case ENTER_AUDIO_PROCESSING_FOCUS_FOR_TESTING: transitionTo(mAudioProcessingFocusState); return HANDLED; case ENTER_STREAMING_FOCUS_FOR_TESTING: transitionTo(mStreamingFocusState); return HANDLED; case ABANDON_FOCUS_FOR_TESTING: transitionTo(mUnfocusedState); return HANDLED; case INITIALIZE: mIsInitialized = true; return HANDLED; case RUN_RUNNABLE: java.lang.Runnable r = (java.lang.Runnable) msg.obj; r.run(); return HANDLED; default: return NOT_HANDLED; } } } private class UnfocusedState extends BaseState { @Override public void enter() { Log.i(LOG_TAG, "Audio focus entering UNFOCUSED state"); mLocalLog.log("Enter UNFOCUSED"); if (mIsInitialized) { // Clear any communication device that was requested previously. // Todo: Remove once clearCommunicationDeviceAfterAudioOpsComplete is // completely rolled out. if (mFeatureFlags.callAudioCommunicationDeviceRefactor() && !mFeatureFlags.clearCommunicationDeviceAfterAudioOpsComplete()) { mCommunicationDeviceTracker.clearCommunicationDevice(mCommunicationDeviceTracker .getCurrentLocallyRequestedCommunicationDevice()); } if (mFeatureFlags.setAudioModeBeforeAbandonFocus()) { mAudioManager.setMode(AudioManager.MODE_NORMAL); mCallAudioManager.setCallAudioRouteFocusState( CallAudioRouteStateMachine.NO_FOCUS); } else { mCallAudioManager.setCallAudioRouteFocusState( CallAudioRouteStateMachine.NO_FOCUS); mAudioManager.setMode(AudioManager.MODE_NORMAL); } mLocalLog.log("Mode MODE_NORMAL"); mMostRecentMode = AudioManager.MODE_NORMAL; // Don't release focus here -- wait until we get a signal that any other audio // operations triggered by this are done before releasing focus. } } @Override public boolean processMessage(Message msg) { if (super.processMessage(msg) == HANDLED) { return HANDLED; } MessageArgs args = (MessageArgs) msg.obj; switch (msg.what) { case NO_MORE_ACTIVE_OR_DIALING_CALLS: // Do nothing. return HANDLED; case NO_MORE_RINGING_CALLS: // Do nothing. return HANDLED; case NO_MORE_HOLDING_CALLS: // Do nothing. return HANDLED; case NO_MORE_AUDIO_PROCESSING_CALLS: // Do nothing. return HANDLED; case NEW_ACTIVE_OR_DIALING_CALL: transitionTo(args.foregroundCallIsVoip ? mVoipCallFocusState : mSimCallFocusState); return HANDLED; case NEW_RINGING_CALL: transitionTo(mRingingFocusState); return HANDLED; case NEW_AUDIO_PROCESSING_CALL: transitionTo(mAudioProcessingFocusState); return HANDLED; case NEW_HOLDING_CALL: // This really shouldn't happen, but transition to the focused state anyway. Log.w(LOG_TAG, "Call was surprisingly put into hold from an unknown state." + " Args are: \n" + args.toString()); transitionTo(mOtherFocusState); return HANDLED; case START_CALL_STREAMING: transitionTo(mStreamingFocusState); return HANDLED; case TONE_STARTED_PLAYING: // This shouldn't happen either, but perform the action anyway. Log.w(LOG_TAG, "Tone started playing unexpectedly. Args are: \n" + args.toString()); return HANDLED; case AUDIO_OPERATIONS_COMPLETE: Log.i(LOG_TAG, "Abandoning audio focus: now UNFOCUSED"); if (mFeatureFlags.telecomResolveHiddenDependencies()) { if (mCurrentAudioFocusRequest != null) { mAudioManager.abandonAudioFocusRequest(mCurrentAudioFocusRequest); mCurrentAudioFocusRequest = null; } } else { mAudioManager.abandonAudioFocusForCall(); } // Clear requested communication device after the call ends. if (mFeatureFlags.clearCommunicationDeviceAfterAudioOpsComplete()) { mCommunicationDeviceTracker.clearCommunicationDevice( mCommunicationDeviceTracker .getCurrentLocallyRequestedCommunicationDevice()); } return HANDLED; default: // The forced focus switch commands are handled by BaseState. return NOT_HANDLED; } } } private class AudioProcessingFocusState extends BaseState { @Override public void enter() { Log.i(LOG_TAG, "Audio focus entering AUDIO_PROCESSING state"); mLocalLog.log("Enter AUDIO_PROCESSING"); if (mIsInitialized) { mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.NO_FOCUS); mAudioManager.setMode(NEW_AUDIO_MODE_FOR_AUDIO_PROCESSING); mLocalLog.log("Mode MODE_CALL_SCREENING"); mMostRecentMode = NEW_AUDIO_MODE_FOR_AUDIO_PROCESSING; } } @Override public boolean processMessage(Message msg) { if (super.processMessage(msg) == HANDLED) { return HANDLED; } MessageArgs args = (MessageArgs) msg.obj; switch (msg.what) { case NO_MORE_ACTIVE_OR_DIALING_CALLS: // Do nothing. return HANDLED; case NO_MORE_RINGING_CALLS: // Do nothing. return HANDLED; case NO_MORE_HOLDING_CALLS: // Do nothing. return HANDLED; case NO_MORE_AUDIO_PROCESSING_CALLS: BaseState destState = calculateProperStateFromArgs(args); if (destState == this) { Log.w(LOG_TAG, "Got spurious NO_MORE_AUDIO_PROCESSING_CALLS"); } transitionTo(destState); return HANDLED; case NEW_ACTIVE_OR_DIALING_CALL: transitionTo(args.foregroundCallIsVoip ? mVoipCallFocusState : mSimCallFocusState); return HANDLED; case NEW_RINGING_CALL: transitionTo(mRingingFocusState); return HANDLED; case NEW_HOLDING_CALL: // This really shouldn't happen, but recalculate from args and do the transition Log.w(LOG_TAG, "Call was surprisingly put into hold from an unknown state." + " Args are: \n" + args.toString()); transitionTo(mOtherFocusState); return HANDLED; case NEW_AUDIO_PROCESSING_CALL: // Can happen as a duplicate message return HANDLED; case TONE_STARTED_PLAYING: // This shouldn't happen either, but perform the action anyway. Log.w(LOG_TAG, "Tone started playing unexpectedly. Args are: \n" + args.toString()); return HANDLED; case START_CALL_STREAMING: transitionTo(mStreamingFocusState); return HANDLED; case AUDIO_OPERATIONS_COMPLETE: Log.i(LOG_TAG, "Abandoning audio focus: now AUDIO_PROCESSING"); if (mFeatureFlags.telecomResolveHiddenDependencies()) { if (mCurrentAudioFocusRequest != null) { mAudioManager.abandonAudioFocusRequest(mCurrentAudioFocusRequest); mCurrentAudioFocusRequest = null; } } else { mAudioManager.abandonAudioFocusForCall(); } return HANDLED; default: // The forced focus switch commands are handled by BaseState. return NOT_HANDLED; } } } private class RingingFocusState extends BaseState { // Keeps track of whether we're ringing with audio focus or if we've just entered the state // without acquiring focus because of a silent ringtone or something. private boolean mHasFocus = false; private void tryStartRinging() { Trace.traceBegin(Trace.TRACE_TAG_AUDIO, "CallAudioMode.tryStartRinging"); try { if (mHasFocus && mCallAudioManager.isRingtonePlaying()) { Log.i(LOG_TAG, "RingingFocusState#tryStartRinging -- audio focus previously" + " acquired and ringtone already playing -- skipping."); return; } if (mCallAudioManager.startRinging()) { if (mFeatureFlags.telecomResolveHiddenDependencies()) { mCurrentAudioFocusRequest = RING_AUDIO_FOCUS_REQUEST; mAudioManager.requestAudioFocus(RING_AUDIO_FOCUS_REQUEST); } else { mAudioManager.requestAudioFocusForCall( AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); } // Do not set MODE_RINGTONE if we were previously in the CALL_SCREENING mode -- // this trips up the audio system. if (mAudioManager.getMode() != AudioManager.MODE_CALL_SCREENING) { mAudioManager.setMode(AudioManager.MODE_RINGTONE); mLocalLog.log("Mode MODE_RINGTONE"); } mCallAudioManager.setCallAudioRouteFocusState( CallAudioRouteStateMachine.RINGING_FOCUS); mHasFocus = true; } else { Log.i( LOG_TAG, "RINGING state, try start ringing but not acquiring audio focus"); } } finally { Trace.traceEnd(Trace.TRACE_TAG_AUDIO); } } @Override public void enter() { Log.i(LOG_TAG, "Audio focus entering RINGING state"); mLocalLog.log("Enter RINGING"); tryStartRinging(); mCallAudioManager.stopCallWaiting(); } @Override public void exit() { // Audio mode and audio stream will be set by the next state. mCallAudioManager.stopRinging(); mHasFocus = false; } @Override public boolean processMessage(Message msg) { if (super.processMessage(msg) == HANDLED) { return HANDLED; } MessageArgs args = (MessageArgs) msg.obj; switch (msg.what) { case NO_MORE_ACTIVE_OR_DIALING_CALLS: // Do nothing. Loss of an active call should not impact ringer. return HANDLED; case NO_MORE_HOLDING_CALLS: // Do nothing and keep ringing. return HANDLED; case NO_MORE_RINGING_CALLS: BaseState destState = calculateProperStateFromArgs(args); if (destState == this) { Log.w(LOG_TAG, "Got spurious NO_MORE_RINGING_CALLS"); } transitionTo(destState); return HANDLED; case NEW_ACTIVE_OR_DIALING_CALL: // If a call becomes active suddenly, give it priority over ringing. transitionTo(args.foregroundCallIsVoip ? mVoipCallFocusState : mSimCallFocusState); return HANDLED; case NEW_AUDIO_PROCESSING_CALL: // If we don't have any more ringing calls, transition to audio processing. if (!args.hasRingingCalls) { transitionTo(mAudioProcessingFocusState); } else { Log.w(LOG_TAG, "Got a audio processing call while there's still a call " + "ringing"); } case NEW_RINGING_CALL: // Can happen as a duplicate message return HANDLED; case NEW_HOLDING_CALL: // This really shouldn't happen, but transition to the focused state anyway. Log.w(LOG_TAG, "Call was surprisingly put into hold while ringing." + " Args are: " + args.toString()); transitionTo(mOtherFocusState); return HANDLED; case RINGER_MODE_CHANGE: { Log.i(LOG_TAG, "RINGING state, received RINGER_MODE_CHANGE"); tryStartRinging(); return HANDLED; } case AUDIO_OPERATIONS_COMPLETE: Log.w(LOG_TAG, "Should not be seeing AUDIO_OPERATIONS_COMPLETE in a focused" + " state"); return HANDLED; default: // The forced focus switch commands are handled by BaseState. return NOT_HANDLED; } } } private class SimCallFocusState extends BaseState { @Override public void enter() { Log.i(LOG_TAG, "Audio focus entering SIM CALL state"); mLocalLog.log("Enter SIM_CALL"); if (mFeatureFlags.telecomResolveHiddenDependencies()) { mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST; mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST); } else { mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); } mAudioManager.setMode(AudioManager.MODE_IN_CALL); mLocalLog.log("Mode MODE_IN_CALL"); mMostRecentMode = AudioManager.MODE_IN_CALL; mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS); } @Override public boolean processMessage(Message msg) { if (super.processMessage(msg) == HANDLED) { return HANDLED; } MessageArgs args = (MessageArgs) msg.obj; switch (msg.what) { case NO_MORE_ACTIVE_OR_DIALING_CALLS: // Switch to either ringing, holding, or inactive transitionTo(calculateProperStateFromArgs(args)); return HANDLED; case NO_MORE_RINGING_CALLS: // Don't transition state, but stop any call-waiting tones that may have been // playing. if (args.isTonePlaying) { mCallAudioManager.stopCallWaiting(); } // If a MT-audio-speedup call gets disconnected by the connection service // concurrently with the user answering it, we may get this message // indicating that a ringing call has disconnected while this state machine // is in the SimCallFocusState. if (!args.hasActiveOrDialingCalls) { transitionTo(calculateProperStateFromArgs(args)); } return HANDLED; case NO_MORE_HOLDING_CALLS: if (args.foregroundCallIsVoip) { transitionTo(mVoipCallFocusState); } return HANDLED; case NEW_ACTIVE_OR_DIALING_CALL: if (args.foregroundCallIsVoip) { transitionTo(mVoipCallFocusState); } return HANDLED; case NEW_RINGING_CALL: // Don't make a call ring over an active call, but do play a call waiting tone. mCallAudioManager.startCallWaiting("call already active"); return HANDLED; case NEW_HOLDING_CALL: // Just check the voip mode. Putting an active call on hold will be handled when // NO_MORE_ACTIVE_CALLS is processed. if (args.foregroundCallIsVoip) { transitionTo(mVoipCallFocusState); } return HANDLED; case NEW_AUDIO_PROCESSING_CALL: // If we don't have any more active calls, transition to audio processing. if (!args.hasActiveOrDialingCalls) { transitionTo(mAudioProcessingFocusState); } else { Log.w(LOG_TAG, "Got a audio processing call while there's still a call " + "active"); } case FOREGROUND_VOIP_MODE_CHANGE: if (args.foregroundCallIsVoip) { transitionTo(mVoipCallFocusState); } return HANDLED; case AUDIO_OPERATIONS_COMPLETE: Log.w(LOG_TAG, "Should not be seeing AUDIO_OPERATIONS_COMPLETE in a focused" + " state"); return HANDLED; default: // The forced focus switch commands are handled by BaseState. return NOT_HANDLED; } } } private class VoipCallFocusState extends BaseState { @Override public void enter() { Log.i(LOG_TAG, "Audio focus entering VOIP CALL state"); mLocalLog.log("Enter VOIP_CALL"); if (mFeatureFlags.telecomResolveHiddenDependencies()) { mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST; mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST); } else { mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); } mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); mLocalLog.log("Mode MODE_IN_COMMUNICATION"); mMostRecentMode = AudioManager.MODE_IN_COMMUNICATION; mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS); } @Override public boolean processMessage(Message msg) { if (super.processMessage(msg) == HANDLED) { return HANDLED; } MessageArgs args = (MessageArgs) msg.obj; switch (msg.what) { case NO_MORE_ACTIVE_OR_DIALING_CALLS: // Switch to either ringing, holding, or inactive transitionTo(calculateProperStateFromArgs(args)); return HANDLED; case NO_MORE_RINGING_CALLS: // Don't transition state, but stop any call-waiting tones that may have been // playing. if (args.isTonePlaying) { mCallAudioManager.stopCallWaiting(); } return HANDLED; case NO_MORE_HOLDING_CALLS: if (!args.foregroundCallIsVoip) { transitionTo(mSimCallFocusState); } return HANDLED; case NEW_ACTIVE_OR_DIALING_CALL: if (!args.foregroundCallIsVoip) { transitionTo(mSimCallFocusState); } return HANDLED; case NEW_RINGING_CALL: // Don't make a call ring over an active call, but do play a call waiting tone. mCallAudioManager.startCallWaiting("call already active"); return HANDLED; case NEW_HOLDING_CALL: // Just check the voip mode. Putting an active call on hold will be handled when // NO_MORE_ACTIVE_CALLS is processed. if (!args.foregroundCallIsVoip) { transitionTo(mSimCallFocusState); } return HANDLED; case NEW_AUDIO_PROCESSING_CALL: // If we don't have any more active calls, transition to audio processing. if (!args.hasActiveOrDialingCalls) { transitionTo(mAudioProcessingFocusState); } else { Log.w(LOG_TAG, "Got a audio processing call while there's still a call " + "active"); } case FOREGROUND_VOIP_MODE_CHANGE: if (!args.foregroundCallIsVoip) { transitionTo(mSimCallFocusState); } return HANDLED; case AUDIO_OPERATIONS_COMPLETE: Log.w(LOG_TAG, "Should not be seeing AUDIO_OPERATIONS_COMPLETE in a focused" + " state"); return HANDLED; case START_CALL_STREAMING: transitionTo(mStreamingFocusState); return HANDLED; default: // The forced focus switch commands are handled by BaseState. return NOT_HANDLED; } } } private class StreamingFocusState extends BaseState { @Override public void enter() { Log.i(LOG_TAG, "Audio focus entering streaming state"); mLocalLog.log("Enter Streaming"); mLocalLog.log("Mode MODE_COMMUNICATION_REDIRECT"); mAudioManager.setMode(AudioManager.MODE_COMMUNICATION_REDIRECT); mMostRecentMode = AudioManager.MODE_NORMAL; mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS); mCallAudioManager.getCallAudioRouteAdapter().sendMessageWithSessionInfo( CallAudioRouteStateMachine.STREAMING_FORCE_ENABLED); } private void preExit() { mCallAudioManager.getCallAudioRouteAdapter().sendMessageWithSessionInfo( CallAudioRouteStateMachine.STREAMING_FORCE_DISABLED); } @Override public boolean processMessage(Message msg) { if (super.processMessage(msg) == HANDLED) { return HANDLED; } MessageArgs args = (MessageArgs) msg.obj; switch (msg.what) { case NO_MORE_ACTIVE_OR_DIALING_CALLS: // Switch to either ringing, holding, or inactive transitionTo(calculateProperStateFromArgs(args)); return HANDLED; case NO_MORE_RINGING_CALLS: // Do nothing. return HANDLED; case NO_MORE_HOLDING_CALLS: // Do nothing. return HANDLED; case NO_MORE_AUDIO_PROCESSING_CALLS: // Do nothing. return HANDLED; case NEW_ACTIVE_OR_DIALING_CALL: // Only possible for emergency call BaseState destState = calculateProperStateFromArgs(args); if (destState != this) { preExit(); transitionTo(destState); } return HANDLED; case NEW_RINGING_CALL: // Only possible for emergency call preExit(); transitionTo(mRingingFocusState); return HANDLED; case NEW_HOLDING_CALL: // Do nothing. return HANDLED; case NEW_AUDIO_PROCESSING_CALL: // Do nothing. return HANDLED; case START_CALL_STREAMING: // Can happen as a duplicate message return HANDLED; case TONE_STARTED_PLAYING: // Do nothing. return HANDLED; case STOP_CALL_STREAMING: transitionTo(calculateProperStateFromArgs(args)); return HANDLED; default: // The forced focus switch commands are handled by BaseState. return NOT_HANDLED; } } } /** * This class is used for calls on hold and end-of-call tones. */ private class OtherFocusState extends BaseState { @Override public void enter() { Log.i(LOG_TAG, "Audio focus entering TONE/HOLDING state"); mLocalLog.log("Enter TONE/HOLDING"); if (mFeatureFlags.telecomResolveHiddenDependencies()) { mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST; mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST); } else { mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); } mAudioManager.setMode(mMostRecentMode); mLocalLog.log("Mode " + mMostRecentMode); mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteStateMachine.ACTIVE_FOCUS); } @Override public boolean processMessage(Message msg) { if (super.processMessage(msg) == HANDLED) { return HANDLED; } MessageArgs args = (MessageArgs) msg.obj; switch (msg.what) { case NO_MORE_HOLDING_CALLS: if (args.hasActiveOrDialingCalls) { transitionTo(args.foregroundCallIsVoip ? mVoipCallFocusState : mSimCallFocusState); } else if (args.hasRingingCalls) { transitionTo(mRingingFocusState); } else if (!args.isTonePlaying) { transitionTo(mUnfocusedState); } // Do nothing if a tone is playing. return HANDLED; case NEW_ACTIVE_OR_DIALING_CALL: transitionTo(args.foregroundCallIsVoip ? mVoipCallFocusState : mSimCallFocusState); return HANDLED; case NEW_RINGING_CALL: // TODO: consider whether to move this into MessageArgs if more things start // to use it. if (args.hasHoldingCalls && mSystemStateHelper.isDeviceAtEar()) { mCallAudioManager.startCallWaiting( "Device is at ear with held call"); } else { transitionTo(mRingingFocusState); } return HANDLED; case NEW_HOLDING_CALL: // Do nothing. return HANDLED; case NO_MORE_RINGING_CALLS: // If there are no more ringing calls in this state, then stop any call-waiting // tones that may be playing. mCallAudioManager.stopCallWaiting(); return HANDLED; case TONE_STOPPED_PLAYING: transitionTo(calculateProperStateFromArgs(args)); return HANDLED; case AUDIO_OPERATIONS_COMPLETE: Log.w(LOG_TAG, "Should not be seeing AUDIO_OPERATIONS_COMPLETE in a focused" + " state"); return HANDLED; default: return NOT_HANDLED; } } } private static final String LOG_TAG = CallAudioModeStateMachine.class.getSimpleName(); private final BaseState mUnfocusedState = new UnfocusedState(); private final BaseState mRingingFocusState = new RingingFocusState(); private final BaseState mSimCallFocusState = new SimCallFocusState(); private final BaseState mVoipCallFocusState = new VoipCallFocusState(); private final BaseState mAudioProcessingFocusState = new AudioProcessingFocusState(); private final BaseState mStreamingFocusState = new StreamingFocusState(); private final BaseState mOtherFocusState = new OtherFocusState(); private final AudioManager mAudioManager; private final SystemStateHelper mSystemStateHelper; private CallAudioManager mCallAudioManager; private FeatureFlags mFeatureFlags; private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker; private int mMostRecentMode; private boolean mIsInitialized = false; public CallAudioModeStateMachine(SystemStateHelper systemStateHelper, AudioManager audioManager, FeatureFlags featureFlags, CallAudioCommunicationDeviceTracker callAudioCommunicationDeviceTracker) { super(CallAudioModeStateMachine.class.getSimpleName()); mAudioManager = audioManager; mSystemStateHelper = systemStateHelper; mMostRecentMode = AudioManager.MODE_NORMAL; mFeatureFlags = featureFlags; mCommunicationDeviceTracker = callAudioCommunicationDeviceTracker; createStates(); } /** * Used for testing */ public CallAudioModeStateMachine(SystemStateHelper systemStateHelper, AudioManager audioManager, Looper looper, FeatureFlags featureFlags, CallAudioCommunicationDeviceTracker communicationDeviceTracker) { super(CallAudioModeStateMachine.class.getSimpleName(), looper); mAudioManager = audioManager; mSystemStateHelper = systemStateHelper; mMostRecentMode = AudioManager.MODE_NORMAL; mFeatureFlags = featureFlags; mCommunicationDeviceTracker = communicationDeviceTracker; createStates(); } private void createStates() { addState(mUnfocusedState); addState(mRingingFocusState); addState(mSimCallFocusState); addState(mVoipCallFocusState); addState(mAudioProcessingFocusState); addState(mStreamingFocusState); addState(mOtherFocusState); setInitialState(mUnfocusedState); start(); sendMessage(INITIALIZE, new MessageArgs.Builder() .setHasActiveOrDialingCalls(false) .setHasRingingCalls(false) .setHasHoldingCalls(false) .setIsTonePlaying(false) .setForegroundCallIsVoip(false) .setIsStreaming(false) .setSession(Log.createSubsession()) .build()); } public void setCallAudioManager(CallAudioManager callAudioManager) { mCallAudioManager = callAudioManager; } public String getCurrentStateName() { IState currentState = getCurrentState(); return currentState == null ? "no state" : currentState.getName(); } public void sendMessageWithArgs(int messageCode, MessageArgs args) { sendMessage(messageCode, args); } @Override protected void onPreHandleMessage(Message msg) { if (msg.obj != null && msg.obj instanceof MessageArgs) { Log.continueSession(((MessageArgs) msg.obj).session, "CAMSM.pM_" + msg.what); Log.i(LOG_TAG, "Message received: %s.", MESSAGE_CODE_TO_NAME.get(msg.what)); } else if (msg.what == RUN_RUNNABLE && msg.obj instanceof Runnable) { Log.i(LOG_TAG, "Running runnable for testing"); } else { Log.w(LOG_TAG, "Message sent must be of type nonnull MessageArgs, but got " + (msg.obj == null ? "null" : msg.obj.getClass().getSimpleName())); Log.w(LOG_TAG, "The message was of code %d = %s", msg.what, MESSAGE_CODE_TO_NAME.get(msg.what)); } } public void dumpPendingMessages(IndentingPrintWriter pw) { getHandler().getLooper().dump(pw::println, ""); } public void dump(IndentingPrintWriter pw) { pw.println("History:"); mLocalLog.dump(pw); pw.println("Pending Msg:"); dumpPendingMessages(pw); } @Override protected void onPostHandleMessage(Message msg) { Log.endSession(); } private BaseState calculateProperStateFromArgs(MessageArgs args) { // If there are active, audio-processing, holding, or ringing calls, // switch to the appropriate focus. // Otherwise abandon focus. // The order matters here. If there is streaming call, holding streaming route for them // takes priority. After that, holding focus for active calls takes priority. After that, we // want to prioritize holding calls over ringing calls so that when a call-waiting call gets // answered, there's no transition in and out of the ringing focus state. After that, we // want tones since we actually hold focus during them, then the audio processing state // because that will release focus. if (args.isStreaming) { return mSimCallFocusState; } else if (args.hasActiveOrDialingCalls) { if (args.foregroundCallIsVoip) { return mVoipCallFocusState; } else { return mSimCallFocusState; } } else if (args.hasHoldingCalls) { return mOtherFocusState; } else if (args.hasRingingCalls) { return mRingingFocusState; } else if (args.isTonePlaying) { return mOtherFocusState; } else if (args.hasAudioProcessingCalls) { return mAudioProcessingFocusState; } return mUnfocusedState; } }