Showing posts with label HDMI CEC. Show all posts
Showing posts with label HDMI CEC. Show all posts

Saturday, August 29, 2015

TvInputService Implementation : KeyEvents to a HDMI-CEC Device

TvInputService Implementation : KeyEvents to a HDMI-CEC Device


As we know Android Tv Input Framework, a data source or Tv Input like IP-TV, Tuner, HDMI etc. is nothing else but the implementation of TvInputService. So if any of these datasource/device need KeyEvents, that should be implemented in TvInputService.

A TvInputService implementation can be found in this post.

For this following TvInputService$Session must be overridden in order to intercept key down events before they are processed by the application.

public boolean onKeyDown(int keyCode, KeyEvent event) {
    return false;
}

public boolean onKeyUp(int keyCode, KeyEvent event) {
    return false;
}

If true is returned, the application will not process the event itself.
If false is returned, the normal application processing will occur as if the TV input had not seen the event at all.

We can overide these methods to control a connected HDMI-CEC device.
Following code should be added to our TvInputService implementation in order to work this.

mHdmiControlService = IHdmiControlService.Stub
.asInterface(ServiceManager.getService(Context.HDMI_CONTROL_SERVICE));
mHdmiControlService.deviceSelect(currentHdmiDeviceInfo.getDeviceId(), mHdmiControlCallbackImpl);

public boolean onKeyDown(int keyCode, KeyEvent event) {
    if(interestedToHandle(keyCode)) 
    {
mHdmiControlService.sendKeyEvent(activeLocalDeviceType, keyCode, true);
return true;
    }
    return false;
}

public boolean onKeyUp(int keyCode, KeyEvent event) {
    if(interestedToHandle(keyCode)) 
    {
mHdmiControlService.sendKeyEvent(activeLocalDeviceType, keyCode, false);
return true;
    }
    return false;
}

So now let's see how the Android system key event will flow through HdmiControlService and mapped as CEC message, passed to hdmi_cec HAL.

Here is the call flow,
HdmiControlService.java
sendKeyEvent(final int deviceType, final int keyCode, final boolean isPressed) {
    HdmiCecLocalDevice localDevice = mCecController.getLocalDevice(deviceType);
    localDevice.sendKeyEvent(keyCode, isPressed);
}

HdmiCecLocalDeviceTv.java
sendKeyEvent(int keyCode, boolean isPressed) {
      action.get(0).processKeyEvent(keyCode, isPressed);
}

SendKeyAction.java
processKeyEvent(int keycode, boolean isPressed) {
    if (isPressed) {
            sendKeyDown(keycode);
    } else {
            sendKeyUp();
}

sendKeyDown(int keycode) {
    byte[] cecKeycodeAndParams = HdmiCecKeycode.androidKeyToCecKey(keycode);
    sendCommand(HdmiCecMessageBuilder.buildUserControlPressed(getSourceAddress(),
            mTargetAddress, cecKeycodeAndParams));
}

sendKeyUp() {
    sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(getSourceAddress(),
            mTargetAddress));
}

HdmiCecMessageBuilder builds the HdmiCecMessage/Commands. HdmiCecKeycode class contains Android Key to CEC command mapping.
HdmiCecMessage contains source and destination address, command (or opcode) and optional params.

HdmiCecFeatureAction.java
sendCommand(HdmiCecMessage cmd) {
        mService.sendCecCommand(cmd);
}

HdmiControlService.java
Transmit a CEC command to CEC bus.
sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) {
   mCecController.sendCommand(command, callback);
}

HdmiCecController.java
sendCommand(final HdmiCecMessage cecMessage,
     final HdmiControlService.SendMessageCallback callback) {

      byte[] body = buildBody(cecMessage.getOpcode(), cecMessage.getParams());
      errorCode = nativeSendCecCommand(mNativePtr, cecMessage.getSource(),
                       cecMessage.getDestination(), body);

       callback.onSendCompleted(finalError);
}

/jni/com_android_server_hdmi_HdmiCecController.cpp
nativeSendCecCommand(JNIEnv* env, jclass clazz, jlong controllerPtr,
       jint srcAddr, jint dstAddr, jbyteArray body) {
    cec_message_t message;
    return controller->sendMessage(message);
}

HdmiCecController::sendMessage(const cec_message_t& message) {

   return mDevice->send_message(mDevice, &message);
}

Finally the call in hdmi_cec HAL, and if following method implemented to interface hdmi-cec driver,
the device will respond to the keys pressed on remote of hosting device.

(*send_message)() //transmits HDMI-CEC message to other HDMI device.




TvInputService Implementation : Creating TvInputInfo

TvInputService Implementation : Creating TvInputInfo 


As we know Android Tv Input Framework, a data source or Tv Input like IP-TV, Tuner, HDMI etc. is nothing else but the implementation of TvInputService.

TvInputManagerService provides the info of Hardware/HDMI devices available in the system, to all concrete classes of TvInputService by following callbcaks,

    @SystemApi
    public TvInputInfo onHardwareAdded(TvInputHardwareInfo hardwareInfo) {
        return null;
    }

    @SystemApi
    public TvInputInfo onHdmiDeviceAdded(HdmiDeviceInfo deviceInfo) {
        return null;
    }

While implementing TvInputService for any Hardware or HDMI-CEC, we need to override these methods carefuly, so a create correct TvInputInfo which will be returned back to TvInputManagerService for storing it.

TvInputInfo.java
private final String mId; // a string representation(example at end) 
private final String mParentId; //relevant in case of HDMI-CEC

private int mType = TYPE_TUNER;
private HdmiDeviceInfo mHdmiDeviceInfo; //relevant in case of HDMI-CEC

For creating TvInputInfo we will use one of the overloaded static methods createTvInputInfo::createTvInputInfo,

1. For Built-in Tuner, createTvInputInfo(Context context, ResolveInfo service)
2. For HDMI-CEC, createTvInputInfo(Context, ResolveInfo service, HdmiDeviceInfo, String parentId, label, iconUri)
3. For other hardware like HDMI-Input, createTvInputInfo(Context, ResolveInfo service, TvInputHardwareInfo, label, iconUri)

Here is an example of method implementation,
public class MyTvInputService extends TvInputService
{
    private final Context context;
    private final ResolveInfo service;

    public void onCreate( 
    {
context = getApplicationContext();
service = context.getPackageManager().
resolveService(new Intent(TvInputService.SERVICE_INTERFACE), PackageManager.GET_INTENT_FILTERS | PackageManager.GET_META_DATA);

    }

    public TvInputInfo onHardwareAdded(TvInputHardwareInfo hardwareInfo)
    {

        TvInputInfo info;
        try    
        {
        String lebel = new StringBuilder("HDMI").append(hardwareInfo.getDeviceId()).toString();
        info = TvInputInfo.createTvInputInfo(context, service, hardwareInfo, lebel, iconUri);
        } catch (Exception ex)   {        }
        return info;
    }

    public TvInputInfo onHdmiDeviceAdded(HdmiDeviceInfo deviceInfo) 
    {
        TvInputInfo info;
        try    
        {
String lebel = deviceInfo.getDisplayName();
info = TvInputInfo.createTvInputInfo(context, service, deviceInfo, parentId, lebel, iconUri);
} catch (Exception ex)   {        }
        return info;
    }
}

In case of onHdmiDeviceAdded, parentId is ID of this TV input's parent input, this is the Id of previously registered TvInput's, on same port-id as this HdmiDeviceInfo's port.

In "adb shell dumpsys tv_input" we can see all the TvInputs available in system,
For example,
Id will be "com.xyz.android.tv/.MyTvInputService/HW1" for 1st HW of MyTvInputService in package com.xyz.android.tv



Wednesday, August 26, 2015

Android TV Framework : HDMI Events Call Flow to TvInputService

HDMI Events Call Flow to TvInputService

In previous post we understood how HdmiControlManager.DEVICE_EVENT_ADD_DEVICE event is generated in Hdmi framework, now we will check further how this event is reached in TvInputFramework and finaly to a TvInputService implementation.

There are 3 categories of events notified from HdmiControlService to TvInputFramework,
1.HotplugEvent and
2.DeviceEvent
3.SystemAudioModeChange

TvInputManagerService registers for these as EventListener in TvInputHardwareManager class.
TvInputHardwareManager manages the hardwares either of TvInputHal(tv_input_device_t) type implemented in tv_input.cpp or hdmi_cec_device Type implemented in HDMI-CEC hal.

public TvInputManagerService(){
   mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener());
}

//TvInputHardwareManager.java
public void onBootPhase(int phase) {
   mHdmiControlService.addHotplugEventListener(mHdmiHotplugEventListener);
   mHdmiControlService.addDeviceEventListener(mHdmiDeviceEventListener);
   mHdmiControlService.addSystemAudioModeChangeListener(
mHdmiSystemAudioModeChangeListener);
   mHdmiDeviceList.addAll(mHdmiControlService.getInputDevices());

}

HdmiHotplugEventListener extends IHdmiHotplugEventListener.Stub {
   public void onReceived(HdmiHotplugEvent event) {
      mHdmiStateMap.put(event.getPort(), event.isConnected());

      mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
         convertConnectedToState(event.isConnected()), 0, inputId).sendToTarget();
}

HdmiDeviceEventListener extends IHdmiDeviceEventListener.Stub {
  public void onStatusChanged(HdmiDeviceInfo deviceInfo, int status) {
    switch (status) {
         case HdmiControlManager.DEVICE_EVENT_ADD_DEVICE: {
            mHdmiDeviceList.add(deviceInfo);
            messageType = ListenerHandler.HDMI_DEVICE_ADDED;
         }
         case HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE: {
            if (!mHdmiDeviceList.remove(originalDeviceInfo)) 
            messageType = ListenerHandler.HDMI_DEVICE_REMOVED;
         }
         case HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE: {
            if (!mHdmiDeviceList.remove(originalDeviceInfo)) {
            mHdmiDeviceList.add(deviceInfo);
            messageType = ListenerHandler.HDMI_DEVICE_UPDATED;
         }
         Message msg = mHandler.obtainMessage(messageType, 0, 0, obj);
msg.sendToTarget();
}

This handler notifies to TvInputManagerService.
ListenerHandler extends Handler {
  public final void handleMessage(Message msg) {
      switch (msg.what) {
          case STATE_CHANGED: {
              mListener.onStateChanged(inputId, state);
              break;
          }
          case HDMI_DEVICE_ADDED: {
              mListener.onHdmiDeviceAdded(info);
              break;
          }
          case HDMI_DEVICE_REMOVED: {
              mListener.onHdmiDeviceRemoved(info);
              break;
          }

TvInputManagerService broadcasts this event to all services currently available in system.

public void onHdmiDeviceAdded(HdmiDeviceInfo deviceInfo) {
   // Broadcast the event to all hardware inputs.
   serviceState.service.notifyHdmiDeviceAdded(deviceInfo);
}

TvInputService gets the event and posts to its handler which will call the onHdmiDeviceAdded.

TvInputService.java
void notifyHdmiDeviceAdded(HdmiDeviceInfo deviceInfo) {
   mServiceHandler.obtainMessage(ServiceHandler.DO_ADD_HDMI_TV_INPUT,
          deviceInfo).sendToTarget();
}

ServiceHandler extends Handler {
   case DO_ADD_HDMI_TV_INPUT: {
        HdmiDeviceInfo deviceInfo = (HdmiDeviceInfo) msg.obj;
        TvInputInfo inputInfo = onHdmiDeviceAdded(deviceInfo);
        if (inputInfo != null) {
              broadcastAddHdmiTvInput(deviceInfo.getId(), inputInfo);
        }
}

Note here that a custom TvInputService must override to modify default behavior of ignoring all HDMI logical input device i.e. build a TvInputInfo using this HdmiDeviceInfo and return it.

public TvInputInfo onHdmiDeviceAdded(HdmiDeviceInfo deviceInfo) {
     return null;
}
(How to override this? link TvInputService Implementation : Creating TvInputInfo )

With this TvInputInfo, TvInputService broadcasts to TvInputManagerService so that it can add/update this new TvInputInfo in TvInputList.
broadcastAddHdmiTvInput(int id, TvInputInfo inputInfo) {
                  mCallbacks.getBroadcastItem(i).addHdmiTvInput(id, inputInfo);
}

In next post we will use this HdmiDeviceInfo/TvInputInfo for CEC communication implementation.


Sunday, August 23, 2015

Android TV Framework : HDMI CEC Introduction

Android HDMI-CEC and Tv Input Framework

HDMI CEC feature enables a user to command different electronics devices, CEC enabled and connected through HDMI, with any of their remote control.
Android TV framework provides support for HDMI-CEC features like One Touch Play, Audio Control, Remote Control Pass-through etc.

This is accomplished by HdmiControlService, a system service which interacts with TvInput Framework and AudioSystem.

Full design and integration with TIF can be found on Android official site.

A HDMI input device connected to Android TV, can be controlled by the TV-remote.
The prebuilt TV application will get the all keyEvents and will pass to currently active Session(which is TvInputService). If TvInput Framework has already mapped the corresponding HDMI-ports to CEC logical addresses and these things are stored in corresponding TvInputHardwareInfo, currently active Session on a HDMI port, will dispatch the keys to same port via HdmiControlManager  to hdmi_cec HAL and finally CEC device will respond to command.

Like TvInputService and tv_input HAL dummy implementation, will implement a hdmi_cec HAL and modify our TvInputService to get the keyEvents and pass it as CEC command.

First let's see HDMI CEC service, it's initialization, interaction to HAL and TvInputFramework.

HdmiControlService is a SystemService which creates the HdmiCecController in it's onStart() call and enables OPTION_CEC_SERVICE_CONTROL option.

HdmiControlService::onStart() {

            if (mHdmiControlEnabled) {
                initializeCec(INITIATED_BY_BOOT_UP);

        mCecController = HdmiCecController.create(this);
            mCecController.setOption(OPTION_CEC_ENABLE, ENABLED);
        initPortInfo();
    }

HdmiCecController is the class which interacts to HDMI_CEC Hal through com_android_server_hdmi_HdmiCecController jni.
HdmiCecController::init() registers the HdmiCecController::onReceived callback for device events.

HdmiCecController.java
    static HdmiCecController create(HdmiControlService service) {
        HdmiCecController controller = new HdmiCecController(service);
        long nativePtr = nativeInit(controller, service.getServiceLooper().getQueue());
        controller.init(nativePtr);
    }

    private void init(long nativePtr) {
        mNativePtr = nativePtr;
    }

com_android_server_hdmi_HdmiCecController.cpp
void HdmiCecController::init() {
    mDevice->register_event_callback(mDevice, HdmiCecController::onReceived, this);
}

Now in HdmiControlService::onStart(), initializeCec is called,

HdmiControlService.java
private void initializeCec(int initiatedBy) {
        mCecController.setOption(OPTION_CEC_SERVICE_CONTROL, ENABLED);
        initializeLocalDevices(initiatedBy);
    }

private void initializeLocalDevices(final int initiatedBy) {
                localDevice = HdmiCecLocalDevice.create(this, type);
        allocateLogicalAddress(localDevices, initiatedBy);
    }

private void allocateLogicalAddress(final ArrayList<HdmiCecLocalDevice> allocatingDevices,
            final int initiatedBy) {

            mCecController.allocateLogicalAddress(localDevice.getType(),//call to HAL
                    localDevice.getPreferredAddress(), new AllocateAddressCallback() {
                @Override
                public void onAllocated(int deviceType, int logicalAddress) {//Callback from controller

                        HdmiDeviceInfo deviceInfo = createDeviceInfo(logicalAddress, deviceType,
                                HdmiControlManager.POWER_STATUS_ON);
                        localDevice.setDeviceInfo(deviceInfo);
                        mCecController.addLocalDevice(deviceType, localDevice);
                        mCecController.addLogicalAddress(logicalAddress);
                        allocatedDevices.add(localDevice);
                    }

                        notifyAddressAllocated(allocatedDevices, initiatedBy);
                    }
                }
            });
        }
    }

    private void notifyAddressAllocated(ArrayList<HdmiCecLocalDevice> devices, int initiatedBy) {
        for (HdmiCecLocalDevice device : devices) {
            int address = device.getDeviceInfo().getLogicalAddress();
            device.handleAddressAllocated(address, initiatedBy);
        }
    }

HdmiCecLocalDevice.java
   final void handleAddressAllocated(int logicalAddress, int reason) {
       onAddressAllocated(logicalAddress, reason);
   }

HdmiCecLocalDevice has two type of specialization
1.HdmiCecLocalDeviceTv for local TV type
2.HdmiCecLocalDevicePlayback for a MediaPlayer(CEC enabled) device connected to HDMI input.
And based on type, respective onAddressAllocated callback will be made.
For now let's follow Tv,

HdmiCecLocalDeviceTv::onAddressAllocated(int logicalAddress, int reason) {

       mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
               mAddress, mService.getPhysicalAddress(), mDeviceType));
       mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
               mAddress, mService.getVendorId()));
   
       launchDeviceDiscovery();
   }

HdmiControlService will again notify to  CecController addreses and vendor-id as CECMessage,
HdmiControlService.java
    void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) {
            mCecController.sendCommand(command, callback);
            }

The CEC features are are encapsulated in multiple type of actions like OneTouchPlayAction,  OneTouchRecordAction, SystemAudioAction, VolumeControlAction, DeviceDiscoveryAction etc. which are started as needed, here first DeviceDiscoveryAction will be started.

HdmiCecLocalDeviceTv.java 
 void launchDeviceDiscovery() { 

       DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
               new DeviceDiscoveryCallback() {
                   @Override
                   public void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos) {
                       for (HdmiDeviceInfo info : deviceInfos) {
                           addCecDevice(info);
                       }
                       addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));

   }

    final void addCecDevice(HdmiDeviceInfo info) {
        invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE);
    }


    private void invokeDeviceEventListener(HdmiDeviceInfo info, int status) {

        if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) {
            mService.invokeDeviceEventListeners(info, status);
        }
    }

And finally HdmiControlService will invoke it's listeners, which is Tv Input Framework.

HdmiControlService::invokeDeviceEventListeners(HdmiDeviceInfo device, int status) {
                    record.mListener.onStatusChanged(device, status);
    }

In next post, we will see how HdmiControlManager.DEVICE_EVENT_ADD_DEVICE event is reached to a TvInputService implementation(like our implementation).