1
0
Fork 0

Enable HDMI CEC and add HDMI hotplug monitoring

Install the HDMI CEC and HDMI connection APEX packages plus the
android.hardware.hdmi.cec.xml feature file, set the playback-device
CEC properties, and give system ownership of /dev/cec0 in ueventd.

The connection HAL now reports a single HDMI output port and monitors
/sys/class/drm/card0-HDMI-A-1/status with poll(), reporting hotplug
transitions through the AIDL callback and shutting the monitor thread
down through an eventfd.

The CEC HAL now polls POLLPRI for CEC_DQEVENT and POLLIN for
CEC_RECEIVE, guards the ioctl paths against an uninitialised adapter,
and bounds message length by CEC_MAX_MSG_SIZE.
This commit is contained in:
Brendan Szymanski 2026-08-09 22:18:52 -04:00
parent 04e94e9938
commit 8b7cdd6f38
6 changed files with 245 additions and 100 deletions

View file

@ -113,18 +113,14 @@ PRODUCT_COPY_FILES += \
# PRODUCT_COPY_FILES += \
# $(DEVICE_PATH)/camera/media_profiles_V1_0.xml:$(TARGET_COPY_OUT_VENDOR)/etc/media_profiles_V1_0.xml
# # CEC
# PRODUCT_PACKAGES += \
# com.android.hardware.tv.hdmi.cec.opi5 \
# com.android.hardware.tv.hdmi.connection.opi5
# PRODUCT_COPY_FILES += \
# frameworks/native/data/etc/android.hardware.hdmi.cec.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.hdmi.cec.xml
#cec
# CEC
PRODUCT_PACKAGES += \
com.android.hardware.tv.hdmi.cec.opi5 \
com.android.hardware.tv.hdmi.connection.opi5
PRODUCT_COPY_FILES += \
frameworks/native/data/etc/android.hardware.hdmi.cec.xml:$(TARGET_COPY_OUT_SYSTEM)/etc/permissions/android.hardware.hdmi.cec.xml
# Debugfs
PRODUCT_SET_DEBUGFS_RESTRICTIONS := false

View file

@ -64,6 +64,10 @@ ScopedAStatus HdmiCec::addLogicalAddress(CecLogicalAddress addr, Result* _aidl_r
*_aidl_return = Result::FAILURE_INVALID_ARGS;
return ScopedAStatus::ok();
}
if (mHdmiCecPorts.empty()) {
*_aidl_return = Result::FAILURE_INVALID_STATE;
return ScopedAStatus::ok();
}
cec_log_addrs cecLogAddrs;
int ret = ioctl(mHdmiCecPorts[0]->mCecFd, CEC_ADAP_G_LOG_ADDRS, &cecLogAddrs);
@ -72,7 +76,6 @@ ScopedAStatus HdmiCec::addLogicalAddress(CecLogicalAddress addr, Result* _aidl_r
*_aidl_return = Result::FAILURE_BUSY;
return ScopedAStatus::ok();
}
cecLogAddrs.cec_version = CEC_OP_CEC_VERSION_1_4;
cecLogAddrs.vendor_id = 0x000c03; // HDMI LLC vendor ID
@ -148,15 +151,18 @@ ScopedAStatus HdmiCec::addLogicalAddress(CecLogicalAddress addr, Result* _aidl_r
}
ScopedAStatus HdmiCec::clearLogicalAddress() {
if (mHdmiCecPorts.empty()) {
LOG(ERROR) << "Clear logical address failed: no CEC adapter";
return ScopedAStatus::ok();
}
cec_log_addrs cecLogAddrs;
memset(&cecLogAddrs, 0, sizeof(cecLogAddrs));
int ret = ioctl(mHdmiCecPorts[0]->mCecFd, CEC_ADAP_S_LOG_ADDRS, &cecLogAddrs);
if (ret) {
LOG(ERROR) << "Clear logical Address failed for port " << mHdmiCecPorts[0]->mPortId
<< ", Error = " << strerror(errno);
}
return ScopedAStatus::ok();
}
@ -170,8 +176,12 @@ ScopedAStatus HdmiCec::getCecVersion(int32_t* _aidl_return) {
}
ScopedAStatus HdmiCec::getPhysicalAddress(int32_t* _aidl_return) {
uint16_t addr;
if (mHdmiCecPorts.empty()) {
return ScopedAStatus::fromServiceSpecificError(
static_cast<int32_t>(Result::FAILURE_INVALID_STATE));
}
uint16_t addr;
int ret = ioctl(mHdmiCecPorts[0]->mCecFd, CEC_ADAP_G_PHYS_ADDR, &addr);
if (ret) {
LOG(ERROR) << "Get physical address failed, Error = " << strerror(errno);
@ -193,6 +203,10 @@ ScopedAStatus HdmiCec::sendMessage(const CecMessage& message, SendMessageResult*
*_aidl_return = SendMessageResult::FAIL;
return ScopedAStatus::ok();
}
if (mHdmiCecPorts.empty() || message.body.size() + 1 > CEC_MAX_MSG_SIZE) {
*_aidl_return = SendMessageResult::FAIL;
return ScopedAStatus::ok();
}
cec_msg cecMsg;
memset(&cecMsg, 0, sizeof(cec_msg));
@ -297,50 +311,51 @@ void HdmiCec::release() {
}
void HdmiCec::event_thread(HdmiCecPort* hdmiCecPort) {
struct pollfd ufds[3] = {
{hdmiCecPort->mCecFd, POLLIN, 0},
{hdmiCecPort->mCecFd, POLLERR, 0},
struct pollfd ufds[2] = {
{hdmiCecPort->mCecFd, POLLIN | POLLPRI, 0},
{hdmiCecPort->mExitFd, POLLIN, 0},
};
while (1) {
ufds[0].revents = 0;
ufds[1].revents = 0;
ufds[2].revents = 0;
int ret = poll(ufds, /* size(ufds) = */ 3, /* timeout = */ -1);
int ret = poll(ufds, 2, -1);
if (ret <= 0) {
continue;
}
if (ufds[2].revents == POLLIN) { /* Exit */
if (ufds[1].revents & POLLIN) {
break;
}
if (ufds[1].revents == POLLERR) { /* CEC Event */
if (ufds[0].revents & POLLPRI) {
cec_event ev;
ret = ioctl(hdmiCecPort->mCecFd, CEC_DQEVENT, &ev);
if (ret) {
LOG(ERROR) << "CEC_DQEVENT failed, Error = " << strerror(errno);
continue;
}
if (!mCecEnabled) {
continue;
}
}
if (ufds[0].revents == POLLIN) { /* CEC Driver */
if (!(ufds[0].revents & POLLIN)) {
continue;
}
cec_msg msg = {};
ret = ioctl(hdmiCecPort->mCecFd, CEC_RECEIVE, &msg);
if (ret) {
LOG(ERROR) << "CEC_RECEIVE failed, Error = " << strerror(errno);
continue;
}
if (msg.len == 0 || msg.len > CEC_MAX_MSG_SIZE) {
LOG(ERROR) << "Invalid CEC message length: " << msg.len;
continue;
}
if (msg.rx_status != CEC_RX_STATUS_OK) {
LOG(ERROR) << "msg rx_status = " << msg.rx_status;
continue;
@ -361,7 +376,8 @@ void HdmiCec::event_thread(HdmiCecPort* hdmiCecPort) {
}
if (mCallback != nullptr) {
size_t length = std::min(msg.len - 1, (uint32_t)(CEC_MESSAGE_BODY_MAX_LENGTH - 1));
size_t length = std::min(static_cast<size_t>(msg.len - 1),
static_cast<size_t>(CEC_MESSAGE_BODY_MAX_LENGTH - 1));
CecMessage cecMessage{
.initiator = static_cast<CecLogicalAddress>(msg.msg[0] >> 4),
.destination = static_cast<CecLogicalAddress>(msg.msg[0] & 0xf),
@ -376,7 +392,6 @@ void HdmiCec::event_thread(HdmiCecPort* hdmiCecPort) {
}
}
}
}
int HdmiCec::getOpcode(cec_msg message) {
return static_cast<uint8_t>(message.msg[1]);

View file

@ -19,12 +19,16 @@
#include "HdmiConnection.h"
#include <android-base/file.h>
#include <android-base/logging.h>
using android::base::ReadFileToString;
#include <cerrno>
#include <cstring>
#include <fcntl.h>
#include <poll.h>
#include <sys/eventfd.h>
#include <unistd.h>
using ndk::ScopedAStatus;
using std::string;
namespace android {
namespace hardware {
@ -33,26 +37,49 @@ namespace hdmi {
namespace connection {
namespace implementation {
static const string drmCard = "card0";
static constexpr char kHdmiStatusPath[] = "/sys/class/drm/card0-HDMI-A-1/status";
static bool readStatus(int fd, bool* connected) {
char status[32] = {};
if (lseek(fd, 0, SEEK_SET) < 0) {
return false;
}
ssize_t length = read(fd, status, sizeof(status) - 1);
if (length < 0) {
return false;
}
*connected = length >= 9 && !strncmp(status, "connected", 9);
return true;
}
HdmiConnection::HdmiConnection() {
mCallback = nullptr;
for (int i = 0; i < 2; i++) {
mPortInfos.push_back(
{.type = HdmiPortType::OUTPUT,
.portId = i,
mPortInfos.push_back({.type = HdmiPortType::OUTPUT,
.portId = 1,
.cecSupported = true,
.arcSupported = false,
.eArcSupported = false,
.physicalAddress = 0xFFFF});
mHpdSignal.push_back(HpdSignal::HDMI_HPD_PHYSICAL);
mStopFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (mStopFd < 0) {
PLOG(ERROR) << "Failed to create HDMI monitor eventfd";
return;
}
mMonitorThread = std::thread(&HdmiConnection::monitorLoop, this);
}
HdmiConnection::~HdmiConnection() {
if (mCallback != nullptr) {
mCallback = nullptr;
signalMonitorStop();
if (mMonitorThread.joinable()) {
mMonitorThread.join();
}
if (mStopFd >= 0) {
close(mStopFd);
mStopFd = -1;
}
}
@ -62,55 +89,152 @@ ScopedAStatus HdmiConnection::getPortInfo(std::vector<HdmiPortInfo>* _aidl_retur
}
ScopedAStatus HdmiConnection::isConnected(int32_t portId, bool* _aidl_return) {
if (portId != 0 && portId != 1) {
if (portId != 1) {
*_aidl_return = false;
return ScopedAStatus::ok();
}
int fd = open(kHdmiStatusPath, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
*_aidl_return = false;
return ScopedAStatus::ok();
}
bool connected = false;
string hdmiStatusPath = "/sys/class/drm/" + drmCard + "-HDMI-A-" + to_string(portId + 1) + "/status";
if (!access(hdmiStatusPath.c_str(), R_OK)) {
string connectedValue;
if (ReadFileToString(hdmiStatusPath, &connectedValue)) {
connected = !connectedValue.compare("connected\n");
if (!readStatus(fd, &connected)) {
connected = false;
}
}
LOG(INFO) << "portId: " << portId << ", connected: " << connected;
close(fd);
LOG(INFO) << "portId: " << portId << ", connected: " << connected;
*_aidl_return = connected;
return ScopedAStatus::ok();
}
ScopedAStatus HdmiConnection::setCallback(const std::shared_ptr<IHdmiConnectionCallback>& callback) {
if (mCallback != nullptr) {
mCallback = nullptr;
}
if (callback != nullptr) {
ScopedAStatus HdmiConnection::setCallback(
const std::shared_ptr<IHdmiConnectionCallback>& callback) {
std::lock_guard<std::mutex> lock(mCallbackMutex);
mCallback = callback;
}
return ScopedAStatus::ok();
}
ScopedAStatus HdmiConnection::setHpdSignal(HpdSignal signal, int32_t portId) {
if (portId != 0 && portId != 1) {
if (portId != 1) {
return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
}
mHpdSignal.at(portId) = signal;
mHpdSignal[0] = signal;
return ScopedAStatus::ok();
}
ScopedAStatus HdmiConnection::getHpdSignal(int32_t portId, HpdSignal* _aidl_return) {
if (portId != 0 && portId != 1) {
if (portId != 1) {
return ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
}
*_aidl_return = mHpdSignal.at(portId);
*_aidl_return = mHpdSignal[0];
return ScopedAStatus::ok();
}
void HdmiConnection::signalMonitorStop() {
if (mStopFd < 0) {
return;
}
uint64_t value = 1;
if (write(mStopFd, &value, sizeof(value)) < 0 && errno != EAGAIN) {
PLOG(ERROR) << "Failed to stop HDMI monitor";
}
}
void HdmiConnection::monitorLoop() {
int statusFd = -1;
bool haveStatus = false;
bool lastConnected = false;
bool openErrorReported = false;
while (true) {
if (statusFd < 0) {
pollfd stopPoll = {.fd = mStopFd, .events = POLLIN, .revents = 0};
int ret = poll(&stopPoll, 1, 1000);
if (ret < 0 && errno == EINTR) {
continue;
}
if (ret > 0 && (stopPoll.revents & POLLIN)) {
break;
}
if (ret == 0) {
statusFd = open(kHdmiStatusPath, O_RDONLY | O_CLOEXEC);
if (statusFd >= 0) {
haveStatus = false;
openErrorReported = false;
} else if (!openErrorReported) {
PLOG(ERROR) << "Failed to open HDMI status file " << kHdmiStatusPath;
openErrorReported = true;
}
}
continue;
}
if (!haveStatus && !readStatus(statusFd, &lastConnected)) {
close(statusFd);
statusFd = -1;
continue;
}
haveStatus = true;
pollfd fds[2] = {
{.fd = statusFd, .events = POLLPRI | POLLERR, .revents = 0},
{.fd = mStopFd, .events = POLLIN, .revents = 0},
};
int ret = poll(fds, 2, 1000);
if (ret < 0) {
if (errno == EINTR) {
continue;
}
close(statusFd);
statusFd = -1;
haveStatus = false;
continue;
}
if (fds[1].revents & POLLIN) {
break;
}
if (ret == 0 || !(fds[0].revents & (POLLPRI | POLLERR))) {
continue;
}
bool connected = false;
if (!readStatus(statusFd, &connected)) {
close(statusFd);
statusFd = -1;
haveStatus = false;
continue;
}
if (connected == lastConnected) {
continue;
}
lastConnected = connected;
std::shared_ptr<IHdmiConnectionCallback> callback;
{
std::lock_guard<std::mutex> lock(mCallbackMutex);
callback = mCallback;
}
if (callback != nullptr) {
ScopedAStatus status = callback->onHotplugEvent(connected, 1);
if (!status.isOk()) {
LOG(ERROR) << "HDMI hotplug callback failed";
}
}
}
if (statusFd >= 0) {
close(statusFd);
}
}
} // namespace implementation
} // namespace connection
} // namespace hdmi

View file

@ -18,8 +18,10 @@
#include <aidl/android/hardware/tv/hdmi/connection/BnHdmiConnection.h>
#include <aidl/android/hardware/tv/hdmi/connection/Result.h>
#include <algorithm>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
using namespace std;
namespace android {
@ -48,11 +50,16 @@ struct HdmiConnection : public BnHdmiConnection {
::ndk::ScopedAStatus getHpdSignal(int32_t portId, HpdSignal* _aidl_return) override;
private:
void monitorLoop();
void signalMonitorStop();
std::vector<HdmiPortInfo> mPortInfos;
std::vector<HpdSignal> mHpdSignal;
std::mutex mCallbackMutex;
std::shared_ptr<IHdmiConnectionCallback> mCallback;
};
std::thread mMonitorThread;
int mStopFd = -1;
} // namespace implementation
} // namespace connection

View file

@ -6,6 +6,9 @@
/dev/v4l-subdev* 0660 system camera
/dev/video* 0660 system camera
# HDMI CEC
/dev/cec0 0660 system system
# DMA
/dev/dma_heap/linux,cma 0666 system graphics
/dev/dma_heap/system 0666 system graphics

View file

@ -50,9 +50,9 @@ persist.bluetooth.a2dp_aac.vbr_supported=true
ro.hardware.camera=libcamera
# CEC
#persist.vendor.hdmi.cec_device=cec0
#ro.hdmi.cec_device_types=playback_device
#ro.hdmi.device_type=4
persist.vendor.hdmi.cec_device=cec0
ro.hdmi.cec_device_types=playback_device
ro.hdmi.device_type=4
# Chipset
ro.soc.manufacturer=Rockchip