/*
 * Copyright (C) 2018 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.hdmi;

import static android.hardware.hdmi.DeviceFeatures.FEATURE_NOT_SUPPORTED;
import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORTED;

import static com.android.server.hdmi.Constants.ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON;
import static com.android.server.hdmi.Constants.PROPERTY_SYSTEM_AUDIO_CONTROL_ON_POWER_ON;
import static com.android.server.hdmi.Constants.USE_LAST_STATE_SYSTEM_AUDIO_CONTROL_ON_POWER_ON;
import static com.android.server.hdmi.HdmiControlService.SendMessageCallback;

import android.annotation.Nullable;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.hardware.hdmi.DeviceFeatures;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.HdmiPortInfo;
import android.hardware.hdmi.IHdmiControlCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.tv.TvContract;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager.TvInputCallback;
import android.os.SystemProperties;
import android.sysprop.HdmiProperties;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.hdmi.Constants.AudioCodec;
import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback;
import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
import com.android.server.hdmi.HdmiUtils.CodecSad;
import com.android.server.hdmi.HdmiUtils.DeviceConfig;

import org.xmlpull.v1.XmlPullParserException;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Represent a logical device of type {@link HdmiDeviceInfo#DEVICE_AUDIO_SYSTEM} residing in Android
 * system.
 */
public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource {

    private static final String TAG = "HdmiCecLocalDeviceAudioSystem";

    private static final boolean WAKE_ON_HOTPLUG = false;
    private static final int MAX_CHANNELS = 8;
    private static final HashMap<Integer, List<Integer>> AUDIO_CODECS_MAP =
            mapAudioCodecWithAudioFormat();

    // Whether the System Audio Control feature is enabled or not. True by default.
    @GuardedBy("mLock")
    private boolean mSystemAudioControlFeatureEnabled;

    /**
     * Indicates if the TV that the current device is connected to supports System Audio Mode or not
     *
     * <p>If the current device has no information on this, keep mTvSystemAudioModeSupport null
     *
     * <p>The boolean will be reset to null every time when the current device goes to standby
     * or loses its physical address.
     */
    private Boolean mTvSystemAudioModeSupport = null;

    // Whether ARC is available or not. "true" means that ARC is established between TV and
    // AVR as audio receiver.
    @ServiceThreadOnly private boolean mArcEstablished = false;

    // If the current device uses TvInput for ARC. We assume all other inputs also use TvInput
    // when ARC is using TvInput.
    private boolean mArcIntentUsed = HdmiProperties.arc_port().orElse("0").contains("tvinput");

    // Keeps the mapping (HDMI port ID to TV input URI) to keep track of the TV inputs ready to
    // accept input switching request from HDMI devices.
    @GuardedBy("mLock")
    private final HashMap<Integer, String> mPortIdToTvInputs = new HashMap<>();

    // A map from TV input id to HDMI device info.
    @GuardedBy("mLock")
    private final HashMap<String, HdmiDeviceInfo> mTvInputsToDeviceInfo = new HashMap<>();

    // Message buffer used to buffer selected messages to process later. <Active Source>
    // from a source device, for instance, needs to be buffered if the device is not
    // discovered yet. The buffered commands are taken out and when they are ready to
    // handle.
    private final DelayedMessageBuffer mDelayedMessageBuffer = new DelayedMessageBuffer(this);

    protected HdmiCecLocalDeviceAudioSystem(HdmiControlService service) {
        super(service, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
        mRoutingControlFeatureEnabled = mService.getHdmiCecConfig().getIntValue(
                HdmiControlManager.CEC_SETTING_NAME_ROUTING_CONTROL)
                    == HdmiControlManager.ROUTING_CONTROL_ENABLED;
        mSystemAudioControlFeatureEnabled = mService.getHdmiCecConfig().getIntValue(
                HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL)
                    == HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED;
        mStandbyHandler = new HdmiCecStandbyModeHandler(service, this);
    }

    private static final String SHORT_AUDIO_DESCRIPTOR_CONFIG_PATH = "/vendor/etc/sadConfig.xml";

    private final TvInputCallback mTvInputCallback = new TvInputCallback() {
        @Override
        public void onInputAdded(String inputId) {
            addOrUpdateTvInput(inputId);
        }

        @Override
        public void onInputRemoved(String inputId) {
            removeTvInput(inputId);
        }

        @Override
        public void onInputUpdated(String inputId) {
            addOrUpdateTvInput(inputId);
        }
    };

    @ServiceThreadOnly
    private void addOrUpdateTvInput(String inputId) {
        assertRunOnServiceThread();
        synchronized (mLock) {
            TvInputInfo tvInfo = mService.getTvInputManager().getTvInputInfo(inputId);
            if (tvInfo == null) {
                return;
            }
            HdmiDeviceInfo info = tvInfo.getHdmiDeviceInfo();
            if (info == null) {
                return;
            }
            mPortIdToTvInputs.put(info.getPortId(), inputId);
            mTvInputsToDeviceInfo.put(inputId, info);
            if (info.isCecDevice()) {
                processDelayedActiveSource(info.getLogicalAddress());
            }
        }
    }

    @ServiceThreadOnly
    private void removeTvInput(String inputId) {
        assertRunOnServiceThread();
        synchronized (mLock) {
            if (mTvInputsToDeviceInfo.get(inputId) == null) {
                return;
            }
            int portId = mTvInputsToDeviceInfo.get(inputId).getPortId();
            mPortIdToTvInputs.remove(portId);
            mTvInputsToDeviceInfo.remove(inputId);
        }
    }

    @Override
    @ServiceThreadOnly
    protected boolean isInputReady(int portId) {
        assertRunOnServiceThread();
        String tvInputId = mPortIdToTvInputs.get(portId);
        HdmiDeviceInfo info = mTvInputsToDeviceInfo.get(tvInputId);
        return info != null;
    }

    @Override
    protected DeviceFeatures computeDeviceFeatures() {
        boolean arcSupport = SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true);

        return DeviceFeatures.NO_FEATURES_SUPPORTED.toBuilder()
                .setArcRxSupport(arcSupport ? FEATURE_SUPPORTED : FEATURE_NOT_SUPPORTED)
                .build();
    }

    @Override
    @ServiceThreadOnly
    void onHotplug(int portId, boolean connected) {
        assertRunOnServiceThread();
        if (WAKE_ON_HOTPLUG && connected) {
            mService.wakeUp();
        }
        HdmiPortInfo portInfo = mService.getPortInfo(portId);
        if (portInfo != null && portInfo.getType() == HdmiPortInfo.PORT_OUTPUT) {
            mCecMessageCache.flushAll();
            if (!connected) {
                if (isSystemAudioActivated()) {
                    mTvSystemAudioModeSupport = null;
                    checkSupportAndSetSystemAudioMode(false);
                }
                if (isArcEnabled()) {
                    setArcStatus(false);
                }
            }
        } else if (!connected && mPortIdToTvInputs.get(portId) != null) {
            String tvInputId = mPortIdToTvInputs.get(portId);
            HdmiDeviceInfo info = mTvInputsToDeviceInfo.get(tvInputId);
            if (info == null) {
                return;
            }
            // Update with TIF on the device removal. TIF callback will update
            // mPortIdToTvInputs and mPortIdToTvInputs.
            mService.getHdmiCecNetwork().removeCecDevice(this, info.getLogicalAddress());
        }
    }

    @Override
    @ServiceThreadOnly
    protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
        terminateAudioReturnChannel();

        super.disableDevice(initiatedByCec, callback);
        assertRunOnServiceThread();
        mService.unregisterTvInputCallback(mTvInputCallback);
        // Removing actions and invoking the callback is similar to
        // HdmiCecLocalDevicePlayback#disableDevice and HdmiCecLocalDeviceTv#disableDevice,
        // with the difference that in those classes only specific actions are removed and
        // here we remove all actions. We don't expect any issues with removing all actions
        // at this time, but we have to pay attention in the future.
        removeAllActions();
        // Call the callback instantly or else it will be called 5 seconds later.
        checkIfPendingActionsCleared();
    }

    @Override
    @ServiceThreadOnly
    protected void onStandby(boolean initiatedByCec, int standbyAction,
            StandbyCompletedCallback callback) {
        assertRunOnServiceThread();
        // Invalidate the internal active source record when goes to standby
        // This set will also update mIsActiveSource
        mService.setActiveSource(Constants.ADDR_INVALID, Constants.INVALID_PHYSICAL_ADDRESS,
                "HdmiCecLocalDeviceAudioSystem#onStandby()");
        mTvSystemAudioModeSupport = null;
        // Record the last state of System Audio Control before going to standby
        synchronized (mLock) {
            mService.writeStringSystemProperty(
                    Constants.PROPERTY_LAST_SYSTEM_AUDIO_CONTROL,
                    isSystemAudioActivated() ? "true" : "false");
        }
        terminateSystemAudioMode(callback);
    }

    @Override
    @ServiceThreadOnly
    protected void onAddressAllocated(int logicalAddress, int reason) {
        assertRunOnServiceThread();
        if (reason == mService.INITIATED_BY_ENABLE_CEC) {
            mService.setAndBroadcastActiveSource(mService.getPhysicalAddress(),
                    getDeviceInfo().getDeviceType(), Constants.ADDR_BROADCAST,
                    "HdmiCecLocalDeviceAudioSystem#onAddressAllocated()");
        }
        mService.sendCecCommand(
                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
                        getDeviceInfo().getLogicalAddress(),
                        mService.getPhysicalAddress(),
                        mDeviceType));
        mService.sendCecCommand(
                HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
                        getDeviceInfo().getLogicalAddress(), mService.getVendorId()));
        mService.registerTvInputCallback(mTvInputCallback);
        // Some TVs, for example Mi TV, need ARC on before turning System Audio Mode on
        // to request Short Audio Descriptor. Since ARC and SAM are independent,
        // we can turn on ARC anyways when audio system device just boots up.
        initArcOnFromAvr();

        // This prevents turning on of System Audio Mode during a quiescent boot. If the quiescent
        // boot is exited just after this check, this code will be executed only at the next
        // wake-up.
        if (!mService.isScreenOff()) {
            int systemAudioControlOnPowerOnProp =
                    SystemProperties.getInt(
                            PROPERTY_SYSTEM_AUDIO_CONTROL_ON_POWER_ON,
                            ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON);
            boolean lastSystemAudioControlStatus =
                    SystemProperties.getBoolean(Constants.PROPERTY_LAST_SYSTEM_AUDIO_CONTROL, true);
            systemAudioControlOnPowerOn(
                    systemAudioControlOnPowerOnProp, lastSystemAudioControlStatus);
        }
        mService.getHdmiCecNetwork().clearDeviceList();
        launchDeviceDiscovery();
        startQueuedActions();
    }

    @Override
    protected int findKeyReceiverAddress() {
        if (getActiveSource().isValid()) {
            return getActiveSource().logicalAddress;
        }
        return Constants.ADDR_INVALID;
    }

    @VisibleForTesting
    protected void systemAudioControlOnPowerOn(
            int systemAudioOnPowerOnProp, boolean lastSystemAudioControlStatus) {
        if ((systemAudioOnPowerOnProp == ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON)
                || ((systemAudioOnPowerOnProp == USE_LAST_STATE_SYSTEM_AUDIO_CONTROL_ON_POWER_ON)
                && lastSystemAudioControlStatus && isSystemAudioControlFeatureEnabled())) {
            addAndStartAction(new SystemAudioInitiationActionFromAvr(this), true);
        }
    }

    @Override
    @ServiceThreadOnly
    protected int getPreferredAddress() {
        assertRunOnServiceThread();
        return SystemProperties.getInt(
            Constants.PROPERTY_PREFERRED_ADDRESS_AUDIO_SYSTEM, Constants.ADDR_UNREGISTERED);
    }

    @Override
    @ServiceThreadOnly
    protected void setPreferredAddress(int addr) {
        assertRunOnServiceThread();
        mService.writeStringSystemProperty(
                Constants.PROPERTY_PREFERRED_ADDRESS_AUDIO_SYSTEM, String.valueOf(addr));
    }

    @ServiceThreadOnly
    void processDelayedActiveSource(int address) {
        assertRunOnServiceThread();
        mDelayedMessageBuffer.processActiveSource(address);
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleActiveSource(HdmiCecMessage message) {
        assertRunOnServiceThread();
        int logicalAddress = message.getSource();
        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
        if (HdmiUtils.getLocalPortFromPhysicalAddress(
            physicalAddress, mService.getPhysicalAddress())
                == HdmiUtils.TARGET_NOT_UNDER_LOCAL_DEVICE) {
            return super.handleActiveSource(message);
        }
        // If the new Active Source is under the current device, check if the device info and the TV
        // input is ready to switch to the new Active Source. If not ready, buffer the cec command
        // to handle later when the device is ready.
        HdmiDeviceInfo info = mService.getHdmiCecNetwork().getCecDeviceInfo(logicalAddress);
        if (info == null) {
            HdmiLogger.debug("Device info %X not found; buffering the command", logicalAddress);
            mDelayedMessageBuffer.add(message);
        } else if (!isInputReady(info.getPortId())){
            HdmiLogger.debug("Input not ready for device: %X; buffering the command", info.getId());
            mDelayedMessageBuffer.add(message);
        } else {
            mDelayedMessageBuffer.removeActiveSource();
            return super.handleActiveSource(message);
        }
        return Constants.HANDLED;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleInitiateArc(HdmiCecMessage message) {
        assertRunOnServiceThread();
        // TODO(amyjojo): implement initiate arc handler
        HdmiLogger.debug(TAG + "Stub handleInitiateArc");
        return Constants.HANDLED;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleReportArcInitiate(HdmiCecMessage message) {
        assertRunOnServiceThread();
        /*
         * Ideally, we should have got this response before the {@link ArcInitiationActionFromAvr}
         * has timed out. Even if the response is late, {@link ArcInitiationActionFromAvr
         * #handleInitiateArcTimeout()} would not have disabled ARC. So nothing needs to be done
         * here.
         */
        return Constants.HANDLED;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleReportArcTermination(HdmiCecMessage message) {
        assertRunOnServiceThread();
        processArcTermination();
        return Constants.HANDLED;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleGiveAudioStatus(HdmiCecMessage message) {
        assertRunOnServiceThread();
        if (isSystemAudioControlFeatureEnabled() && mService.getHdmiCecVolumeControl()
                == HdmiControlManager.VOLUME_CONTROL_ENABLED) {
            reportAudioStatus(message.getSource());
            return Constants.HANDLED;
        }
        return Constants.ABORT_REFUSED;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleGiveSystemAudioModeStatus(HdmiCecMessage message) {
        assertRunOnServiceThread();
        // If the audio system is initiating the system audio mode on and TV asks the sam status at
        // the same time, respond with true. Since we know TV supports sam in this situation.
        // If the query comes from STB, we should respond with the current sam status and the STB
        // should listen to the <Set System Audio Mode> broadcasting.
        boolean isSystemAudioModeOnOrTurningOn = isSystemAudioActivated();
        if (!isSystemAudioModeOnOrTurningOn
                && message.getSource() == Constants.ADDR_TV
                && hasAction(SystemAudioInitiationActionFromAvr.class)) {
            isSystemAudioModeOnOrTurningOn = true;
        }
        mService.sendCecCommand(
                HdmiCecMessageBuilder.buildReportSystemAudioMode(
                        getDeviceInfo().getLogicalAddress(),
                        message.getSource(),
                        isSystemAudioModeOnOrTurningOn));
        return Constants.HANDLED;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleRequestArcInitiate(HdmiCecMessage message) {
        assertRunOnServiceThread();
        removeAction(ArcInitiationActionFromAvr.class);
        if (!mService.readBooleanSystemProperty(Constants.PROPERTY_ARC_SUPPORT, true)) {
            return Constants.ABORT_UNRECOGNIZED_OPCODE;
        } else if (!isDirectConnectToTv()) {
            HdmiLogger.debug("AVR device is not directly connected with TV");
            return Constants.ABORT_NOT_IN_CORRECT_MODE;
        } else {
            // Action has been removed if it existed, do not attempt to remove again before start.
            addAndStartAction(new ArcInitiationActionFromAvr(this));
            return Constants.HANDLED;
        }
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleRequestArcTermination(HdmiCecMessage message) {
        assertRunOnServiceThread();
        if (!SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)) {
            return Constants.ABORT_UNRECOGNIZED_OPCODE;
        } else if (!isArcEnabled()) {
            HdmiLogger.debug("ARC is not established between TV and AVR device");
            return Constants.ABORT_NOT_IN_CORRECT_MODE;
        } else {
            if (!getActions(ArcTerminationActionFromAvr.class).isEmpty()
                    && !getActions(ArcTerminationActionFromAvr.class).get(0).mCallbacks.isEmpty()) {
                IHdmiControlCallback callback =
                        getActions(ArcTerminationActionFromAvr.class).get(0).mCallbacks.get(0);
                addAndStartAction(new ArcTerminationActionFromAvr(this, callback), true);
            } else {
                addAndStartAction(new ArcTerminationActionFromAvr(this), true);
            }
            return Constants.HANDLED;
        }
    }

    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleRequestShortAudioDescriptor(HdmiCecMessage message) {
        assertRunOnServiceThread();
        HdmiLogger.debug(TAG + "Stub handleRequestShortAudioDescriptor");
        if (!isSystemAudioControlFeatureEnabled()) {
            return Constants.ABORT_REFUSED;
        }
        if (!isSystemAudioActivated()) {
            return Constants.ABORT_NOT_IN_CORRECT_MODE;
        }

        List<DeviceConfig> config = null;
        File file = new File(SHORT_AUDIO_DESCRIPTOR_CONFIG_PATH);
        if (file.exists()) {
            try {
                InputStream in = new FileInputStream(file);
                config = HdmiUtils.ShortAudioDescriptorXmlParser.parse(in);
                in.close();
            } catch (IOException e) {
                Slog.e(TAG, "Error reading file: " + file, e);
            } catch (XmlPullParserException e) {
                Slog.e(TAG, "Unable to parse file: " + file, e);
            }
        }

        @AudioCodec int[] audioCodecs = parseAudioCodecs(message.getParams());
        byte[] sadBytes;
        if (config != null && config.size() > 0) {
            sadBytes = getSupportedShortAudioDescriptorsFromConfig(config, audioCodecs);
        } else {
            AudioDeviceInfo deviceInfo = getSystemAudioDeviceInfo();
            if (deviceInfo == null) {
                return Constants.ABORT_UNABLE_TO_DETERMINE;
            }

            sadBytes = getSupportedShortAudioDescriptors(deviceInfo, audioCodecs);
        }

        if (sadBytes.length == 0) {
            return Constants.ABORT_INVALID_OPERAND;
        } else {
            mService.sendCecCommand(
                    HdmiCecMessageBuilder.buildReportShortAudioDescriptor(
                            getDeviceInfo().getLogicalAddress(), message.getSource(), sadBytes));
            return Constants.HANDLED;
        }
    }

    @VisibleForTesting
    byte[] getSupportedShortAudioDescriptors(
            AudioDeviceInfo deviceInfo, @AudioCodec int[] audioCodecs) {
        ArrayList<byte[]> sads = new ArrayList<>(audioCodecs.length);
        for (@AudioCodec int audioCodec : audioCodecs) {
            byte[] sad = getSupportedShortAudioDescriptor(deviceInfo, audioCodec);
            if (sad != null) {
                if (sad.length == 3) {

                    sads.add(sad);
                } else {
                    HdmiLogger.warning(
                            "Dropping Short Audio Descriptor with length %d for requested codec %x",
                            sad.length, audioCodec);
                }
            }
        }
        return getShortAudioDescriptorBytes(sads);
    }

    private byte[] getSupportedShortAudioDescriptorsFromConfig(
            List<DeviceConfig> deviceConfig, @AudioCodec int[] audioCodecs) {
        DeviceConfig deviceConfigToUse = null;
        String audioDeviceName = SystemProperties.get(
                Constants.PROPERTY_SYSTEM_AUDIO_MODE_AUDIO_PORT,
                "VX_AUDIO_DEVICE_IN_HDMI_ARC");
        for (DeviceConfig device : deviceConfig) {
            if (device.name.equals(audioDeviceName)) {
                deviceConfigToUse = device;
                break;
            }
        }
        if (deviceConfigToUse == null) {
            Slog.w(TAG, "sadConfig.xml does not have required device info for " + audioDeviceName);
            return new byte[0];
        }
        HashMap<Integer, byte[]> map = new HashMap<>();
        ArrayList<byte[]> sads = new ArrayList<>(audioCodecs.length);
        for (CodecSad codecSad : deviceConfigToUse.supportedCodecs) {
            map.put(codecSad.audioCodec, codecSad.sad);
        }
        for (int i = 0; i < audioCodecs.length; i++) {
            if (map.containsKey(audioCodecs[i])) {
                byte[] sad = map.get(audioCodecs[i]);
                if (sad != null && sad.length == 3) {
                    sads.add(sad);
                }
            }
        }
        return getShortAudioDescriptorBytes(sads);
    }

    private byte[] getShortAudioDescriptorBytes(ArrayList<byte[]> sads) {
        // Short Audio Descriptors are always 3 bytes long.
        byte[] bytes = new byte[sads.size() * 3];
        int index = 0;
        for (byte[] sad : sads) {
            System.arraycopy(sad, 0, bytes, index, 3);
            index += 3;
        }
        return bytes;
    }

    /**
     * Returns a 3 byte short audio descriptor as described in CEC 1.4 table 29 or null if the
     * audioCodec is not supported.
     */
    @Nullable
    @VisibleForTesting
    byte[] getSupportedShortAudioDescriptor(
            AudioDeviceInfo deviceInfo, @AudioCodec int audioCodec) {
        byte[] shortAudioDescriptor = new byte[3];

        int[] deviceSupportedAudioFormats = deviceInfo.getEncodings();
        // Return null when audioCodec or device does not support any audio formats.
        if (!AUDIO_CODECS_MAP.containsKey(audioCodec) || deviceSupportedAudioFormats.length == 0) {
            return null;
        }
        List<Integer> audioCodecSupportedAudioFormats = AUDIO_CODECS_MAP.get(audioCodec);

        for (int supportedAudioFormat : deviceSupportedAudioFormats) {
            if (audioCodecSupportedAudioFormats.contains(supportedAudioFormat)) {
                // Initialise the first two bytes of short audio descriptor.
                shortAudioDescriptor[0] = getFirstByteOfSAD(deviceInfo, audioCodec);
                shortAudioDescriptor[1] = getSecondByteOfSAD(deviceInfo);
                switch (audioCodec) {
                    case Constants.AUDIO_CODEC_NONE: {
                        return null;
                    }
                    case Constants.AUDIO_CODEC_LPCM: {
                        if (supportedAudioFormat == AudioFormat.ENCODING_PCM_16BIT) {
                            shortAudioDescriptor[2] = (byte) 0x01;
                        } else if (supportedAudioFormat
                                == AudioFormat.ENCODING_PCM_24BIT_PACKED) {
                            shortAudioDescriptor[2] = (byte) 0x04;
                        } else {
                            // Since no bit is reserved for these audio formats in LPCM codec.
                            shortAudioDescriptor[2] = (byte) 0x00;
                        }
                        return shortAudioDescriptor;
                    }
                    case Constants.AUDIO_CODEC_DD:
                    case Constants.AUDIO_CODEC_MPEG1:
                    case Constants.AUDIO_CODEC_MP3:
                    case Constants.AUDIO_CODEC_MPEG2:
                    case Constants.AUDIO_CODEC_AAC:
                    case Constants.AUDIO_CODEC_DTS: {
                        shortAudioDescriptor[2] = getThirdSadByteForCodecs2Through8(deviceInfo);
                        return shortAudioDescriptor;
                    }
                    case Constants.AUDIO_CODEC_DDP:
                    case Constants.AUDIO_CODEC_DTSHD:
                    case Constants.AUDIO_CODEC_TRUEHD: {
                        // Default value is 0x0 unless defined by Audio Codec Vendor.
                        shortAudioDescriptor[2] = (byte) 0x00;
                        return shortAudioDescriptor;
                    }
                    case Constants.AUDIO_CODEC_ATRAC:
                    case Constants.AUDIO_CODEC_ONEBITAUDIO:
                    case Constants.AUDIO_CODEC_DST:
                    case Constants.AUDIO_CODEC_WMAPRO:
                        // Not supported.
                    default: {
                        return null;
                    }
                }
            }
        }
        return null;
    }

    private static HashMap<Integer, List<Integer>> mapAudioCodecWithAudioFormat() {
        // Mapping the values of @AudioCodec audio codecs with @AudioFormat audio formats.
        HashMap<Integer, List<Integer>> audioCodecsMap = new HashMap<Integer, List<Integer>>();

        audioCodecsMap.put(Constants.AUDIO_CODEC_NONE, List.of(AudioFormat.ENCODING_DEFAULT));
        audioCodecsMap.put(
                Constants.AUDIO_CODEC_LPCM,
                List.of(
                        AudioFormat.ENCODING_PCM_8BIT,
                        AudioFormat.ENCODING_PCM_16BIT,
                        AudioFormat.ENCODING_PCM_FLOAT,
                        AudioFormat.ENCODING_PCM_24BIT_PACKED,
                        AudioFormat.ENCODING_PCM_32BIT));
        audioCodecsMap.put(Constants.AUDIO_CODEC_DD, List.of(AudioFormat.ENCODING_AC3));
        audioCodecsMap.put(Constants.AUDIO_CODEC_MPEG1, List.of(AudioFormat.ENCODING_AAC_HE_V1));
        audioCodecsMap.put(Constants.AUDIO_CODEC_MPEG2, List.of(AudioFormat.ENCODING_AAC_HE_V2));
        audioCodecsMap.put(Constants.AUDIO_CODEC_MP3, List.of(AudioFormat.ENCODING_MP3));
        audioCodecsMap.put(Constants.AUDIO_CODEC_AAC, List.of(AudioFormat.ENCODING_AAC_LC));
        audioCodecsMap.put(Constants.AUDIO_CODEC_DTS, List.of(AudioFormat.ENCODING_DTS));
        audioCodecsMap.put(
                Constants.AUDIO_CODEC_DDP,
                List.of(AudioFormat.ENCODING_E_AC3, AudioFormat.ENCODING_E_AC3_JOC));
        audioCodecsMap.put(Constants.AUDIO_CODEC_DTSHD, List.of(AudioFormat.ENCODING_DTS_HD));
        audioCodecsMap.put(
                Constants.AUDIO_CODEC_TRUEHD,
                List.of(AudioFormat.ENCODING_DOLBY_TRUEHD, AudioFormat.ENCODING_DOLBY_MAT));

        return audioCodecsMap;
    }

    private byte getFirstByteOfSAD(AudioDeviceInfo deviceInfo, @AudioCodec int audioCodec) {
        byte firstByte = 0;
        int maxNumberOfChannels = getMaxNumberOfChannels(deviceInfo);

        // Fill bits 0-2 of the first byte.
        firstByte |= (maxNumberOfChannels - 1);

        // Fill bits 3-6 of the first byte.
        firstByte |= (audioCodec << 3);

        return firstByte;
    }

    private byte getSecondByteOfSAD(AudioDeviceInfo deviceInfo) {
        ArrayList<Integer> samplingRates =
                new ArrayList<Integer>(Arrays.asList(32, 44, 48, 88, 96, 176, 192));

        // samplingRatesdevicesupports is guaranteed to be not null
        int[] samplingRatesDeviceSupports = deviceInfo.getSampleRates();
        if (samplingRatesDeviceSupports.length == 0) {
            Slog.e(TAG, "Device supports arbitrary rates");
            // Since device supports arbitrary rates, we will return 0x7f since bit 7 is reserved.
            return (byte) 0x7f;
        }
        byte secondByte = 0;
        for (int supportedSampleRate : samplingRatesDeviceSupports) {
            if (samplingRates.contains(supportedSampleRate)) {
                int index = samplingRates.indexOf(supportedSampleRate);
                // Setting the bit of a sample rate which is being supported.
                secondByte |= (1 << index);
            }
        }

        return secondByte;
    }

    /**
     * Empty array from deviceInfo.getChannelCounts() implies device supports arbitrary channel
     * counts and hence we assume max channels are supported by the device.
     */
    private int getMaxNumberOfChannels(AudioDeviceInfo deviceInfo) {
        int maxNumberOfChannels = MAX_CHANNELS;
        int[] channelCounts = deviceInfo.getChannelCounts();
        if (channelCounts.length != 0) {
            maxNumberOfChannels = channelCounts[channelCounts.length - 1];
            maxNumberOfChannels =
                    (maxNumberOfChannels > MAX_CHANNELS ? MAX_CHANNELS : maxNumberOfChannels);
        }
        return maxNumberOfChannels;
    }

    private byte getThirdSadByteForCodecs2Through8(AudioDeviceInfo deviceInfo) {
        /*
         * Here, we are assuming that max bit rate is closely equals to the max sampling rate the
         * device supports.
         */
        int maxSamplingRate = 0;
        int[] samplingRatesDeviceSupports = deviceInfo.getSampleRates();
        if (samplingRatesDeviceSupports.length == 0) {
            maxSamplingRate = 192;
        } else {
            for (int sampleRate : samplingRatesDeviceSupports) {
                if (maxSamplingRate < sampleRate) {
                    maxSamplingRate = sampleRate;
                }
            }
        }

        return (byte) (maxSamplingRate / 8);
    }

    @Nullable
    private AudioDeviceInfo getSystemAudioDeviceInfo() {
        AudioManager audioManager = mService.getContext().getSystemService(AudioManager.class);
        if (audioManager == null) {
            HdmiLogger.error(
                    "Error getting system audio device because AudioManager not available.");
            return null;
        }
        AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
        HdmiLogger.debug("Found %d audio input devices", devices.length);
        for (AudioDeviceInfo device : devices) {
            HdmiLogger.debug("%s at port %s", device.getProductName(), device.getPort());
            HdmiLogger.debug("Supported encodings are %s",
                    Arrays.stream(device.getEncodings()).mapToObj(
                            AudioFormat::toLogFriendlyEncoding
                    ).collect(Collectors.joining(", ")));
            if (device.getType() == AudioDeviceInfo.TYPE_HDMI_ARC) {
                return device;
            }
        }
        return null;
    }

    @AudioCodec
    private int[] parseAudioCodecs(byte[] params) {
        @AudioCodec int[] audioCodecs = new int[params.length];
        for (int i = 0; i < params.length; i++) {
            byte val = params[i];
            audioCodecs[i] =
                    val >= 1 && val <= Constants.AUDIO_CODEC_MAX ? val : Constants.AUDIO_CODEC_NONE;
        }
        return audioCodecs;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleSystemAudioModeRequest(HdmiCecMessage message) {
        assertRunOnServiceThread();
        boolean systemAudioStatusOn = message.getParams().length != 0;
        // Check if the request comes from a non-TV device.
        // Need to check if TV supports System Audio Control
        // if non-TV device tries to turn on the feature
        if (message.getSource() != Constants.ADDR_TV) {
            if (systemAudioStatusOn) {
                return handleSystemAudioModeOnFromNonTvDevice(message);
            }
        } else {
            // If TV request the feature on
            // cache TV supporting System Audio Control
            // until Audio System loses its physical address.
            setTvSystemAudioModeSupport(true);
        }
        // If TV or Audio System does not support the feature,
        // will send abort command.
        if (!checkSupportAndSetSystemAudioMode(systemAudioStatusOn)) {
            return Constants.ABORT_REFUSED;
        }

        mService.sendCecCommand(
                HdmiCecMessageBuilder.buildSetSystemAudioMode(
                        getDeviceInfo().getLogicalAddress(),
                        Constants.ADDR_BROADCAST,
                        systemAudioStatusOn));

        if (systemAudioStatusOn) {
            // If TV sends out SAM Request with a path of a non-CEC device, which should not show
            // up in the CEC device list and not under the current AVR device, the AVR would switch
            // to ARC.
            int sourcePhysicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
            if (HdmiUtils.getLocalPortFromPhysicalAddress(
                    sourcePhysicalAddress, getDeviceInfo().getPhysicalAddress())
                            != HdmiUtils.TARGET_NOT_UNDER_LOCAL_DEVICE) {
                return Constants.HANDLED;
            }
            HdmiDeviceInfo safeDeviceInfoByPath =
                    mService.getHdmiCecNetwork().getSafeDeviceInfoByPath(sourcePhysicalAddress);
            if (safeDeviceInfoByPath == null) {
                switchInputOnReceivingNewActivePath(sourcePhysicalAddress);
            }
        }
        return Constants.HANDLED;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleSetSystemAudioMode(HdmiCecMessage message) {
        assertRunOnServiceThread();
        if (!checkSupportAndSetSystemAudioMode(
                HdmiUtils.parseCommandParamSystemAudioStatus(message))) {
            return Constants.ABORT_REFUSED;
        }
        return Constants.HANDLED;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleSystemAudioModeStatus(HdmiCecMessage message) {
        assertRunOnServiceThread();
        if (!checkSupportAndSetSystemAudioMode(
                HdmiUtils.parseCommandParamSystemAudioStatus(message))) {
            return Constants.ABORT_REFUSED;
        }
        return Constants.HANDLED;
    }

    @ServiceThreadOnly
    void setArcStatus(boolean enabled) {
        assertRunOnServiceThread();

        HdmiLogger.debug("Set Arc Status[old:%b new:%b]", mArcEstablished, enabled);
        // 1. Enable/disable ARC circuit.
        enableAudioReturnChannel(enabled);
        // 2. Notify arc status to audio service.
        notifyArcStatusToAudioService(enabled);
        // 3. Update arc status;
        mArcEstablished = enabled;
    }

    void processArcTermination() {
        setArcStatus(false);
        // Switch away from ARC input when ARC is terminated.
        if (getLocalActivePort() == Constants.CEC_SWITCH_ARC) {
            routeToInputFromPortId(getRoutingPort());
        }
    }

    /** Switch hardware ARC circuit in the system. */
    @ServiceThreadOnly
    private void enableAudioReturnChannel(boolean enabled) {
        assertRunOnServiceThread();
        mService.enableAudioReturnChannel(
                Integer.parseInt(HdmiProperties.arc_port().orElse("0")),
                enabled);
    }

    private void notifyArcStatusToAudioService(boolean enabled) {
        // Note that we don't set any name to ARC.
        mService.getAudioManager()
            .setWiredDeviceConnectionState(AudioSystem.DEVICE_IN_HDMI_ARC, enabled ? 1 : 0, "", "");
    }

    void reportAudioStatus(int source) {
        assertRunOnServiceThread();
        if (mService.getHdmiCecVolumeControl()
                == HdmiControlManager.VOLUME_CONTROL_DISABLED) {
            return;
        }

        int volume = mService.getAudioManager().getStreamVolume(AudioManager.STREAM_MUSIC);
        boolean mute = mService.getAudioManager().isStreamMute(AudioManager.STREAM_MUSIC);
        int maxVolume = mService.getAudioManager().getStreamMaxVolume(AudioManager.STREAM_MUSIC);
        int minVolume = mService.getAudioManager().getStreamMinVolume(AudioManager.STREAM_MUSIC);
        int scaledVolume = VolumeControlAction.scaleToCecVolume(volume, maxVolume);
        HdmiLogger.debug("Reporting volume %d (%d-%d) as CEC volume %d", volume,
                minVolume, maxVolume, scaledVolume);

        mService.sendCecCommand(
                HdmiCecMessageBuilder.buildReportAudioStatus(
                        getDeviceInfo().getLogicalAddress(), source, scaledVolume, mute));
    }

    /**
     * Method to check if device support System Audio Control. If so, wake up device if necessary.
     *
     * <p> then call {@link #setSystemAudioMode(boolean)} to turn on or off System Audio Mode
     * @param newSystemAudioMode turning feature on or off. True is on. False is off.
     * @return true or false.
     *
     * <p>False when device does not support the feature. Otherwise returns true.
     */
    protected boolean checkSupportAndSetSystemAudioMode(boolean newSystemAudioMode) {
        if (!isSystemAudioControlFeatureEnabled()) {
            HdmiLogger.debug(
                    "Cannot turn "
                            + (newSystemAudioMode ? "on" : "off")
                            + "system audio mode "
                            + "because the System Audio Control feature is disabled.");
            return false;
        }
        HdmiLogger.debug(
                "System Audio Mode change[old:%b new:%b]",
                isSystemAudioActivated(), newSystemAudioMode);
        // Wake up device if System Audio Control is turned on
        if (newSystemAudioMode) {
            mService.wakeUp();
        }
        setSystemAudioMode(newSystemAudioMode);
        return true;
    }

    /**
     * Real work to turn on or off System Audio Mode.
     *
     * Use {@link #checkSupportAndSetSystemAudioMode(boolean)}
     * if trying to turn on or off the feature.
     */
    private void setSystemAudioMode(boolean newSystemAudioMode) {
        int targetPhysicalAddress = getActiveSource().physicalAddress;
        int port = mService.pathToPortId(targetPhysicalAddress);
        if (newSystemAudioMode && port >= 0) {
            switchToAudioInput();
        }
        // Mute device when feature is turned off and unmute device when feature is turned on.
        // CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING is false when device never needs to be muted.
        boolean systemAudioModeMutingEnabled = mService.getHdmiCecConfig().getIntValue(
                    HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING)
                        == HdmiControlManager.SYSTEM_AUDIO_MODE_MUTING_ENABLED;
        boolean currentMuteStatus =
                mService.getAudioManager().isStreamMute(AudioManager.STREAM_MUSIC);
        if (currentMuteStatus == newSystemAudioMode) {
            if (systemAudioModeMutingEnabled || newSystemAudioMode) {
                mService.getAudioManager()
                        .adjustStreamVolume(
                                AudioManager.STREAM_MUSIC,
                                newSystemAudioMode
                                        ? AudioManager.ADJUST_UNMUTE
                                        : AudioManager.ADJUST_MUTE,
                                0);
            }
        }
        updateAudioManagerForSystemAudio(newSystemAudioMode);
        synchronized (mLock) {
            if (isSystemAudioActivated() != newSystemAudioMode) {
                mService.setSystemAudioActivated(newSystemAudioMode);
                mService.announceSystemAudioModeChange(newSystemAudioMode);
            }
        }
        // Since ARC is independent from System Audio Mode control, when the TV requests
        // System Audio Mode off, it does not need to terminate ARC at the same time.
        // When the current audio device is using ARC as a TV input and disables muting,
        // it needs to automatically switch to the previous active input source when System
        // Audio Mode is off even without terminating the ARC. This can stop the current
        // audio device from playing audio when system audio mode is off.
        if (mArcIntentUsed
                && !systemAudioModeMutingEnabled
                && !newSystemAudioMode
                && getLocalActivePort() == Constants.CEC_SWITCH_ARC) {
            routeToInputFromPortId(getRoutingPort());
        }
        // Init arc whenever System Audio Mode is on
        // Since some TVs don't request ARC on with System Audio Mode on request
        if (SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)
                && isDirectConnectToTv() && mService.isSystemAudioActivated()) {
            if (!hasAction(ArcInitiationActionFromAvr.class)) {
                addAndStartAction(new ArcInitiationActionFromAvr(this));
            }
        }
    }

    protected void switchToAudioInput() {
    }

    protected boolean isDirectConnectToTv() {
        int myPhysicalAddress = mService.getPhysicalAddress();
        return (myPhysicalAddress & Constants.ROUTING_PATH_TOP_MASK) == myPhysicalAddress;
    }

    private void updateAudioManagerForSystemAudio(boolean on) {
        int device = mService.getAudioManager().setHdmiSystemAudioSupported(on);
        HdmiLogger.debug("[A]UpdateSystemAudio mode[on=%b] output=[%X]", on, device);
    }

    void onSystemAudioControlFeatureSupportChanged(boolean enabled) {
        setSystemAudioControlFeatureEnabled(enabled);
        if (enabled) {
            addAndStartAction(new SystemAudioInitiationActionFromAvr(this), true);
        }
    }

    @ServiceThreadOnly
    void setSystemAudioControlFeatureEnabled(boolean enabled) {
        assertRunOnServiceThread();
        synchronized (mLock) {
            mSystemAudioControlFeatureEnabled = enabled;
        }
    }

    @ServiceThreadOnly
    void setRoutingControlFeatureEnabled(boolean enabled) {
        assertRunOnServiceThread();
        synchronized (mLock) {
            mRoutingControlFeatureEnabled = enabled;
        }
    }

    @ServiceThreadOnly
    void doManualPortSwitching(int portId, IHdmiControlCallback callback) {
        assertRunOnServiceThread();
        if (!mService.isValidPortId(portId)) {
            invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
            return;
        }
        if (portId == getLocalActivePort()) {
            invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS);
            return;
        }
        if (!mService.isCecControlEnabled()) {
            setRoutingPort(portId);
            setLocalActivePort(portId);
            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
            return;
        }
        int oldPath = getRoutingPort() != Constants.CEC_SWITCH_HOME
                ? mService.portIdToPath(getRoutingPort())
                : getDeviceInfo().getPhysicalAddress();
        int newPath = mService.portIdToPath(portId);
        if (oldPath == newPath) {
            return;
        }
        setRoutingPort(portId);
        setLocalActivePort(portId);
        HdmiCecMessage routingChange =
                HdmiCecMessageBuilder.buildRoutingChange(
                        getDeviceInfo().getLogicalAddress(), oldPath, newPath);
        mService.sendCecCommand(routingChange);
    }

    boolean isSystemAudioControlFeatureEnabled() {
        synchronized (mLock) {
            return mSystemAudioControlFeatureEnabled;
        }
    }

    protected boolean isSystemAudioActivated() {
        return mService.isSystemAudioActivated();
    }

    protected void terminateSystemAudioMode() {
        terminateSystemAudioMode(null);
    }

    // Since this method is not just called during the standby process, the callback should be
    // generalized in the future.
    protected void terminateSystemAudioMode(StandbyCompletedCallback callback) {
        // remove pending initiation actions
        removeAction(SystemAudioInitiationActionFromAvr.class);
        if (!isSystemAudioActivated()) {
            invokeStandbyCompletedCallback(callback);
            return;
        }

        if (checkSupportAndSetSystemAudioMode(false)) {
            // send <Set System Audio Mode> [“Off”]
            mService.sendCecCommand(
                    HdmiCecMessageBuilder.buildSetSystemAudioMode(
                            getDeviceInfo().getLogicalAddress(), Constants.ADDR_BROADCAST, false),
                    new SendMessageCallback() {
                        @Override
                        public void onSendCompleted(int error) {
                            invokeStandbyCompletedCallback(callback);
                        }
                    });
        }
    }

    private void terminateAudioReturnChannel() {
        // remove pending initiation actions
        removeAction(ArcInitiationActionFromAvr.class);
        if (!isArcEnabled()
                || !mService.readBooleanSystemProperty(Constants.PROPERTY_ARC_SUPPORT, true)) {
            return;
        }
        addAndStartAction(new ArcTerminationActionFromAvr(this));
    }

    /** Reports if System Audio Mode is supported by the connected TV */
    interface TvSystemAudioModeSupportedCallback {

        /** {@code supported} is true if the TV is connected and supports System Audio Mode. */
        void onResult(boolean supported);
    }

    /**
     * Queries the connected TV to detect if System Audio Mode is supported by the TV.
     *
     * <p>This query may take up to 2 seconds to complete.
     *
     * <p>The result of the query may be cached until Audio device type is put in standby or loses
     * its physical address.
     */
    void queryTvSystemAudioModeSupport(TvSystemAudioModeSupportedCallback callback) {
        if (mTvSystemAudioModeSupport == null) {
            addAndStartAction(new DetectTvSystemAudioModeSupportAction(this, callback));
        } else {
            callback.onResult(mTvSystemAudioModeSupport);
        }
    }

    /**
     * Handler of System Audio Mode Request on from non TV device
     */
    @Constants.HandleMessageResult
    int handleSystemAudioModeOnFromNonTvDevice(HdmiCecMessage message) {
        if (!isSystemAudioControlFeatureEnabled()) {
            HdmiLogger.debug(
                    "Cannot turn on" + "system audio mode "
                            + "because the System Audio Control feature is disabled.");
            return Constants.ABORT_REFUSED;
        }
        // Wake up device
        mService.wakeUp();
        // If Audio device is the active source or is on the active path,
        // enable system audio mode without querying TV's support on sam.
        // This is per HDMI spec 1.4b CEC 13.15.4.2.
        if (mService.pathToPortId(getActiveSource().physicalAddress)
                != Constants.INVALID_PORT_ID) {
            setSystemAudioMode(true);
            mService.sendCecCommand(
                HdmiCecMessageBuilder.buildSetSystemAudioMode(
                    getDeviceInfo().getLogicalAddress(), Constants.ADDR_BROADCAST, true));
            return Constants.HANDLED;
        }
        // Check if TV supports System Audio Control.
        // Handle broadcasting setSystemAudioMode on or aborting message on callback.
        queryTvSystemAudioModeSupport(new TvSystemAudioModeSupportedCallback() {
            public void onResult(boolean supported) {
                if (supported) {
                    setSystemAudioMode(true);
                    mService.sendCecCommand(
                            HdmiCecMessageBuilder.buildSetSystemAudioMode(
                                    getDeviceInfo().getLogicalAddress(),
                                    Constants.ADDR_BROADCAST,
                                    true));
                } else {
                    mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
                }
            }
        });
        return Constants.HANDLED;
    }

    void setTvSystemAudioModeSupport(boolean supported) {
        mTvSystemAudioModeSupport = supported;
    }

    @VisibleForTesting
    protected boolean isArcEnabled() {
        synchronized (mLock) {
            return mArcEstablished;
        }
    }

    private void initArcOnFromAvr() {
        removeAction(ArcTerminationActionFromAvr.class);
        if (SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)
                && isDirectConnectToTv() && !isArcEnabled()) {
            addAndStartAction(new ArcInitiationActionFromAvr(this), true);
        }
    }

    @Override
    protected void switchInputOnReceivingNewActivePath(int physicalAddress) {
        int port = mService.pathToPortId(physicalAddress);
        if (isSystemAudioActivated() && port < 0) {
            // If system audio mode is on and the new active source is not under the current device,
            // Will switch to ARC input.
            routeToInputFromPortId(Constants.CEC_SWITCH_ARC);
        } else if (mIsSwitchDevice && port >= 0) {
            // If current device is a switch and the new active source is under it,
            // will switch to the corresponding active path.
            routeToInputFromPortId(port);
        }
    }

    protected void routeToInputFromPortId(int portId) {
        if (!isRoutingControlFeatureEnabled()) {
            HdmiLogger.debug("Routing Control Feature is not enabled.");
            return;
        }
        if (mArcIntentUsed) {
            routeToTvInputFromPortId(portId);
        } else {
            // TODO(): implement input switching for devices not using TvInput.
        }
    }

    protected void routeToTvInputFromPortId(int portId) {
        if (portId < 0 || portId >= Constants.CEC_SWITCH_PORT_MAX) {
            HdmiLogger.debug("Invalid port number for Tv Input switching.");
            return;
        }
        // Wake up if the current device if ready to route.
        mService.wakeUp();
        if ((getLocalActivePort() == portId) && (portId != Constants.CEC_SWITCH_ARC)) {
            HdmiLogger.debug("Not switching to the same port " + portId + " except for arc");
            return;
        }
        // Switch to HOME if the current active port is not HOME yet
        if (portId == Constants.CEC_SWITCH_HOME && mService.isPlaybackDevice()) {
            switchToHomeTvInput();
        } else if (portId == Constants.CEC_SWITCH_ARC) {
            switchToTvInput(HdmiProperties.arc_port().orElse("0"));
            setLocalActivePort(portId);
            return;
        } else {
            String uri = mPortIdToTvInputs.get(portId);
            if (uri != null) {
                switchToTvInput(uri);
            } else {
                HdmiLogger.debug("Port number does not match any Tv Input.");
                return;
            }
        }

        setLocalActivePort(portId);
        setRoutingPort(portId);
    }

    // For device to switch to specific TvInput with corresponding URI.
    private void switchToTvInput(String uri) {
        try {
            mService.getContext().startActivity(new Intent(Intent.ACTION_VIEW,
                    TvContract.buildChannelUriForPassthroughInput(uri))
                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
        } catch (ActivityNotFoundException e) {
            Slog.e(TAG, "Can't find activity to switch to " + uri, e);
        }
    }

    // For device using TvInput to switch to Home.
    private void switchToHomeTvInput() {
        try {
            Intent activityIntent = new Intent(Intent.ACTION_MAIN)
                    .addCategory(Intent.CATEGORY_HOME)
                    .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
                    | Intent.FLAG_ACTIVITY_SINGLE_TOP
                    | Intent.FLAG_ACTIVITY_NEW_TASK
                    | Intent.FLAG_ACTIVITY_NO_ANIMATION);
            mService.getContext().startActivity(activityIntent);
        } catch (ActivityNotFoundException e) {
            Slog.e(TAG, "Can't find activity to switch to HOME", e);
        }
    }

    @Override
    protected void handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message) {
        int port = mService.pathToPortId(physicalAddress);
        // Routing change or information sent from switches under the current device can be ignored.
        if (port > 0) {
            return;
        }
        // When other switches route to some other devices not under the current device,
        // check system audio mode status and do ARC switch if needed.
        if (port < 0 && isSystemAudioActivated()) {
            handleRoutingChangeAndInformationForSystemAudio();
            return;
        }
        // When other switches route to the current device
        // and the current device is also a switch.
        if (port == 0) {
            handleRoutingChangeAndInformationForSwitch(message);
        }
    }

    // Handle the system audio(ARC) part of the logic on receiving routing change or information.
    private void handleRoutingChangeAndInformationForSystemAudio() {
        routeToInputFromPortId(Constants.CEC_SWITCH_ARC);
    }

    // Handle the routing control part of the logic on receiving routing change or information.
    private void handleRoutingChangeAndInformationForSwitch(HdmiCecMessage message) {
        if (getRoutingPort() == Constants.CEC_SWITCH_HOME && mService.isPlaybackDevice()) {
            routeToInputFromPortId(Constants.CEC_SWITCH_HOME);
            mService.setAndBroadcastActiveSourceFromOneDeviceType(
                    message.getSource(), mService.getPhysicalAddress(),
                    "HdmiCecLocalDeviceAudioSystem#handleRoutingChangeAndInformationForSwitch()");
            return;
        }

        int routingInformationPath = mService.portIdToPath(getRoutingPort());
        // If current device is already the leaf of the whole HDMI system, will do nothing.
        if (routingInformationPath == mService.getPhysicalAddress()) {
            HdmiLogger.debug("Current device can't assign valid physical address"
                    + "to devices under it any more. "
                    + "It's physical address is "
                    + routingInformationPath);
            return;
        }
        // Otherwise will switch to the current active port and broadcast routing information.
        mService.sendCecCommand(
                HdmiCecMessageBuilder.buildRoutingInformation(
                        getDeviceInfo().getLogicalAddress(), routingInformationPath));
        routeToInputFromPortId(getRoutingPort());
    }

    @ServiceThreadOnly
    private void launchDeviceDiscovery() {
        assertRunOnServiceThread();
        if (mService.isDeviceDiscoveryHandledByPlayback()) {
            return;
        }
        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
                new DeviceDiscoveryCallback() {
                    @Override
                    public void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos) {
                        for (HdmiDeviceInfo info : deviceInfos) {
                            mService.getHdmiCecNetwork().addCecDevice(info);
                        }
                    }
                });
        addAndStartAction(action, true);
    }

    @Override
    protected void dump(IndentingPrintWriter pw) {
        pw.println("HdmiCecLocalDeviceAudioSystem:");
        pw.increaseIndent();
        pw.println("isRoutingFeatureEnabled " + isRoutingControlFeatureEnabled());
        pw.println("mSystemAudioControlFeatureEnabled: " + mSystemAudioControlFeatureEnabled);
        pw.println("mTvSystemAudioModeSupport: " + mTvSystemAudioModeSupport);
        pw.println("mArcEstablished: " + mArcEstablished);
        pw.println("mArcIntentUsed: " + mArcIntentUsed);
        pw.println("mRoutingPort: " + getRoutingPort());
        pw.println("mLocalActivePort: " + getLocalActivePort());
        HdmiUtils.dumpMap(pw, "mPortIdToTvInputs:", mPortIdToTvInputs);
        HdmiUtils.dumpMap(pw, "mTvInputsToDeviceInfo:", mTvInputsToDeviceInfo);
        pw.decreaseIndent();
        super.dump(pw);
    }
}
