Support Inputdevice LightsManager feature in frameworks.

Add lights manager support to input frameworks.

Bug: 161633625
Test: atest LightsManagerTest, atest InputDeviceLightsManagerTest
Change-Id: Ie00357bce0f6c98e9eada5e0a79f93f48e7a4d1b
diff --git a/include/android/input.h b/include/android/input.h
index b70d424..7973487 100644
--- a/include/android/input.h
+++ b/include/android/input.h
@@ -859,7 +859,7 @@
     /** HDMI */
     AINPUT_SOURCE_HDMI = 0x02000000 | AINPUT_SOURCE_CLASS_BUTTON,
     /** sensor */
-    AINPUT_SOURCE_SENSOR = 0x04000000 | AINPUT_SOURCE_UNKNOWN,
+    AINPUT_SOURCE_SENSOR = 0x04000000 | AINPUT_SOURCE_CLASS_NONE,
     /** rotary encoder */
     AINPUT_SOURCE_ROTARY_ENCODER = 0x00400000 | AINPUT_SOURCE_CLASS_NONE,
 
diff --git a/include/input/InputDevice.h b/include/input/InputDevice.h
index 2bd7bd2..2deb99d 100644
--- a/include/input/InputDevice.h
+++ b/include/input/InputDevice.h
@@ -100,6 +100,13 @@
     SPECIAL_TRIGGER = 3,
 };
 
+enum class InputDeviceLightType : int32_t {
+    SINGLE = 0,
+    PLAYER_ID = 1,
+    RGB = 2,
+    MULTI_COLOR = 3,
+};
+
 struct InputDeviceSensorInfo {
     explicit InputDeviceSensorInfo(std::string name, std::string vendor, int32_t version,
                                    InputDeviceSensorType type, InputDeviceSensorAccuracy accuracy,
@@ -156,6 +163,20 @@
     int32_t id;
 };
 
+struct InputDeviceLightInfo {
+    explicit InputDeviceLightInfo(std::string name, int32_t id, InputDeviceLightType type,
+                                  int32_t ordinal)
+          : name(name), id(id), type(type), ordinal(ordinal) {}
+    // Name string of the light.
+    std::string name;
+    // Light id
+    int32_t id;
+    // Type of the light.
+    InputDeviceLightType type;
+    // Ordinal of the light
+    int32_t ordinal;
+};
+
 /*
  * Describes the characteristics and capabilities of an input device.
  */
@@ -198,6 +219,7 @@
             float min, float max, float flat, float fuzz, float resolution);
     void addMotionRange(const MotionRange& range);
     void addSensorInfo(const InputDeviceSensorInfo& info);
+    void addLightInfo(const InputDeviceLightInfo& info);
 
     inline void setKeyboardType(int32_t keyboardType) { mKeyboardType = keyboardType; }
     inline int32_t getKeyboardType() const { return mKeyboardType; }
@@ -230,6 +252,10 @@
 
     const std::vector<InputDeviceSensorType> getSensorTypes();
 
+    const std::vector<int32_t> getLightIds();
+
+    const InputDeviceLightInfo* getLightInfo(int32_t id);
+
 private:
     int32_t mId;
     int32_t mGeneration;
@@ -248,6 +274,8 @@
 
     std::vector<MotionRange> mMotionRanges;
     std::unordered_map<InputDeviceSensorType, InputDeviceSensorInfo> mSensors;
+    /* Map from light ID to light info */
+    std::unordered_map<int32_t, InputDeviceLightInfo> mLights;
 };
 
 /* Types of input device configuration files. */
diff --git a/libs/input/InputDevice.cpp b/libs/input/InputDevice.cpp
index 698cf6e..ffcc1cd 100644
--- a/libs/input/InputDevice.cpp
+++ b/libs/input/InputDevice.cpp
@@ -170,7 +170,8 @@
         mHasButtonUnderPad(other.mHasButtonUnderPad),
         mHasSensor(other.mHasSensor),
         mMotionRanges(other.mMotionRanges),
-        mSensors(other.mSensors) {}
+        mSensors(other.mSensors),
+        mLights(other.mLights) {}
 
 InputDeviceInfo::~InputDeviceInfo() {
 }
@@ -193,6 +194,7 @@
     mHasSensor = false;
     mMotionRanges.clear();
     mSensors.clear();
+    mLights.clear();
 }
 
 const InputDeviceInfo::MotionRange* InputDeviceInfo::getMotionRange(
@@ -229,6 +231,13 @@
     mSensors.insert_or_assign(info.type, info);
 }
 
+void InputDeviceInfo::addLightInfo(const InputDeviceLightInfo& info) {
+    if (mLights.find(info.id) != mLights.end()) {
+        ALOGW("Light id %d already exists, will be replaced by new light added.", info.id);
+    }
+    mLights.insert_or_assign(info.id, info);
+}
+
 const std::vector<InputDeviceSensorType> InputDeviceInfo::getSensorTypes() {
     std::vector<InputDeviceSensorType> types;
     for (const auto& [type, info] : mSensors) {
@@ -245,4 +254,20 @@
     return &it->second;
 }
 
+const std::vector<int32_t> InputDeviceInfo::getLightIds() {
+    std::vector<int32_t> ids;
+    for (const auto& [id, info] : mLights) {
+        ids.push_back(id);
+    }
+    return ids;
+}
+
+const InputDeviceLightInfo* InputDeviceInfo::getLightInfo(int32_t id) {
+    auto it = mLights.find(id);
+    if (it == mLights.end()) {
+        return nullptr;
+    }
+    return &it->second;
+}
+
 } // namespace android
diff --git a/services/inputflinger/include/InputReaderBase.h b/services/inputflinger/include/InputReaderBase.h
index 69aea84..3bf212a 100644
--- a/services/inputflinger/include/InputReaderBase.h
+++ b/services/inputflinger/include/InputReaderBase.h
@@ -113,6 +113,10 @@
     /* Get battery status of a particular input device. */
     virtual std::optional<int32_t> getBatteryStatus(int32_t deviceId) = 0;
 
+    virtual std::vector<int32_t> getLightIds(int32_t deviceId) = 0;
+
+    virtual const InputDeviceLightInfo* getLightInfo(int32_t deviceId, int32_t lightId) = 0;
+
     /* Return true if the device can send input events to the specified display. */
     virtual bool canDispatchToDisplay(int32_t deviceId, int32_t displayId) = 0;
 
@@ -126,6 +130,15 @@
 
     /* Flush sensor data in input reader mapper. */
     virtual void flushSensor(int32_t deviceId, InputDeviceSensorType sensorType) = 0;
+
+    /* Set color for the light */
+    virtual bool setLightColor(int32_t deviceId, int32_t lightId, int32_t color) = 0;
+    /* Set player ID for the light */
+    virtual bool setLightPlayerId(int32_t deviceId, int32_t lightId, int32_t playerId) = 0;
+    /* Get light color */
+    virtual std::optional<int32_t> getLightColor(int32_t deviceId, int32_t lightId) = 0;
+    /* Get light player ID */
+    virtual std::optional<int32_t> getLightPlayerId(int32_t deviceId, int32_t lightId) = 0;
 };
 
 // --- InputReaderConfiguration ---
diff --git a/services/inputflinger/reader/Android.bp b/services/inputflinger/reader/Android.bp
index 7f979f2..ffb98e0 100644
--- a/services/inputflinger/reader/Android.bp
+++ b/services/inputflinger/reader/Android.bp
@@ -36,6 +36,7 @@
         "mapper/InputMapper.cpp",
         "mapper/JoystickInputMapper.cpp",
         "mapper/KeyboardInputMapper.cpp",
+        "mapper/LightInputMapper.cpp",
         "mapper/MultiTouchInputMapper.cpp",
         "mapper/RotaryEncoderInputMapper.cpp",
         "mapper/SensorInputMapper.cpp",
diff --git a/services/inputflinger/reader/EventHub.cpp b/services/inputflinger/reader/EventHub.cpp
index 8f8c051..e939d1c 100644
--- a/services/inputflinger/reader/EventHub.cpp
+++ b/services/inputflinger/reader/EventHub.cpp
@@ -48,6 +48,7 @@
 #include <utils/Timers.h>
 
 #include <filesystem>
+#include <regex>
 
 #include "EventHub.h"
 
@@ -84,6 +85,30 @@
                                                                        {"Full", 100},
                                                                        {"Unknown", 50}};
 
+// Mapping for input led class node names lookup.
+// https://www.kernel.org/doc/html/latest/leds/leds-class.html
+static const std::unordered_map<std::string, InputLightClass> LIGHT_CLASSES =
+        {{"red", InputLightClass::RED},
+         {"green", InputLightClass::GREEN},
+         {"blue", InputLightClass::BLUE},
+         {"global", InputLightClass::GLOBAL},
+         {"brightness", InputLightClass::BRIGHTNESS},
+         {"multi_index", InputLightClass::MULTI_INDEX},
+         {"multi_intensity", InputLightClass::MULTI_INTENSITY},
+         {"max_brightness", InputLightClass::MAX_BRIGHTNESS}};
+
+// Mapping for input multicolor led class node names.
+// https://www.kernel.org/doc/html/latest/leds/leds-class-multicolor.html
+static const std::unordered_map<InputLightClass, std::string> LIGHT_NODES =
+        {{InputLightClass::BRIGHTNESS, "brightness"},
+         {InputLightClass::MULTI_INDEX, "multi_index"},
+         {InputLightClass::MULTI_INTENSITY, "multi_intensity"}};
+
+// Mapping for light color name and the light color
+const std::unordered_map<std::string, LightColor> LIGHT_COLORS = {{"red", LightColor::RED},
+                                                                  {"green", LightColor::GREEN},
+                                                                  {"blue", LightColor::BLUE}};
+
 static inline const char* toString(bool value) {
     return value ? "true" : "false";
 }
@@ -151,14 +176,14 @@
  * Returns the sysfs root path of the input device
  *
  */
-static std::filesystem::path getSysfsRootPath(const char* devicePath) {
+static std::optional<std::filesystem::path> getSysfsRootPath(const char* devicePath) {
     std::error_code errorCode;
 
     // Stat the device path to get the major and minor number of the character file
     struct stat statbuf;
     if (stat(devicePath, &statbuf) == -1) {
         ALOGE("Could not stat device %s due to error: %s.", devicePath, std::strerror(errno));
-        return std::filesystem::path();
+        return std::nullopt;
     }
 
     unsigned int major_num = major(statbuf.st_rdev);
@@ -173,7 +198,7 @@
     if (errorCode) {
         ALOGW("Could not run filesystem::canonical() due to error %d : %s.", errorCode.value(),
               errorCode.message().c_str());
-        return std::filesystem::path();
+        return std::nullopt;
     }
 
     // Continue to go up a directory until we reach a directory named "input"
@@ -192,26 +217,68 @@
         }
 
         // Not found
-        return std::filesystem::path();
+        return std::nullopt;
     }
 
     return sysfsPath;
 }
 
 /**
- * Returns the power supply node in sys fs
- *
+ * Returns the list of files under a specified path.
  */
-static std::filesystem::path findPowerSupplyNode(const std::filesystem::path& sysfsRootPath) {
-    for (auto path = sysfsRootPath; path != "/"; path = path.parent_path()) {
-        std::error_code errorCode;
-        auto iter = std::filesystem::directory_iterator(path / "power_supply", errorCode);
-        if (!errorCode && iter != std::filesystem::directory_iterator()) {
-            return iter->path();
+static std::vector<std::filesystem::path> allFilesInPath(const std::filesystem::path& path) {
+    std::vector<std::filesystem::path> nodes;
+    std::error_code errorCode;
+    auto iter = std::filesystem::directory_iterator(path, errorCode);
+    while (!errorCode && iter != std::filesystem::directory_iterator()) {
+        nodes.push_back(iter->path());
+        iter++;
+    }
+    return nodes;
+}
+
+/**
+ * Returns the list of files under a specified directory in a sysfs path.
+ * Example:
+ * findSysfsNodes(sysfsRootPath, SysfsClass::LEDS) will return all led nodes under "leds" directory
+ * in the sysfs path.
+ */
+static std::vector<std::filesystem::path> findSysfsNodes(const std::filesystem::path& sysfsRoot,
+                                                         SysfsClass clazz) {
+    std::string nodeStr = NamedEnum::string(clazz);
+    std::for_each(nodeStr.begin(), nodeStr.end(),
+                  [](char& c) { c = std::tolower(static_cast<unsigned char>(c)); });
+    std::vector<std::filesystem::path> nodes;
+    for (auto path = sysfsRoot; path != "/" && nodes.empty(); path = path.parent_path()) {
+        nodes = allFilesInPath(path / nodeStr);
+    }
+    return nodes;
+}
+
+static std::optional<std::array<LightColor, COLOR_NUM>> getColorIndexArray(
+        std::filesystem::path path) {
+    std::string indexStr;
+    if (!base::ReadFileToString(path, &indexStr)) {
+        return std::nullopt;
+    }
+
+    // Parse the multi color LED index file, refer to kernel docs
+    // leds/leds-class-multicolor.html
+    std::regex indexPattern("(red|green|blue)\\s(red|green|blue)\\s(red|green|blue)[\\n]");
+    std::smatch results;
+    std::array<LightColor, COLOR_NUM> colors;
+    if (!std::regex_match(indexStr, results, indexPattern)) {
+        return std::nullopt;
+    }
+
+    for (size_t i = 1; i < results.size(); i++) {
+        const auto it = LIGHT_COLORS.find(results[i].str());
+        if (it != LIGHT_COLORS.end()) {
+            // intensities.emplace(it->second, 0);
+            colors[i - 1] = it->second;
         }
     }
-    // Not found
-    return std::filesystem::path();
+    return colors;
 }
 
 // --- Global Functions ---
@@ -280,6 +347,7 @@
         virtualKeyMap(nullptr),
         ffEffectPlaying(false),
         ffEffectId(-1),
+        nextLightId(0),
         controllerNumber(0),
         enabled(true),
         isVirtual(fd < 0) {}
@@ -469,6 +537,70 @@
     return NAME_NOT_FOUND;
 }
 
+// Check the sysfs path for any input device batteries, returns true if battery found.
+bool EventHub::Device::configureBatteryLocked() {
+    if (!sysfsRootPath.has_value()) {
+        return false;
+    }
+    // Check if device has any batteries.
+    std::vector<std::filesystem::path> batteryPaths =
+            findSysfsNodes(sysfsRootPath.value(), SysfsClass::POWER_SUPPLY);
+    // We only support single battery for an input device, if multiple batteries exist only the
+    // first one is supported.
+    if (batteryPaths.empty()) {
+        // Set path to be empty
+        sysfsBatteryPath = std::nullopt;
+        return false;
+    }
+    // If a battery exists
+    sysfsBatteryPath = batteryPaths[0];
+    return true;
+}
+
+// Check the sysfs path for any input device lights, returns true if lights found.
+bool EventHub::Device::configureLightsLocked() {
+    if (!sysfsRootPath.has_value()) {
+        return false;
+    }
+    // Check if device has any lights.
+    const auto& paths = findSysfsNodes(sysfsRootPath.value(), SysfsClass::LEDS);
+    for (const auto& nodePath : paths) {
+        RawLightInfo info;
+        info.id = ++nextLightId;
+        info.path = nodePath;
+        info.name = nodePath.filename();
+        info.maxBrightness = std::nullopt;
+        size_t nameStart = info.name.rfind(":");
+        if (nameStart != std::string::npos) {
+            // Trim the name to color name
+            info.name = info.name.substr(nameStart + 1);
+            // Set InputLightClass flag for colors
+            const auto it = LIGHT_CLASSES.find(info.name);
+            if (it != LIGHT_CLASSES.end()) {
+                info.flags |= it->second;
+            }
+        }
+        // Scan the path for all the files
+        // Refer to https://www.kernel.org/doc/Documentation/leds/leds-class.txt
+        const auto& files = allFilesInPath(nodePath);
+        for (const auto& file : files) {
+            const auto it = LIGHT_CLASSES.find(file.filename().string());
+            if (it != LIGHT_CLASSES.end()) {
+                info.flags |= it->second;
+                // If the node has maximum brightness, read it
+                if (it->second == InputLightClass::MAX_BRIGHTNESS) {
+                    std::string str;
+                    if (base::ReadFileToString(file, &str)) {
+                        info.maxBrightness = std::stoi(str);
+                    }
+                }
+            }
+        }
+        lightInfos.insert_or_assign(info.id, info);
+    }
+    return !lightInfos.empty();
+}
+
 /**
  * Get the capabilities for the current process.
  * Crashes the system if unable to create / check / destroy the capabilities object.
@@ -829,6 +961,161 @@
     return Errorf("Device not found or device has no key layout.");
 }
 
+const std::vector<int32_t> EventHub::getRawLightIds(int32_t deviceId) {
+    std::scoped_lock _l(mLock);
+    Device* device = getDeviceLocked(deviceId);
+    std::vector<int32_t> lightIds;
+
+    if (device != nullptr) {
+        for (const auto [id, info] : device->lightInfos) {
+            lightIds.push_back(id);
+        }
+    }
+    return lightIds;
+}
+
+std::optional<RawLightInfo> EventHub::getRawLightInfo(int32_t deviceId, int32_t lightId) {
+    std::scoped_lock _l(mLock);
+    Device* device = getDeviceLocked(deviceId);
+
+    if (device != nullptr) {
+        auto it = device->lightInfos.find(lightId);
+        if (it != device->lightInfos.end()) {
+            return it->second;
+        }
+    }
+    return std::nullopt;
+}
+
+std::optional<int32_t> EventHub::getLightBrightness(int32_t deviceId, int32_t lightId) {
+    std::scoped_lock _l(mLock);
+
+    Device* device = getDeviceLocked(deviceId);
+    if (device == nullptr) {
+        return std::nullopt;
+    }
+
+    auto it = device->lightInfos.find(lightId);
+    if (it == device->lightInfos.end()) {
+        return std::nullopt;
+    }
+    std::string buffer;
+    if (!base::ReadFileToString(it->second.path / LIGHT_NODES.at(InputLightClass::BRIGHTNESS),
+                                &buffer)) {
+        return std::nullopt;
+    }
+    return std::stoi(buffer);
+}
+
+std::optional<std::unordered_map<LightColor, int32_t>> EventHub::getLightIntensities(
+        int32_t deviceId, int32_t lightId) {
+    std::scoped_lock _l(mLock);
+
+    Device* device = getDeviceLocked(deviceId);
+    if (device == nullptr) {
+        return std::nullopt;
+    }
+
+    auto lightIt = device->lightInfos.find(lightId);
+    if (lightIt == device->lightInfos.end()) {
+        return std::nullopt;
+    }
+
+    auto ret =
+            getColorIndexArray(lightIt->second.path / LIGHT_NODES.at(InputLightClass::MULTI_INDEX));
+
+    if (!ret.has_value()) {
+        return std::nullopt;
+    }
+    std::array<LightColor, COLOR_NUM> colors = ret.value();
+
+    std::string intensityStr;
+    if (!base::ReadFileToString(lightIt->second.path /
+                                        LIGHT_NODES.at(InputLightClass::MULTI_INTENSITY),
+                                &intensityStr)) {
+        return std::nullopt;
+    }
+
+    // Intensity node outputs 3 color values
+    std::regex intensityPattern("([0-9]+)\\s([0-9]+)\\s([0-9]+)[\\n]");
+    std::smatch results;
+
+    if (!std::regex_match(intensityStr, results, intensityPattern)) {
+        return std::nullopt;
+    }
+    std::unordered_map<LightColor, int32_t> intensities;
+    for (size_t i = 1; i < results.size(); i++) {
+        int value = std::stoi(results[i].str());
+        intensities.emplace(colors[i - 1], value);
+    }
+    return intensities;
+}
+
+void EventHub::setLightBrightness(int32_t deviceId, int32_t lightId, int32_t brightness) {
+    std::scoped_lock _l(mLock);
+
+    Device* device = getDeviceLocked(deviceId);
+    if (device == nullptr) {
+        ALOGE("Device Id %d does not exist", deviceId);
+        return;
+    }
+    auto lightIt = device->lightInfos.find(lightId);
+    if (lightIt == device->lightInfos.end()) {
+        ALOGE("Light Id %d does not exist.", lightId);
+        return;
+    }
+
+    if (!base::WriteStringToFile(std::to_string(brightness),
+                                 lightIt->second.path /
+                                         LIGHT_NODES.at(InputLightClass::BRIGHTNESS))) {
+        ALOGE("Can not write to file, error: %s", strerror(errno));
+    }
+}
+
+void EventHub::setLightIntensities(int32_t deviceId, int32_t lightId,
+                                   std::unordered_map<LightColor, int32_t> intensities) {
+    std::scoped_lock _l(mLock);
+
+    Device* device = getDeviceLocked(deviceId);
+    if (device == nullptr) {
+        ALOGE("Device Id %d does not exist", deviceId);
+        return;
+    }
+    auto lightIt = device->lightInfos.find(lightId);
+    if (lightIt == device->lightInfos.end()) {
+        ALOGE("Light Id %d does not exist.", lightId);
+        return;
+    }
+
+    auto ret =
+            getColorIndexArray(lightIt->second.path / LIGHT_NODES.at(InputLightClass::MULTI_INDEX));
+
+    if (!ret.has_value()) {
+        return;
+    }
+    std::array<LightColor, COLOR_NUM> colors = ret.value();
+
+    std::string rgbStr;
+    for (size_t i = 0; i < COLOR_NUM; i++) {
+        auto it = intensities.find(colors[i]);
+        if (it != intensities.end()) {
+            rgbStr += std::to_string(it->second);
+            // Insert space between colors
+            if (i < COLOR_NUM - 1) {
+                rgbStr += " ";
+            }
+        }
+    }
+    // Append new line
+    rgbStr += "\n";
+
+    if (!base::WriteStringToFile(rgbStr,
+                                 lightIt->second.path /
+                                         LIGHT_NODES.at(InputLightClass::MULTI_INTENSITY))) {
+        ALOGE("Can not write to file, error: %s", strerror(errno));
+    }
+}
+
 void EventHub::setExcludedDevices(const std::vector<std::string>& devices) {
     std::scoped_lock _l(mLock);
 
@@ -1068,18 +1355,18 @@
     Device* device = getDeviceLocked(deviceId);
     std::string buffer;
 
-    if (!device || (device->sysfsBatteryPath.empty())) {
+    if (device == nullptr || !device->sysfsBatteryPath.has_value()) {
         return std::nullopt;
     }
 
     // Some devices report battery capacity as an integer through the "capacity" file
-    if (base::ReadFileToString(device->sysfsBatteryPath / "capacity", &buffer)) {
+    if (base::ReadFileToString(device->sysfsBatteryPath.value() / "capacity", &buffer)) {
         return std::stoi(buffer);
     }
 
     // Other devices report capacity as an enum value POWER_SUPPLY_CAPACITY_LEVEL_XXX
     // These values are taken from kernel source code include/linux/power_supply.h
-    if (base::ReadFileToString(device->sysfsBatteryPath / "capacity_level", &buffer)) {
+    if (base::ReadFileToString(device->sysfsBatteryPath.value() / "capacity_level", &buffer)) {
         const auto it = BATTERY_LEVEL.find(buffer);
         if (it != BATTERY_LEVEL.end()) {
             return it->second;
@@ -1093,11 +1380,11 @@
     Device* device = getDeviceLocked(deviceId);
     std::string buffer;
 
-    if (!device || (device->sysfsBatteryPath.empty())) {
+    if (device == nullptr || !device->sysfsBatteryPath.has_value()) {
         return std::nullopt;
     }
 
-    if (!base::ReadFileToString(device->sysfsBatteryPath / "status", &buffer)) {
+    if (!base::ReadFileToString(device->sysfsBatteryPath.value() / "status", &buffer)) {
         ALOGE("Failed to read sysfs battery info: %s", strerror(errno));
         return std::nullopt;
     }
@@ -1572,6 +1859,12 @@
     // Load the configuration file for the device.
     device->loadConfigurationLocked();
 
+    // Grab the device's sysfs path
+    device->sysfsRootPath = getSysfsRootPath(devicePath.c_str());
+    // find related components
+    bool hasBattery = device->configureBatteryLocked();
+    bool hasLights = device->configureLightsLocked();
+
     // Figure out the kinds of events the device reports.
     device->readDeviceBitMask(EVIOCGBIT(EV_KEY, 0), device->keyBitmask);
     device->readDeviceBitMask(EVIOCGBIT(EV_ABS, 0), device->absBitmask);
@@ -1721,16 +2014,14 @@
         return -1;
     }
 
-    // Grab the device's sysfs path
-    device->sysfsRootPath = getSysfsRootPath(devicePath.c_str());
+    // Classify InputDeviceClass::BATTERY.
+    if (hasBattery) {
+        device->classes |= InputDeviceClass::BATTERY;
+    }
 
-    if (!device->sysfsRootPath.empty()) {
-        device->sysfsBatteryPath = findPowerSupplyNode(device->sysfsRootPath);
-
-        // Check if a battery exists
-        if (!device->sysfsBatteryPath.empty()) {
-            device->classes |= InputDeviceClass::BATTERY;
-        }
+    // Classify InputDeviceClass::LIGHT.
+    if (hasLights) {
+        device->classes |= InputDeviceClass::LIGHT;
     }
 
     // Determine whether the device has a mic.
diff --git a/services/inputflinger/reader/InputDevice.cpp b/services/inputflinger/reader/InputDevice.cpp
index 574f651..cbf3b69 100644
--- a/services/inputflinger/reader/InputDevice.cpp
+++ b/services/inputflinger/reader/InputDevice.cpp
@@ -27,6 +27,7 @@
 #include "InputReaderContext.h"
 #include "JoystickInputMapper.h"
 #include "KeyboardInputMapper.h"
+#include "LightInputMapper.h"
 #include "MultiTouchInputMapper.h"
 #include "RotaryEncoderInputMapper.h"
 #include "SensorInputMapper.h"
@@ -161,9 +162,22 @@
         mappers.push_back(std::make_unique<VibratorInputMapper>(*contextPtr));
     }
 
-    // Battery-like devices.
+    // Battery-like devices. Only one battery mapper for each EventHub device.
     if (classes.test(InputDeviceClass::BATTERY)) {
-        mappers.push_back(std::make_unique<BatteryInputMapper>(*contextPtr));
+        InputDeviceInfo deviceInfo;
+        getDeviceInfo(&deviceInfo);
+        if (!deviceInfo.hasBattery()) {
+            mappers.push_back(std::make_unique<BatteryInputMapper>(*contextPtr));
+        }
+    }
+
+    // Light-containing devices. Only one light mapper for each EventHub device.
+    if (classes.test(InputDeviceClass::LIGHT)) {
+        InputDeviceInfo deviceInfo;
+        getDeviceInfo(&deviceInfo);
+        if (deviceInfo.getLightIds().empty()) {
+            mappers.push_back(std::make_unique<LightInputMapper>(*contextPtr));
+        }
     }
 
     // Keyboard-like devices.
@@ -505,6 +519,32 @@
     return first_in_mappers<int32_t>([](InputMapper& mapper) { return mapper.getBatteryStatus(); });
 }
 
+bool InputDevice::setLightColor(int32_t lightId, int32_t color) {
+    bool success = true;
+    for_each_mapper([&success, lightId, color](InputMapper& mapper) {
+        success &= mapper.setLightColor(lightId, color);
+    });
+    return success;
+}
+
+bool InputDevice::setLightPlayerId(int32_t lightId, int32_t playerId) {
+    bool success = true;
+    for_each_mapper([&success, lightId, playerId](InputMapper& mapper) {
+        success &= mapper.setLightPlayerId(lightId, playerId);
+    });
+    return success;
+}
+
+std::optional<int32_t> InputDevice::getLightColor(int32_t lightId) {
+    return first_in_mappers<int32_t>(
+            [lightId](InputMapper& mapper) { return mapper.getLightColor(lightId); });
+}
+
+std::optional<int32_t> InputDevice::getLightPlayerId(int32_t lightId) {
+    return first_in_mappers<int32_t>(
+            [lightId](InputMapper& mapper) { return mapper.getLightPlayerId(lightId); });
+}
+
 int32_t InputDevice::getMetaState() {
     int32_t result = 0;
     for_each_mapper([&result](InputMapper& mapper) { result |= mapper.getMetaState(); });
diff --git a/services/inputflinger/reader/InputReader.cpp b/services/inputflinger/reader/InputReader.cpp
index 14fb77b..2d0fdf7 100644
--- a/services/inputflinger/reader/InputReader.cpp
+++ b/services/inputflinger/reader/InputReader.cpp
@@ -696,6 +696,70 @@
     return std::nullopt;
 }
 
+std::vector<int32_t> InputReader::getLightIds(int32_t deviceId) {
+    std::scoped_lock _l(mLock);
+
+    InputDevice* device = findInputDeviceLocked(deviceId);
+    if (device) {
+        InputDeviceInfo info;
+        device->getDeviceInfo(&info);
+        return info.getLightIds();
+    }
+    return {};
+}
+
+const InputDeviceLightInfo* InputReader::getLightInfo(int32_t deviceId, int32_t lightId) {
+    std::scoped_lock _l(mLock);
+
+    InputDevice* device = findInputDeviceLocked(deviceId);
+    if (device) {
+        InputDeviceInfo info;
+        device->getDeviceInfo(&info);
+        return info.getLightInfo(lightId);
+    }
+    return nullptr;
+}
+
+bool InputReader::setLightColor(int32_t deviceId, int32_t lightId, int32_t color) {
+    std::scoped_lock _l(mLock);
+
+    InputDevice* device = findInputDeviceLocked(deviceId);
+    if (device) {
+        return device->setLightColor(lightId, color);
+    }
+    return false;
+}
+
+bool InputReader::setLightPlayerId(int32_t deviceId, int32_t lightId, int32_t playerId) {
+    std::scoped_lock _l(mLock);
+
+    InputDevice* device = findInputDeviceLocked(deviceId);
+    if (device) {
+        return device->setLightPlayerId(lightId, playerId);
+    }
+    return false;
+}
+
+std::optional<int32_t> InputReader::getLightColor(int32_t deviceId, int32_t lightId) {
+    std::scoped_lock _l(mLock);
+
+    InputDevice* device = findInputDeviceLocked(deviceId);
+    if (device) {
+        return device->getLightColor(lightId);
+    }
+    return std::nullopt;
+}
+
+std::optional<int32_t> InputReader::getLightPlayerId(int32_t deviceId, int32_t lightId) {
+    std::scoped_lock _l(mLock);
+
+    InputDevice* device = findInputDeviceLocked(deviceId);
+    if (device) {
+        return device->getLightPlayerId(lightId);
+    }
+    return std::nullopt;
+}
+
 bool InputReader::isInputDeviceEnabled(int32_t deviceId) {
     std::scoped_lock _l(mLock);
 
diff --git a/services/inputflinger/reader/include/EventHub.h b/services/inputflinger/reader/include/EventHub.h
index 30967df..e6164d3 100644
--- a/services/inputflinger/reader/include/EventHub.h
+++ b/services/inputflinger/reader/include/EventHub.h
@@ -47,6 +47,8 @@
 
 namespace android {
 
+/* Number of colors : {red, green, blue} */
+static constexpr size_t COLOR_NUM = 3;
 /*
  * A raw event as retrieved from the EventHub.
  */
@@ -127,6 +129,9 @@
     /* The input device has a battery */
     BATTERY = 0x00004000,
 
+    /* The input device has sysfs controllable lights */
+    LIGHT = 0x00008000,
+
     /* The input device is virtual (not a real device, not part of UI configuration). */
     VIRTUAL = 0x40000000,
 
@@ -134,6 +139,46 @@
     EXTERNAL = 0x80000000,
 };
 
+enum class SysfsClass : uint32_t {
+    POWER_SUPPLY = 0,
+    LEDS = 1,
+};
+
+enum class LightColor : uint32_t {
+    RED = 0,
+    GREEN = 1,
+    BLUE = 2,
+};
+
+enum class InputLightClass : uint32_t {
+    /* The input light has brightness node. */
+    BRIGHTNESS = 0x00000001,
+    /* The input light has red name. */
+    RED = 0x00000002,
+    /* The input light has green name. */
+    GREEN = 0x00000004,
+    /* The input light has blue name. */
+    BLUE = 0x00000008,
+    /* The input light has global name. */
+    GLOBAL = 0x00000010,
+    /* The input light has multi index node. */
+    MULTI_INDEX = 0x00000020,
+    /* The input light has multi intensity node. */
+    MULTI_INTENSITY = 0x00000040,
+    /* The input light has max brightness node. */
+    MAX_BRIGHTNESS = 0x00000080,
+};
+
+/* Describes a raw light. */
+struct RawLightInfo {
+    int32_t id;
+    std::string name;
+    std::optional<int32_t> maxBrightness;
+    Flags<InputLightClass> flags;
+    std::array<int32_t, COLOR_NUM> rgbIndex;
+    std::filesystem::path path;
+};
+
 /*
  * Gets the class that owns an axis, in cases where multiple classes might claim
  * the same axis for different purposes.
@@ -214,7 +259,16 @@
     virtual std::vector<TouchVideoFrame> getVideoFrames(int32_t deviceId) = 0;
     virtual base::Result<std::pair<InputDeviceSensorType, int32_t>> mapSensor(int32_t deviceId,
                                                                               int32_t absCode) = 0;
-
+    // Raw lights are sysfs led light nodes we found from the EventHub device sysfs node,
+    // containing the raw info of the sysfs node structure.
+    virtual const std::vector<int32_t> getRawLightIds(int32_t deviceId) = 0;
+    virtual std::optional<RawLightInfo> getRawLightInfo(int32_t deviceId, int32_t lightId) = 0;
+    virtual std::optional<int32_t> getLightBrightness(int32_t deviceId, int32_t lightId) = 0;
+    virtual void setLightBrightness(int32_t deviceId, int32_t lightId, int32_t brightness) = 0;
+    virtual std::optional<std::unordered_map<LightColor, int32_t>> getLightIntensities(
+            int32_t deviceId, int32_t lightId) = 0;
+    virtual void setLightIntensities(int32_t deviceId, int32_t lightId,
+                                     std::unordered_map<LightColor, int32_t> intensities) = 0;
     /*
      * Query current input state.
      */
@@ -377,6 +431,17 @@
     base::Result<std::pair<InputDeviceSensorType, int32_t>> mapSensor(
             int32_t deviceId, int32_t absCode) override final;
 
+    const std::vector<int32_t> getRawLightIds(int32_t deviceId) override final;
+
+    std::optional<RawLightInfo> getRawLightInfo(int32_t deviceId, int32_t lightId) override final;
+
+    std::optional<int32_t> getLightBrightness(int32_t deviceId, int32_t lightId) override final;
+    void setLightBrightness(int32_t deviceId, int32_t lightId, int32_t brightness) override final;
+    std::optional<std::unordered_map<LightColor, int32_t>> getLightIntensities(
+            int32_t deviceId, int32_t lightId) override final;
+    void setLightIntensities(int32_t deviceId, int32_t lightId,
+                             std::unordered_map<LightColor, int32_t> intensities) override final;
+
     void setExcludedDevices(const std::vector<std::string>& devices) override final;
 
     int32_t getScanCodeState(int32_t deviceId, int32_t scanCode) const override final;
@@ -458,9 +523,12 @@
         bool ffEffectPlaying;
         int16_t ffEffectId; // initially -1
 
-        // The paths are invalid when .empty() returns true
-        std::filesystem::path sysfsRootPath;
-        std::filesystem::path sysfsBatteryPath;
+        // The paths are invalid when they're std::nullopt
+        std::optional<std::filesystem::path> sysfsRootPath;
+        std::optional<std::filesystem::path> sysfsBatteryPath;
+        // maps from light id to light info
+        std::unordered_map<int32_t, RawLightInfo> lightInfos;
+        int32_t nextLightId;
 
         int32_t controllerNumber;
 
@@ -491,6 +559,8 @@
         void setLedForControllerLocked();
         status_t mapLed(int32_t led, int32_t* outScanCode) const;
         void setLedStateLocked(int32_t led, bool on);
+        bool configureBatteryLocked();
+        bool configureLightsLocked();
     };
 
     status_t openDeviceLocked(const std::string& devicePath);
diff --git a/services/inputflinger/reader/include/InputDevice.h b/services/inputflinger/reader/include/InputDevice.h
index e4186c8..34c330b 100644
--- a/services/inputflinger/reader/include/InputDevice.h
+++ b/services/inputflinger/reader/include/InputDevice.h
@@ -95,6 +95,11 @@
     std::optional<int32_t> getBatteryCapacity();
     std::optional<int32_t> getBatteryStatus();
 
+    bool setLightColor(int32_t lightId, int32_t color);
+    bool setLightPlayerId(int32_t lightId, int32_t playerId);
+    std::optional<int32_t> getLightColor(int32_t lightId);
+    std::optional<int32_t> getLightPlayerId(int32_t lightId);
+
     int32_t getMetaState();
     void updateMetaState(int32_t keyCode);
 
@@ -254,6 +259,30 @@
         return mEventHub->mapSensor(mId, absCode);
     }
 
+    inline const std::vector<int32_t> getRawLightIds() { return mEventHub->getRawLightIds(mId); }
+
+    inline std::optional<RawLightInfo> getRawLightInfo(int32_t lightId) {
+        return mEventHub->getRawLightInfo(mId, lightId);
+    }
+
+    inline std::optional<int32_t> getLightBrightness(int32_t lightId) {
+        return mEventHub->getLightBrightness(mId, lightId);
+    }
+
+    inline void setLightBrightness(int32_t lightId, int32_t brightness) {
+        return mEventHub->setLightBrightness(mId, lightId, brightness);
+    }
+
+    inline std::optional<std::unordered_map<LightColor, int32_t>> getLightIntensities(
+            int32_t lightId) {
+        return mEventHub->getLightIntensities(mId, lightId);
+    }
+
+    inline void setLightIntensities(int32_t lightId,
+                                    std::unordered_map<LightColor, int32_t> intensities) {
+        return mEventHub->setLightIntensities(mId, lightId, intensities);
+    }
+
     inline std::vector<TouchVideoFrame> getVideoFrames() { return mEventHub->getVideoFrames(mId); }
     inline int32_t getScanCodeState(int32_t scanCode) const {
         return mEventHub->getScanCodeState(mId, scanCode);
diff --git a/services/inputflinger/reader/include/InputReader.h b/services/inputflinger/reader/include/InputReader.h
index 81e3e9a..1405671 100644
--- a/services/inputflinger/reader/include/InputReader.h
+++ b/services/inputflinger/reader/include/InputReader.h
@@ -99,6 +99,18 @@
 
     std::optional<int32_t> getBatteryStatus(int32_t deviceId) override;
 
+    std::vector<int32_t> getLightIds(int32_t deviceId) override;
+
+    const InputDeviceLightInfo* getLightInfo(int32_t deviceId, int32_t lightId) override;
+
+    bool setLightColor(int32_t deviceId, int32_t lightId, int32_t color) override;
+
+    bool setLightPlayerId(int32_t deviceId, int32_t lightId, int32_t playerId) override;
+
+    std::optional<int32_t> getLightColor(int32_t deviceId, int32_t lightId) override;
+
+    std::optional<int32_t> getLightPlayerId(int32_t deviceId, int32_t lightId) override;
+
 protected:
     // These members are protected so they can be instrumented by test cases.
     virtual std::shared_ptr<InputDevice> createDeviceLocked(int32_t deviceId,
diff --git a/services/inputflinger/reader/mapper/BatteryInputMapper.cpp b/services/inputflinger/reader/mapper/BatteryInputMapper.cpp
index afdc5ab..e4fb3a6 100644
--- a/services/inputflinger/reader/mapper/BatteryInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/BatteryInputMapper.cpp
@@ -24,7 +24,7 @@
       : InputMapper(deviceContext) {}
 
 uint32_t BatteryInputMapper::getSources() {
-    return 0;
+    return AINPUT_SOURCE_UNKNOWN;
 }
 
 void BatteryInputMapper::populateDeviceInfo(InputDeviceInfo* info) {
diff --git a/services/inputflinger/reader/mapper/InputMapper.h b/services/inputflinger/reader/mapper/InputMapper.h
index 1cc5979..bd543e5 100644
--- a/services/inputflinger/reader/mapper/InputMapper.h
+++ b/services/inputflinger/reader/mapper/InputMapper.h
@@ -77,6 +77,11 @@
     virtual std::optional<int32_t> getBatteryCapacity() { return std::nullopt; }
     virtual std::optional<int32_t> getBatteryStatus() { return std::nullopt; }
 
+    virtual bool setLightColor(int32_t lightId, int32_t color) { return true; }
+    virtual bool setLightPlayerId(int32_t lightId, int32_t playerId) { return true; }
+    virtual std::optional<int32_t> getLightColor(int32_t lightId) { return std::nullopt; }
+    virtual std::optional<int32_t> getLightPlayerId(int32_t lightId) { return std::nullopt; }
+
     virtual int32_t getMetaState();
     virtual void updateMetaState(int32_t keyCode);
 
diff --git a/services/inputflinger/reader/mapper/LightInputMapper.cpp b/services/inputflinger/reader/mapper/LightInputMapper.cpp
new file mode 100644
index 0000000..be1f722
--- /dev/null
+++ b/services/inputflinger/reader/mapper/LightInputMapper.cpp
@@ -0,0 +1,478 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#include <locale>
+#include <regex>
+
+#include "../Macros.h"
+
+#include "LightInputMapper.h"
+#include "input/NamedEnum.h"
+
+// Log detailed debug messages about input device lights.
+static constexpr bool DEBUG_LIGHT_DETAILS = false;
+
+namespace android {
+
+static inline int32_t getAlpha(int32_t color) {
+    return (color >> 24) & 0xff;
+}
+
+static inline int32_t getRed(int32_t color) {
+    return (color >> 16) & 0xff;
+}
+
+static inline int32_t getGreen(int32_t color) {
+    return (color >> 8) & 0xff;
+}
+
+static inline int32_t getBlue(int32_t color) {
+    return color & 0xff;
+}
+
+static inline int32_t toArgb(int32_t brightness, int32_t red, int32_t green, int32_t blue) {
+    return (brightness & 0xff) << 24 | (red & 0xff) << 16 | (green & 0xff) << 8 | (blue & 0xff);
+}
+
+/**
+ * Light input mapper owned by InputReader device, implements the native API for querying input
+ * lights, getting and setting the lights brightness and color, by interacting with EventHub
+ * devices.
+ * TODO b/180342233: Reconsider the inputflinger design to accommodate the device class
+ * like lights and battery.
+ */
+LightInputMapper::LightInputMapper(InputDeviceContext& deviceContext)
+      : InputMapper(deviceContext) {}
+
+LightInputMapper::~LightInputMapper() {}
+
+std::optional<std::int32_t> LightInputMapper::Light::getRawLightBrightness(int32_t rawLightId) {
+    std::optional<RawLightInfo> rawInfo = context.getRawLightInfo(rawLightId);
+    std::optional<int32_t> ret = context.getLightBrightness(rawLightId);
+    if (!rawInfo.has_value() || !ret.has_value()) {
+        return std::nullopt;
+    }
+    int brightness = ret.value();
+
+    // If the light node doesn't have max brightness, use the default max brightness.
+    int rawMaxBrightness = rawInfo->maxBrightness.value_or(MAX_BRIGHTNESS);
+    float ratio = MAX_BRIGHTNESS / rawMaxBrightness;
+    // Scale the returned brightness in [0, rawMaxBrightness] to [0, 255]
+    if (rawMaxBrightness != MAX_BRIGHTNESS) {
+        brightness = brightness * ratio;
+    }
+    if (DEBUG_LIGHT_DETAILS) {
+        ALOGD("getRawLightBrightness rawLightId %d brightness 0x%x ratio %.2f", rawLightId,
+              brightness, ratio);
+    }
+    return brightness;
+}
+
+void LightInputMapper::Light::setRawLightBrightness(int32_t rawLightId, int32_t brightness) {
+    std::optional<RawLightInfo> rawInfo = context.getRawLightInfo(rawLightId);
+    if (!rawInfo.has_value()) {
+        return;
+    }
+    // If the light node doesn't have max brightness, use the default max brightness.
+    int rawMaxBrightness = rawInfo->maxBrightness.value_or(MAX_BRIGHTNESS);
+    float ratio = MAX_BRIGHTNESS / rawMaxBrightness;
+    // Scale the requested brightness in [0, 255] to [0, rawMaxBrightness]
+    if (rawMaxBrightness != MAX_BRIGHTNESS) {
+        brightness = ceil(brightness / ratio);
+    }
+    if (DEBUG_LIGHT_DETAILS) {
+        ALOGD("setRawLightBrightness rawLightId %d brightness 0x%x ratio %.2f", rawLightId,
+              brightness, ratio);
+    }
+    context.setLightBrightness(rawLightId, brightness);
+}
+
+bool LightInputMapper::SingleLight::setLightColor(int32_t color) {
+    int32_t brightness = getAlpha(color);
+    setRawLightBrightness(rawId, brightness);
+
+    return true;
+}
+
+bool LightInputMapper::RgbLight::setLightColor(int32_t color) {
+    // Compose color value as per:
+    // https://developer.android.com/reference/android/graphics/Color?hl=en
+    // int color = (A & 0xff) << 24 | (R & 0xff) << 16 | (G & 0xff) << 8 | (B & 0xff);
+    // The alpha component is used to scale the R,G,B leds brightness, with the ratio to
+    // MAX_BRIGHTNESS.
+    brightness = getAlpha(color);
+    int32_t red = 0;
+    int32_t green = 0;
+    int32_t blue = 0;
+    if (brightness > 0) {
+        float ratio = MAX_BRIGHTNESS / brightness;
+        red = ceil(getRed(color) / ratio);
+        green = ceil(getGreen(color) / ratio);
+        blue = ceil(getBlue(color) / ratio);
+    }
+    setRawLightBrightness(rawRgbIds.at(LightColor::RED), red);
+    setRawLightBrightness(rawRgbIds.at(LightColor::GREEN), green);
+    setRawLightBrightness(rawRgbIds.at(LightColor::BLUE), blue);
+    if (rawGlobalId.has_value()) {
+        setRawLightBrightness(rawGlobalId.value(), brightness);
+    }
+
+    return true;
+}
+
+bool LightInputMapper::MultiColorLight::setLightColor(int32_t color) {
+    std::unordered_map<LightColor, int32_t> intensities;
+    intensities.emplace(LightColor::RED, getRed(color));
+    intensities.emplace(LightColor::GREEN, getGreen(color));
+    intensities.emplace(LightColor::BLUE, getBlue(color));
+
+    context.setLightIntensities(rawId, intensities);
+    setRawLightBrightness(rawId, getAlpha(color));
+    return true;
+}
+
+std::optional<int32_t> LightInputMapper::SingleLight::getLightColor() {
+    std::optional<int32_t> brightness = getRawLightBrightness(rawId);
+    if (!brightness.has_value()) {
+        return std::nullopt;
+    }
+
+    return toArgb(brightness.value(), 0 /* red */, 0 /* green */, 0 /* blue */);
+}
+
+std::optional<int32_t> LightInputMapper::RgbLight::getLightColor() {
+    // If the Alpha component is zero, then return color 0.
+    if (brightness == 0) {
+        return 0;
+    }
+    // Compose color value as per:
+    // https://developer.android.com/reference/android/graphics/Color?hl=en
+    // int color = (A & 0xff) << 24 | (R & 0xff) << 16 | (G & 0xff) << 8 | (B & 0xff);
+    std::optional<int32_t> redOr = getRawLightBrightness(rawRgbIds.at(LightColor::RED));
+    std::optional<int32_t> greenOr = getRawLightBrightness(rawRgbIds.at(LightColor::GREEN));
+    std::optional<int32_t> blueOr = getRawLightBrightness(rawRgbIds.at(LightColor::BLUE));
+    // If we can't get brightness for any of the RGB light
+    if (!redOr.has_value() || !greenOr.has_value() || !blueOr.has_value()) {
+        return std::nullopt;
+    }
+
+    // Compose the ARGB format color. As the R,G,B color led brightness is scaled by Alpha
+    // value, scale it back to return the nominal color value.
+    float ratio = MAX_BRIGHTNESS / brightness;
+    int32_t red = round(redOr.value() * ratio);
+    int32_t green = round(greenOr.value() * ratio);
+    int32_t blue = round(blueOr.value() * ratio);
+
+    if (red > MAX_BRIGHTNESS || green > MAX_BRIGHTNESS || blue > MAX_BRIGHTNESS) {
+        // Previously stored brightness isn't valid for current LED values, so just reset to max
+        // brightness since an app couldn't have provided these values in the first place.
+        red = redOr.value();
+        green = greenOr.value();
+        blue = blueOr.value();
+        brightness = MAX_BRIGHTNESS;
+    }
+
+    return toArgb(brightness, red, green, blue);
+}
+
+std::optional<int32_t> LightInputMapper::MultiColorLight::getLightColor() {
+    auto ret = context.getLightIntensities(rawId);
+    if (!ret.has_value()) {
+        return std::nullopt;
+    }
+    std::unordered_map<LightColor, int32_t> intensities = ret.value();
+    // Get red, green, blue colors
+    int32_t color = toArgb(0 /* brightness */, intensities.at(LightColor::RED) /* red */,
+                           intensities.at(LightColor::GREEN) /* green */,
+                           intensities.at(LightColor::BLUE) /* blue */);
+    // Get brightness
+    std::optional<int32_t> brightness = getRawLightBrightness(rawId);
+    if (brightness.has_value()) {
+        return toArgb(brightness.value() /* A */, 0, 0, 0) | color;
+    }
+    return std::nullopt;
+}
+
+bool LightInputMapper::PlayerIdLight::setLightPlayerId(int32_t playerId) {
+    if (rawLightIds.find(playerId) == rawLightIds.end()) {
+        return false;
+    }
+    for (const auto& [id, rawId] : rawLightIds) {
+        if (playerId == id) {
+            setRawLightBrightness(rawId, MAX_BRIGHTNESS);
+        } else {
+            setRawLightBrightness(rawId, 0);
+        }
+    }
+    return true;
+}
+
+std::optional<int32_t> LightInputMapper::PlayerIdLight::getLightPlayerId() {
+    for (const auto& [id, rawId] : rawLightIds) {
+        std::optional<int32_t> brightness = getRawLightBrightness(rawId);
+        if (brightness.has_value() && brightness.value() > 0) {
+            return id;
+        }
+    }
+    return std::nullopt;
+}
+
+void LightInputMapper::SingleLight::dump(std::string& dump) {
+    dump += StringPrintf(INDENT4 "Color: 0x%x\n", getLightColor().value_or(0));
+}
+
+void LightInputMapper::PlayerIdLight::dump(std::string& dump) {
+    dump += StringPrintf(INDENT4 "PlayerId: %d\n", getLightPlayerId().value_or(-1));
+    dump += StringPrintf(INDENT4 "Raw Player ID LEDs:");
+    for (const auto& [id, rawId] : rawLightIds) {
+        dump += StringPrintf("id %d -> %d ", id, rawId);
+    }
+    dump += "\n";
+}
+
+void LightInputMapper::RgbLight::dump(std::string& dump) {
+    dump += StringPrintf(INDENT4 "Color: 0x%x\n", getLightColor().value_or(0));
+    dump += StringPrintf(INDENT4 "Raw RGB LEDs: [%d, %d, %d] ", rawRgbIds.at(LightColor::RED),
+                         rawRgbIds.at(LightColor::GREEN), rawRgbIds.at(LightColor::BLUE));
+    if (rawGlobalId.has_value()) {
+        dump += StringPrintf(INDENT4 "Raw Global LED: [%d] ", rawGlobalId.value());
+    }
+    dump += "\n";
+}
+
+void LightInputMapper::MultiColorLight::dump(std::string& dump) {
+    dump += StringPrintf(INDENT4 "Color: 0x%x\n", getLightColor().value_or(0));
+}
+
+uint32_t LightInputMapper::getSources() {
+    return AINPUT_SOURCE_UNKNOWN;
+}
+
+void LightInputMapper::populateDeviceInfo(InputDeviceInfo* info) {
+    InputMapper::populateDeviceInfo(info);
+
+    for (const auto& [lightId, light] : mLights) {
+        // Input device light doesn't support ordinal, always pass 1.
+        InputDeviceLightInfo lightInfo(light->name, light->id, light->type, 1 /* ordinal */);
+        info->addLightInfo(lightInfo);
+    }
+}
+
+void LightInputMapper::dump(std::string& dump) {
+    dump += INDENT2 "Light Input Mapper:\n";
+    dump += INDENT3 "Lights:\n";
+    for (const auto& [lightId, light] : mLights) {
+        dump += StringPrintf(INDENT4 "Id: %d", lightId);
+        dump += StringPrintf(INDENT4 "Name: %s", light->name.c_str());
+        dump += StringPrintf(INDENT4 "Type: %s", NamedEnum::string(light->type).c_str());
+        light->dump(dump);
+    }
+    // Dump raw lights
+    dump += INDENT3 "RawLights:\n";
+    dump += INDENT4 "Id:\t Name:\t Flags:\t Max brightness:\t Brightness\n";
+    const std::vector<int32_t> rawLightIds = getDeviceContext().getRawLightIds();
+    // Map from raw light id to raw light info
+    std::unordered_map<int32_t, RawLightInfo> rawInfos;
+    for (const auto& rawId : rawLightIds) {
+        std::optional<RawLightInfo> rawInfo = getDeviceContext().getRawLightInfo(rawId);
+        if (!rawInfo.has_value()) {
+            continue;
+        }
+        dump += StringPrintf(INDENT4 "%d", rawId);
+        dump += StringPrintf(INDENT4 "%s", rawInfo->name.c_str());
+        dump += StringPrintf(INDENT4 "%s", rawInfo->flags.string().c_str());
+        dump += StringPrintf(INDENT4 "%d", rawInfo->maxBrightness.value_or(MAX_BRIGHTNESS));
+        dump += StringPrintf(INDENT4 "%d\n",
+                             getDeviceContext().getLightBrightness(rawId).value_or(-1));
+    }
+}
+
+void LightInputMapper::configure(nsecs_t when, const InputReaderConfiguration* config,
+                                 uint32_t changes) {
+    InputMapper::configure(when, config, changes);
+
+    if (!changes) { // first time only
+        bool hasRedLed = false;
+        bool hasGreenLed = false;
+        bool hasBlueLed = false;
+        std::optional<int32_t> rawGlobalId = std::nullopt;
+        // Player ID light common name string
+        std::string playerIdName;
+        // Raw RGB color to raw light ID
+        std::unordered_map<LightColor, int32_t /* rawLightId */> rawRgbIds;
+        // Map from player Id to raw light Id
+        std::unordered_map<int32_t, int32_t> playerIdLightIds;
+        mLights.clear();
+
+        // Check raw lights
+        const std::vector<int32_t> rawLightIds = getDeviceContext().getRawLightIds();
+        // Map from raw light id to raw light info
+        std::unordered_map<int32_t, RawLightInfo> rawInfos;
+        for (const auto& rawId : rawLightIds) {
+            std::optional<RawLightInfo> rawInfo = getDeviceContext().getRawLightInfo(rawId);
+            if (!rawInfo.has_value()) {
+                continue;
+            }
+            rawInfos.insert_or_assign(rawId, rawInfo.value());
+            // Check if this is a group LEDs for player ID
+            std::regex lightPattern("([a-z]+)([0-9]+)");
+            std::smatch results;
+            if (std::regex_match(rawInfo->name, results, lightPattern)) {
+                std::string commonName = results[1].str();
+                int32_t playerId = std::stoi(results[2]);
+                if (playerIdLightIds.empty()) {
+                    playerIdName = commonName;
+                    playerIdLightIds.insert_or_assign(playerId, rawId);
+                } else {
+                    // Make sure the player ID leds have common string name
+                    if (playerIdName.compare(commonName) == 0 &&
+                        playerIdLightIds.find(playerId) == playerIdLightIds.end()) {
+                        playerIdLightIds.insert_or_assign(playerId, rawId);
+                    }
+                }
+            }
+            // Check if this is an LED of RGB light
+            if (rawInfo->flags.test(InputLightClass::RED)) {
+                hasRedLed = true;
+                rawRgbIds.emplace(LightColor::RED, rawId);
+            }
+            if (rawInfo->flags.test(InputLightClass::GREEN)) {
+                hasGreenLed = true;
+                rawRgbIds.emplace(LightColor::GREEN, rawId);
+            }
+            if (rawInfo->flags.test(InputLightClass::BLUE)) {
+                hasBlueLed = true;
+                rawRgbIds.emplace(LightColor::BLUE, rawId);
+            }
+            if (rawInfo->flags.test(InputLightClass::GLOBAL)) {
+                rawGlobalId = rawId;
+            }
+            if (DEBUG_LIGHT_DETAILS) {
+                ALOGD("Light rawId %d name %s max %d flags %s \n", rawInfo->id,
+                      rawInfo->name.c_str(), rawInfo->maxBrightness.value_or(MAX_BRIGHTNESS),
+                      rawInfo->flags.string().c_str());
+            }
+        }
+
+        // Construct a player ID light
+        if (playerIdLightIds.size() > 1) {
+            std::unique_ptr<Light> light =
+                    std::make_unique<PlayerIdLight>(getDeviceContext(), playerIdName, ++mNextId,
+                                                    playerIdLightIds);
+            mLights.insert_or_assign(light->id, std::move(light));
+            // Remove these raw lights from raw light info as they've been used to compose a
+            // Player ID light, so we do not expose these raw lights as single lights.
+            for (const auto& [playerId, rawId] : playerIdLightIds) {
+                rawInfos.erase(rawId);
+            }
+        }
+        // Construct a RGB light for composed RGB light
+        if (hasRedLed && hasGreenLed && hasBlueLed) {
+            if (DEBUG_LIGHT_DETAILS) {
+                ALOGD("Rgb light ids [%d, %d, %d] \n", rawRgbIds.at(LightColor::RED),
+                      rawRgbIds.at(LightColor::GREEN), rawRgbIds.at(LightColor::BLUE));
+            }
+            std::unique_ptr<Light> light = std::make_unique<RgbLight>(getDeviceContext(), ++mNextId,
+                                                                      rawRgbIds, rawGlobalId);
+            mLights.insert_or_assign(light->id, std::move(light));
+            // Remove from raw light info as they've been composed a RBG light.
+            rawInfos.erase(rawRgbIds.at(LightColor::RED));
+            rawInfos.erase(rawRgbIds.at(LightColor::GREEN));
+            rawInfos.erase(rawRgbIds.at(LightColor::BLUE));
+            if (rawGlobalId.has_value()) {
+                rawInfos.erase(rawGlobalId.value());
+            }
+        }
+
+        // Check the rest of raw light infos
+        for (const auto& [rawId, rawInfo] : rawInfos) {
+            // If the node is multi-color led, construct a MULTI_COLOR light
+            if (rawInfo.flags.test(InputLightClass::MULTI_INDEX) &&
+                rawInfo.flags.test(InputLightClass::MULTI_INTENSITY)) {
+                if (DEBUG_LIGHT_DETAILS) {
+                    ALOGD("Multicolor light Id %d name %s \n", rawInfo.id, rawInfo.name.c_str());
+                }
+                std::unique_ptr<Light> light =
+                        std::make_unique<MultiColorLight>(getDeviceContext(), rawInfo.name,
+                                                          ++mNextId, rawInfo.id);
+                mLights.insert_or_assign(light->id, std::move(light));
+                continue;
+            }
+            // Construct a single LED light
+            if (DEBUG_LIGHT_DETAILS) {
+                ALOGD("Single light Id %d name %s \n", rawInfo.id, rawInfo.name.c_str());
+            }
+            std::unique_ptr<Light> light =
+                    std::make_unique<SingleLight>(getDeviceContext(), rawInfo.name, ++mNextId,
+                                                  rawInfo.id);
+
+            mLights.insert_or_assign(light->id, std::move(light));
+        }
+    }
+}
+
+void LightInputMapper::reset(nsecs_t when) {
+    InputMapper::reset(when);
+}
+
+void LightInputMapper::process(const RawEvent* rawEvent) {}
+
+bool LightInputMapper::setLightColor(int32_t lightId, int32_t color) {
+    auto it = mLights.find(lightId);
+    if (it == mLights.end()) {
+        return false;
+    }
+    auto& light = it->second;
+    if (DEBUG_LIGHT_DETAILS) {
+        ALOGD("setLightColor lightId %d type %s color 0x%x", lightId,
+              NamedEnum::string(light->type).c_str(), color);
+    }
+    return light->setLightColor(color);
+}
+
+std::optional<int32_t> LightInputMapper::getLightColor(int32_t lightId) {
+    auto it = mLights.find(lightId);
+    if (it == mLights.end()) {
+        return std::nullopt;
+    }
+    auto& light = it->second;
+    std::optional<int32_t> color = light->getLightColor();
+    if (DEBUG_LIGHT_DETAILS) {
+        ALOGD("getLightColor lightId %d type %s color 0x%x", lightId,
+              NamedEnum::string(light->type).c_str(), color.value_or(0));
+    }
+    return color;
+}
+
+bool LightInputMapper::setLightPlayerId(int32_t lightId, int32_t playerId) {
+    auto it = mLights.find(lightId);
+    if (it == mLights.end()) {
+        return false;
+    }
+    auto& light = it->second;
+    return light->setLightPlayerId(playerId);
+}
+
+std::optional<int32_t> LightInputMapper::getLightPlayerId(int32_t lightId) {
+    auto it = mLights.find(lightId);
+    if (it == mLights.end()) {
+        return std::nullopt;
+    }
+    auto& light = it->second;
+    return light->getLightPlayerId();
+}
+
+} // namespace android
diff --git a/services/inputflinger/reader/mapper/LightInputMapper.h b/services/inputflinger/reader/mapper/LightInputMapper.h
new file mode 100644
index 0000000..9254720
--- /dev/null
+++ b/services/inputflinger/reader/mapper/LightInputMapper.h
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#ifndef _UI_INPUTREADER_LIGHT_INPUT_MAPPER_H
+#define _UI_INPUTREADER_LIGHT_INPUT_MAPPER_H
+
+#include "InputMapper.h"
+
+namespace android {
+
+class LightInputMapper : public InputMapper {
+    // Refer to https://developer.android.com/reference/kotlin/android/graphics/Color
+    /* Number of colors : {red, green, blue} */
+    static constexpr size_t COLOR_NUM = 3;
+    static constexpr int32_t MAX_BRIGHTNESS = 0xff;
+
+public:
+    explicit LightInputMapper(InputDeviceContext& deviceContext);
+    ~LightInputMapper() override;
+
+    uint32_t getSources() override;
+    void populateDeviceInfo(InputDeviceInfo* deviceInfo) override;
+    void dump(std::string& dump) override;
+    void configure(nsecs_t when, const InputReaderConfiguration* config, uint32_t changes) override;
+    void reset(nsecs_t when) override;
+    void process(const RawEvent* rawEvent) override;
+    bool setLightColor(int32_t lightId, int32_t color) override;
+    bool setLightPlayerId(int32_t lightId, int32_t playerId) override;
+    std::optional<int32_t> getLightColor(int32_t lightId) override;
+    std::optional<int32_t> getLightPlayerId(int32_t lightId) override;
+
+private:
+    struct Light {
+        explicit Light(InputDeviceContext& context, std::string name, int32_t id,
+                       InputDeviceLightType type)
+              : context(context), name(name), id(id), type(type) {}
+        virtual ~Light() {}
+        InputDeviceContext& context;
+        std::string name;
+        int32_t id;
+        InputDeviceLightType type;
+
+        virtual bool setLightColor(int32_t color) { return false; }
+        virtual std::optional<int32_t> getLightColor() { return std::nullopt; }
+        virtual bool setLightPlayerId(int32_t playerId) { return false; }
+        virtual std::optional<int32_t> getLightPlayerId() { return std::nullopt; }
+
+        virtual void dump(std::string& dump) {}
+
+        std::optional<std::int32_t> getRawLightBrightness(int32_t rawLightId);
+        void setRawLightBrightness(int32_t rawLightId, int32_t brightness);
+    };
+
+    struct SingleLight : public Light {
+        explicit SingleLight(InputDeviceContext& context, std::string name, int32_t id,
+                             int32_t rawId)
+              : Light(context, name, id, InputDeviceLightType::SINGLE), rawId(rawId) {}
+        int32_t rawId;
+
+        bool setLightColor(int32_t color) override;
+        std::optional<int32_t> getLightColor() override;
+        void dump(std::string& dump) override;
+    };
+
+    struct RgbLight : public Light {
+        explicit RgbLight(InputDeviceContext& context, int32_t id,
+                          std::unordered_map<LightColor, int32_t> rawRgbIds,
+                          std::optional<int32_t> rawGlobalId)
+              : Light(context, "RGB", id, InputDeviceLightType::RGB),
+                rawRgbIds(rawRgbIds),
+                rawGlobalId(rawGlobalId) {
+            brightness = rawGlobalId.has_value()
+                    ? getRawLightBrightness(rawGlobalId.value()).value_or(MAX_BRIGHTNESS)
+                    : MAX_BRIGHTNESS;
+        }
+        // Map from color to raw light id.
+        std::unordered_map<LightColor, int32_t /* rawLightId */> rawRgbIds;
+        // Optional global control raw light id.
+        std::optional<int32_t> rawGlobalId;
+        int32_t brightness;
+
+        bool setLightColor(int32_t color) override;
+        std::optional<int32_t> getLightColor() override;
+        void dump(std::string& dump) override;
+    };
+
+    struct MultiColorLight : public Light {
+        explicit MultiColorLight(InputDeviceContext& context, std::string name, int32_t id,
+                                 int32_t rawId)
+              : Light(context, name, id, InputDeviceLightType::MULTI_COLOR), rawId(rawId) {}
+        int32_t rawId;
+
+        bool setLightColor(int32_t color) override;
+        std::optional<int32_t> getLightColor() override;
+        void dump(std::string& dump) override;
+    };
+
+    struct PlayerIdLight : public Light {
+        explicit PlayerIdLight(InputDeviceContext& context, std::string name, int32_t id,
+                               std::unordered_map<int32_t, int32_t> rawLightIds)
+              : Light(context, name, id, InputDeviceLightType::PLAYER_ID),
+                rawLightIds(rawLightIds) {}
+        // Map from player Id to raw light Id
+        std::unordered_map<int32_t, int32_t> rawLightIds;
+
+        bool setLightPlayerId(int32_t palyerId) override;
+        std::optional<int32_t> getLightPlayerId() override;
+        void dump(std::string& dump) override;
+    };
+
+    int32_t mNextId = 0;
+
+    // Light color map from light color to the color index.
+    static const std::unordered_map<std::string, size_t> LIGHT_COLORS;
+
+    // Light map from light ID to Light
+    std::unordered_map<int32_t, std::unique_ptr<Light>> mLights;
+};
+
+} // namespace android
+
+#endif // _UI_INPUTREADER_LIGHT_INPUT_MAPPER_H
\ No newline at end of file
diff --git a/services/inputflinger/tests/InputReader_test.cpp b/services/inputflinger/tests/InputReader_test.cpp
index 409c62a..2836516 100644
--- a/services/inputflinger/tests/InputReader_test.cpp
+++ b/services/inputflinger/tests/InputReader_test.cpp
@@ -22,6 +22,7 @@
 #include <InputReaderBase.h>
 #include <InputReaderFactory.h>
 #include <KeyboardInputMapper.h>
+#include <LightInputMapper.h>
 #include <MultiTouchInputMapper.h>
 #include <SensorInputMapper.h>
 #include <SingleTouchInputMapper.h>
@@ -36,6 +37,7 @@
 #include <math.h>
 
 #include <memory>
+#include <regex>
 #include "input/DisplayViewport.h"
 #include "input/Input.h"
 
@@ -70,6 +72,9 @@
 static constexpr int32_t THIRD_TRACKING_ID = 2;
 static constexpr int32_t BATTERY_STATUS = 4;
 static constexpr int32_t BATTERY_CAPACITY = 66;
+static constexpr int32_t LIGHT_BRIGHTNESS = 0x55000000;
+static constexpr int32_t LIGHT_COLOR = 0x7F448866;
+static constexpr int32_t LIGHT_PLAYER_ID = 2;
 
 // Error tolerance for floating point assertions.
 static const float EPSILON = 0.001f;
@@ -83,6 +88,10 @@
     return (x + y) / 2;
 }
 
+// Mapping for light color name and the light color
+const std::unordered_map<std::string, LightColor> LIGHT_COLORS = {{"red", LightColor::RED},
+                                                                  {"green", LightColor::GREEN},
+                                                                  {"blue", LightColor::BLUE}};
 
 // --- FakePointerController ---
 
@@ -412,6 +421,12 @@
     std::vector<RawEvent> mEvents GUARDED_BY(mLock);
     std::unordered_map<int32_t /*deviceId*/, std::vector<TouchVideoFrame>> mVideoFrames;
     std::vector<int32_t> mVibrators = {0, 1};
+    std::unordered_map<int32_t, RawLightInfo> mRawLightInfos;
+    // Simulates a device light brightness, from light id to light brightness.
+    std::unordered_map<int32_t /* lightId */, int32_t /* brightness*/> mLightBrightness;
+    // Simulates a device light intensities, from light id to light intensities map.
+    std::unordered_map<int32_t /* lightId */, std::unordered_map<LightColor, int32_t>>
+            mLightIntensities;
 
 public:
     virtual ~FakeEventHub() {
@@ -562,6 +577,19 @@
         device->mscBitmask.loadFromBuffer(buffer);
     }
 
+    void addRawLightInfo(int32_t rawId, RawLightInfo&& info) {
+        mRawLightInfos.emplace(rawId, std::move(info));
+    }
+
+    void fakeLightBrightness(int32_t rawId, int32_t brightness) {
+        mLightBrightness.emplace(rawId, brightness);
+    }
+
+    void fakeLightIntensities(int32_t rawId,
+                              const std::unordered_map<LightColor, int32_t> intensities) {
+        mLightIntensities.emplace(rawId, std::move(intensities));
+    }
+
     bool getLedState(int32_t deviceId, int32_t led) {
         Device* device = getDevice(deviceId);
         return device->leds.valueFor(led);
@@ -869,6 +897,48 @@
 
     std::optional<int32_t> getBatteryStatus(int32_t) const override { return BATTERY_STATUS; }
 
+    const std::vector<int32_t> getRawLightIds(int32_t deviceId) override {
+        std::vector<int32_t> ids;
+        for (const auto& [rawId, info] : mRawLightInfos) {
+            ids.push_back(rawId);
+        }
+        return ids;
+    }
+
+    std::optional<RawLightInfo> getRawLightInfo(int32_t deviceId, int32_t lightId) override {
+        auto it = mRawLightInfos.find(lightId);
+        if (it == mRawLightInfos.end()) {
+            return std::nullopt;
+        }
+        return it->second;
+    }
+
+    void setLightBrightness(int32_t deviceId, int32_t lightId, int32_t brightness) override {
+        mLightBrightness.emplace(lightId, brightness);
+    }
+
+    void setLightIntensities(int32_t deviceId, int32_t lightId,
+                             std::unordered_map<LightColor, int32_t> intensities) override {
+        mLightIntensities.emplace(lightId, intensities);
+    };
+
+    std::optional<int32_t> getLightBrightness(int32_t deviceId, int32_t lightId) override {
+        auto lightIt = mLightBrightness.find(lightId);
+        if (lightIt == mLightBrightness.end()) {
+            return std::nullopt;
+        }
+        return lightIt->second;
+    }
+
+    std::optional<std::unordered_map<LightColor, int32_t>> getLightIntensities(
+            int32_t deviceId, int32_t lightId) override {
+        auto lightIt = mLightIntensities.find(lightId);
+        if (lightIt == mLightIntensities.end()) {
+            return std::nullopt;
+        }
+        return lightIt->second;
+    };
+
     virtual bool isExternal(int32_t) const {
         return false;
     }
@@ -1976,6 +2046,49 @@
     ASSERT_EQ(mReader->getBatteryStatus(deviceId), BATTERY_STATUS);
 }
 
+class FakeLightInputMapper : public FakeInputMapper {
+public:
+    FakeLightInputMapper(InputDeviceContext& deviceContext, uint32_t sources)
+          : FakeInputMapper(deviceContext, sources) {}
+
+    bool setLightColor(int32_t lightId, int32_t color) override {
+        getDeviceContext().setLightBrightness(lightId, color >> 24);
+        return true;
+    }
+
+    std::optional<int32_t> getLightColor(int32_t lightId) override {
+        std::optional<int32_t> result = getDeviceContext().getLightBrightness(lightId);
+        if (!result.has_value()) {
+            return std::nullopt;
+        }
+        return result.value() << 24;
+    }
+};
+
+TEST_F(InputReaderTest, LightGetColor) {
+    constexpr int32_t deviceId = END_RESERVED_ID + 1000;
+    Flags<InputDeviceClass> deviceClass = InputDeviceClass::KEYBOARD | InputDeviceClass::LIGHT;
+    constexpr int32_t eventHubId = 1;
+    const char* DEVICE_LOCATION = "BLUETOOTH";
+    std::shared_ptr<InputDevice> device = mReader->newDevice(deviceId, "fake", DEVICE_LOCATION);
+    FakeLightInputMapper& mapper =
+            device->addMapper<FakeLightInputMapper>(eventHubId, AINPUT_SOURCE_KEYBOARD);
+    mReader->pushNextDevice(device);
+    RawLightInfo info = {.id = 1,
+                         .name = "Mono",
+                         .maxBrightness = 255,
+                         .flags = InputLightClass::BRIGHTNESS,
+                         .path = ""};
+    mFakeEventHub->addRawLightInfo(1 /* rawId */, std::move(info));
+    mFakeEventHub->fakeLightBrightness(1 /* rawId */, 0x55);
+
+    ASSERT_NO_FATAL_FAILURE(addDevice(eventHubId, "fake", deviceClass, nullptr));
+    ASSERT_NO_FATAL_FAILURE(mapper.assertConfigureWasCalled());
+
+    ASSERT_TRUE(mReader->setLightColor(deviceId, 1 /* lightId */, LIGHT_BRIGHTNESS));
+    ASSERT_EQ(mReader->getLightColor(deviceId, 1 /* lightId */), LIGHT_BRIGHTNESS);
+}
+
 // --- InputReaderIntegrationTest ---
 
 // These tests create and interact with the InputReader only through its interface.
@@ -2883,14 +2996,136 @@
     BatteryInputMapper& mapper = addMapperAndConfigure<BatteryInputMapper>();
 
     ASSERT_TRUE(mapper.getBatteryCapacity());
-    ASSERT_EQ(*mapper.getBatteryCapacity(), BATTERY_CAPACITY);
+    ASSERT_EQ(mapper.getBatteryCapacity().value_or(-1), BATTERY_CAPACITY);
 }
 
 TEST_F(BatteryInputMapperTest, GetBatteryStatus) {
     BatteryInputMapper& mapper = addMapperAndConfigure<BatteryInputMapper>();
 
     ASSERT_TRUE(mapper.getBatteryStatus());
-    ASSERT_EQ(*mapper.getBatteryStatus(), BATTERY_STATUS);
+    ASSERT_EQ(mapper.getBatteryStatus().value_or(-1), BATTERY_STATUS);
+}
+
+// --- LightInputMapperTest ---
+class LightInputMapperTest : public InputMapperTest {
+protected:
+    void SetUp() override { InputMapperTest::SetUp(DEVICE_CLASSES | InputDeviceClass::LIGHT); }
+};
+
+TEST_F(LightInputMapperTest, GetSources) {
+    LightInputMapper& mapper = addMapperAndConfigure<LightInputMapper>();
+
+    ASSERT_EQ(AINPUT_SOURCE_UNKNOWN, mapper.getSources());
+}
+
+TEST_F(LightInputMapperTest, SingleLight) {
+    RawLightInfo infoSingle = {.id = 1,
+                               .name = "Mono",
+                               .maxBrightness = 255,
+                               .flags = InputLightClass::BRIGHTNESS,
+                               .path = ""};
+    mFakeEventHub->addRawLightInfo(infoSingle.id, std::move(infoSingle));
+
+    LightInputMapper& mapper = addMapperAndConfigure<LightInputMapper>();
+    InputDeviceInfo info;
+    mapper.populateDeviceInfo(&info);
+    const auto& ids = info.getLightIds();
+    ASSERT_EQ(1UL, ids.size());
+    ASSERT_EQ(InputDeviceLightType::SINGLE, info.getLightInfo(ids[0])->type);
+
+    ASSERT_TRUE(mapper.setLightColor(ids[0], LIGHT_BRIGHTNESS));
+    ASSERT_EQ(mapper.getLightColor(ids[0]).value_or(-1), LIGHT_BRIGHTNESS);
+}
+
+TEST_F(LightInputMapperTest, RGBLight) {
+    RawLightInfo infoRed = {.id = 1,
+                            .name = "red",
+                            .maxBrightness = 255,
+                            .flags = InputLightClass::BRIGHTNESS | InputLightClass::RED,
+                            .path = ""};
+    RawLightInfo infoGreen = {.id = 2,
+                              .name = "green",
+                              .maxBrightness = 255,
+                              .flags = InputLightClass::BRIGHTNESS | InputLightClass::GREEN,
+                              .path = ""};
+    RawLightInfo infoBlue = {.id = 3,
+                             .name = "blue",
+                             .maxBrightness = 255,
+                             .flags = InputLightClass::BRIGHTNESS | InputLightClass::BLUE,
+                             .path = ""};
+    mFakeEventHub->addRawLightInfo(infoRed.id, std::move(infoRed));
+    mFakeEventHub->addRawLightInfo(infoGreen.id, std::move(infoGreen));
+    mFakeEventHub->addRawLightInfo(infoBlue.id, std::move(infoBlue));
+
+    LightInputMapper& mapper = addMapperAndConfigure<LightInputMapper>();
+    InputDeviceInfo info;
+    mapper.populateDeviceInfo(&info);
+    const auto& ids = info.getLightIds();
+    ASSERT_EQ(1UL, ids.size());
+    ASSERT_EQ(InputDeviceLightType::RGB, info.getLightInfo(ids[0])->type);
+
+    ASSERT_TRUE(mapper.setLightColor(ids[0], LIGHT_COLOR));
+    ASSERT_EQ(mapper.getLightColor(ids[0]).value_or(-1), LIGHT_COLOR);
+}
+
+TEST_F(LightInputMapperTest, MultiColorRGBLight) {
+    RawLightInfo infoColor = {.id = 1,
+                              .name = "red",
+                              .maxBrightness = 255,
+                              .flags = InputLightClass::BRIGHTNESS |
+                                      InputLightClass::MULTI_INTENSITY |
+                                      InputLightClass::MULTI_INDEX,
+                              .path = ""};
+
+    mFakeEventHub->addRawLightInfo(infoColor.id, std::move(infoColor));
+
+    LightInputMapper& mapper = addMapperAndConfigure<LightInputMapper>();
+    InputDeviceInfo info;
+    mapper.populateDeviceInfo(&info);
+    const auto& ids = info.getLightIds();
+    ASSERT_EQ(1UL, ids.size());
+    ASSERT_EQ(InputDeviceLightType::MULTI_COLOR, info.getLightInfo(ids[0])->type);
+
+    ASSERT_TRUE(mapper.setLightColor(ids[0], LIGHT_COLOR));
+    ASSERT_EQ(mapper.getLightColor(ids[0]).value_or(-1), LIGHT_COLOR);
+}
+
+TEST_F(LightInputMapperTest, PlayerIdLight) {
+    RawLightInfo info1 = {.id = 1,
+                          .name = "player1",
+                          .maxBrightness = 255,
+                          .flags = InputLightClass::BRIGHTNESS,
+                          .path = ""};
+    RawLightInfo info2 = {.id = 2,
+                          .name = "player2",
+                          .maxBrightness = 255,
+                          .flags = InputLightClass::BRIGHTNESS,
+                          .path = ""};
+    RawLightInfo info3 = {.id = 3,
+                          .name = "player3",
+                          .maxBrightness = 255,
+                          .flags = InputLightClass::BRIGHTNESS,
+                          .path = ""};
+    RawLightInfo info4 = {.id = 4,
+                          .name = "player4",
+                          .maxBrightness = 255,
+                          .flags = InputLightClass::BRIGHTNESS,
+                          .path = ""};
+    mFakeEventHub->addRawLightInfo(info1.id, std::move(info1));
+    mFakeEventHub->addRawLightInfo(info2.id, std::move(info2));
+    mFakeEventHub->addRawLightInfo(info3.id, std::move(info3));
+    mFakeEventHub->addRawLightInfo(info4.id, std::move(info4));
+
+    LightInputMapper& mapper = addMapperAndConfigure<LightInputMapper>();
+    InputDeviceInfo info;
+    mapper.populateDeviceInfo(&info);
+    const auto& ids = info.getLightIds();
+    ASSERT_EQ(1UL, ids.size());
+    ASSERT_EQ(InputDeviceLightType::PLAYER_ID, info.getLightInfo(ids[0])->type);
+
+    ASSERT_FALSE(mapper.setLightColor(ids[0], LIGHT_COLOR));
+    ASSERT_TRUE(mapper.setLightPlayerId(ids[0], LIGHT_PLAYER_ID));
+    ASSERT_EQ(mapper.getLightPlayerId(ids[0]).value_or(-1), LIGHT_PLAYER_ID);
 }
 
 // --- KeyboardInputMapperTest ---