/*
 * 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<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(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;
    }

}
