/*
 * 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 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 android.annotation.Nullable;
import android.content.ActivityNotFoundException;
import android.content.Intent;
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.provider.Settings.Global;
import android.util.Slog;
import android.util.SparseArray;

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.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
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 =
            SystemProperties.getBoolean(Constants.PROPERTY_WAKE_ON_HOTPLUG, false);

    // 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 = SystemProperties
            .get(Constants.PROPERTY_SYSTEM_AUDIO_DEVICE_ARC_PORT, "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<>();

    // Copy of mDeviceInfos to guarantee thread-safety.
    @GuardedBy("mLock")
    private List<HdmiDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList();

    // Map-like container of all cec devices.
    // device id is used as key of container.
    private final SparseArray<HdmiDeviceInfo> mDeviceInfos = new SparseArray<>();

    // 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.readBooleanSetting(Global.HDMI_CEC_SWITCH_ENABLED, false);
        mSystemAudioControlFeatureEnabled =
            mService.readBooleanSetting(Global.HDMI_SYSTEM_AUDIO_CONTROL_ENABLED, true);
    }

    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;
    }

    /**
     * Called when a device is newly added or a new device is detected or
     * an existing device is updated.
     *
     * @param info device info of a new device.
     */
    @ServiceThreadOnly
    final void addCecDevice(HdmiDeviceInfo info) {
        assertRunOnServiceThread();
        HdmiDeviceInfo old = addDeviceInfo(info);
        if (info.getPhysicalAddress() == mService.getPhysicalAddress()) {
            // The addition of the device itself should not be notified.
            // Note that different logical address could still be the same local device.
            return;
        }
        if (old == null) {
            invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE);
        } else if (!old.equals(info)) {
            invokeDeviceEventListener(old, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE);
            invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE);
        }
    }

    /**
     * Called when a device is removed or removal of device is detected.
     *
     * @param address a logical address of a device to be removed
     */
    @ServiceThreadOnly
    final void removeCecDevice(int address) {
        assertRunOnServiceThread();
        HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address));

        mCecMessageCache.flushMessagesFrom(address);
        invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE);
    }

    /**
     * Called when a device is updated.
     *
     * @param info device info of the updating device.
     */
    @ServiceThreadOnly
    final void updateCecDevice(HdmiDeviceInfo info) {
        assertRunOnServiceThread();
        HdmiDeviceInfo old = addDeviceInfo(info);

        if (old == null) {
            invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE);
        } else if (!old.equals(info)) {
            invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE);
        }
    }

    /**
    * Add a new {@link HdmiDeviceInfo}. It returns old device info which has the same
     * logical address as new device info's.
     *
     * @param deviceInfo a new {@link HdmiDeviceInfo} to be added.
     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiDeviceInfo}
     *         that has the same logical address as new one has.
     */
    @ServiceThreadOnly
    @VisibleForTesting
    protected HdmiDeviceInfo addDeviceInfo(HdmiDeviceInfo deviceInfo) {
        assertRunOnServiceThread();
        mService.checkLogicalAddressConflictAndReallocate(deviceInfo.getLogicalAddress());
        HdmiDeviceInfo oldDeviceInfo = getCecDeviceInfo(deviceInfo.getLogicalAddress());
        if (oldDeviceInfo != null) {
            removeDeviceInfo(deviceInfo.getId());
        }
        mDeviceInfos.append(deviceInfo.getId(), deviceInfo);
        updateSafeDeviceInfoList();
        return oldDeviceInfo;
    }

    /**
     * Remove a device info corresponding to the given {@code logicalAddress}.
     * It returns removed {@link HdmiDeviceInfo} if exists.
     *
     * @param id id of device to be removed
     * @return removed {@link HdmiDeviceInfo} it exists. Otherwise, returns {@code null}
     */
    @ServiceThreadOnly
    private HdmiDeviceInfo removeDeviceInfo(int id) {
        assertRunOnServiceThread();
        HdmiDeviceInfo deviceInfo = mDeviceInfos.get(id);
        if (deviceInfo != null) {
            mDeviceInfos.remove(id);
        }
        updateSafeDeviceInfoList();
        return deviceInfo;
    }

    /**
     * Return a {@link HdmiDeviceInfo} corresponding to the given {@code logicalAddress}.
     *
     * @param logicalAddress logical address of the device to be retrieved
     * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}.
     *         Returns null if no logical address matched
     */
    @ServiceThreadOnly
    HdmiDeviceInfo getCecDeviceInfo(int logicalAddress) {
        assertRunOnServiceThread();
        return mDeviceInfos.get(HdmiDeviceInfo.idForCecDevice(logicalAddress));
    }

    @ServiceThreadOnly
    private void updateSafeDeviceInfoList() {
        assertRunOnServiceThread();
        List<HdmiDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
        synchronized (mLock) {
            mSafeAllDeviceInfos = copiedDevices;
        }
    }

    @GuardedBy("mLock")
    List<HdmiDeviceInfo> getSafeCecDevicesLocked() {
        ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
        for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
            infoList.add(info);
        }
        return infoList;
    }

    private void invokeDeviceEventListener(HdmiDeviceInfo info, int status) {
        mService.invokeDeviceEventListeners(info, status);
    }

    @Override
    @ServiceThreadOnly
    void onHotplug(int portId, boolean connected) {
        assertRunOnServiceThread();
        if (WAKE_ON_HOTPLUG && connected) {
            mService.wakeUp();
        }
        if (mService.getPortInfo(portId).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.
            removeCecDevice(info.getLogicalAddress());
        }
    }

    @Override
    @ServiceThreadOnly
    protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
        super.disableDevice(initiatedByCec, callback);
        assertRunOnServiceThread();
        mService.unregisterTvInputCallback(mTvInputCallback);
        // TODO(b/129088603): check disableDevice and onStandby behaviors per spec
    }

    @Override
    @ServiceThreadOnly
    protected void onStandby(boolean initiatedByCec, int standbyAction) {
        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);
        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();
    }

    @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);
        }
        mService.sendCecCommand(
                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
                        mAddress, mService.getPhysicalAddress(), mDeviceType));
        mService.sendCecCommand(
                HdmiCecMessageBuilder.buildDeviceVendorIdCommand(mAddress, 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();
        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);
        clearDeviceInfoList();
        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));
        }
    }

    @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
    protected boolean 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 = 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 true;
    }

    @Override
    @ServiceThreadOnly
    protected boolean handleReportPhysicalAddress(HdmiCecMessage message) {
        assertRunOnServiceThread();
        int path = HdmiUtils.twoBytesToInt(message.getParams());
        int address = message.getSource();
        int type = message.getParams()[2];

        // Ignore if [Device Discovery Action] is going on.
        if (hasAction(DeviceDiscoveryAction.class)) {
            Slog.i(TAG, "Ignored while Device Discovery Action is in progress: " + message);
            return true;
        }

        // Update the device info with TIF, note that the same device info could have added in
        // device discovery and we do not want to override it with default OSD name. Therefore we
        // need the following check to skip redundant device info updating.
        HdmiDeviceInfo oldDevice = getCecDeviceInfo(address);
        if (oldDevice == null || oldDevice.getPhysicalAddress() != path) {
            addCecDevice(new HdmiDeviceInfo(
                    address, path, mService.pathToPortId(path), type,
                    Constants.UNKNOWN_VENDOR_ID, ""));
            // if we are adding a new device info, send out a give osd name command
            // to update the name of the device in TIF
            mService.sendCecCommand(
                    HdmiCecMessageBuilder.buildGiveOsdNameCommand(mAddress, address));
            return true;
        }

        Slog.w(TAG, "Device info exists. Not updating on Physical Address.");
        return true;
    }

    @Override
    protected boolean handleReportPowerStatus(HdmiCecMessage command) {
        int newStatus = command.getParams()[0] & 0xFF;
        updateDevicePowerStatus(command.getSource(), newStatus);
        return true;
    }

    @Override
    @ServiceThreadOnly
    protected boolean handleSetOsdName(HdmiCecMessage message) {
        int source = message.getSource();
        String osdName;
        HdmiDeviceInfo deviceInfo = getCecDeviceInfo(source);
        // If the device is not in device list, ignore it.
        if (deviceInfo == null) {
            Slog.i(TAG, "No source device info for <Set Osd Name>." + message);
            return true;
        }
        try {
            osdName = new String(message.getParams(), "US-ASCII");
        } catch (UnsupportedEncodingException e) {
            Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e);
            return true;
        }

        if (deviceInfo.getDisplayName() != null
            && deviceInfo.getDisplayName().equals(osdName)) {
            Slog.d(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message);
            return true;
        }

        Slog.d(TAG, "Updating device OSD name from "
                + deviceInfo.getDisplayName()
                + " to " + osdName);
        updateCecDevice(new HdmiDeviceInfo(deviceInfo.getLogicalAddress(),
                deviceInfo.getPhysicalAddress(), deviceInfo.getPortId(),
                deviceInfo.getDeviceType(), deviceInfo.getVendorId(), osdName));
        return true;
    }

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

    @Override
    @ServiceThreadOnly
    protected boolean handleReportArcInitiate(HdmiCecMessage message) {
        assertRunOnServiceThread();
        // TODO(amyjojo): implement report arc initiate handler
        HdmiLogger.debug(TAG + "Stub handleReportArcInitiate");
        return true;
    }

    @Override
    @ServiceThreadOnly
    protected boolean handleReportArcTermination(HdmiCecMessage message) {
        assertRunOnServiceThread();
        // TODO(amyjojo): implement report arc terminate handler
        HdmiLogger.debug(TAG + "Stub handleReportArcTermination");
        return true;
    }

    @Override
    @ServiceThreadOnly
    protected boolean handleGiveAudioStatus(HdmiCecMessage message) {
        assertRunOnServiceThread();
        if (isSystemAudioControlFeatureEnabled() && mService.isHdmiCecVolumeControlEnabled()) {
            reportAudioStatus(message.getSource());
        } else {
            mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
        }
        return true;
    }

    @Override
    @ServiceThreadOnly
    protected boolean 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(
                        mAddress, message.getSource(), isSystemAudioModeOnOrTurningOn));
        return true;
    }

    @Override
    @ServiceThreadOnly
    protected boolean handleRequestArcInitiate(HdmiCecMessage message) {
        assertRunOnServiceThread();
        removeAction(ArcInitiationActionFromAvr.class);
        if (!mService.readBooleanSystemProperty(Constants.PROPERTY_ARC_SUPPORT, true)) {
            mService.maySendFeatureAbortCommand(message, Constants.ABORT_UNRECOGNIZED_OPCODE);
        } else if (!isDirectConnectToTv()) {
            HdmiLogger.debug("AVR device is not directly connected with TV");
            mService.maySendFeatureAbortCommand(message, Constants.ABORT_NOT_IN_CORRECT_MODE);
        } else {
            addAndStartAction(new ArcInitiationActionFromAvr(this));
        }
        return true;
    }

    @Override
    @ServiceThreadOnly
    protected boolean handleRequestArcTermination(HdmiCecMessage message) {
        assertRunOnServiceThread();
        if (!SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)) {
            mService.maySendFeatureAbortCommand(message, Constants.ABORT_UNRECOGNIZED_OPCODE);
        } else if (!isArcEnabled()) {
            HdmiLogger.debug("ARC is not established between TV and AVR device");
            mService.maySendFeatureAbortCommand(message, Constants.ABORT_NOT_IN_CORRECT_MODE);
        } else {
            removeAction(ArcTerminationActionFromAvr.class);
            addAndStartAction(new ArcTerminationActionFromAvr(this));
        }
        return true;
    }

    @ServiceThreadOnly
    protected boolean handleRequestShortAudioDescriptor(HdmiCecMessage message) {
        assertRunOnServiceThread();
        HdmiLogger.debug(TAG + "Stub handleRequestShortAudioDescriptor");
        if (!isSystemAudioControlFeatureEnabled()) {
            mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
            return true;
        }
        if (!isSystemAudioActivated()) {
            mService.maySendFeatureAbortCommand(message, Constants.ABORT_NOT_IN_CORRECT_MODE);
            return true;
        }

        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[] audioFormatCodes = parseAudioFormatCodes(message.getParams());
        byte[] sadBytes;
        if (config != null && config.size() > 0) {
            sadBytes = getSupportedShortAudioDescriptorsFromConfig(config, audioFormatCodes);
        } else {
            AudioDeviceInfo deviceInfo = getSystemAudioDeviceInfo();
            if (deviceInfo == null) {
                mService.maySendFeatureAbortCommand(message, Constants.ABORT_UNABLE_TO_DETERMINE);
                return true;
            }

            sadBytes = getSupportedShortAudioDescriptors(deviceInfo, audioFormatCodes);
        }

        if (sadBytes.length == 0) {
            mService.maySendFeatureAbortCommand(message, Constants.ABORT_INVALID_OPERAND);
        } else {
            mService.sendCecCommand(
                    HdmiCecMessageBuilder.buildReportShortAudioDescriptor(
                            mAddress, message.getSource(), sadBytes));
        }
        return true;
    }

    private byte[] getSupportedShortAudioDescriptors(
            AudioDeviceInfo deviceInfo, @AudioCodec int[] audioFormatCodes) {
        ArrayList<byte[]> sads = new ArrayList<>(audioFormatCodes.length);
        for (@AudioCodec int audioFormatCode : audioFormatCodes) {
            byte[] sad = getSupportedShortAudioDescriptor(deviceInfo, audioFormatCode);
            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, audioFormatCode);
                }
            }
        }
        return getShortAudioDescriptorBytes(sads);
    }

    private byte[] getSupportedShortAudioDescriptorsFromConfig(
            List<DeviceConfig> deviceConfig, @AudioCodec int[] audioFormatCodes) {
        DeviceConfig deviceConfigToUse = null;
        for (DeviceConfig device : deviceConfig) {
            // TODO(amyjojo) use PROPERTY_SYSTEM_AUDIO_MODE_AUDIO_PORT to get the audio device name
            if (device.name.equals("VX_AUDIO_DEVICE_IN_HDMI_ARC")) {
                deviceConfigToUse = device;
                break;
            }
        }
        if (deviceConfigToUse == null) {
            // TODO(amyjojo) use PROPERTY_SYSTEM_AUDIO_MODE_AUDIO_PORT to get the audio device name
            Slog.w(TAG, "sadConfig.xml does not have required device info for "
                        + "VX_AUDIO_DEVICE_IN_HDMI_ARC");
            return new byte[0];
        }
        HashMap<Integer, byte[]> map = new HashMap<>();
        ArrayList<byte[]> sads = new ArrayList<>(audioFormatCodes.length);
        for (CodecSad codecSad : deviceConfigToUse.supportedCodecs) {
            map.put(codecSad.audioCodec, codecSad.sad);
        }
        for (int i = 0; i < audioFormatCodes.length; i++) {
            if (map.containsKey(audioFormatCodes[i])) {
                byte[] sad = map.get(audioFormatCodes[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
     * audioFormatCode is not supported.
     */
    @Nullable
    private byte[] getSupportedShortAudioDescriptor(
            AudioDeviceInfo deviceInfo, @AudioCodec int audioFormatCode) {
        switch (audioFormatCode) {
            case Constants.AUDIO_CODEC_NONE: {
                return null;
            }
            case Constants.AUDIO_CODEC_LPCM: {
                return getLpcmShortAudioDescriptor(deviceInfo);
            }
            // TODO(b/80297701): implement the rest of the codecs
            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:
            case Constants.AUDIO_CODEC_ATRAC:
            case Constants.AUDIO_CODEC_ONEBITAUDIO:
            case Constants.AUDIO_CODEC_DDP:
            case Constants.AUDIO_CODEC_DTSHD:
            case Constants.AUDIO_CODEC_TRUEHD:
            case Constants.AUDIO_CODEC_DST:
            case Constants.AUDIO_CODEC_WMAPRO:
            default: {
                return null;
            }
        }
    }

    @Nullable
    private byte[] getLpcmShortAudioDescriptor(AudioDeviceInfo deviceInfo) {
        // TODO(b/80297701): implement
        return null;
    }

    @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(", ")));
            // TODO(b/80297701) use the actual device type that system audio mode is connected to.
            if (device.getType() == AudioDeviceInfo.TYPE_HDMI_ARC) {
                return device;
            }
        }
        return null;
    }

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

    @Override
    @ServiceThreadOnly
    protected boolean 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) {
                handleSystemAudioModeOnFromNonTvDevice(message);
                return true;
            }
        } 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)) {
            mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
            return true;
        }

        mService.sendCecCommand(
                HdmiCecMessageBuilder.buildSetSystemAudioMode(
                        mAddress, 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 true;
            }
            boolean isDeviceInCecDeviceList = false;
            for (HdmiDeviceInfo info : HdmiUtils.sparseArrayToList(mDeviceInfos)) {
                if (info.getPhysicalAddress() == sourcePhysicalAddress) {
                    isDeviceInCecDeviceList = true;
                    break;
                }
            }
            if (!isDeviceInCecDeviceList) {
                switchInputOnReceivingNewActivePath(sourcePhysicalAddress);
            }
        }
        return true;
    }

    @Override
    @ServiceThreadOnly
    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
        assertRunOnServiceThread();
        if (!checkSupportAndSetSystemAudioMode(
                HdmiUtils.parseCommandParamSystemAudioStatus(message))) {
            mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
        }
        return true;
    }

    @Override
    @ServiceThreadOnly
    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
        assertRunOnServiceThread();
        if (!checkSupportAndSetSystemAudioMode(
                HdmiUtils.parseCommandParamSystemAudioStatus(message))) {
            mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
        }
        return true;
    }

    @ServiceThreadOnly
    void setArcStatus(boolean enabled) {
        // TODO(shubang): add tests
        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;
    }

    /** Switch hardware ARC circuit in the system. */
    @ServiceThreadOnly
    private void enableAudioReturnChannel(boolean enabled) {
        assertRunOnServiceThread();
        mService.enableAudioReturnChannel(
                SystemProperties.getInt(Constants.PROPERTY_SYSTEM_AUDIO_DEVICE_ARC_PORT, 0),
                enabled);
    }

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

    void reportAudioStatus(int source) {
        assertRunOnServiceThread();
        if (!mService.isHdmiCecVolumeControlEnabled()) {
            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(
                        mAddress, 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.
        // PROPERTY_SYSTEM_AUDIO_MODE_MUTING_ENABLE is false when device never needs to be muted.
        boolean currentMuteStatus =
                mService.getAudioManager().isStreamMute(AudioManager.STREAM_MUSIC);
        if (currentMuteStatus == newSystemAudioMode) {
            if (mService.readBooleanSystemProperty(
                    Constants.PROPERTY_SYSTEM_AUDIO_MODE_MUTING_ENABLE, true)
                            || 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
            && !mService.readBooleanSystemProperty(
                    Constants.PROPERTY_SYSTEM_AUDIO_MODE_MUTING_ENABLE, true)
            && !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() {
        // TODO(b/111396634): switch input according to PROPERTY_SYSTEM_AUDIO_MODE_AUDIO_PORT
    }

    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 onSystemAduioControlFeatureSupportChanged(boolean enabled) {
        setSystemAudioControlFeatureEnabled(enabled);
        if (enabled) {
            addAndStartAction(new SystemAudioInitiationActionFromAvr(this));
        }
    }

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

    @ServiceThreadOnly
    void setRoutingControlFeatureEnables(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.isControlEnabled()) {
            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(mAddress, oldPath, newPath);
        mService.sendCecCommand(routingChange);
    }

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

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

    protected void terminateSystemAudioMode() {
        // remove pending initiation actions
        removeAction(SystemAudioInitiationActionFromAvr.class);
        if (!isSystemAudioActivated()) {
            return;
        }

        if (checkSupportAndSetSystemAudioMode(false)) {
            // send <Set System Audio Mode> [“Off”]
            mService.sendCecCommand(
                    HdmiCecMessageBuilder.buildSetSystemAudioMode(
                            mAddress, Constants.ADDR_BROADCAST, false));
        }
    }

    /** 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
     */
    void handleSystemAudioModeOnFromNonTvDevice(HdmiCecMessage message) {
        if (!isSystemAudioControlFeatureEnabled()) {
            HdmiLogger.debug(
                    "Cannot turn on" + "system audio mode "
                            + "because the System Audio Control feature is disabled.");
            mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
            return;
        }
        // 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(
                    mAddress, Constants.ADDR_BROADCAST, true));
            return;
        }
        // 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(
                                    mAddress, Constants.ADDR_BROADCAST, true));
                } else {
                    mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
                }
            }
        });
    }

    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()) {
            removeAction(ArcInitiationActionFromAvr.class);
            addAndStartAction(new ArcInitiationActionFromAvr(this));
        }
    }

    @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.
            // TODO(b/115637145): handle system aduio without ARC
            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(SystemProperties.get(Constants.PROPERTY_SYSTEM_AUDIO_DEVICE_ARC_PORT));
            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() {
        // TODO(b/115637145): handle system aduio without ARC
        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());
            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(
                mAddress, routingInformationPath));
        routeToInputFromPortId(getRoutingPort());
    }

    protected void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) {
        HdmiDeviceInfo info = getCecDeviceInfo(logicalAddress);
        if (info == null) {
            Slog.w(TAG, "Can not update power status of non-existing device:" + logicalAddress);
            return;
        }

        if (info.getDevicePowerStatus() == newPowerStatus) {
            return;
        }

        HdmiDeviceInfo newInfo = HdmiUtils.cloneHdmiDeviceInfo(info, newPowerStatus);
        // addDeviceInfo replaces old device info with new one if exists.
        addDeviceInfo(newInfo);

        invokeDeviceEventListener(newInfo, HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE);
    }

    @ServiceThreadOnly
    private void launchDeviceDiscovery() {
        assertRunOnServiceThread();
        if (hasAction(DeviceDiscoveryAction.class)) {
            Slog.i(TAG, "Device Discovery Action is in progress. Restarting.");
            removeAction(DeviceDiscoveryAction.class);
        }
        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
                new DeviceDiscoveryCallback() {
                    @Override
                    public void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos) {
                        for (HdmiDeviceInfo info : deviceInfos) {
                            addCecDevice(info);
                        }
                    }
                });
        addAndStartAction(action);
    }

    // Clear all device info.
    @ServiceThreadOnly
    private void clearDeviceInfoList() {
        assertRunOnServiceThread();
        for (HdmiDeviceInfo info : HdmiUtils.sparseArrayToList(mDeviceInfos)) {
            if (info.getPhysicalAddress() == mService.getPhysicalAddress()) {
                continue;
            }
            invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE);
        }
        mDeviceInfos.clear();
        updateSafeDeviceInfoList();
    }

    @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);
        HdmiUtils.dumpSparseArray(pw, "mDeviceInfos:", mDeviceInfos);
        pw.decreaseIndent();
        super.dump(pw);
    }
}
