Add vendor-specific command API for HdmiControl

Vendor-specific commands are not handled by the service. This CL
opens an API for vendors to implement customized handling of
CEC commands specific to their needs.

Change-Id: I8bfa3b891bd7994a903b3b41d7c2b27464167afa
diff --git a/Android.mk b/Android.mk
index 581f2da..6859678 100644
--- a/Android.mk
+++ b/Android.mk
@@ -153,6 +153,7 @@
 	core/java/android/hardware/hdmi/IHdmiHotplugEventListener.aidl \
 	core/java/android/hardware/hdmi/IHdmiInputChangeListener.aidl \
 	core/java/android/hardware/hdmi/IHdmiSystemAudioModeChangeListener.aidl \
+	core/java/android/hardware/hdmi/IHdmiVendorCommandListener.aidl \
 	core/java/android/hardware/input/IInputManager.aidl \
 	core/java/android/hardware/input/IInputDevicesChangedListener.aidl \
 	core/java/android/hardware/location/IFusedLocationHardware.aidl \
diff --git a/core/java/android/hardware/hdmi/HdmiClient.java b/core/java/android/hardware/hdmi/HdmiClient.java
new file mode 100644
index 0000000..7bdcca2
--- /dev/null
+++ b/core/java/android/hardware/hdmi/HdmiClient.java
@@ -0,0 +1,51 @@
+package android.hardware.hdmi;
+
+import android.annotation.SystemApi;
+import android.hardware.hdmi.HdmiControlManager.VendorCommandListener;
+import android.hardware.hdmi.IHdmiVendorCommandListener;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * Parent for classes of various HDMI-CEC device type used to access
+ * {@link HdmiControlService}. Contains methods and data used in common.
+ *
+ * @hide
+ */
+public abstract class HdmiClient {
+    private static final String TAG = "HdmiClient";
+
+    protected final IHdmiControlService mService;
+
+    protected abstract int getDeviceType();
+
+    public HdmiClient(IHdmiControlService service) {
+        mService = service;
+    }
+
+    public void sendVendorCommand(int targetAddress, byte[] params, boolean hasVendorId) {
+        try {
+            mService.sendVendorCommand(getDeviceType(), targetAddress, params, hasVendorId);
+        } catch (RemoteException e) {
+            Log.e(TAG, "failed to send vendor command: ", e);
+        }
+    }
+
+    public void addVendorCommandListener(VendorCommandListener listener) {
+        try {
+            mService.addVendorCommandListener(getListenerWrapper(listener), getDeviceType());
+        } catch (RemoteException e) {
+            Log.e(TAG, "failed to add vendor command listener: ", e);
+        }
+    }
+
+    private static IHdmiVendorCommandListener getListenerWrapper(
+            final VendorCommandListener listener) {
+        return new IHdmiVendorCommandListener.Stub() {
+            @Override
+            public void onReceived(int srcAddress, byte[] params, boolean hasVendorId) {
+                listener.onReceived(srcAddress, params, hasVendorId);
+            }
+        };
+    }
+}
diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java
index 80ec6f8..53d1476 100644
--- a/core/java/android/hardware/hdmi/HdmiControlManager.java
+++ b/core/java/android/hardware/hdmi/HdmiControlManager.java
@@ -130,6 +130,21 @@
     }
 
     /**
+     * Listener used to get vendor-specific commands.
+     */
+    public interface VendorCommandListener {
+        /**
+         * Called when a vendor command is received.
+         *
+         * @param srcAddress source logical address
+         * @param params vendor-specific parameters
+         * @param hasVendorId {@code true} if the command is <Vendor Command
+         *        With ID>. The first 3 bytes of params is vendor id.
+         */
+        void onReceived(int srcAddress, byte[] params, boolean hasVendorId);
+    }
+
+    /**
      * Adds a listener to get informed of {@link HdmiHotplugEvent}.
      *
      * <p>To stop getting the notification,
diff --git a/core/java/android/hardware/hdmi/HdmiPlaybackClient.java b/core/java/android/hardware/hdmi/HdmiPlaybackClient.java
index d7e9a9a..74cdc4e 100644
--- a/core/java/android/hardware/hdmi/HdmiPlaybackClient.java
+++ b/core/java/android/hardware/hdmi/HdmiPlaybackClient.java
@@ -18,7 +18,6 @@
 
 import android.annotation.SystemApi;
 import android.os.RemoteException;
-
 import android.util.Log;
 
 /**
@@ -30,11 +29,9 @@
  * @hide
  */
 @SystemApi
-public final class HdmiPlaybackClient {
+public final class HdmiPlaybackClient extends HdmiClient {
     private static final String TAG = "HdmiPlaybackClient";
 
-    private final IHdmiControlService mService;
-
     /**
      * Listener used by the client to get the result of one touch play operation.
      */
@@ -66,7 +63,7 @@
     }
 
     HdmiPlaybackClient(IHdmiControlService service) {
-        mService = service;
+        super(service);
     }
 
     /**
@@ -85,6 +82,10 @@
         }
     }
 
+    public int getDeviceType() {
+        return HdmiCecDeviceInfo.DEVICE_PLAYBACK;
+    }
+
     /**
      * Get the status of display device connected through HDMI bus.
      *
diff --git a/core/java/android/hardware/hdmi/HdmiTvClient.java b/core/java/android/hardware/hdmi/HdmiTvClient.java
index 6f65efc..1285ccb 100644
--- a/core/java/android/hardware/hdmi/HdmiTvClient.java
+++ b/core/java/android/hardware/hdmi/HdmiTvClient.java
@@ -16,6 +16,8 @@
 package android.hardware.hdmi;
 
 import android.annotation.SystemApi;
+import android.hardware.hdmi.HdmiControlManager.VendorCommandListener;
+import android.hardware.hdmi.IHdmiVendorCommandListener;
 import android.os.RemoteException;
 import android.util.Log;
 
@@ -27,7 +29,7 @@
  * @hide
  */
 @SystemApi
-public final class HdmiTvClient {
+public final class HdmiTvClient extends HdmiClient {
     private static final String TAG = "HdmiTvClient";
 
     // Definitions used for setOption(). These should be in sync with the definition
@@ -79,10 +81,8 @@
     public static final int DISABLED = 0;
     public static final int ENABLED = 1;
 
-    private final IHdmiControlService mService;
-
     HdmiTvClient(IHdmiControlService service) {
-        mService = service;
+        super(service);
     }
 
     // Factory method for HdmiTvClient.
@@ -91,6 +91,10 @@
         return new HdmiTvClient(service);
     }
 
+    public int getDeviceType() {
+        return HdmiCecDeviceInfo.DEVICE_TV;
+    }
+
     /**
      * Callback interface used to get the result of {@link #deviceSelect}.
      */
diff --git a/core/java/android/hardware/hdmi/IHdmiControlService.aidl b/core/java/android/hardware/hdmi/IHdmiControlService.aidl
index 3f0f2ae..3a477fb 100644
--- a/core/java/android/hardware/hdmi/IHdmiControlService.aidl
+++ b/core/java/android/hardware/hdmi/IHdmiControlService.aidl
@@ -23,6 +23,7 @@
 import android.hardware.hdmi.IHdmiHotplugEventListener;
 import android.hardware.hdmi.IHdmiInputChangeListener;
 import android.hardware.hdmi.IHdmiSystemAudioModeChangeListener;
+import android.hardware.hdmi.IHdmiVendorCommandListener;
 
 import java.util.List;
 
@@ -57,4 +58,7 @@
     oneway void setSystemAudioMute(boolean mute);
     void setInputChangeListener(IHdmiInputChangeListener listener);
     List<HdmiCecDeviceInfo> getInputDevices();
+    void sendVendorCommand(int deviceType, int targetAddress, in byte[] params,
+            boolean hasVendorId);
+    void addVendorCommandListener(IHdmiVendorCommandListener listener, int deviceType);
 }
diff --git a/core/java/android/hardware/hdmi/IHdmiVendorCommandListener.aidl b/core/java/android/hardware/hdmi/IHdmiVendorCommandListener.aidl
new file mode 100644
index 0000000..55cc925
--- /dev/null
+++ b/core/java/android/hardware/hdmi/IHdmiVendorCommandListener.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+package android.hardware.hdmi;
+
+/**
+ * Callback interface definition for HDMI client to get the vendor-specific
+ * commands.
+ *
+ * @hide
+ */
+oneway interface IHdmiVendorCommandListener {
+    void onReceived(int logicalAddress, in byte[] operands, boolean hasVendorId);
+}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index e87db50..43debda 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -156,6 +156,10 @@
                 return handleSetStreamPath(message);
             case Constants.MESSAGE_GIVE_DEVICE_POWER_STATUS:
                 return handleGiveDevicePowerStatus(message);
+            case Constants.MESSAGE_VENDOR_COMMAND:
+                return handleVendorCommand(message);
+            case Constants.MESSAGE_VENDOR_COMMAND_WITH_ID:
+                return handleVendorCommandWithId(message);
             default:
                 return false;
         }
@@ -244,10 +248,6 @@
         return true;
     }
 
-    protected boolean handleVendorSpecificCommand(HdmiCecMessage message) {
-        return false;
-    }
-
     protected boolean handleRoutingChange(HdmiCecMessage message) {
         return false;
     }
@@ -337,6 +337,29 @@
         return true;
     }
 
+    protected boolean handleVendorCommand(HdmiCecMessage message) {
+        mService.invokeVendorCommandListeners(mDeviceType, message.getSource(),
+                message.getParams(), false);
+        return true;
+    }
+
+    protected boolean handleVendorCommandWithId(HdmiCecMessage message) {
+        byte[] params = message.getParams();
+        int vendorId = HdmiUtils.threeBytesToInt(params);
+        if (vendorId == mService.getVendorId()) {
+            mService.invokeVendorCommandListeners(mDeviceType, message.getSource(), params, true);
+        } else if (message.getDestination() != Constants.ADDR_BROADCAST &&
+                message.getSource() != Constants.ADDR_UNREGISTERED) {
+            Slog.v(TAG, "Wrong direct vendor command. Replying with <Feature Abort>");
+            mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand(mAddress,
+                    message.getSource(), Constants.MESSAGE_VENDOR_COMMAND_WITH_ID,
+                    Constants.ABORT_UNRECOGNIZED_MODE));
+        } else {
+            Slog.v(TAG, "Wrong broadcast vendor command. Ignoring");
+        }
+        return true;
+    }
+
     @ServiceThreadOnly
     final void handleAddressAllocated(int logicalAddress) {
         assertRunOnServiceThread();
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 223deab..13d42a5 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -404,33 +404,6 @@
 
     @Override
     @ServiceThreadOnly
-    protected boolean handleVendorSpecificCommand(HdmiCecMessage message) {
-        assertRunOnServiceThread();
-        List<VendorSpecificAction> actions = Collections.emptyList();
-        // TODO: Call mService.getActions(VendorSpecificAction.class) to get all the actions.
-
-        // We assume that there can be multiple vendor-specific command actions running
-        // at the same time. Pass the message to each action to see if one of them needs it.
-        for (VendorSpecificAction action : actions) {
-            if (action.processCommand(message)) {
-                return true;
-            }
-        }
-        // Handle the message here if it is not already consumed by one of the running actions.
-        // Respond with a appropriate vendor-specific command or <Feature Abort>, or create another
-        // vendor-specific action:
-        //
-        // mService.addAndStartAction(new VendorSpecificAction(mService, mAddress));
-        //
-        // For now, simply reply with <Feature Abort> and mark it consumed by returning true.
-        mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand(
-                message.getDestination(), message.getSource(), message.getOpcode(),
-                Constants.ABORT_REFUSED));
-        return true;
-    }
-
-    @Override
-    @ServiceThreadOnly
     protected boolean handleReportAudioStatus(HdmiCecMessage message) {
         assertRunOnServiceThread();
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessage.java b/services/core/java/com/android/server/hdmi/HdmiCecMessage.java
index c0ff81d..970568a 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessage.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessage.java
@@ -172,6 +172,10 @@
                 return "Active Source";
             case Constants.MESSAGE_GIVE_DEVICE_POWER_STATUS:
                 return "Give Device Power Status";
+            case Constants.MESSAGE_VENDOR_COMMAND:
+                return "Vendor Command";
+            case Constants.MESSAGE_VENDOR_COMMAND_WITH_ID:
+                return "Vendor Command With ID";
             default:
                 return String.format("Opcode: %02X", opcode);
         }
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java
index fe35b24..57f89b1 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java
@@ -424,6 +424,37 @@
         return buildCommand(src, dest, Constants.MESSAGE_STANDBY);
     }
 
+    /**
+     * Build &lt;Vendor Command&gt; command.
+     *
+     * @param src source address of command
+     * @param dest destination address of command
+     * @param params vendor-specific parameters
+     * @return newly created {@link HdmiCecMessage}
+     */
+    static HdmiCecMessage buildVendorCommand(int src, int dest, byte[] params) {
+        return buildCommand(src, dest, Constants.MESSAGE_VENDOR_COMMAND, params);
+    }
+
+    /**
+     * Build &lt;Vendor Command With ID&gt; command.
+     *
+     * @param src source address of command
+     * @param dest destination address of command
+     * @param vendorId vendor ID
+     * @param operands vendor-specific parameters
+     * @return newly created {@link HdmiCecMessage}
+     */
+    static HdmiCecMessage buildVendorCommandWithId(int src, int dest, int vendorId,
+            byte[] operands) {
+        byte[] params = new byte[operands.length + 3];  // parameter plus len(vendorId)
+        params[0] = (byte) ((vendorId >> 16) & 0xFF);
+        params[1] = (byte) ((vendorId >> 8) & 0xFF);
+        params[2] = (byte) (vendorId & 0xFF);
+        System.arraycopy(operands, 0, params, 3, operands.length);
+        return buildCommand(src, dest, Constants.MESSAGE_VENDOR_COMMAND_WITH_ID, params);
+    }
+
     /***** Please ADD new buildXXX() methods above. ******/
 
     /**
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index f927cb6..65eea30 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -32,6 +32,7 @@
 import android.hardware.hdmi.IHdmiHotplugEventListener;
 import android.hardware.hdmi.IHdmiInputChangeListener;
 import android.hardware.hdmi.IHdmiSystemAudioModeChangeListener;
+import android.hardware.hdmi.IHdmiVendorCommandListener;
 import android.media.AudioManager;
 import android.os.Build;
 import android.os.Handler;
@@ -140,6 +141,11 @@
     private final ArrayList<DeviceEventListenerRecord> mDeviceEventListenerRecords =
             new ArrayList<>();
 
+    // List of records for vendor command listener to handle the the caller killed in action.
+    @GuardedBy("mLock")
+    private final ArrayList<VendorCommandListenerRecord> mVendorCommandListenerRecords =
+            new ArrayList<>();
+
     @GuardedBy("mLock")
     private IHdmiInputChangeListener mInputChangeListener;
 
@@ -608,6 +614,23 @@
         }
     }
 
+    class VendorCommandListenerRecord implements IBinder.DeathRecipient {
+        private final IHdmiVendorCommandListener mListener;
+        private final int mDeviceType;
+
+        public VendorCommandListenerRecord(IHdmiVendorCommandListener listener, int deviceType) {
+            mListener = listener;
+            mDeviceType = deviceType;
+        }
+
+        @Override
+        public void binderDied() {
+            synchronized (mLock) {
+                mVendorCommandListenerRecords.remove(this);
+            }
+        }
+    }
+
     private void enforceAccessPermission() {
         getContext().enforceCallingOrSelfPermission(PERMISSION, TAG);
     }
@@ -917,6 +940,42 @@
             }
             HdmiControlService.this.setProhibitMode(enabled);
         }
+
+        @Override
+        public void addVendorCommandListener(final IHdmiVendorCommandListener listener,
+                final int deviceType) {
+            enforceAccessPermission();
+            runOnServiceThread(new Runnable() {
+                @Override
+                public void run() {
+                    HdmiControlService.this.addVendorCommandListener(listener, deviceType);
+                }
+            });
+        }
+
+        @Override
+        public void sendVendorCommand(final int deviceType, final int targetAddress,
+                final byte[] params, final boolean hasVendorId) {
+            enforceAccessPermission();
+            runOnServiceThread(new Runnable() {
+                @Override
+                public void run() {
+                    HdmiCecLocalDevice device = mCecController.getLocalDevice(deviceType);
+                    if (device == null) {
+                        Slog.w(TAG, "Local device not available");
+                        return;
+                    }
+                    if (hasVendorId) {
+                        sendCecCommand(HdmiCecMessageBuilder.buildVendorCommandWithId(
+                                device.getDeviceInfo().getLogicalAddress(), targetAddress,
+                                getVendorId(), params));
+                    } else {
+                        sendCecCommand(HdmiCecMessageBuilder.buildVendorCommand(
+                                device.getDeviceInfo().getLogicalAddress(), targetAddress, params));
+                    }
+                }
+            });
+         }
     }
 
     @ServiceThreadOnly
@@ -1197,6 +1256,35 @@
         mCecController.setOption(HdmiTvClient.OPTION_CEC_SERVICE_CONTROL, HdmiTvClient.DISABLED);
     }
 
+    private void addVendorCommandListener(IHdmiVendorCommandListener listener, int deviceType) {
+        VendorCommandListenerRecord record = new VendorCommandListenerRecord(listener, deviceType);
+        try {
+            listener.asBinder().linkToDeath(record, 0);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Listener already died");
+            return;
+        }
+        synchronized (mLock) {
+            mVendorCommandListenerRecords.add(record);
+        }
+    }
+
+    void invokeVendorCommandListeners(int deviceType, int srcAddress, byte[] params,
+            boolean hasVendorId) {
+        synchronized (mLock) {
+            for (VendorCommandListenerRecord record : mVendorCommandListenerRecords) {
+                if (record.mDeviceType != deviceType) {
+                    continue;
+                }
+                try {
+                    record.mListener.onReceived(srcAddress, params, hasVendorId);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Failed to notify vendor command reception", e);
+                }
+            }
+        }
+    }
+
     boolean isProhibitMode() {
         synchronized (mLock) {
             return mProhibitMode;
diff --git a/services/core/java/com/android/server/hdmi/VendorSpecificAction.java b/services/core/java/com/android/server/hdmi/VendorSpecificAction.java
deleted file mode 100644
index ff21a57..0000000
--- a/services/core/java/com/android/server/hdmi/VendorSpecificAction.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.android.server.hdmi;
-
-/**
- * Handles vendor-specific commands that require a sequence of command exchange,
- * or need to manage some states to complete the processing.
- */
-public class VendorSpecificAction extends FeatureAction {
-
-    // Sample state this action can be in.
-    private static final int STATE_1 = 1;
-    private static final int STATE_2 = 2;
-
-    VendorSpecificAction(HdmiCecLocalDevice source) {
-        super(source);
-        // Modify the constructor if additional arguments are necessary.
-    }
-
-    @Override
-    boolean start() {
-        // Do initialization step and update the state accordingly here.
-        mState = STATE_1;
-        addTimer(STATE_1, TIMEOUT_MS);
-        return true;
-    }
-
-    @Override
-    boolean processCommand(HdmiCecMessage cmd) {
-        // Returns true if the command was consumed. Otherwise return false for other
-        // actions in progress can be given its turn to process it.
-        return false;
-    }
-
-    @Override
-    void handleTimerEvent(int state) {
-        // Ignore the timer event if the current state and the state this event should be
-        // handled in are different. Could be an outdated event which should have been cleared by
-        // calling {@code mActionTimer.clearTimerMessage()}.
-        if (mState != state) {
-            return;
-        }
-
-        switch (state) {
-            case STATE_1:
-                mState = STATE_2;
-                addTimer(STATE_2, TIMEOUT_MS);
-                break;
-            case STATE_2:
-                finish();
-                break;
-        }
-    }
-}