/*
 * Copyright (C) 2020 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.HdmiAnnotations.ServiceThreadOnly;

import android.annotation.Nullable;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.HdmiPortInfo;
import android.os.Handler;
import android.os.Looper;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;

import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;

/**
 * Holds information about the current state of the HDMI CEC network. It is the sole source of
 * truth for device information in the CEC network.
 *
 * This information includes:
 * - All local devices
 * - All HDMI ports, their capabilities and status
 * - All devices connected to the CEC bus
 *
 * This class receives all incoming CEC messages and passively listens to device updates to fill
 * out the above information.
 * This class should not take any active action in sending CEC messages.
 *
 * Note that the information cached in this class is not guaranteed to be up-to-date, especially OSD
 * names, power states can be outdated.
 */
@VisibleForTesting
public class HdmiCecNetwork {
    private static final String TAG = "HdmiCecNetwork";

    protected final Object mLock;
    private final HdmiControlService mHdmiControlService;
    private final HdmiCecController mHdmiCecController;
    private final HdmiMhlControllerStub mHdmiMhlController;
    private final Handler mHandler;
    // Stores the local CEC devices in the system. Device type is used for key.
    private final SparseArray<HdmiCecLocalDevice> mLocalDevices = new SparseArray<>();

    // Map-like container of all cec devices including local ones.
    // device id is used as key of container.
    // This is not thread-safe. For external purpose use mSafeDeviceInfos.
    private final SparseArray<HdmiDeviceInfo> mDeviceInfos = new SparseArray<>();
    // Set of physical addresses of CEC switches on the CEC bus. Managed independently from
    // other CEC devices since they might not have logical address.
    private final ArraySet<Integer> mCecSwitches = new ArraySet<>();
    // Copy of mDeviceInfos to guarantee thread-safety.
    @GuardedBy("mLock")
    private List<HdmiDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList();
    // All external cec input(source) devices. Does not include system audio device.
    @GuardedBy("mLock")
    private List<HdmiDeviceInfo> mSafeExternalInputs = Collections.emptyList();
    // HDMI port information. Stored in the unmodifiable list to keep the static information
    // from being modified.
    @GuardedBy("mLock")
    private List<HdmiPortInfo> mPortInfo = Collections.emptyList();

    // Map from path(physical address) to port ID.
    private UnmodifiableSparseIntArray mPortIdMap;

    // Map from port ID to HdmiPortInfo.
    private UnmodifiableSparseArray<HdmiPortInfo> mPortInfoMap;

    // Map from port ID to HdmiDeviceInfo.
    private UnmodifiableSparseArray<HdmiDeviceInfo> mPortDeviceMap;

    HdmiCecNetwork(HdmiControlService hdmiControlService,
            HdmiCecController hdmiCecController,
            HdmiMhlControllerStub hdmiMhlController) {
        mHdmiControlService = hdmiControlService;
        mHdmiCecController = hdmiCecController;
        mHdmiMhlController = hdmiMhlController;
        mHandler = new Handler(mHdmiControlService.getServiceLooper());
        mLock = mHdmiControlService.getServiceLock();
    }

    private static boolean isConnectedToCecSwitch(int path, Collection<Integer> switches) {
        for (int switchPath : switches) {
            if (isParentPath(switchPath, path)) {
                return true;
            }
        }
        return false;
    }

    private static boolean isParentPath(int parentPath, int childPath) {
        // (A000, AB00) (AB00, ABC0), (ABC0, ABCD)
        // If child's last non-zero nibble is removed, the result equals to the parent.
        for (int i = 0; i <= 12; i += 4) {
            int nibble = (childPath >> i) & 0xF;
            if (nibble != 0) {
                int parentNibble = (parentPath >> i) & 0xF;
                return parentNibble == 0 && (childPath >> i + 4) == (parentPath >> i + 4);
            }
        }
        return false;
    }

    public void addLocalDevice(int deviceType, HdmiCecLocalDevice device) {
        mLocalDevices.put(deviceType, device);
    }

    /**
     * Return the locally hosted logical device of a given type.
     *
     * @param deviceType logical device type
     * @return {@link HdmiCecLocalDevice} instance if the instance of the type is available;
     * otherwise null.
     */
    HdmiCecLocalDevice getLocalDevice(int deviceType) {
        return mLocalDevices.get(deviceType);
    }

    /**
     * Return a list of all {@link HdmiCecLocalDevice}s.
     *
     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
     */
    @ServiceThreadOnly
    List<HdmiCecLocalDevice> getLocalDeviceList() {
        assertRunOnServiceThread();
        return HdmiUtils.sparseArrayToList(mLocalDevices);
    }

    @ServiceThreadOnly
    boolean isAllocatedLocalDeviceAddress(int address) {
        assertRunOnServiceThread();
        for (int i = 0; i < mLocalDevices.size(); ++i) {
            if (mLocalDevices.valueAt(i).isAddressOf(address)) {
                return true;
            }
        }
        return false;
    }
    /**
     * Clear all logical addresses registered in the device.
     *
     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
     */
    @ServiceThreadOnly
    void clearLogicalAddress() {
        assertRunOnServiceThread();
        for (int i = 0; i < mLocalDevices.size(); ++i) {
            mLocalDevices.valueAt(i).clearAddress();
        }
    }

    @ServiceThreadOnly
    void clearLocalDevices() {
        assertRunOnServiceThread();
        mLocalDevices.clear();
    }

    /**
     * Get the device info of a local device or a device in the CEC network by a device id.
     * @param id id of the device to get
     * @return the device with the given id, or {@code null}
     */
    @Nullable
    public HdmiDeviceInfo getDeviceInfo(int id) {
        return mDeviceInfos.get(id);
    }

    /**
     * Add a new {@link HdmiDeviceInfo}. It returns old device info which has the same
     * logical address as new device info's.
     *
     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
     *
     * @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
    private HdmiDeviceInfo addDeviceInfo(HdmiDeviceInfo deviceInfo) {
        assertRunOnServiceThread();
        HdmiDeviceInfo oldDeviceInfo = getCecDeviceInfo(deviceInfo.getLogicalAddress());
        mHdmiControlService.checkLogicalAddressConflictAndReallocate(
                deviceInfo.getLogicalAddress(), deviceInfo.getPhysicalAddress());
        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.
     *
     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
     *
     * @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}.
     *
     * This is not thread-safe. For thread safety, call {@link #getSafeCecDeviceInfo(int)}.
     *
     * @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
    @Nullable
    HdmiDeviceInfo getCecDeviceInfo(int logicalAddress) {
        assertRunOnServiceThread();
        return mDeviceInfos.get(HdmiDeviceInfo.idForCecDevice(logicalAddress));
    }

    /**
     * Called when a device is newly added or a new device is detected or
     * existing device is updated.
     *
     * @param info device info of a new device.
     */
    @ServiceThreadOnly
    final void addCecDevice(HdmiDeviceInfo info) {
        assertRunOnServiceThread();
        HdmiDeviceInfo old = addDeviceInfo(info);
        if (isLocalDeviceAddress(info.getLogicalAddress())) {
            // The addition of a local device should not notify listeners
            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);
        }
    }

    private void invokeDeviceEventListener(HdmiDeviceInfo info, int event) {
        if (!hideDevicesBehindLegacySwitch(info)) {
            mHdmiControlService.invokeDeviceEventListeners(info, event);
        }
    }

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

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

    /**
     * Return a list of all {@link HdmiDeviceInfo}.
     *
     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
     * This is not thread-safe. For thread safety, call {@link #getSafeExternalInputsLocked} which
     * does not include local device.
     */
    @ServiceThreadOnly
    List<HdmiDeviceInfo> getDeviceInfoList(boolean includeLocalDevice) {
        assertRunOnServiceThread();
        if (includeLocalDevice) {
            return HdmiUtils.sparseArrayToList(mDeviceInfos);
        } else {
            ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
            for (int i = 0; i < mDeviceInfos.size(); ++i) {
                HdmiDeviceInfo info = mDeviceInfos.valueAt(i);
                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
                    infoList.add(info);
                }
            }
            return infoList;
        }
    }

    /**
     * Return external input devices.
     */
    @GuardedBy("mLock")
    List<HdmiDeviceInfo> getSafeExternalInputsLocked() {
        return mSafeExternalInputs;
    }

    /**
     * Return a list of external cec input (source) devices.
     *
     * <p>Note that this effectively excludes non-source devices like system audio,
     * secondary TV.
     */
    private List<HdmiDeviceInfo> getInputDevices() {
        ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
        for (int i = 0; i < mDeviceInfos.size(); ++i) {
            HdmiDeviceInfo info = mDeviceInfos.valueAt(i);
            if (isLocalDeviceAddress(info.getLogicalAddress())) {
                continue;
            }
            if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) {
                infoList.add(info);
            }
        }
        return infoList;
    }

    // Check if we are hiding CEC devices connected to a legacy (non-CEC) switch.
    // This only applies to TV devices.
    // Returns true if the policy is set to true, and the device to check does not have
    // a parent CEC device (which should be the CEC-enabled switch) in the list.
    private boolean hideDevicesBehindLegacySwitch(HdmiDeviceInfo info) {
        return isLocalDeviceAddress(Constants.ADDR_TV)
                && HdmiConfig.HIDE_DEVICES_BEHIND_LEGACY_SWITCH
                && !isConnectedToCecSwitch(info.getPhysicalAddress(), getCecSwitches());
    }

    /**
     * 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(HdmiCecLocalDevice localDevice, int address) {
        assertRunOnServiceThread();
        HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address));

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

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

        updateCecDevice(HdmiUtils.cloneHdmiDeviceInfo(info, newPowerStatus));
    }

    /**
     * Whether a device of the specified physical address is connected to ARC enabled port.
     */
    boolean isConnectedToArcPort(int physicalAddress) {
        int portId = physicalAddressToPortId(physicalAddress);
        if (portId != Constants.INVALID_PORT_ID) {
            return mPortInfoMap.get(portId).isArcSupported();
        }
        return false;
    }


    // Initialize HDMI port information. Combine the information from CEC and MHL HAL and
    // keep them in one place.
    @ServiceThreadOnly
    @VisibleForTesting
    public void initPortInfo() {
        assertRunOnServiceThread();
        HdmiPortInfo[] cecPortInfo = null;
        // CEC HAL provides majority of the info while MHL does only MHL support flag for
        // each port. Return empty array if CEC HAL didn't provide the info.
        if (mHdmiCecController != null) {
            cecPortInfo = mHdmiCecController.getPortInfos();
        }
        if (cecPortInfo == null) {
            return;
        }

        SparseArray<HdmiPortInfo> portInfoMap = new SparseArray<>();
        SparseIntArray portIdMap = new SparseIntArray();
        SparseArray<HdmiDeviceInfo> portDeviceMap = new SparseArray<>();
        for (HdmiPortInfo info : cecPortInfo) {
            portIdMap.put(info.getAddress(), info.getId());
            portInfoMap.put(info.getId(), info);
            portDeviceMap.put(info.getId(), new HdmiDeviceInfo(info.getAddress(), info.getId()));
        }
        mPortIdMap = new UnmodifiableSparseIntArray(portIdMap);
        mPortInfoMap = new UnmodifiableSparseArray<>(portInfoMap);
        mPortDeviceMap = new UnmodifiableSparseArray<>(portDeviceMap);

        if (mHdmiMhlController == null) {
            return;
        }
        HdmiPortInfo[] mhlPortInfo = mHdmiMhlController.getPortInfos();
        ArraySet<Integer> mhlSupportedPorts = new ArraySet<Integer>(mhlPortInfo.length);
        for (HdmiPortInfo info : mhlPortInfo) {
            if (info.isMhlSupported()) {
                mhlSupportedPorts.add(info.getId());
            }
        }

        // Build HDMI port info list with CEC port info plus MHL supported flag. We can just use
        // cec port info if we do not have have port that supports MHL.
        if (mhlSupportedPorts.isEmpty()) {
            setPortInfo(Collections.unmodifiableList(Arrays.asList(cecPortInfo)));
            return;
        }
        ArrayList<HdmiPortInfo> result = new ArrayList<>(cecPortInfo.length);
        for (HdmiPortInfo info : cecPortInfo) {
            if (mhlSupportedPorts.contains(info.getId())) {
                result.add(new HdmiPortInfo(info.getId(), info.getType(), info.getAddress(),
                        info.isCecSupported(), true, info.isArcSupported()));
            } else {
                result.add(info);
            }
        }
        setPortInfo(Collections.unmodifiableList(result));
    }

    HdmiDeviceInfo getDeviceForPortId(int portId) {
        return mPortDeviceMap.get(portId, HdmiDeviceInfo.INACTIVE_DEVICE);
    }

    /**
     * Whether a device of the specified physical address and logical address exists
     * in a device info list. However, both are minimal condition and it could
     * be different device from the original one.
     *
     * @param logicalAddress  logical address of a device to be searched
     * @param physicalAddress physical address of a device to be searched
     * @return true if exist; otherwise false
     */
    @ServiceThreadOnly
    boolean isInDeviceList(int logicalAddress, int physicalAddress) {
        assertRunOnServiceThread();
        HdmiDeviceInfo device = getCecDeviceInfo(logicalAddress);
        if (device == null) {
            return false;
        }
        return device.getPhysicalAddress() == physicalAddress;
    }

    /**
     * Passively listen to incoming CEC messages.
     *
     * This shall not result in any CEC messages being sent.
     */
    @ServiceThreadOnly
    public void handleCecMessage(HdmiCecMessage message) {
        assertRunOnServiceThread();
        // Add device by logical address if it's not already known
        int sourceAddress = message.getSource();
        if (getCecDeviceInfo(sourceAddress) == null) {
            HdmiDeviceInfo newDevice = new HdmiDeviceInfo(sourceAddress,
                    HdmiDeviceInfo.PATH_INVALID, HdmiDeviceInfo.PORT_INVALID,
                    HdmiDeviceInfo.DEVICE_RESERVED, Constants.UNKNOWN_VENDOR_ID,
                    HdmiUtils.getDefaultDeviceName(sourceAddress));
            addCecDevice(newDevice);
        }

        switch (message.getOpcode()) {
            case Constants.MESSAGE_REPORT_PHYSICAL_ADDRESS:
                handleReportPhysicalAddress(message);
                break;
            case Constants.MESSAGE_REPORT_POWER_STATUS:
                handleReportPowerStatus(message);
                break;
            case Constants.MESSAGE_SET_OSD_NAME:
                handleSetOsdName(message);
                break;
            case Constants.MESSAGE_DEVICE_VENDOR_ID:
                handleDeviceVendorId(message);
                break;
            case Constants.MESSAGE_CEC_VERSION:
                handleCecVersion(message);
                break;
            case Constants.MESSAGE_REPORT_FEATURES:
                handleReportFeatures(message);
                break;
        }
    }

    @ServiceThreadOnly
    private void handleReportFeatures(HdmiCecMessage message) {
        assertRunOnServiceThread();

        int version = Byte.toUnsignedInt(message.getParams()[0]);
        updateDeviceCecVersion(message.getSource(), version);
    }

    @ServiceThreadOnly
    private void handleCecVersion(HdmiCecMessage message) {
        assertRunOnServiceThread();

        int version = Byte.toUnsignedInt(message.getParams()[0]);
        updateDeviceCecVersion(message.getSource(), version);
    }

    @ServiceThreadOnly
    private void handleReportPhysicalAddress(HdmiCecMessage message) {
        assertRunOnServiceThread();
        int logicalAddress = message.getSource();
        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
        int type = message.getParams()[2];

        if (updateCecSwitchInfo(logicalAddress, type, physicalAddress)) return;

        HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress);
        if (deviceInfo == null) {
            Slog.i(TAG, "Unknown source device info for <Report Physical Address> " + message);
        } else {
            HdmiDeviceInfo updatedDeviceInfo = new HdmiDeviceInfo(deviceInfo.getLogicalAddress(),
                    physicalAddress,
                    physicalAddressToPortId(physicalAddress), type, deviceInfo.getVendorId(),
                    deviceInfo.getDisplayName(), deviceInfo.getDevicePowerStatus(),
                    deviceInfo.getCecVersion());
            updateCecDevice(updatedDeviceInfo);
        }
    }

    @ServiceThreadOnly
    private void handleReportPowerStatus(HdmiCecMessage message) {
        assertRunOnServiceThread();
        // Update power status of device
        int newStatus = message.getParams()[0] & 0xFF;
        updateDevicePowerStatus(message.getSource(), newStatus);

        if (message.getDestination() == Constants.ADDR_BROADCAST) {
            updateDeviceCecVersion(message.getSource(), HdmiControlManager.HDMI_CEC_VERSION_2_0);
        }
    }

    @ServiceThreadOnly
    private void updateDeviceCecVersion(int logicalAddress, int hdmiCecVersion) {
        assertRunOnServiceThread();
        HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress);
        if (deviceInfo == null) {
            Slog.w(TAG, "Can not update CEC version of non-existing device:" + logicalAddress);
            return;
        }

        if (deviceInfo.getCecVersion() == hdmiCecVersion) {
            return;
        }

        HdmiDeviceInfo updatedDeviceInfo = new HdmiDeviceInfo(deviceInfo.getLogicalAddress(),
                deviceInfo.getPhysicalAddress(), deviceInfo.getPortId(), deviceInfo.getDeviceType(),
                deviceInfo.getVendorId(),
                deviceInfo.getDisplayName(), deviceInfo.getDevicePowerStatus(),
                hdmiCecVersion);
        updateCecDevice(updatedDeviceInfo);
    }

    @ServiceThreadOnly
    private void handleSetOsdName(HdmiCecMessage message) {
        assertRunOnServiceThread();
        int logicalAddress = message.getSource();
        String osdName;
        HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress);
        // 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;
        }
        try {
            osdName = new String(message.getParams(), "US-ASCII");
        } catch (UnsupportedEncodingException e) {
            Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e);
            return;
        }

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

        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,
                deviceInfo.getDevicePowerStatus(), deviceInfo.getCecVersion()));
    }

    @ServiceThreadOnly
    private void handleDeviceVendorId(HdmiCecMessage message) {
        assertRunOnServiceThread();
        int logicalAddress = message.getSource();
        int vendorId = HdmiUtils.threeBytesToInt(message.getParams());

        HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress);
        if (deviceInfo == null) {
            Slog.i(TAG, "Unknown source device info for <Device Vendor ID> " + message);
        } else {
            HdmiDeviceInfo updatedDeviceInfo = new HdmiDeviceInfo(deviceInfo.getLogicalAddress(),
                    deviceInfo.getPhysicalAddress(),
                    deviceInfo.getPortId(), deviceInfo.getDeviceType(), vendorId,
                    deviceInfo.getDisplayName(), deviceInfo.getDevicePowerStatus(),
                    deviceInfo.getCecVersion());
            updateCecDevice(updatedDeviceInfo);
        }
    }

    void addCecSwitch(int physicalAddress) {
        mCecSwitches.add(physicalAddress);
    }

    public ArraySet<Integer> getCecSwitches() {
        return mCecSwitches;
    }

    void removeDevicesConnectedToPort(int portId) {
        Iterator<Integer> it = mCecSwitches.iterator();
        while (it.hasNext()) {
            int path = it.next();
            int devicePortId = physicalAddressToPortId(path);
            if (devicePortId == portId || devicePortId == Constants.INVALID_PORT_ID) {
                it.remove();
            }
        }
        List<Integer> toRemove = new ArrayList<>();
        for (int i = 0; i < mDeviceInfos.size(); i++) {
            int key = mDeviceInfos.keyAt(i);
            int physicalAddress = mDeviceInfos.get(key).getPhysicalAddress();
            int devicePortId = physicalAddressToPortId(physicalAddress);
            if (devicePortId == portId || devicePortId == Constants.INVALID_PORT_ID) {
                toRemove.add(key);
            }
        }
        for (Integer key : toRemove) {
            removeDeviceInfo(key);
        }
    }

    boolean updateCecSwitchInfo(int address, int type, int path) {
        if (address == Constants.ADDR_UNREGISTERED
                && type == HdmiDeviceInfo.DEVICE_PURE_CEC_SWITCH) {
            mCecSwitches.add(path);
            updateSafeDeviceInfoList();
            return true;  // Pure switch does not need further processing. Return here.
        }
        if (type == HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM) {
            mCecSwitches.add(path);
        }
        return false;
    }

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

    /**
     * Thread safe version of {@link #getCecDeviceInfo(int)}.
     *
     * @param logicalAddress logical address to be retrieved
     * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}.
     * Returns null if no logical address matched
     */
    @Nullable
    HdmiDeviceInfo getSafeCecDeviceInfo(int logicalAddress) {
        for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
            if (info.isCecDevice() && info.getLogicalAddress() == logicalAddress) {
                return info;
            }
        }
        return null;
    }

    /**
     * Returns the {@link HdmiDeviceInfo} instance whose physical address matches
     *
     *
     *
     * qq   * the given routing path. CEC devices use routing path for its physical address to
     * describe the hierarchy of the devices in the network.
     *
     * @param path routing path or physical address
     * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null
     */
    @ServiceThreadOnly
    final HdmiDeviceInfo getDeviceInfoByPath(int path) {
        assertRunOnServiceThread();
        for (HdmiDeviceInfo info : getDeviceInfoList(false)) {
            if (info.getPhysicalAddress() == path) {
                return info;
            }
        }
        return null;
    }

    /**
     * Returns the {@link HdmiDeviceInfo} instance whose physical address matches
     * the given routing path. This is the version accessible safely from threads
     * other than service thread.
     *
     * @param path routing path or physical address
     * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null
     */
    HdmiDeviceInfo getSafeDeviceInfoByPath(int path) {
        for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
            if (info.getPhysicalAddress() == path) {
                return info;
            }
        }
        return null;
    }

    public int getPhysicalAddress() {
        return mHdmiCecController.getPhysicalAddress();
    }

    @ServiceThreadOnly
    public void clear() {
        assertRunOnServiceThread();
        initPortInfo();
        clearDeviceList();
        clearLocalDevices();
    }

    @ServiceThreadOnly
    public void clearDeviceList() {
        assertRunOnServiceThread();
        for (HdmiDeviceInfo info : HdmiUtils.sparseArrayToList(mDeviceInfos)) {
            if (info.getPhysicalAddress() == getPhysicalAddress()) {
                continue;
            }
            invokeDeviceEventListener(info,
                    HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE);
        }
        mDeviceInfos.clear();
        updateSafeDeviceInfoList();
    }

    /**
     * Returns HDMI port information for the given port id.
     *
     * @param portId HDMI port id
     * @return {@link HdmiPortInfo} for the given port
     */
    HdmiPortInfo getPortInfo(int portId) {
        return mPortInfoMap.get(portId, null);
    }

    /**
     * Returns the routing path (physical address) of the HDMI port for the given
     * port id.
     */
    int portIdToPath(int portId) {
        HdmiPortInfo portInfo = getPortInfo(portId);
        if (portInfo == null) {
            Slog.e(TAG, "Cannot find the port info: " + portId);
            return Constants.INVALID_PHYSICAL_ADDRESS;
        }
        return portInfo.getAddress();
    }

    /**
     * Returns the id of HDMI port located at the current device that runs this method.
     *
     * For TV with physical address 0x0000, target device 0x1120, we want port physical address
     * 0x1000 to get the correct port id from {@link #mPortIdMap}. For device with Physical Address
     * 0x2000, target device 0x2420, we want port address 0x24000 to get the port id.
     *
     * <p>Return {@link Constants#INVALID_PORT_ID} if target device does not connect to.
     *
     * @param path the target device's physical address.
     * @return the id of the port that the target device eventually connects to
     * on the current device.
     */
    int physicalAddressToPortId(int path) {
        int mask = 0xF000;
        int finalMask = 0xF000;
        int physicalAddress;
        physicalAddress = getPhysicalAddress();
        int maskedAddress = physicalAddress;

        while (maskedAddress != 0) {
            maskedAddress = physicalAddress & mask;
            finalMask |= mask;
            mask >>= 4;
        }

        int portAddress = path & finalMask;
        return mPortIdMap.get(portAddress, Constants.INVALID_PORT_ID);
    }

    List<HdmiPortInfo> getPortInfo() {
        return mPortInfo;
    }

    void setPortInfo(List<HdmiPortInfo> portInfo) {
        mPortInfo = portInfo;
    }

    private boolean isLocalDeviceAddress(int address) {
        for (int i = 0; i < mLocalDevices.size(); i++) {
            int key = mLocalDevices.keyAt(i);
            if (mLocalDevices.get(key).mAddress == address) {
                return true;
            }
        }
        return false;
    }

    private void assertRunOnServiceThread() {
        if (Looper.myLooper() != mHandler.getLooper()) {
            throw new IllegalStateException("Should run on service thread.");
        }
    }

    protected void dump(IndentingPrintWriter pw) {
        pw.println("HDMI CEC Network");
        pw.increaseIndent();
        HdmiUtils.dumpIterable(pw, "mPortInfo:", mPortInfo);
        for (int i = 0; i < mLocalDevices.size(); ++i) {
            pw.println("HdmiCecLocalDevice #" + mLocalDevices.keyAt(i) + ":");
            pw.increaseIndent();
            mLocalDevices.valueAt(i).dump(pw);

            pw.println("Active Source history:");
            pw.increaseIndent();
            final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            ArrayBlockingQueue<HdmiCecController.Dumpable> activeSourceHistory =
                    mLocalDevices.valueAt(i).getActiveSourceHistory();
            for (HdmiCecController.Dumpable activeSourceEvent : activeSourceHistory) {
                activeSourceEvent.dump(pw, sdf);
            }
            pw.decreaseIndent();
            pw.decreaseIndent();
        }
        HdmiUtils.dumpIterable(pw, "mDeviceInfos:", mSafeAllDeviceInfos);
        pw.decreaseIndent();
    }
}
