CEC: Handle Remote Control command

Generate an Android key event when a UserControl message is received.
And report menu state active when a MenuRequest message is received to
notify sender can send a UserControl message.

Bug: 16938007
Change-Id: Id8f393dc254508b9e7a6fa203f8e817fbe807e38
diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java
index b55cf62..48a0b68 100644
--- a/services/core/java/com/android/server/hdmi/Constants.java
+++ b/services/core/java/com/android/server/hdmi/Constants.java
@@ -173,6 +173,10 @@
     static final int SYSTEM_AUDIO_STATUS_OFF = 0;
     static final int SYSTEM_AUDIO_STATUS_ON = 1;
 
+    // [Menu State]
+    static final int MENU_STATE_ACTIVATED = 0;
+    static final int MENU_STATE_DEACTIVATED = 1;
+
     // Bit mask used to get the routing path of the top level device.
     // When &'d with the path 1.2.2.0 (0x1220), for instance, gives 1.0.0.0.
     static final int ROUTING_PATH_TOP_MASK = 0xF000;
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index b894fd7..4862f93 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -17,10 +17,15 @@
 package com.android.server.hdmi;
 
 import android.hardware.hdmi.HdmiDeviceInfo;
+import android.hardware.input.InputManager;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.os.SystemClock;
 import android.util.Slog;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
@@ -39,15 +44,21 @@
     private static final String TAG = "HdmiCecLocalDevice";
 
     private static final int MSG_DISABLE_DEVICE_TIMEOUT = 1;
+    private static final int MSG_USER_CONTROL_RELEASE_TIMEOUT = 2;
     // Timeout in millisecond for device clean up (5s).
     // Normal actions timeout is 2s but some of them would have several sequence of timeout.
     private static final int DEVICE_CLEANUP_TIMEOUT = 5000;
+    // Within the timer, a received <User Control Pressed> will start "Press and Hold" behavior.
+    // When it expires, we can assume <User Control Release> is received.
+    private static final int FOLLOWER_SAFETY_TIMEOUT = 550;
 
     protected final HdmiControlService mService;
     protected final int mDeviceType;
     protected int mAddress;
     protected int mPreferredAddress;
     protected HdmiDeviceInfo mDeviceInfo;
+    protected int mLastKeycode = HdmiCecKeycode.UNSUPPORTED_KEYCODE;
+    protected int mLastKeyRepeatCount = 0;
 
     static class ActiveSource {
         int logicalAddress;
@@ -111,6 +122,9 @@
                 case MSG_DISABLE_DEVICE_TIMEOUT:
                     handleDisableDeviceTimeout();
                     break;
+                case MSG_USER_CONTROL_RELEASE_TIMEOUT:
+                    handleUserControlReleased();
+                    break;
             }
         }
     };
@@ -230,10 +244,14 @@
                 return handleImageViewOn(message);
             case Constants.MESSAGE_USER_CONTROL_PRESSED:
                 return handleUserControlPressed(message);
+            case Constants.MESSAGE_USER_CONTROL_RELEASED:
+                return handleUserControlReleased();
             case Constants.MESSAGE_SET_STREAM_PATH:
                 return handleSetStreamPath(message);
             case Constants.MESSAGE_GIVE_DEVICE_POWER_STATUS:
                 return handleGiveDevicePowerStatus(message);
+            case Constants.MESSAGE_MENU_REQUEST:
+                return handleGiveDeviceMenuStatus(message);
             case Constants.MESSAGE_VENDOR_COMMAND:
                 return handleVendorCommand(message);
             case Constants.MESSAGE_VENDOR_COMMAND_WITH_ID:
@@ -376,6 +394,7 @@
     @ServiceThreadOnly
     protected boolean handleUserControlPressed(HdmiCecMessage message) {
         assertRunOnServiceThread();
+        mHandler.removeMessages(MSG_USER_CONTROL_RELEASE_TIMEOUT);
         if (mService.isPowerOnOrTransient() && isPowerOffOrToggleCommand(message)) {
             mService.standby();
             return true;
@@ -383,9 +402,54 @@
             mService.wakeUp();
             return true;
         }
+
+        final long downTime = SystemClock.uptimeMillis();
+        final byte[] params = message.getParams();
+        final int keycode = HdmiCecKeycode.cecKeyToAndroidKey(params[0],
+                params.length > 1 ? params[1] : HdmiCecKeycode.NO_PARAM);
+        int keyRepeatCount = 0;
+        if (mLastKeycode != HdmiCecKeycode.UNSUPPORTED_KEYCODE) {
+            if (keycode == mLastKeycode) {
+                keyRepeatCount = mLastKeyRepeatCount + 1;
+            } else {
+                injectKeyEvent(downTime, KeyEvent.ACTION_UP, mLastKeycode, 0);
+            }
+        }
+        mLastKeycode = keycode;
+        mLastKeyRepeatCount = keyRepeatCount;
+
+        if (keycode != HdmiCecKeycode.UNSUPPORTED_KEYCODE) {
+            injectKeyEvent(downTime, KeyEvent.ACTION_DOWN, keycode, keyRepeatCount);
+            mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_USER_CONTROL_RELEASE_TIMEOUT),
+                    FOLLOWER_SAFETY_TIMEOUT);
+            return true;
+        }
         return false;
     }
 
+    @ServiceThreadOnly
+    protected boolean handleUserControlReleased() {
+        assertRunOnServiceThread();
+        mHandler.removeMessages(MSG_USER_CONTROL_RELEASE_TIMEOUT);
+        mLastKeyRepeatCount = 0;
+        if (mLastKeycode != HdmiCecKeycode.UNSUPPORTED_KEYCODE) {
+            final long upTime = SystemClock.uptimeMillis();
+            injectKeyEvent(upTime, KeyEvent.ACTION_UP, mLastKeycode, 0);
+            mLastKeycode = HdmiCecKeycode.UNSUPPORTED_KEYCODE;
+            return true;
+        }
+        return false;
+    }
+
+    static void injectKeyEvent(long time, int action, int keycode, int repeat) {
+        KeyEvent keyEvent = KeyEvent.obtain(time, time, action, keycode,
+                repeat, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FROM_SYSTEM,
+                InputDevice.SOURCE_HDMI, null);
+        InputManager.getInstance().injectInputEvent(keyEvent,
+                InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
+        keyEvent.recycle();
+   }
+
     static boolean isPowerOnOrToggleCommand(HdmiCecMessage message) {
         byte[] params = message.getParams();
         return message.getOpcode() == Constants.MESSAGE_USER_CONTROL_PRESSED
@@ -420,6 +484,13 @@
         return true;
     }
 
+    protected boolean handleGiveDeviceMenuStatus(HdmiCecMessage message) {
+        // Always report menu active to receive Remote Control.
+        mService.sendCecCommand(HdmiCecMessageBuilder.buildReportMenuStatus(
+                mAddress, message.getSource(), Constants.MENU_STATE_ACTIVATED));
+        return true;
+    }
+
     protected boolean handleVendorCommand(HdmiCecMessage message) {
         mService.invokeVendorCommandListeners(mDeviceType, message.getSource(),
                 message.getParams(), false);
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java
index 0855bfa..b53cd45 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java
@@ -338,6 +338,21 @@
     }
 
     /**
+     * Build &lt;Report Menu Status&gt; command.
+     *
+     * @param src source address of command
+     * @param dest destination address of command
+     * @param menuStatus menu status of the device
+     * @return newly created {@link HdmiCecMessage}
+     */
+    static HdmiCecMessage buildReportMenuStatus(int src, int dest, int menuStatus) {
+        byte[] param = new byte[] {
+                (byte) (menuStatus)
+        };
+        return buildCommand(src, dest, Constants.MESSAGE_MENU_STATUS, param);
+    }
+
+    /**
      * Build &lt;System Audio Mode Request&gt; command.
      *
      * @param src source address of command