Implement handlers for system information query command.

System information query commands are stateless command and
it should be replied immediately when it receives query command.
Here is a list of command handler (and it's reply) implemented
in this change.
1. <Get Menu Language> -> <Set Menu Language>
2. <Give(Get) OSD Name> -> <Set OSD Name>
3. <Give Physical Address> -> <Report Physical Address>
4. <Give Device Vendor ID> -> <Device Vendor Id>
5. <Give CEC Version> -> <CEC Version>

In order to centralize all cec message building, added new
builder class, HdmiCecMessageBuilder for HdmiCecMessage.
Accordingly, all helper methods for building of HdmiCecMessage
are moved to HdmiCecMessageBuilder class.

Change-Id: Ib7c5d2b0cb3d69d51159af8bc277ffb49a60909b
diff --git a/services/core/java/com/android/server/hdmi/FeatureAction.java b/services/core/java/com/android/server/hdmi/FeatureAction.java
index 296cc5b..d747cd0 100644
--- a/services/core/java/com/android/server/hdmi/FeatureAction.java
+++ b/services/core/java/com/android/server/hdmi/FeatureAction.java
@@ -15,7 +15,6 @@
  */
 package com.android.server.hdmi;
 
-import android.hardware.hdmi.HdmiCec;
 import android.hardware.hdmi.HdmiCecMessage;
 import android.os.Handler;
 import android.os.Looper;
@@ -155,23 +154,10 @@
         mActionTimer.sendTimerMessage(state, delayMillis);
     }
 
-    static HdmiCecMessage buildCommand(int src, int dst, int opcode, byte[] params) {
-        return new HdmiCecMessage(src, dst, opcode, params);
-    }
-
-    // Build a CEC command that does not have parameter.
-    static HdmiCecMessage buildCommand(int src, int dst, int opcode) {
-        return new HdmiCecMessage(src, dst, opcode, HdmiCecMessage.EMPTY_PARAM);
-    }
-
     protected final void sendCommand(HdmiCecMessage cmd) {
         mService.sendCecCommand(cmd);
     }
 
-    protected final void sendBroadcastCommand(int opcode, byte[] param) {
-        sendCommand(buildCommand(mSourceAddress, HdmiCec.ADDR_BROADCAST, opcode, param));
-    }
-
     /**
      * Finish up the action. Reset the state, and remove itself from the action queue.
      */
@@ -189,23 +175,4 @@
     private void removeAction(FeatureAction action) {
         mService.removeAction(action);
     }
-
-    // Utility methods for generating parameter byte arrays for CEC commands.
-    protected static byte[] uiCommandParam(int uiCommand) {
-        return new byte[] {(byte) uiCommand};
-    }
-
-    protected static byte[] physicalAddressParam(int physicalAddress) {
-        return new byte[] {
-                (byte) ((physicalAddress >> 8) & 0xFF),
-                (byte) (physicalAddress & 0xFF)
-        };
-    }
-
-    protected static byte[] pathPairParam(int oldPath, int newPath) {
-        return new byte[] {
-                (byte) ((oldPath >> 8) & 0xFF), (byte) (oldPath & 0xFF),
-                (byte) ((newPath >> 8) & 0xFF), (byte) (newPath & 0xFF)
-        };
-    }
 }
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java
index b103a4d..5327ef4 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecController.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java
@@ -55,15 +55,6 @@
     // A message to report allocated logical address to main control looper.
     private final static int MSG_REPORT_LOGICAL_ADDRESS = 2;
 
-    // TODO: move these values to HdmiCec.java once make it internal constant class.
-    // CEC's ABORT reason values.
-    private static final int ABORT_UNRECOGNIZED_MODE = 0;
-    private static final int ABORT_NOT_IN_CORRECT_MODE = 1;
-    private static final int ABORT_CANNOT_PROVIDE_SOURCE = 2;
-    private static final int ABORT_INVALID_OPERAND = 3;
-    private static final int ABORT_REFUSED = 4;
-    private static final int ABORT_UNABLE_TO_DETERMINE = 5;
-
     private static final int NUM_LOGICAL_ADDRESS = 16;
 
     // TODO: define other constants for errors.
@@ -80,10 +71,16 @@
     // interacts with HAL.
     private long mNativePtr;
 
+    private HdmiControlService mService;
+
     // Map-like container of all cec devices. A logical address of device is
     // used as key of container.
     private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos =
             new SparseArray<HdmiCecDeviceInfo>();
+    // Set-like container for all local devices' logical address.
+    // Key and value are same.
+    private final SparseArray<Integer> mLocalLogicalAddresses =
+            new SparseArray<Integer>();
 
     // Private constructor.  Use HdmiCecController.create().
     private HdmiCecController() {
@@ -325,6 +322,7 @@
      */
     int addLogicalAddress(int newLogicalAddress) {
         if (HdmiCec.isValidAddress(newLogicalAddress)) {
+            mLocalLogicalAddresses.append(newLogicalAddress, newLogicalAddress);
             return nativeAddLogicalAddress(mNativePtr, newLogicalAddress);
         } else {
             return -1;
@@ -337,6 +335,9 @@
      * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
      */
     void clearLogicalAddress() {
+        // TODO: consider to backup logical address so that new logical address
+        // allocation can use it as preferred address.
+        mLocalLogicalAddresses.clear();
         nativeClearLogicalAddress(mNativePtr);
     }
 
@@ -371,30 +372,37 @@
     }
 
     private void init(HdmiControlService service, long nativePtr) {
+        mService = service;
         mIoHandler = new IoHandler(service.getServiceLooper());
         mControlHandler = new ControlHandler(service.getServiceLooper());
         mNativePtr = nativePtr;
     }
 
+    private boolean isAcceptableAddress(int address) {
+        // Can access command targeting devices available in local device or
+        // broadcast command.
+        return address == HdmiCec.ADDR_BROADCAST
+                || mLocalLogicalAddresses.get(address) != null;
+    }
+
     private void onReceiveCommand(HdmiCecMessage message) {
-        // TODO: Handle message according to opcode type.
+        if (isAcceptableAddress(message.getDestination()) &&
+                mService.handleCecCommand(message)) {
+            return;
+        }
 
         // TODO: Use device's source address for broadcast message.
         int sourceAddress = message.getDestination() != HdmiCec.ADDR_BROADCAST ?
                 message.getDestination() : 0;
         // Reply <Feature Abort> to initiator (source) for all requests.
-        sendFeatureAbort(sourceAddress, message.getSource(), message.getOpcode(),
-                ABORT_REFUSED);
+        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildFeatureAbortCommand
+                (sourceAddress, message.getSource(), message.getOpcode(),
+                        HdmiCecMessageBuilder.ABORT_REFUSED);
+        sendCommand(cecMessage);
+
     }
 
-    private void sendFeatureAbort(int srcAddress, int destAddress, int originalOpcode,
-            int reason) {
-        byte[] params = new byte[2];
-        params[0] = (byte) originalOpcode;
-        params[1] = (byte) reason;
-
-        HdmiCecMessage cecMessage = new HdmiCecMessage(srcAddress, destAddress,
-                HdmiCec.MESSAGE_FEATURE_ABORT, params);
+    void sendCommand(HdmiCecMessage cecMessage) {
         Message message = mIoHandler.obtainMessage(MSG_SEND_CEC_COMMAND, cecMessage);
         mIoHandler.sendMessage(message);
     }
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java
new file mode 100644
index 0000000..fc6183c
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageBuilder.java
@@ -0,0 +1,212 @@
+/*
+ * 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 com.android.server.hdmi;
+
+import android.hardware.hdmi.HdmiCec;
+import android.hardware.hdmi.HdmiCecMessage;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * A helper class to build {@link HdmiCecMessage} from various cec commands.
+ */
+public class HdmiCecMessageBuilder {
+    // TODO: move these values to HdmiCec.java once make it internal constant class.
+    // CEC's ABORT reason values.
+    static final int ABORT_UNRECOGNIZED_MODE = 0;
+    static final int ABORT_NOT_IN_CORRECT_MODE = 1;
+    static final int ABORT_CANNOT_PROVIDE_SOURCE = 2;
+    static final int ABORT_INVALID_OPERAND = 3;
+    static final int ABORT_REFUSED = 4;
+    static final int ABORT_UNABLE_TO_DETERMINE = 5;
+
+    private static final int OSD_NAME_MAX_LENGTH = 13;
+
+    private HdmiCecMessageBuilder() {}
+
+    /**
+     * Build &lt;Feature Abort&gt; command. &lt;Feature Abort&gt; consists of
+     * 1 byte original opcode and 1 byte reason fields with basic fields.
+     *
+     * @param src source address of command
+     * @param dest destination address of command
+     * @param originalOpcode original opcode causing feature abort
+     * @param reason reason of feature abort
+     * @return newly created {@link HdmiCecMessage}
+     */
+    static HdmiCecMessage buildFeatureAbortCommand(int src, int dest, int originalOpcode,
+            int reason) {
+        byte[] params = new byte[] {
+                (byte) originalOpcode,
+                (byte) reason,
+        };
+        return buildCommand(src, dest, HdmiCec.MESSAGE_FEATURE_ABORT, params);
+    }
+
+    /**
+     * Build &lt;Get Osd Name&gt; command.
+     *
+     * @param src source address of command
+     * @param dest destination address of command
+     * @return newly created {@link HdmiCecMessage}
+     */
+    static HdmiCecMessage buildGetOsdNameCommand(int src, int dest) {
+        return buildCommand(src, dest, HdmiCec.MESSAGE_GET_OSD_NAME);
+    }
+
+    /**
+     * Build &lt;Give Vendor Id Command&gt; command.
+     *
+     * @param src source address of command
+     * @param dest destination address of command
+     * @return newly created {@link HdmiCecMessage}
+     */
+    static HdmiCecMessage buildGiveDeviceVendorIdCommand(int src, int dest) {
+        return buildCommand(src, dest, HdmiCec.MESSAGE_GIVE_DEVICE_VENDOR_ID);
+    }
+
+    /**
+     * Build &lt;Set Menu Language &gt; command.
+     *
+     * <p>This is a broadcast message sent to all devices on the bus.
+     *
+     * @param src source address of command
+     * @param language 3-letter ISO639-2 based language code
+     * @return newly created {@link HdmiCecMessage} if language is valid.
+     *         Otherwise, return null
+     */
+    static HdmiCecMessage buildSetMenuLanguageCommand(int src, String language) {
+        if (language.length() != 3) {
+            return null;
+        }
+        // Hdmi CEC uses lower-cased ISO 639-2 (3 letters code).
+        String normalized = language.toLowerCase();
+        byte[] params = new byte[] {
+                (byte) normalized.charAt(0),
+                (byte) normalized.charAt(1),
+                (byte) normalized.charAt(2),
+        };
+        // <Set Menu Language> is broadcast message.
+        return buildCommand(src, HdmiCec.ADDR_BROADCAST, HdmiCec.MESSAGE_SET_MENU_LANGUAGE,
+                params);
+    }
+
+    /**
+     * Build &lt;Set Osd Name &gt; command.
+     *
+     * @param src source address of command
+     * @param name display (OSD) name of device
+     * @return newly created {@link HdmiCecMessage} if valid name. Otherwise,
+     *         return null
+     */
+    static HdmiCecMessage buildSetOsdNameCommand(int src, int dest, String name) {
+        int length = Math.min(name.length(), OSD_NAME_MAX_LENGTH);
+        byte[] params;
+        try {
+            params = name.substring(0, length).getBytes("US-ASCII");
+        } catch (UnsupportedEncodingException e) {
+            return null;
+        }
+        return buildCommand(src, dest, HdmiCec.MESSAGE_SET_OSD_NAME, params);
+    }
+
+    /**
+     * Build &lt;Report Physical Address&gt; command. It has two bytes physical
+     * address and one byte device type as parameter.
+     *
+     * <p>This is a broadcast message sent to all devices on the bus.
+     *
+     * @param src source address of command
+     * @param address physical address of device
+     * @param deviceType type of device
+     * @return newly created {@link HdmiCecMessage}
+     */
+    static HdmiCecMessage buildReportPhysicalAddressCommand(int src, int address, int deviceType) {
+        byte[] params = new byte[] {
+                // Two bytes for physical address
+                (byte) ((address >> 8) & 0xFF),
+                (byte) (address & 0xFF),
+                // One byte device type
+                (byte) deviceType
+        };
+        // <Report Physical Address> is broadcast message.
+        return buildCommand(src, HdmiCec.ADDR_BROADCAST, HdmiCec.MESSAGE_REPORT_PHYSICAL_ADDRESS,
+                params);
+    }
+
+    /**
+     * Build &lt;Device Vendor Id&gt; command. It has three bytes vendor id as
+     * parameter.
+     *
+     * <p>This is a broadcast message sent to all devices on the bus.
+     *
+     * @param src source address of command
+     * @param vendorId device's vendor id
+     * @return newly created {@link HdmiCecMessage}
+     */
+    static HdmiCecMessage buildDeviceVendorIdCommand(int src, int vendorId) {
+        byte[] params = new byte[] {
+                (byte) ((vendorId >> 16) & 0xFF),
+                (byte) ((vendorId >> 8) & 0xFF),
+                (byte) (vendorId & 0xFF)
+        };
+        // <Device Vendor Id> is broadcast message.
+        return buildCommand(src, HdmiCec.ADDR_BROADCAST, HdmiCec.MESSAGE_DEVICE_VENDOR_ID,
+                params);
+    }
+
+    /**
+     * Build &lt;Device Vendor Id&gt; command. It has one byte cec version as parameter.
+     *
+     * @param src source address of command
+     * @param dest destination address of command
+     * @param version version of cec. Use 0x04 for "Version 1.3a" and 0x05 for
+     *                "Version 1.4 or 1.4a or 1.4b
+     * @return newly created {@link HdmiCecMessage}
+     */
+    static HdmiCecMessage buildCecVersion(int src, int dest, int version) {
+        byte[] params = new byte[] {
+                (byte) version
+        };
+        return buildCommand(src, dest, HdmiCec.MESSAGE_CEC_VERSION, params);
+    }
+
+    /**
+     * Build a {@link HdmiCecMessage} without extra parameter.
+     *
+     * @param src source address of command
+     * @param dest destination address of command
+     * @param opcode opcode for a message
+     * @return newly created {@link HdmiCecMessage}
+     */
+    private static HdmiCecMessage buildCommand(int src, int dest, int opcode) {
+        return new HdmiCecMessage(src, dest, opcode, HdmiCecMessage.EMPTY_PARAM);
+    }
+
+    /**
+     * Build a {@link HdmiCecMessage} with given values.
+     *
+     * @param src source address of command
+     * @param dest destination address of command
+     * @param opcode opcode for a message
+     * @param params extra parameters for command
+     * @return newly created {@link HdmiCecMessage}
+     */
+    private static HdmiCecMessage buildCommand(int src, int dest, int opcode, byte[] params) {
+        return new HdmiCecMessage(src, dest, opcode, params);
+    }
+}
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index f99c717..c122645 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -18,6 +18,7 @@
 
 import android.annotation.Nullable;
 import android.content.Context;
+import android.hardware.hdmi.HdmiCec;
 import android.hardware.hdmi.HdmiCecDeviceInfo;
 import android.hardware.hdmi.HdmiCecMessage;
 import android.os.HandlerThread;
@@ -26,6 +27,8 @@
 
 import com.android.server.SystemService;
 
+import java.util.Locale;
+
 /**
  * Provides a service for sending and processing HDMI control messages,
  * HDMI-CEC and MHL control command, and providing the information on both standard.
@@ -105,7 +108,7 @@
      * @param command CEC command to send out
      */
     void sendCecCommand(HdmiCecMessage command) {
-        // TODO: Implement this.
+        mCecController.sendCommand(command);
     }
 
     /**
@@ -116,4 +119,89 @@
     void addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
         // TODO: Implement this.
     }
+
+    boolean handleCecCommand(HdmiCecMessage message) {
+        // Commands that queries system information replies directly instead
+        // of creating FeatureAction because they are state-less.
+        switch (message.getOpcode()) {
+            case HdmiCec.MESSAGE_GET_MENU_LANGUAGE:
+                handleGetMenuLanguage(message);
+                return true;
+            case HdmiCec.MESSAGE_GET_OSD_NAME:
+                handleGetOsdName(message);
+                return true;
+            case HdmiCec.MESSAGE_GIVE_PHYSICAL_ADDRESS:
+                handleGivePhysicalAddress(message);
+                return true;
+            case HdmiCec.MESSAGE_GIVE_DEVICE_VENDOR_ID:
+                handleGiveDeviceVendorId(message);
+                return true;
+            case HdmiCec.MESSAGE_GET_CEC_VERSION:
+                handleGetCecVersion(message);
+                return true;
+            // TODO: Add remaining system information query such as
+            // <Give Device Power Status> and <Request Active Source> handler.
+            default:
+                Slog.w(TAG, "Unsupported cec command:" + message.toString());
+                return false;
+        }
+    }
+
+    private void handleGetCecVersion(HdmiCecMessage message) {
+        int version = mCecController.getVersion();
+        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildCecVersion(message.getDestination(),
+                message.getSource(),
+                version);
+        sendCecCommand(cecMessage);
+    }
+
+    private void handleGiveDeviceVendorId(HdmiCecMessage message) {
+        int vendorId = mCecController.getVendorId();
+        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
+                message.getDestination(), vendorId);
+        sendCecCommand(cecMessage);
+    }
+
+    private void handleGivePhysicalAddress(HdmiCecMessage message) {
+        int physicalAddress = mCecController.getPhysicalAddress();
+        int deviceType = HdmiCec.getTypeFromAddress(message.getDestination());
+        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                message.getDestination(), physicalAddress, deviceType);
+        sendCecCommand(cecMessage);
+    }
+
+    private void handleGetOsdName(HdmiCecMessage message) {
+        // TODO: read device name from settings or property.
+        String name = HdmiCec.getDefaultDeviceName(message.getDestination());
+        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildSetOsdNameCommand(
+                message.getDestination(), message.getSource(), name);
+        if (cecMessage != null) {
+            sendCecCommand(cecMessage);
+        } else {
+            Slog.w(TAG, "Failed to build <Get Osd Name>:" + name);
+        }
+    }
+
+    private void handleGetMenuLanguage(HdmiCecMessage message) {
+        // Only 0 (TV), 14 (specific use) can answer.
+        if (message.getDestination() != HdmiCec.ADDR_TV
+                && message.getDestination() != HdmiCec.ADDR_SPECIFIC_USE) {
+            Slog.w(TAG, "Only TV can handle <Get Menu Language>:" + message.toString());
+            sendCecCommand(
+                    HdmiCecMessageBuilder.buildFeatureAbortCommand(message.getDestination(),
+                            message.getSource(), HdmiCec.MESSAGE_GET_MENU_LANGUAGE,
+                            HdmiCecMessageBuilder.ABORT_UNRECOGNIZED_MODE));
+            return;
+        }
+
+        HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
+                message.getDestination(),
+                Locale.getDefault().getISO3Language());
+        // TODO: figure out how to handle failed to get language code.
+        if (command != null) {
+            sendCecCommand(command);
+        } else {
+            Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/hdmi/NewDeviceAction.java b/services/core/java/com/android/server/hdmi/NewDeviceAction.java
index c84a067..98da280 100644
--- a/services/core/java/com/android/server/hdmi/NewDeviceAction.java
+++ b/services/core/java/com/android/server/hdmi/NewDeviceAction.java
@@ -71,7 +71,8 @@
     @Override
     public boolean start() {
         sendCommand(
-                buildCommand(mSourceAddress, mDeviceLogicalAddress, HdmiCec.MESSAGE_GET_OSD_NAME));
+                HdmiCecMessageBuilder.buildGetOsdNameCommand(mSourceAddress,
+                        mDeviceLogicalAddress));
         mState = STATE_WAITING_FOR_SET_OSD_NAME;
         addTimer(mState, TIMEOUT_MS);
         return true;
@@ -132,8 +133,8 @@
     }
 
     private void requestVendorId() {
-        sendCommand(buildCommand(mSourceAddress, mDeviceLogicalAddress,
-                HdmiCec.MESSAGE_GIVE_DEVICE_VENDOR_ID));
+        sendCommand(HdmiCecMessageBuilder.buildGiveDeviceVendorIdCommand(mSourceAddress,
+                mDeviceLogicalAddress));
         addTimer(mState, TIMEOUT_MS);
     }