HDMI CEC on android

while true picture while true · Dec 19, 2017 · Viewed 8.9k times · Source

I've been facing a problem to access to HDMI CEC on this android dongle.

I'm trying to turn on the tv and change the input source of the tv but I was unable to it.

Android API Approach

I'm running a system app and I have settled

<uses-permission android:name="android.permission.HDMI_CEC" /> 

on AndroidManifest.xml.

I'm accessing to HDMI service through reflection since I was not able to access it directly, even being a system app.

public class HdmiHelper {

    public HdmiHelper(Context context) {
        init(context);
    }

    public void init(Context context) {

        try {

            //Interface Callback Proxy
            Class<?> hotplugEventListenerClass = Class.forName("android.hardware.hdmi.HdmiControlManager$HotplugEventListener");
            Class<?> vendorCommandListenerClass = Class.forName("android.hardware.hdmi.HdmiControlManager$VendorCommandListener");
            Class<?> oneTouchPlayCallbackClass = Class.forName("android.hardware.hdmi.HdmiPlaybackClient$OneTouchPlayCallback");
            Class<?> displayStatusCallbackClass = Class.forName("android.hardware.hdmi.HdmiPlaybackClient$DisplayStatusCallback");

            Object interfaceOneTouchPlaybackCallback = Proxy.newProxyInstance(oneTouchPlayCallbackClass.getClassLoader(),
                    new Class<?>[]{ oneTouchPlayCallbackClass } , new callbackProxyListener() );

            Object interfaceHotplugEventCallback = Proxy.newProxyInstance(hotplugEventListenerClass.getClassLoader(),
                    new Class<?>[]{ hotplugEventListenerClass } , new callbackProxyListener() );

            Object interfaceDisplayStatusCallbackClass = Proxy.newProxyInstance(displayStatusCallbackClass.getClassLoader(),
                    new Class<?>[]{ displayStatusCallbackClass } , new callbackProxyListener() );


            Method m = context.getClass().getMethod("getSystemService", String.class);
            Object obj_HdmiControlManager = m.invoke(context, (Object) "hdmi_control");

            Log.d("HdmiHelper", "obj: " + obj_HdmiControlManager + " | " + obj_HdmiControlManager.getClass());

            for( Method method : obj_HdmiControlManager.getClass().getMethods()) {
                Log.d("HdmiHelper", "   method: " + method.getName() );
            }


            Method method_addHotplugEventListener = obj_HdmiControlManager.getClass().getMethod("addHotplugEventListener", hotplugEventListenerClass);
            method_addHotplugEventListener.invoke(obj_HdmiControlManager, interfaceHotplugEventCallback);


            Method m2 = obj_HdmiControlManager.getClass().getMethod("getPlaybackClient");
            Object obj_HdmiPlaybackClient = m2.invoke( obj_HdmiControlManager );
            Log.d("HdmiHelper", "obj_HdmiPlaybackClient: " + obj_HdmiPlaybackClient + " | " + obj_HdmiPlaybackClient.getClass());

            Method method_oneTouchPlay = obj_HdmiPlaybackClient.getClass().getMethod("oneTouchPlay", oneTouchPlayCallbackClass);
            method_oneTouchPlay.invoke( obj_HdmiPlaybackClient, interfaceOneTouchPlaybackCallback);


            Method method_queryDisplayStatus = obj_HdmiPlaybackClient.getClass().getMethod("queryDisplayStatus", displayStatusCallbackClass);

            method_queryDisplayStatus.invoke( obj_HdmiPlaybackClient, interfaceDisplayStatusCallbackClass);

            Method method_getActiveSource = obj_HdmiPlaybackClient.getClass().getMethod("getActiveSource");
            Log.d("HdmiHelper", "getActiveSource: " + method_getActiveSource.invoke(obj_HdmiPlaybackClient));


        }catch (Exception e) {
            e.printStackTrace();
        }

    }

    public class callbackProxyListener implements java.lang.reflect.InvocationHandler {

        public callbackProxyListener() {

        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

            try {
                Log.d("HdmiHelper", "Start method " + method.getName() + " | " + proxy.getClass() + " | " + method.getDeclaringClass() );

                if ( args != null ) {

                    // Prints the method being invoked
                    for (int i = 0; i != args.length; i++) {
                        Log.d("HdmiHelper", "  - Arg(" + i + "): " + args[i].toString());
                    }

                }

                if (method.getName().equals("onReceived")) {

                    if (args.length == 1) {
                        onReceived(args[0]);
                    }else
                    if (args.length == 3) {
                        onReceived( (int) args[0], BytesUtil.toByteArray( args[1] ), (boolean) args[2]  );
                    }


                }else
                if (method.getName().equals("onComplete")) {
                    onComplete( (int) args[0] );
                }else
                if (method.getName().equals("toString")) {
                    return this.toString();
                }else {
                    return method.invoke(this, args);
                }

            }catch (Exception e) {
                e.printStackTrace();
            }

            return null;
        }

        void onComplete(int result) {
            Log.d("HdmiHelper", "onComplete: " + result);
        }

        void onReceived(Object event) {

            Class eventClass = event.getClass();

            Log.d("HdmiHelper", "onReceived(1): " + event.toString() + " | " + eventClass);

            try {
                Method method_getPort = eventClass.getMethod("getPort");
                Method method_isConnected = eventClass.getMethod("isConnected");
                Method method_describeContents = eventClass.getMethod("describeContents");

                Log.d("HdmiHelper", "    - " + method_getPort.invoke(event) + " | " + method_isConnected.invoke(event) + " | " + method_describeContents.invoke(event) );

            }catch (Exception e) {

                e.printStackTrace();

            }


        }


        void onReceived(int srcAddress, byte[] params, boolean hasVendorId) {
            Log.d("HdmiHelper", "onReceived(3): " + srcAddress + " | " + params + " | " + hasVendorId);
        }


    }

Log answers:

D/HdmiHelper: obj: android.hardware.hdmi.HdmiControlManager@7bca63c | class android.hardware.hdmi.HdmiControlManager

D/HdmiHelper: obj_HdmiPlaybackClient: android.hardware.hdmi.HdmiPlaybackClient@6345d1a | class android.hardware.hdmi.HdmiPlaybackClient

D/HdmiHelper: Start method onReceived | class $Proxy2 | interface android.hardware.hdmi.HdmiControlManager$HotplugEventListener
D/HdmiHelper: onReceived(1): android.hardware.hdmi.HdmiHotplugEvent@4c5c04b | class android.hardware.hdmi.HdmiHotplugEvent
D/HdmiHelper:     - 1 | true | 0  
  • Question 1:

I received true: which means that tv is on what is true. If the tv is off i receive false. That seems to work.

Though, I was expecting to receive a callback every time I change the tv state, which is not happening. Any idea?

  • Question 2:

continuing with the logs for the OneTouchPlayCallback:

D/HdmiHelper: Start method onComplete | class $Proxy1 | interface android.hardware.hdmi.HdmiPlaybackClient$OneTouchPlayCallback
D/HdmiHelper: onComplete: 2

Looking into the class HdmiPlaybackClient.java if everything went good the answer would be 0 (@param result the result of the operation. {@link HdmiControlManager#RESULT_SUCCESS . You can find this variable in HdmiControlManager.java class}. Instead, I receive 2 which i assume that is RESULT_SOURCE_NOT_AVAILABLE.

Any idea why?

  • Question 3

Continuing now with the logs for the DisplayStatusCallback:

D/HdmiHelper: Start method onComplete | class $Proxy3 | interface android.hardware.hdmi.HdmiPlaybackClient$DisplayStatusCallback
D/HdmiHelper: onComplete: 2

According to the definition of this callback:

/**
     * Listener used by the client to get display device status.
     */
    public interface DisplayStatusCallback {
        /**
         * Called when display device status is reported.
         *
         * @param status display device status. It should be one of the following values.
         *            <ul>
         *            <li>{@link HdmiControlManager#POWER_STATUS_ON}
         *            <li>{@link HdmiControlManager#POWER_STATUS_STANDBY}
         *            <li>{@link HdmiControlManager#POWER_STATUS_TRANSIENT_TO_ON}
         *            <li>{@link HdmiControlManager#POWER_STATUS_TRANSIENT_TO_STANDBY}
         *            <li>{@link HdmiControlManager#POWER_STATUS_UNKNOWN}
         *            </ul>
         */
        public void onComplete(int status);
    }

and looking into the HdmiControlManager I receive 2 wich means that is:

public static final int POWER_STATUS_TRANSIENT_TO_ON = 2;

Which is a strange result because that is not what's happening.

  • Continuing with logs for your information:

Answer for:

getActiveSource is null

I also tested this code that calls the getTvClient() method:

Method method_getTvClient = obj_HdmiControlManager.getClass().getMethod("getTvClient");
Object obj_HdmiTvClient = method_getTvClient.invoke( obj_HdmiControlManager );
Log.d("HdmiHelper", "obj_HdmiTvClient: " + obj_HdmiTvClient);

and the result is null.

I also tried the approach of sending a vendor command following CEC-O-MATIC website but I was unable to have success. If you have any instructions about this, please give me some directions and I will test it.

LibCEC approach:

I was able to cross compile libcec to android thanks to this post. But libcec is always answering to me "command 'PING' was not acked by the controller".

I've added the flags -DHAVE_EXYNOS_API=1 and -DHAVE_AOCEC_API=1 to libcec.

System Information

The device /dev/cec is settled:

q8723bs:/ # ls -l /dev/cec                                                                                                                                              
crw-rw-rw- 1 root root 218,   0 2017-12-19 16:33 /dev/cec

I also can find it on /sys/class/cec:

q8723bs:/ # ls -laht /sys/class/cec/                                                                                                                                    
total 0
-r--r--r--   1 root root 4.0K 2017-12-19 16:45 arc_port
lrwxrwxrwx   1 root root    0 2017-12-19 16:45 cec -> ../../devices/aocec/cec
-r--r--r--   1 root root 4.0K 2017-12-19 16:45 cec_version
--w-------   1 root root 4.0K 2017-12-19 16:45 cmd
-rw-rw-r--   1 root root 4.0K 2017-12-19 16:45 dbg_en
-rw-rw-r--   1 root root 4.0K 2017-12-19 16:45 device_type
-r--r--r--   1 root root 4.0K 2017-12-19 16:45 dump_reg
-rw-rw-r--   1 root root 4.0K 2017-12-19 16:45 fun_cfg
-rw-rw-r--   1 root root 4.0K 2017-12-19 16:45 menu_language
-r--r--r--   1 root root 4.0K 2017-12-19 16:45 osd_name
-rw-rw-r--   1 root root 4.0K 2017-12-19 16:45 physical_addr
-r--r--r--   1 root root 4.0K 2017-12-19 16:45 pin_status
-r--r--r--   1 root root 4.0K 2017-12-19 16:45 port_num
-rw-rw-r--   1 root root 4.0K 2017-12-19 16:45 port_seq
-r--r--r--   1 root root 4.0K 2017-12-19 16:45 port_status
-rw-rw-r--   1 root root 4.0K 2017-12-19 16:45 vendor_id
-r--r--r--   1 root root 4.0K 2017-12-19 16:45 wake_up

But when I ran cec-client I receive this answer:

q8723bs:/ # id
uid=0(root) gid=0(root) groups=0(root) context=u:r:toolbox:s0
q8723bs:/ # cec-client -s /dev/cec                                                                                                                                      
opening a connection to the CEC adapter...
DEBUG:   [               1] Broadcast (F): osd name set to 'Broadcast'
DEBUG:   [               2] connection opened, clearing any previous input and waiting for active transmissions to end before starting
DEBUG:   [             396] communication thread started
DEBUG:   [            1396] command 'PING' was not acked by the controller

As a note, I also have the device /dev/input/event2 that is a read only cec_input:

q8723bs:/ # ls -l /dev/input/event2                                                                                                                                     
crw-rw---- 1 root input 13,  66 2017-12-19 16:33 /dev/input/event2
q8723bs:/ # ls /sys/devices/virtual/input/input2/                                                                                                                      
capabilities/  event2/        id/            modalias       name           phys           power/         properties     subsystem/     uevent         uniq
q8723bs:/ # cat /sys/devices/virtual/input/input2/name                                                                                                                  
cec_input

I tried to run it on /dev/input/event2 but obviously it didn't work because it could not open a connection:

q8723bs:/ # cec-client /dev/input/event2                                                                                                                                
No device type given. Using 'recording device'
CEC Parser created - libCEC version 4.0.2
opening a connection to the CEC adapter...
DEBUG:   [               1] Broadcast (F): osd name set to 'Broadcast'
ERROR:   [            3335] error opening serial port '/dev/input/event2': Couldn't lock the serial port
ERROR:   [            3335] could not open a connection (try 1)

in Summary:

I could not get the command to turn on or change the input source of tv working in either of the cases. Any direction would be very helpful. Thanks in advance.

note: I was able to accomplish it with libcec and raspberry pi on the same tv

Answer

while true picture while true · Jan 3, 2018

So after a lot of work around this issue I figured out that, in order to have CEC control enabled in android you need to run this command on shell:

settings put global hdmi_control_enabled 1 

#if you want, you can also enable this self-explanatory command
settings put global hdmi_control_auto_wakeup_enabled 1 

#and this
settings put global hdmi_control_auto_device_off_enabled 1

After this, android automatically started to make use of cec, after a boot, for example, it change the input source of tv and/or turn on the tv.

Now, regarding developer control:

I understood, that when I call the method:

Method m2 = obj_HdmiControlManager.getClass().getMethod("getPlaybackClient");
         

I'm basically getting the access to CEC of the dongle itself (not the tv).

Though, I still continue to receive null when I run the method:

Method method_getTvClient = obj_HdmiControlManager.getClass().getMethod("getTvClient");

My guess here is that this is a normal behaviour since the dongle itself is a playback type and not a TV type.

So I tried to use the function sendVendorCommand but I was not able to figure out how to use it. I couldn't found any documentation/examples around this subject that could help me.

So I decided to go directly through OS level and it worked. Specifically in this dongle, you have at /sys/class/cec two important files:

. cmd (to send cec commands)

e.g. (as root @ android shell )

#turn on tv
echo 0x40 0x04 > /sys/class/cec/cmd

#change input source to HDMI 1
echo 0x4F 0x82 0x10 0x00 > /sys/class/cec/cmd

. dump_reg ( to read output of cec)

Use this site to check codes for other commands

And That's it! I would prefer to execute those commands through android framework, but at least, this works.