blob: bb0be7b3f7fd022f8a13b8a92705e6d4ee976e73 [file]
/*
* 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.telecom.Log;
import android.telecom.Logging.Runnable;
import android.telecom.Logging.Session;
import android.util.IndentingPrintWriter;
import android.util.LocalLog;
import android.util.SparseArray;
import com.android.internal.util.IState;
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) {
return new CallAudioModeStateMachine(systemStateHelper, am,
featureFlags);
}
}
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);
}
}
}
private static final AudioAttributes RING_AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.setLegacyStreamType(AudioManager.STREAM_RING)
.build();
/**
* Audio focus request used when ringing for a call.
*/
public static final AudioFocusRequest RING_AUDIO_FOCUS_REQUEST = new AudioFocusRequest
.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
.setAudioAttributes(RING_AUDIO_ATTRIBUTES)
// Important!!! Need to lock focus else other things can steal from us.
.setLocksFocus(true)
.build();
private static final AudioAttributes CALL_AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setLegacyStreamType(AudioManager.STREAM_VOICE_CALL)
.build();
/**
* Audio focus request used while in a call.
*/
public static final AudioFocusRequest CALL_AUDIO_FOCUS_REQUEST = new AudioFocusRequest
.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
.setAudioAttributes(CALL_AUDIO_ATTRIBUTES)
// Important!!! Need to lock focus else other things can steal from us.
.setLocksFocus(true)
.build();
// 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 NO_MORE_LOCAL_VOICEMAIL_CALLS = 1005;
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 NEW_LOCAL_VOICEMAIL_CALL = 2005;
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;
public static final int CRS_FALLBACK_TO_LOCAL_RINGING = 5002;
// 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<String> MESSAGE_CODE_TO_NAME = new SparseArray<String>() {{
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(NEW_LOCAL_VOICEMAIL_CALL, "START_LOCAL_VOICEMAIL");
put(NO_MORE_LOCAL_VOICEMAIL_CALLS, "STOP_LOCAL_VOICEMAIL");
put(CRS_FALLBACK_TO_LOCAL_RINGING, "CRS_FALLBACK_TO_LOCAL_RINGING");
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();
/**
* Need to track the current ongoing audio focus request so we can abandon it later.
*/
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) {
Log.i(this, "enter: AudioManager#setMode(MODE_NORMAL)");
setMode(AudioManager.MODE_NORMAL);
mCallAudioManager.setCallAudioRouteFocusState(
CallAudioRouteController.NO_FOCUS);
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 NEW_LOCAL_VOICEMAIL_CALL:
transitionTo(mLocalVoicemailFocusState);
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:
abandonAudioFocus();
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(CallAudioRouteController.NO_FOCUS);
Log.i(this, "enter: AudioManager#setMode(MODE_AUDIO_PROCESSING)");
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 NEW_LOCAL_VOICEMAIL_CALL:
transitionTo(mLocalVoicemailFocusState);
return HANDLED;
case AUDIO_OPERATIONS_COMPLETE:
Log.i(LOG_TAG, "AudioManager#abandonAudioFocusRequest: now "
+ "AUDIO_PROCESSING");
abandonAudioFocus();
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() {
if (mHasFocus && mCallAudioManager.isRingtonePlaying()) {
Log.i(LOG_TAG,
"RingingFocusState#tryStartRinging -- audio focus previously"
+ " acquired and ringtone already playing -- skipping.");
return;
}
// Note: startRinging will take DND into account; if a call is suppressed by DND,
// the method will return false and we will not get audio focus.
if (mCallAudioManager.startRinging()) {
mCurrentAudioFocusRequest = RING_AUDIO_FOCUS_REQUEST;
int focusResult = mAudioManager.requestAudioFocus(RING_AUDIO_FOCUS_REQUEST,
null /* no policy */);
Log.i(this, "tryStartRinging: AudioManager#requestAudioFocus(RING)=%s",
audioFocusRequestResultToString(focusResult));
// 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) {
if (mCallAudioManager.isCrsInCallMode()
&& mCallAudioManager.getCrsAudioController() != null) {
mCallAudioManager.getCrsAudioController().setAudioModeForCrs();
mLocalLog.log("Mode MODE_IN_CALL , It is CRS CALL");
} else {
setMode(AudioManager.MODE_RINGTONE);
mLocalLog.log("Mode MODE_RINGTONE");
}
}
mCallAudioManager.setCallAudioRouteFocusState(
CallAudioRouteController.RINGING_FOCUS);
mHasFocus = true;
} else {
Log.i(LOG_TAG, "RINGING state, try start ringing but not acquiring audio focus");
}
}
@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.
if (mCallAudioManager.getCrsAudioController() != null) {
mCallAudioManager.getCrsAudioController().removeListener();
}
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;
case CRS_FALLBACK_TO_LOCAL_RINGING:
Log.i(LOG_TAG, "RINGING state, received CRS_FALLBACK_TO_LOCAL_RINGING");
//Ringing call changed, so stop current ring first.
mCallAudioManager.stopRinging();
tryStartRinging();
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");
Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)");
mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
int focusResult = mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST,
null /* no policy */);
Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)=%s",
audioFocusRequestResultToString(focusResult));
Log.i(this, "enter: AudioManager#setMode(MODE_IN_CALL)");
setMode(AudioManager.MODE_IN_CALL);
mLocalLog.log("Mode MODE_IN_CALL");
mMostRecentMode = AudioManager.MODE_IN_CALL;
mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteController.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");
Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)");
mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
int focusResult = mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST,
null /* no policy */);
Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)=%s",
audioFocusRequestResultToString(focusResult));
setMode(AudioManager.MODE_IN_COMMUNICATION);
mLocalLog.log("Mode MODE_IN_COMMUNICATION");
mMostRecentMode = AudioManager.MODE_IN_COMMUNICATION;
mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteController.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;
case NEW_LOCAL_VOICEMAIL_CALL:
transitionTo(mLocalVoicemailFocusState);
return HANDLED;
default:
// The forced focus switch commands are handled by BaseState.
return NOT_HANDLED;
}
}
}
/**
* This state is used when there is a call which is undergoing local voicemail processing via
* a {@link LocalVoicemailService}.
*/
private class LocalVoicemailFocusState extends BaseState {
@Override
public void enter() {
Log.i(LOG_TAG, "Audio focus entering local voicemail state");
mLocalLog.log("Enter local voicemail");
mLocalLog.log("Mode MODE_CALL_REDIRECT");
Log.i(this, "enter: AudioManager#setMode(MODE_CALL_REDIRECT");
setMode(AudioManager.MODE_CALL_REDIRECT);
mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteController.ACTIVE_FOCUS);
}
private void preExit() {
// N/A at the moment
}
@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:
// You can't go from local voicemail to streaming.
return HANDLED;
case NEW_LOCAL_VOICEMAIL_CALL:
return HANDLED;
case TONE_STARTED_PLAYING:
// Do nothing.
return HANDLED;
case STOP_CALL_STREAMING:
// Not possible.
return HANDLED;
case NO_MORE_LOCAL_VOICEMAIL_CALLS:
transitionTo(calculateProperStateFromArgs(args));
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");
Log.i(this, "enter: AudioManager#setMode(MODE_COMMUNICATION_REDIRECT");
setMode(AudioManager.MODE_COMMUNICATION_REDIRECT);
mMostRecentMode = AudioManager.MODE_NORMAL;
mCallAudioManager.setCallAudioRouteFocusState(CallAudioRouteController.ACTIVE_FOCUS);
mCallAudioManager.getCallAudioRouteAdapter().sendMessageWithSessionInfo(
CallAudioRouteController.STREAMING_FORCE_ENABLED);
}
private void preExit() {
mCallAudioManager.getCallAudioRouteAdapter().sendMessageWithSessionInfo(
CallAudioRouteController.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 NEW_LOCAL_VOICEMAIL_CALL:
// Do nothing; this can't happen.
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");
mCurrentAudioFocusRequest = CALL_AUDIO_FOCUS_REQUEST;
int focusResult = mAudioManager.requestAudioFocus(CALL_AUDIO_FOCUS_REQUEST,
null /* no policy */);
Log.i(this, "enter: AudioManager#requestAudioFocus(CALL)=%s",
audioFocusRequestResultToString(focusResult));
Log.i(this, "enter: AudioManager#setMode(%d)", mMostRecentMode);
setMode(mMostRecentMode);
mLocalLog.log("Mode " + mMostRecentMode);
mCallAudioManager.setCallAudioRouteFocusStateForEndTone();
}
@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 mLocalVoicemailFocusState = new LocalVoicemailFocusState();
private final BaseState mOtherFocusState = new OtherFocusState();
private final AudioManager mAudioManager;
private final SystemStateHelper mSystemStateHelper;
private CallAudioManager mCallAudioManager;
private FeatureFlags mFeatureFlags;
private int mMostRecentMode;
private boolean mIsInitialized = false;
public CallAudioModeStateMachine(SystemStateHelper systemStateHelper,
AudioManager audioManager, FeatureFlags featureFlags) {
super(CallAudioModeStateMachine.class.getSimpleName());
mAudioManager = audioManager;
mSystemStateHelper = systemStateHelper;
mMostRecentMode = AudioManager.MODE_NORMAL;
mFeatureFlags = featureFlags;
createStates();
}
/**
* Used for testing
*/
public CallAudioModeStateMachine(SystemStateHelper systemStateHelper,
AudioManager audioManager, Looper looper, FeatureFlags featureFlags) {
super(CallAudioModeStateMachine.class.getSimpleName(), looper);
mAudioManager = audioManager;
mSystemStateHelper = systemStateHelper;
mMostRecentMode = AudioManager.MODE_NORMAL;
mFeatureFlags = featureFlags;
createStates();
}
private void createStates() {
addState(mUnfocusedState);
addState(mRingingFocusState);
addState(mSimCallFocusState);
addState(mVoipCallFocusState);
addState(mAudioProcessingFocusState);
addState(mStreamingFocusState);
addState(mLocalVoicemailFocusState);
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;
}
/**
* Abandons the current audio focus request.
*/
private void abandonAudioFocus() {
if (mCurrentAudioFocusRequest != null) {
Log.i(this, "abandonAudioFocus: "
+ "AudioManager#abandonAudioFocusRequest(); now unfocused");
mAudioManager.abandonAudioFocusRequest(mCurrentAudioFocusRequest);
mCurrentAudioFocusRequest = null;
} else {
Log.i(this, "abandonAudioFocus: already unfocused");
}
}
private void setMode(int mode) {
if (!com.android.internal.telecom.flags.Flags.callAudioRouteRf()) {
mAudioManager.setMode(mode);
} else {
mCallAudioManager.setAudioMode(mode);
}
}
private String audioFocusRequestResultToString(int audioFocusRequest) {
switch (audioFocusRequest) {
case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
return "AUDIOFOCUS_REQUEST_FAILED";
case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:
return "AUDIOFOCUS_REQUEST_GRANTED";
case AudioManager.AUDIOFOCUS_REQUEST_DELAYED:
return "AUDIOFOCUS_REQUEST_DELAYED";
default:
return "UNKNOWN";
}
}
}