安卓系统面经_安卓面经(15/20)HDMI CEC详解

面试题预览

1. 安卓系统中HDMI CEC的实现主要依靠哪些类或库? ⭐⭐⭐⭐⭐

2. 如何在安卓应用中实现HDMI CEC的消息处理? ⭐⭐⭐⭐

3. 在安卓应用中建立CEC连接的过程是怎样的? ⭐⭐⭐⭐⭐

4. 如何检测一个HDMI CEC连接是否已被建立,在建立连接之后又该如何处理? ⭐⭐⭐⭐

5. 如何在安卓系统中捕获HDMI CEC消息的过程? ⭐⭐⭐⭐

6. 如何向HDMI CEC总线发送消息,在发送之前需要注意哪些问题?

7. 如何处理HDMI CEC总线上的多个消息,保证消息按顺序处理? ⭐⭐

8. 如何对HDMI CEC消息的响应进行处理,包括接收应答和处理错误? ⭐⭐⭐

9. 安卓系统中的HDMI CEC消息处理与什么其他的通信机制有异同? ⭐⭐⭐⭐⭐

10. 如何在安卓应用中使用HDMI CEC来控制其他已经连接到总线上的设备?⭐⭐⭐⭐

1 HDMI CEC简介

为了迅速了解整个设计架构,可以先去Google官网查阅相关信息: https://source.android.com/devices/tv/hdmi-cec

1.1 CEC的功能最主要包括:

● One Touch Play: 单键播放,可以通过点击单个按键实现媒体source设备打开电视并切换其输入端口。

● System StandBy: 系统休眠,允许设备间相互进行待机与唤醒,实现设备间电源状态一致。

● Deck Control: 录机控制,允许设备(sink)去控制或者询问playback设备(source)

● Remote Control Pass Through: 遥控器透传,允许遥控器透传到另一个设备进行处理。

2 HDMI CEC框架

下面会围绕System StandBy这个通路进行分析,相信分析完之后,其他通路都能够迎刃而解。在Android在版本的升级过程中,为了最大限度减少兼容性问题,创建了HdmiControlService系统服务来解决问题。下图为Android从5.0到之后的设计理念:

下图为HDMIControlService的系统框架图:

可以看到所有的应用,都会间接通过HDMIControlManager或者输入通过Tv Input框架间接与HdmiControlService进行通信,HdmiControlService作为SystemServer服务的一个服务,负责处理CEC的命令并与HDMI-CEC HAl进行交互。HAL层和驱动都需要厂商去适配,最后通过CEC总线与CEC设备通信。

至于HDMI的设计架构,分为source端以及sink端,可以有多个source输入,也可以有多个sink输出。四个TMDS数据和时钟通道用于传输video,audio和辅助数据。DDC(Display Data Channel)用于单个source和sink端进行状态交换。CEC总线能够提供在不同音视频设备中进行控制等等。

3 源码及配置介绍

本文涉及的代码分析包括:

● frameworks/base/services/core/java/com/android/server/hdmi

● frameworks/base/services/core/jni/ #对应的jni实现

● hardware/interfaces/tv/cec/1.0/ # Hal层接口

● device/amlogic/yukawa/hdmicec/ #厂商实现,本例以Amlogic作为分析

3.1 支持HDMI-CEC

本文重点关注自动开机和自动关机两个通路,以OTT作为source端,TV作为sink端为前提进行分析。自动开机的意思是当在TvSettings中设了自动开机后,两个设备均为关机状态,那么无论使用哪一方遥控器,都能够唤醒对端的设备。自动关机的功能与自动开机功能类似。

为了要支持HDMI-CEC,Android官方文档指出需要首先在方案中进行配置:

PRODUCT_COPY_FILES += \ frameworks/native/data/etc/android.hardware.hdmi.cec.xml:system/etc/permissions/android.hardware.hdmi.cec.xml

OTT设备需要设置如下:

PRODUCT_PROPERTY_OVERRIDES += ro.hdmi.device_type=4

Tv设备需要设置如下:

PRODUCT_PROPERTY_OVERRIDES += ro.hdmi.device_type=0

本文以source的角度进行分析。

3.2 HdmiControlService

在分析通路之前,需要了解下HdmiContolService。HdmiControlService是HDMI CEC框架的核心。之前提到为了要开启该服务,需要将android.hardware.hdmi.cec.xml拷贝到system/etc/permissions目录下,这是因为SystemServer在启动时会去检查权限目录下有没有hdmi.cec的xml文件:

//framework/base/services/java/com/android/server/SystemServer.java
private void startOtherServices() {
...
    if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_HDMI_CEC)) {
        traceBeginAndSlog("StartHdmiControlService");
        mSystemServiceManager.startService(HdmiControlService.class);
        traceEnd();
    }
...
}

转到HdmiControlService看其构造以及onStart方法:

//frameworks/base/services/core/java/com/android/server/hdmi/HdmiControlService.java
//mLocalDevices是Integer列表
private final List<Integer> mLocalDevices;
//Handler用于在service线程运行,因为使用了默认的Looper。
private final Handler mHandler = new Handler();
...
public HdmiControlService(Context context) {
    super(context);
    //这里可以看出,当设置了"ro.hdmi.device_type"时,mLocalDevices就会根据属性
    //生成对应类型的LocalDevice
    mLocalDevices = getIntList(SystemProperties.get(Constants.PROPERTY_DEVICE_TYPE));
    //mSettingsObserver是为了后续创建ContentObserver监听数据库的变化做准备
    mSettingsObserver = new SettingsObserver(mHandler);
}

@Override
public void onStart() {
    //启动Io线程    
    if (mIoLooper == null) {
        mIoThread.start();
        //后续将mIoLooper传到HdmiCecController的handler中,使得消息在mIoThread线程中处理。
        mIoLooper = mIoThread.getLooper();
    }
    mPowerStatus = HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON;
    mProhibitMode = false;
    //数据库hdmi_control_enabled用于控制Hdmi控制是否使能
    mHdmiControlEnabled = readBooleanSetting(Global.HDMI_CONTROL_ENABLED, true);
    ...
    //新建CecController,初始化Native层
    if (mCecController == null) {
        mCecController = dHdmiCecController.create(this);
    }
    if (mCecController != null) {
        if (mHdmiControlEnabled) {
            /*
            仅有mHdmiControlEnabled打开时,初始化Cec.
            注意到初始化还会传入初始化原因,包括:
            1. static final int INITIATED_BY_ENABLE_CEC = 0;
            2. static final int INITIATED_BY_BOOT_UP = 1;
            3. static final int INITIATED_BY_SCREEN_ON = 2;
            4. static final int INITIATED_BY_WAKE_UP_MESSAGE = 3;
            5. static final int INITIATED_BY_HOTPLUG = 4;
            */
            initializeCec(INITIATED_BY_BOOT_UP);
        } else {
            //假如mHdmiControlEnabled关闭时,向底层发送ENABLE_CEC设置为false
            mCecController.setOption(OptionKey.ENABLE_CEC, false);
        }
    } else {
        Slog.i(TAG, "Device does not support HDMI-CEC.");
        return;
    }
    ...
    initPortInfo();
    if (mMessageValidator == null) {
        mMessageValidator = new HdmiCecMessageValidator(this);
    }
    //创建Binder服务,用以和HdmiControlService进行交互
    publishBinderService(Context.HDMI_CONTROL_SERVICE, new BinderService());
    //注册广播接收器,用以监听如关屏,开屏,关机,配置改变的广播
    if (mCecController != null) {
        // Register broadcast receiver for power state change.
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_SCREEN_OFF);
        filter.addAction(Intent.ACTION_SCREEN_ON);
        filter.addAction(Intent.ACTION_SHUTDOWN);
        filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
        getContext().registerReceiver(mHdmiControlBroadcastReceiver, filter);
        /*
         注册ContentObserver用以监听数据库的变化,包括:
            Global.HDMI_CONTROL_ENABLED,
            Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED,
            Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED,
            Global.HDMI_SYSTEM_AUDIO_CONTROL_ENABLED,
            Global.MHL_INPUT_SWITCHING_ENABLED,
            Global.MHL_POWER_CHARGE_ENABLED,
            Global.HDMI_CEC_SWITCH_ENABLED,
            Global.DEVICE_NAME
        */
        registerContentObserver();
    }
    ...
}

3.3 HdmiController

看完服务的启动流程后,还需要看下控制器HdmiContoller,它在服务中的启动是调用create方法,跟着入口看实现逻辑:

//frameworks/base/services/core/java/com/android/server/hdmi/HdmiCecController.java
static HdmiCecController create(HdmiControlService service) {
    //新建wrapper类,用于方便调用HdmiCecController的native接口
    return createWithNativeWrapper(service, new NativeWrapperImpl());
}

static HdmiCecController createWithNativeWrapper(
    HdmiControlService service, NativeWrapper nativeWrapper) {
        //这里才新建HdmiCecController
        HdmiCecController controller = new HdmiCecController(service, nativeWrapper);
        //nativeInit返回的是Native层HdmiCecController的对象的地址。
        long nativePtr = nativeWrapper
            .nativeInit(controller, service.getServiceLooper().getQueue());
        if (nativePtr == 0L) {
            controller = null;
            return null;
        }
        //HdmiCecController初始化
        controller.init(nativePtr);
        return controller;
}

private void init(long nativePtr) {
    //将IoLooper设置到新建mIoHandler中,得以将处理流程放在Io线程中。
    mIoHandler = new Handler(mService.getIoLooper());
    //将服务流程Looper放到新建mControlHandler中
    mControlHandler = new Handler(mService.getServiceLooper());
    mNativePtr = nativePtr;
}

至此,可以进入流程分析。

3.4 单键休眠

单键休眠应当分为两个方向,一是从source端,使用source的遥控器单击休眠按键,此时source端进入休眠,sink端也进入休眠。二是使用sink端遥控器,使得sink端进入休眠后,source端也进入休眠。从而真正实现单键休眠功能。

3.4.1 SourceToSink

从source端设置休眠或者关机流程如下所示:

一切分析的源头来源于广播接收器,当PowerManagerService收到关机/休眠命令时,调用goToSleep并发送关屏或者关机广播,而此时HdmiControlService在启动时,会注册广播接收器HdmiControlBroadcastReceiver,用于监听这些关键广播,为的就是及时通知到驱动并将信息传送到CEC总线到sink端。

//frameworks/base/services/core/java/com/android/server/hdmi/HdmiControlService.java
private class HdmiControlBroadcastReceiver extends BroadcastReceiver {
@ServiceThreadOnly
@Override
public void onReceive(Context context, Intent intent) {
    //确定该流程是在Service线程进行的。
    assertRunOnServiceThread();
    boolean isReboot = SystemProperties.get(SHUTDOWN_ACTION_PROPERTY).contains("1");
    switch (intent.getAction()) {
        case Intent.ACTION_SCREEN_OFF:
            //关屏前,检查mPowerStatus是否为POWER_STATUS_ON状态或者POWER_STATUS_TRANSIENT_TO_ON状态
            //且当前不是重启状态
            if (isPowerOnOrTransient() && !isReboot) {
                //调用onStandby,传参STANDBY_SCREEN_OFF
                onStandby(STANDBY_SCREEN_OFF);
            }
            break;
            ...
        case Intent.ACTION_SHUTDOWN:
            if (isPowerOnOrTransient() && !isReboot) {
                //调用onStandby,传参STANDBY_SHUTDOWN
                onStandby(STANDBY_SHUTDOWN);
            }
            break;
    }
}

@ServiceThreadOnly
@VisibleForTesting
protected void onStandby(final int standbyAction) {
    assertRunOnServiceThread();
    mPowerStatus = HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY;
    //假如客户端设置了VendorCommandListener,在调用onStandby时会通知到客户端,
    //并告知原因为CONTROL_STATE_CHANGED_REASON_STANDBY
    invokeVendorCommandListenersOnControlStateChanged(false,
            HdmiControlManager.CONTROL_STATE_CHANGED_REASON_STANDBY);
    //获取LocalDevices,此处为Playback设备。
    final List<HdmiCecLocalDevice> devices = getAllLocalDevices();
    //假如还没收到sink端的消息,且设备设备的canGoToStandby还没有准备好进入休眠模式
    if (!isStandbyMessageReceived() && !canGoToStandby()) {
        //设置全局变量mPowerStatus为POWER_STATUS_STANDBY。
        mPowerStatus = HdmiControlManager.POWER_STATUS_STANDBY;
        for (HdmiCecLocalDevice device : devices) {
            //调用Playback设备的onStandby方法
            device.onStandby(mStandbyMessageReceived, standbyAction);
        }
        return;
    }
    //假设已经收到休眠消息 或者已经能够进入休眠状态了,此时再调用onStandBy,会调用disableDevices,
    //其实质是调用PlayBackdevice的disableDevice.并新建了一个callback,等待disable完成后,调用onCleared设置环境
    disableDevices(new PendingActionClearedCallback() {
        @Override
        public void onCleared(HdmiCecLocalDevice device) {
            Slog.v(TAG, "On standby-action cleared:" + device.mDeviceType);
            devices.remove(device);
            if (devices.isEmpty()) {
                onStandbyCompleted(standbyAction);
            }
        }
    });
}

private void disableDevices(PendingActionClearedCallback callback) {
    if (mCecController != null) {
        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
            device.disableDevice(mStandbyMessageReceived, callback);
        }
    }
    ....
}

HdmiCecLocalDevice为HdmiCecLocalDevicePlayback类型,当收到STANDBY_SCREEN_OFFSTANDBY_SHUTDOWN时,发送CEC命令。

//frameworks/base/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
@Override
    @ServiceThreadOnly
    protected void onStandby(boolean initiatedByCec, int standbyAction) {
        assertRunOnServiceThread();
        if (!mService.isControlEnabled() || initiatedByCec || !mAutoTvOff) {
            return;
        }
        switch (standbyAction) {
            case HdmiControlService.STANDBY_SCREEN_OFF:
                //source端为localDevice,所以为mAddress.dest为ADDR_TV,电视的地址
                mService.sendCecCommand(
                        HdmiCecMessageBuilder.buildStandby(mAddress, Constants.ADDR_TV));
                break;
            case HdmiControlService.STANDBY_SHUTDOWN:
                //source端为localDevice,所以为mAddress.dest为ADDR_BROADCAST,广播的地址
                mService.sendCecCommand(
                        HdmiCecMessageBuilder.buildStandby(mAddress, Constants.ADDR_BROADCAST));
                break;
        }
    }

在深入看HdmiControlService是如何发送Cec命令前,有必要看下HdmiCecMessageBuilder是如何生成命令的:

//frameworks/base/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java
public static HdmiCecMessage buildStandby(int src, int dest) {
    return buildCommand(src, dest, Constants.MESSAGE_STANDBY);
}

private static HdmiCecMessage buildCommand(int src, int dest, int opcode) {
    //原来是返回了一新的HdmiCecMessage对象,并设置了opcode,src端以及dest端。
    return new HdmiCecMessage(src, dest, opcode, HdmiCecMessage.EMPTY_PARAM);
}

public HdmiCecMessage(int source, int destination, int opcode, byte[] params) {
    mSource = source;
    mDestination = destination;
    mOpcode = opcode & 0xFF;
    mParams = Arrays.copyOf(params, params.length);
}

了解了HdmiCecMessage的结构后,再回过头来看下HdmiControlService的sendCecCommand:

//frameworks/base/services/core/java/com/android/server/hdmi/HdmiControlService.java
@ServiceThreadOnly
   void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) {
       assertRunOnServiceThread();
       //MessageValidator会检查command命令是否有效,分别从source,dest等进行分析。
       if (mMessageValidator.isValid(command) == HdmiCecMessageValidator.OK) {
           //有效的命令将允许通过HdmiCecController发送下去
           mCecController.sendCommand(command, callback);
       } else {
           HdmiLogger.error("Invalid message type:" + command);
           if (callback != null) {
               callback.onSendCompleted(SendMessageResult.FAIL);
           }
       }
   }


//frameworks/base/services/core/java/com/android/server/hdmi/HdmiCecController.java
@ServiceThreadOnly
void sendCommand(final HdmiCecMessage cecMessage,
        final HdmiControlService.SendMessageCallback callback) {
    assertRunOnServiceThread();
    //生成一个MessageHistoryRecord对象,加入到ArrayBlockingQueue中进行管理。
    addMessageToHistory(false /* isReceived */, cecMessage);
    //想运行到IoThread?只需要定义一个Runnable对象并post到IoThread中即可。
    runOnIoThread(new Runnable() {
        @Override
        public void run() {
            HdmiLogger.debug("[S]:" + cecMessage);
            //传的是二进制数byte
            byte[] body = buildBody(cecMessage.getOpcode(), cecMessage.getParams());
            int i = 0;
            int errorCode = SendMessageResult.SUCCESS;
            do {
                //核心是通过NativeWrapperImpl将消息发送到Native层。
                errorCode = mNativeWrapperImpl.nativeSendCecCommand(mNativePtr,
                    cecMessage.getSource(), cecMessage.getDestination(), body);
                if (errorCode == SendMessageResult.SUCCESS) {
                    break;
                }
                //将会在限定次数内进行尝试
            } while (i++ < HdmiConfig.RETRANSMISSION_COUNT);

            final int finalError = errorCode;
            if (finalError != SendMessageResult.SUCCESS) {
                Slog.w(TAG, "Failed to send " + cecMessage + " with errorCode=" + finalError);
            }
            //假如回调不为空,将会在Service线程里运行回调方法,告诉调用方已经发送完成了。
            if (callback != null) {
                runOnServiceThread(new Runnable() {
                    @Override
                    public void run() {
                        callback.onSendCompleted(finalError);
                    }
                });
            }
        }
    });
}


调到nativeSnedCecCommand看Native层的逻辑:

//frameworks/base/services/core/jni/com_android_server_hdmi_HdmiCecController.cpp
static jint nativeSendCecCommand(JNIEnv* env, jclass clazz, jlong controllerPtr,
        jint srcAddr, jint dstAddr, jbyteArray body) {
    //将Java层的信息再次封装一层到Native的CecMessage中。
    CecMessage message;
    message.initiator = static_cast<CecLogicalAddress>(srcAddr);
    message.destination = static_cast<CecLogicalAddress>(dstAddr);

    jsize len = env->GetArrayLength(body);
    ScopedByteArrayRO bodyPtr(env, body);
    size_t bodyLength = MIN(static_cast<size_t>(len),
            static_cast<size_t>(MaxLength::MESSAGE_BODY));
    message.body.resize(bodyLength);
    for (size_t i = 0; i < bodyLength; ++i) {
        message.body[i] = static_cast<uint8_t>(bodyPtr[i]);
    }
    //转到Native层的HdmiCecController发送消息
    HdmiCecController* controller =
            reinterpret_cast<HdmiCecController*>(controllerPtr);
    return controller->sendMessage(message);
}

int HdmiCecController::sendMessage(const CecMessage& message) {
    //调用HIDL接口
    Return<SendMessageResult> ret = mHdmiCec->sendMessage(message);
    if (!ret.isOk()) {
        ALOGE("Failed to send CEC message.");
        return static_cast<int>(SendMessageResult::FAIL);
    }
    return static_cast<int>((SendMessageResult) ret);
}

//hardware/interfaces/tv/cec/HdmiCec.cpp
Return<SendMessageResult> HdmiCec::sendMessage(const CecMessage& message) {
    cec_message_t legacyMessage {
        .initiator = static_cast<cec_logical_address_t>(message.initiator),
        .destination = static_cast<cec_logical_address_t>(message.destination),
        .length = message.body.size(),
    };
    for (size_t i = 0; i < message.body.size(); ++i) {
        legacyMessage.body[i] = static_cast<unsigned char>(message.body[i]);
    }
    //此处调用到HAL层实现
    return static_cast<SendMessageResult>(mDevice->send_message(mDevice, &legacyMessage));
}

由于各大厂商的HAL实现均不同,挑选高通的代码简单分析下流程:

//hardware/qcom/sm8150/display/hdmi_cec/qhdmi_cec.cpp

static int cec_device_open(const struct hw_module_t* module,
        const char* name,
        struct hw_device_t** device)
{
    ALOGD_IF(DEBUG, "%s: name: %s", __FUNCTION__, name);
    int status = -EINVAL;
    if (!strcmp(name, HDMI_CEC_HARDWARE_INTERFACE )) {
        struct cec_context_t *dev;
        dev = (cec_context_t *) calloc (1, sizeof(*dev));
        if (dev) {
            cec_init_context(dev);

            //Setup CEC methods
            dev->device.common.tag       = HARDWARE_DEVICE_TAG;
            dev->device.common.version   = HDMI_CEC_DEVICE_API_VERSION_1_0;
            dev->device.common.module    = const_cast<hw_module_t* >(module);
            dev->device.common.close     = cec_device_close;
            dev->device.add_logical_address = cec_add_logical_address;
            dev->device.clear_logical_address = cec_clear_logical_address;
            dev->device.get_physical_address = cec_get_physical_address;
            dev->device.send_message = cec_send_message;//对应的cec_send_message
            dev->device.register_event_callback = cec_register_event_callback;
            dev->device.get_version = cec_get_version;
            dev->device.get_vendor_id = cec_get_vendor_id;
            dev->device.get_port_info = cec_get_port_info;
            dev->device.set_option = cec_set_option;
            dev->device.set_audio_return_channel = cec_set_audio_return_channel;
            dev->device.is_connected = cec_is_connected;

            *device = &dev->device.common;
            status = 0;
        } else {
            status = -EINVAL;
        }
    }
    return status;
}
}; //namespace qhdmicec



static int cec_send_message(const struct hdmi_cec_device* dev,
        const cec_message_t* msg)
{
    ATRACE_CALL();
    if(cec_is_connected(dev, 0) <= 0)
        return HDMI_RESULT_FAIL;

    cec_context_t* ctx = (cec_context_t*)(dev);
    ALOGD_IF(DEBUG, "%s: initiator: %d destination: %d length: %u",
            __FUNCTION__, msg->initiator, msg->destination,
            (uint32_t) msg->length);

    // Dump message received from framework
    char dump[128];
    if(msg->length > 0) {
        hex_to_string((char*)msg->body, msg->length, dump);
        ALOGD_IF(DEBUG, "%s: message from framework: %s", __FUNCTION__, dump);
    }

    char write_msg_path[MAX_PATH_LENGTH];
    char write_msg[MAX_CEC_FRAME_SIZE];
    memset(write_msg, 0, sizeof(write_msg));
    //开始解析msg内容
    write_msg[CEC_OFFSET_SENDER_ID] = msg->initiator;
    write_msg[CEC_OFFSET_RECEIVER_ID] = msg->destination;
    //Kernel splits opcode/operand, but Android sends it in one byte array
    write_msg[CEC_OFFSET_OPCODE] = msg->body[0];
    if(msg->length > 1) {
        memcpy(&write_msg[CEC_OFFSET_OPERAND], &msg->body[1],
                sizeof(char)*(msg->length - 1));
    }
    //msg length + initiator + destination
    write_msg[CEC_OFFSET_FRAME_LENGTH] = (unsigned char) (msg->length + 1);
    hex_to_string(write_msg, sizeof(write_msg), dump);
    ALOGD_IF(DEBUG, "%s: message to driver: %s", __FUNCTION__, dump);
    snprintf(write_msg_path, sizeof(write_msg_path), "%s/cec/wr_msg",
            ctx->fb_sysfs_path);
    int retry_count = 0;
    ssize_t err = 0;
    //HAL spec requires us to retry at least once.
    while (true) {
        //最后调用write_node将信息写入
        err = write_node(write_msg_path, write_msg, sizeof(write_msg));
        retry_count++;
        if (err == -EAGAIN && retry_count <= MAX_SEND_MESSAGE_RETRIES) {
            ALOGE("%s: CEC line busy, retrying", __FUNCTION__);
        } else {
            break;
        }
    }

    if (err < 0) {
       if (err == -ENXIO) {
           ALOGI("%s: No device exists with the destination address",
                   __FUNCTION__);
           return HDMI_RESULT_NACK;
       } else if (err == -EAGAIN) {
            ALOGE("%s: CEC line is busy, max retry count exceeded",
                    __FUNCTION__);
            return HDMI_RESULT_BUSY;
        } else {
            return HDMI_RESULT_FAIL;
            ALOGE("%s: Failed to send CEC message err: %zd - %s",
                    __FUNCTION__, err, strerror(int(-err)));
        }
    } else {
        ALOGD_IF(DEBUG, "%s: Sent CEC message - %zd bytes written",
                __FUNCTION__, err);
        return HDMI_RESULT_SUCCESS;
    }
}

//write_node实际是将data写到对应的节点中
static ssize_t write_node(const char *path, const char *data, size_t len)
{
    ssize_t err = 0;
    int fd = -1;
    err = access(path, W_OK);
    if (!err) {
        fd = open(path, O_WRONLY);
        errno = 0;
        err = write(fd, data, len);
        if (err < 0) {
            err = -errno;
        }
        close(fd);
    } else {
        ALOGE("%s: Failed to access path: %s error: %s",
                __FUNCTION__, path, strerror(errno));
        err = -errno;
    }
    return err;
}

至此分析完source端到sink端的框架流程,HAL层的代码各厂商实现都不同,需要结合实际平台分析。

3.4.2 SinkToSource

首先贴出HAL层以上的流程图:

为了更好的了解整个过程,需要再深入一点qcom的代码:

//hardware/qcom/sm8150/display/sdm/libs/core/fb/hw_events.cpp
DisplayError HWEvents::Init(int fb_num, DisplayType display_type, HWEventHandler *event_handler,
                            const vector<HWEvent> &event_list, const HWInterface *hw_intf) {
  if (!event_handler)
    return kErrorParameters;

  event_handler_ = event_handler;
  fb_num_ = display_type;
  event_list_ = event_list;
  poll_fds_.resize(event_list_.size());
  event_thread_name_ += " - " + std::to_string(fb_num_);
  //读cec/rd_msg节点来获取cec信息
  map_event_to_node_ = {
						{HWEvent::VSYNC, "vsync_event"},
                        {HWEvent::EXIT, "thread_exit"},
                        {HWEvent::IDLE_NOTIFY, "idle_notify"},
                        {HWEvent::SHOW_BLANK_EVENT, "show_blank_event"},
                        {HWEvent::CEC_READ_MESSAGE, "cec/rd_msg"},
                        {HWEvent::THERMAL_LEVEL, "msm_fb_thermal_level"},
                        {HWEvent::IDLE_POWER_COLLAPSE, "idle_power_collapse"},
                        {HWEvent::PINGPONG_TIMEOUT, "pingpong_timeout"}
						};
 //处理HWEventData
  PopulateHWEventData();
  //创建线程读取节点信息
  if (pthread_create(&event_thread_, NULL, &DisplayEventThread, this) < 0) {
    DLOGE("Failed to start %s, error = %s", event_thread_name_.c_str());
    return kErrorResources;
  }

  return kErrorNone;
}


//hardware/qcom/sm8150/display/sdm/libs/core/fb/hw_events.cpp
void* HWEvents::DisplayEventHandler() {
  char data[kMaxStringLength] = {0};

  prctl(PR_SET_NAME, event_thread_name_.c_str(), 0, 0, 0);
  setpriority(PRIO_PROCESS, 0, kThreadPriorityUrgent);

  while (!exit_threads_) {
    //没有消息时阻塞
    int error = Sys::poll_(poll_fds_.data(), UINT32(event_list_.size()), -1);

    if (error <= 0) {
      DLOGW("poll failed. error = %s", strerror(errno));
      continue;
    }

    for (uint32_t event = 0; event < event_list_.size(); event++) {
      pollfd &poll_fd = poll_fds_[event];

      if (event_list_.at(event) == HWEvent::EXIT) {
        if ((poll_fd.revents & POLLIN) && (Sys::read_(poll_fd.fd, data, kMaxStringLength) > 0)) {
         //event_parser为函数指针,在处理cec消息时,指向&HWEvents::HandleCECMessage
          (this->*(event_data_list_[event]).event_parser)(data);
        }
      } else {
        if ((poll_fd.revents & POLLPRI) &&
                (Sys::pread_(poll_fd.fd, data, kMaxStringLength, 0) > 0)) {
          (this->*(event_data_list_[event]).event_parser)(data);
        }
      }
    }
  }
  pthread_exit(0);
  return NULL;
}

//调用CECMessage
void HWEvents::HandleCECMessage(char *data) {
  event_handler_->CECMessage(data);
}

event_handler_指向的是HWCDisplay,其CECMessage实现是:

//hardware/qcom/sm8150/display/sdm/libs/hwc2/hwc_display.cpp
DisplayError HWCDisplay::CECMessage(char *message) {
  if (qservice_) {
    /*
      调用qservice的onCECMessageReceived,qservice是一个binder服务,
      注册到ServiceManager中,其服务名为display.qservice,这里的调用涉及到Binder通讯
    */
    qservice_->onCECMessageReceived(message, 0);
  } else {
    DLOGW("Qservice instance not available.");
  }
  return kErrorNone;
}

//hardware/qcom/sm8150/display/libqservice/QService.cpp
void QService::onCECMessageReceived(char *msg, ssize_t len) {
    if(mHDMIClient.get()) {
        ALOGD_IF(QSERVICE_DEBUG, "%s: CEC message received", __FUNCTION__);
        mHDMIClient->onCECMessageRecieved(msg, len);
    } else {
        ALOGW("%s: Failed to get a valid HDMI client", __FUNCTION__);
    }
}


//hardware/qcom/sm8150/display/hdmi_cec/QHDMIClient.cpp
void QHDMIClient::onCECMessageRecieved(char *msg, ssize_t len)
{
    ALOGD_IF(DEBUG, "%s: CEC message received len: %zd", __FUNCTION__, len);
    //到了关键的步骤
    cec_receive_message(mCtx, msg, len);
}

//
void cec_receive_message(cec_context_t *ctx, char *msg, ssize_t len)
{
    if(!ctx->system_control)
        return;

    char dump[128];
    if(len > 0) {
        hex_to_string(msg, len, dump);
        ALOGD_IF(DEBUG, "%s: Message from driver: %s", __FUNCTION__, dump);
    }
    //使用hdmi_event_t这个结构体,并填充信息
    hdmi_event_t event;
    event.type = HDMI_EVENT_CEC_MESSAGE;
    event.dev = (hdmi_cec_device *) ctx;
    // Remove initiator/destination from this calculation
    event.cec.length = msg[CEC_OFFSET_FRAME_LENGTH] - 1;
    event.cec.initiator = (cec_logical_address_t) msg[CEC_OFFSET_SENDER_ID];
    event.cec.destination = (cec_logical_address_t) msg[CEC_OFFSET_RECEIVER_ID];
    //Copy opcode and operand
    size_t copy_size = event.cec.length > sizeof(event.cec.body) ?
                       sizeof(event.cec.body) : event.cec.length;
    memcpy(event.cec.body, &msg[CEC_OFFSET_OPCODE],copy_size);
    hex_to_string((char *) event.cec.body, copy_size, dump);
    ALOGD_IF(DEBUG, "%s: Message to framework: %s", __FUNCTION__, dump);
    //调用回调方法处理信息
    ctx->callback.callback_func(&event, ctx->callback.callback_arg);
}

至此,可以看到HAL层在通过poll监听cec节点的消息,当有消息时,将其一再封装,并通过binder通信发送,最后使用这个回调方法进行处理,而回调方法正是在上层初始化时注册时设置的。回过头看之前Native的HdmiCecController是如何初始化的:

//frameworks/base/services/core/jni/com_android_server_hdmi_HdmiCecController.cpp
HdmiCecController::HdmiCecController(sp<IHdmiCec> hdmiCec,
        jobject callbacksObj, const sp<Looper>& looper)
        : mHdmiCec(hdmiCec),
          mCallbacksObj(callbacksObj),
          mLooper(looper) {
    //新建了一个HdmiCecCallback对象,通过setCallback设置回调
    mHdmiCecCallback = new HdmiCecCallback(this);
    Return<void> ret = mHdmiCec->setCallback(mHdmiCecCallback);
    if (!ret.isOk()) {
        ALOGE("Failed to set a cec callback.");
    }
}

上面的setCallback实际就是一层一层最后调用到HAL层的register_event_callback,最后对ctx->callback.callback_fun进行了赋值。再看下HdmiCecCallback的实现:

//frameworks/base/services/core/jni/com_android_server_hdmi_HdmiCecController.cpp
class HdmiCecCallback : public IHdmiCecCallback {
    public:
        explicit HdmiCecCallback(HdmiCecController* controller) : mController(controller) {};
        Return<void> onCecMessage(const CecMessage& event)  override;
        Return<void> onHotplugEvent(const HotplugEvent& event)  override;
    private:
        HdmiCecController* mController;
    };
  //实现了onCecMessage方法,即之前cec_receive_message调用的就是这个回调对象的方法
  Return<void> HdmiCecController::HdmiCecCallback::onCecMessage(const CecMessage& message) {
    //处理的Handler为HdmiCecEventHandler
    sp<HdmiCecEventHandler> handler(new HdmiCecEventHandler(mController, message));
    //在Native层发送消息,类型为CEC_MESSAGE
    mController->mLooper->sendMessage(handler, HdmiCecEventHandler::EventType::CEC_MESSAGE);
    return Void();
}

class HdmiCecEventHandler : public MessageHandler {
....
void handleMessage(const Message& message) {
        switch (message.what) {
        case EventType::CEC_MESSAGE:
            //处理CecCommand
            propagateCecCommand(mCecMessage);
            break;
        case EventType::HOT_PLUG:
            propagateHotplugEvent(mHotplugEvent);
            break;
        default:
            // TODO: add more type whenever new type is introduced.
            break;
        }
    }
...
    void propagateCecCommand(const CecMessage& message) {
        JNIEnv* env = AndroidRuntime::getJNIEnv();
        jint srcAddr = static_cast<jint>(message.initiator);
        jint dstAddr = static_cast<jint>(message.destination);
        jbyteArray body = env->NewByteArray(message.body.size());
        const jbyte* bodyPtr = reinterpret_cast<const jbyte *>(message.body.data());
        env->SetByteArrayRegion(body, 0, message.body.size(), bodyPtr);
        //从Native层调用Java方法,handleIncomingCecCommand。
        env->CallVoidMethod(mController->getCallbacksObj(),
                gHdmiCecControllerClassInfo.handleIncomingCecCommand, srcAddr,
                dstAddr, body);
        env->DeleteLocalRef(body);
        checkAndClearExceptionFromCallback(env, __FUNCTION__);
    }
}

再看看Java的实现handleIncomingCecCommand:

    //frameworks/base/services/core/java/com/android/server/hdmi/HdmiCecController.java
    @ServiceThreadOnly
    private void handleIncomingCecCommand(int srcAddress, int dstAddress, byte[] body) {
        assertRunOnServiceThread();
        HdmiCecMessage command = HdmiCecMessageBuilder.of(srcAddress, dstAddress, body);
        HdmiLogger.debug("[R]:" + command);
        //生成一个新的MessageHistoryRecord对象加入到历史中(ArrayBlockingQueue)
        addMessageToHistory(true /* isReceived */, command);
        //调用onReceiveCommand
        onReceiveCommand(command);
    }
    
    @ServiceThreadOnly
    private void onReceiveCommand(HdmiCecMessage message) {
        assertRunOnServiceThread();
        //调用HdmiControlService的handleCecCommand处理消息
        if (isAcceptableAddress(message.getDestination()) && mService.handleCecCommand(message)) {
            return;
        }
        // Not handled message, so we will reply it with <Feature Abort>.
        maySendFeatureAbortCommand(message, Constants.ABORT_UNRECOGNIZED_OPCODE);
    }

//frameworks/base/services/core/java/com/android/server/hdmi/HdmiControlService.java
@ServiceThreadOnly
    boolean handleCecCommand(HdmiCecMessage message) {
        assertRunOnServiceThread();
        int errorCode = mMessageValidator.isValid(message);
        if (errorCode != HdmiCecMessageValidator.OK) {
            // We'll not response on the messages with the invalid source or destination
            // or with parameter length shorter than specified in the standard.
            if (errorCode == HdmiCecMessageValidator.ERROR_PARAMETER) {
                maySendFeatureAbortCommand(message, Constants.ABORT_INVALID_OPERAND);
            }
            return true;
        }
        //传到localDevice中处理
        if (dispatchMessageToLocalDevice(message)) {
            return true;
        }

        return (!mAddressAllocated) ? mCecMessageBuffer.bufferMessage(message) : false;
    }
    
    @ServiceThreadOnly
    private boolean dispatchMessageToLocalDevice(HdmiCecMessage message) {
        assertRunOnServiceThread();
        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
            if (device.dispatchMessage(message)
                    && message.getDestination() != Constants.ADDR_BROADCAST) {
                return true;
            }
        }

        if (message.getDestination() != Constants.ADDR_BROADCAST) {
            HdmiLogger.warning("Unhandled cec command:" + message);
        }
        return false;
    }

//frameworks/base/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
    @ServiceThreadOnly
    boolean dispatchMessage(HdmiCecMessage message) {
        assertRunOnServiceThread();
        int dest = message.getDestination();
        if (dest != mAddress && dest != Constants.ADDR_BROADCAST) {
            return false;
        }
        mCecMessageCache.cacheMessage(message);
        return onMessage(message);
    }

    @ServiceThreadOnly
    protected final boolean onMessage(HdmiCecMessage message) {
        assertRunOnServiceThread();
        if (dispatchMessageToAction(message)) {
            return true;
        }
        switch (message.getOpcode()) {
            ...
            case Constants.MESSAGE_STANDBY:
                //调用handleStandby
                return handleStandby(message);
            ...
            default:
                return false;
        }
    }
    
    @ServiceThreadOnly
    protected boolean handleStandby(HdmiCecMessage message) {
        assertRunOnServiceThread();
        // Seq #12
        if (mService.isControlEnabled()
                && !mService.isProhibitMode()
                && mService.isPowerOnOrTransient()) {
            //调用服务的standby方法
            mService.standby();
            return true;
        }
        return false;
    }

    //frameworks/base/services/core/java/com/android/server/hdmi/HdmiControlService.java
    @ServiceThreadOnly
    void standby() {
        assertRunOnServiceThread();
        if (!canGoToStandby()) {
            return;
        }
        mStandbyMessageReceived = true;
        //最终到PowerManager的goToSleep进入休眠
        mPowerManager.goToSleep(SystemClock.uptimeMillis(), PowerManager.GO_TO_SLEEP_REASON_HDMI, 0);
        // PowerManger will send the broadcast Intent.ACTION_SCREEN_OFF and after this gets
        // the intent, the sequence will continue at onStandby().
    }

至此,完成了从底层到framework层一键休眠的分析流程。

4. 总结

从休眠的通路,可以清晰了解到整个HdmiControlService是如何工作,信息是如何从两个不同方向进行传输。后续如果有扩展,也可能基于该框架进行修改,也可以设计开关控制通路。

面试题解析

回答是作者根据自己知识来回答的,没有标准答案,仅供参考:

1. 安卓系统中HDMI CEC的实现主要依靠哪些类或库? ⭐⭐⭐⭐⭐

回答:安卓系统中HDMI CEC的实现主要依靠android.hardware.hdmi和android.hardware.hdmi.cec这两个java包下的类以及jni层的hdmi_cec库。

2. 如何在安卓应用中实现HDMI CEC的消息处理? ⭐⭐⭐⭐

回答:在安卓应用中实现HDMI CEC的消息处理需要使用android.hardware.hdmi.cec.CecManager类提供的接口,包括registerCecMessageObserver(), unregisterCecMessageObserver(), sendCecCommand()等,来实现接收和发送CEC消息。

3. 在安卓应用中建立CEC连接的过程是怎样的? ⭐⭐⭐⭐⭐

回答:在安卓应用中建立CEC连接的过程是通过调用android.hardware.hdmi.HdmiControlManager类中的addHdmiCecVolumeCallback()接口实现的,并在回调中处理HDMI CEC的消息。

4. 如何检测一个HDMI CEC连接是否已被建立,在建立连接之后又该如何处理? ⭐⭐⭐⭐

回答:在检测一个HDMI CEC连接是否已被建立时,可以通过android.hardware.hdmi.cec.CecManager类提供的isConnected()接口返回一个bool值;在建立连接后,可以通过registerCecMessageObserver()函数注册一个观察器,并实现onReceived()方法来处理接收到的CEC消息。

5. 如何在安卓系统中捕获HDMI CEC消息的过程? ⭐⭐⭐⭐

回答:在安卓系统中捕获HDMI CEC消息的过程需要使用android.hardware.hdmi.cec.CecManager类提供的接口,通过registerCecMessageObserver()注册一个观察器,并在实现的onReceived()方法中处理接收到的CEC消息。

6. 如何向HDMI CEC总线发送消息,在发送之前需要注意哪些问题?

回答:向HDMI CEC总线发送消息需要使用android.hardware.hdmi.cec.CecManager类提供的sendCecCommand()接口,并在发送之前需要注意CEC命令的格式和参数。

7. 如何处理HDMI CEC总线上的多个消息,保证消息按顺序处理? ⭐⭐

回答:在处理HDMI CEC总线上的多个消息时,需要保证先进先出的顺序,并在处理完一条消息后再处理下一条,可以通过CecMessageQueue类实现这个功能。

8. 如何对HDMI CEC消息的响应进行处理,包括接收应答和处理错误? ⭐⭐⭐

回答:对HDMI CEC消息的响应处理需要先使用android.hardware.hdmi.cec.CecManager类提供的registerCecMessageObserver()接口注册一个观察器,然后在实现观察器的onReceived()方法中处理接收到的CEC消息。

接收应答消息时,可以通过CecMessage类提供的getOpcode()函数获取到命令码,并通过判断命令码来检查是否为应答消息。如果是应答消息,则需要检查返回结果是否正确,并根据返回结果采取相应的操作,例如,成功应答后进行下一步操作,否则进行出错处理。

处理出错的情况需要分具体情况而定,具体操作可以根据接收到的错误码和命令码类型,来确定是否需要重试或其他操作。

9. 安卓系统中的HDMI CEC消息处理与什么其他的通信机制(比如Socket通讯)有异同? ⭐⭐⭐⭐⭐

回答:HDMI CEC是通过数据总线来传递数据,而其他通信机制可能使用不同的物理传输方式,如无线通信、网线等

● HDMI CEC通信是通过数据总线来传输数据的,而Socket通信则是通过网络连接来传输数据的。

● HDMI CEC通常是实现设备之间的物理连接,通常不需要额外的网络设施,而Socket通信需要先建立网络连接才能进行通信。

● 在HDMI CEC通信过程中,存在有属于HDMI CEC的特定命令集,通过这些命令来控制设备的工作方式;而Socket通信则可以使用不同协议(例如TCP、UDP等)来传输不同类型的数据。

● 在安卓系统中,HDMI CEC的消息处理通常需要使用专门的类和库,例如android.hardware.hdmi.cec.CecManager等;而Socket通信则更多地使用一些标准的网络通信类库,例如java.net.Socket、java.net.ServerSocket等。

10. 如何在安卓应用中使用HDMI CEC来控制其他已经连接到总线上的设备?⭐⭐⭐⭐

回答:在安卓应用中使用HDMI CEC来控制其他已经连接到总线上的设备需要使用sendCecCommand()接口发送相应的CEC命令,例如通过TV遥控器来控制连接在HDMI端口上的音频接收器

高级系列专栏:

安卓(安卓系统开发也要掌握)

嵌入式

全部评论

相关推荐

安菲尔德星期三:63退休只是说63才能领退休金,不代表63还能有工作
点赞 评论 收藏
分享
精致的小松鼠人狠话不多:哈哈哈 我每次都差点点一下
点赞 评论 收藏
分享
点赞 评论 收藏
分享
1 1 评论
分享
牛客网
牛客企业服务