Add physical port to DisplayViewport

DisplayViewport now contains the information about the physical port
that the corresponding display is connected to (for example, HDMI1,
HDMI2, etc).
This information is needed in order to determine which input device is
associated with which display.

Add a new config file to vendor directory that will contain the actual
associations.

Bug: 116239493
Test: atest ConfigurationProcessorTest
Change-Id: I679203747753803e9635a4eaf74287ce7e69dc3f
diff --git a/core/java/android/hardware/display/DisplayViewport.java b/core/java/android/hardware/display/DisplayViewport.java
index df0d46b..f2c50b5 100644
--- a/core/java/android/hardware/display/DisplayViewport.java
+++ b/core/java/android/hardware/display/DisplayViewport.java
@@ -19,6 +19,7 @@
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.annotation.IntDef;
+import android.annotation.Nullable;
 import android.graphics.Rect;
 import android.text.TextUtils;
 
@@ -71,6 +72,9 @@
     // The ID used to uniquely identify this display.
     public String uniqueId;
 
+    // The physical port that the associated display device is connected to.
+    public @Nullable Byte physicalPort;
+
     public @ViewportType int type;
 
     public void copyFrom(DisplayViewport viewport) {
@@ -82,6 +86,7 @@
         deviceWidth = viewport.deviceWidth;
         deviceHeight = viewport.deviceHeight;
         uniqueId = viewport.uniqueId;
+        physicalPort = viewport.physicalPort;
         type = viewport.type;
     }
 
@@ -113,6 +118,7 @@
               && deviceWidth == other.deviceWidth
               && deviceHeight == other.deviceHeight
               && TextUtils.equals(uniqueId, other.uniqueId)
+              && physicalPort == other.physicalPort
               && type == other.type;
     }
 
@@ -128,6 +134,7 @@
         result += prime * result + deviceWidth;
         result += prime * result + deviceHeight;
         result += prime * result + uniqueId.hashCode();
+        result += prime * result + physicalPort;
         result += prime * result + type;
         return result;
     }
@@ -139,6 +146,7 @@
                 + ", valid=" + valid
                 + ", displayId=" + displayId
                 + ", uniqueId='" + uniqueId + "'"
+                + ", physicalPort=" + physicalPort
                 + ", orientation=" + orientation
                 + ", logicalFrame=" + logicalFrame
                 + ", physicalFrame=" + physicalFrame
diff --git a/core/jni/android_hardware_display_DisplayViewport.cpp b/core/jni/android_hardware_display_DisplayViewport.cpp
index 05f6556..e74aafe 100644
--- a/core/jni/android_hardware_display_DisplayViewport.cpp
+++ b/core/jni/android_hardware_display_DisplayViewport.cpp
@@ -40,6 +40,7 @@
     jfieldID deviceWidth;
     jfieldID deviceHeight;
     jfieldID uniqueId;
+    jfieldID physicalPort;
     jfieldID type;
 } gDisplayViewportClassInfo;
 
@@ -54,6 +55,9 @@
 
 status_t android_hardware_display_DisplayViewport_toNative(JNIEnv* env, jobject viewportObj,
         DisplayViewport* viewport) {
+    static const jclass byteClass = FindClassOrDie(env, "java/lang/Byte");
+    static const jmethodID byteValue = env->GetMethodID(byteClass, "byteValue", "()B");
+
     viewport->displayId = env->GetIntField(viewportObj, gDisplayViewportClassInfo.displayId);
     viewport->orientation = env->GetIntField(viewportObj, gDisplayViewportClassInfo.orientation);
     viewport->deviceWidth = env->GetIntField(viewportObj, gDisplayViewportClassInfo.deviceWidth);
@@ -65,6 +69,12 @@
         viewport->uniqueId = ScopedUtfChars(env, uniqueId).c_str();
     }
 
+    viewport->physicalPort = std::nullopt;
+    jobject physicalPort = env->GetObjectField(viewportObj, gDisplayViewportClassInfo.physicalPort);
+    if (physicalPort != nullptr) {
+        viewport->physicalPort = std::make_optional(env->CallByteMethod(physicalPort, byteValue));
+    }
+
     viewport->type = static_cast<ViewportType>(env->GetIntField(viewportObj,
                 gDisplayViewportClassInfo.type));
 
@@ -112,6 +122,9 @@
     gDisplayViewportClassInfo.uniqueId = GetFieldIDOrDie(env,
             gDisplayViewportClassInfo.clazz, "uniqueId", "Ljava/lang/String;");
 
+    gDisplayViewportClassInfo.physicalPort = GetFieldIDOrDie(env,
+            gDisplayViewportClassInfo.clazz, "physicalPort", "Ljava/lang/Byte;");
+
     gDisplayViewportClassInfo.type = GetFieldIDOrDie(env,
             gDisplayViewportClassInfo.clazz, "type", "I");
 
diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java
index 7bfe9ce..6ee5665 100644
--- a/services/core/java/com/android/server/display/DisplayDevice.java
+++ b/services/core/java/com/android/server/display/DisplayDevice.java
@@ -225,6 +225,8 @@
         viewport.deviceHeight = isRotated ? info.width : info.height;
 
         viewport.uniqueId = info.uniqueId;
+        // TODO(b/112898898) Use an actual port here.
+        viewport.physicalPort = null;
     }
 
     /**
diff --git a/services/core/java/com/android/server/input/ConfigurationProcessor.java b/services/core/java/com/android/server/input/ConfigurationProcessor.java
new file mode 100644
index 0000000..970e86a
--- /dev/null
+++ b/services/core/java/com/android/server/input/ConfigurationProcessor.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2018 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.input;
+
+import android.text.TextUtils;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.Xml;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+
+class ConfigurationProcessor {
+    private static final String TAG = "ConfigurationProcessor";
+
+    static List<String> processExcludedDeviceNames(InputStream xml) throws Exception {
+        List<String> names = new ArrayList<>();
+        try (InputStreamReader confReader = new InputStreamReader(xml)) {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(confReader);
+            XmlUtils.beginDocument(parser, "devices");
+            while (true) {
+                XmlUtils.nextElement(parser);
+                if (!"device".equals(parser.getName())) {
+                    break;
+                }
+                String name = parser.getAttributeValue(null, "name");
+                if (name != null) {
+                    names.add(name);
+                }
+            }
+        }
+        return names;
+    }
+
+    /**
+     * Parse the configuration for input port associations.
+     *
+     * Configuration format:
+     * <code>
+     * &lt;ports>
+     *     &lt;port display="0" input="usb-xhci-hcd.0.auto-1.4.3/input0" />
+     *     &lt;port display="1" input="usb-xhci-hcd.0.auto-1.4.2/input0" />
+     * &lt;/ports>
+     * </code>
+     *
+     * In this example, any input device that has physical port of
+     * "usb-xhci-hcd.0.auto-1.4.3/input0" will be associated with a display
+     * that has the physical port "0". If such a display does not exist, the input device
+     * will be disabled and no input events will be dispatched from that input device until a
+     * matching display appears. Likewise, an input device that has port "..1.4.2.." will have
+     * its input events forwarded to a display that has physical port of "1".
+     *
+     * Note: display port must be a numeric value, and this is checked at runtime for validity.
+     * At the same time, it is specified as a string for simplicity.
+     *
+     * Note: do not confuse "display id" with "display port".
+     * The "display port" is the physical port on which the display is connected. This could
+     * be something like HDMI0, HDMI1, etc. For virtual displays, "display port" will be null.
+     * The "display id" is a way to identify a particular display, and is not a stable API.
+     * All displays, including virtual ones, will have a display id.
+     *
+     * Return the pairs of associations. The first item in the pair is the input port,
+     * the second item in the pair is the display port.
+     */
+    @VisibleForTesting
+    static List<Pair<String, String>> processInputPortAssociations(InputStream xml)
+            throws Exception {
+        List<Pair<String, String>> associations = new ArrayList<>();
+        try (InputStreamReader confReader = new InputStreamReader(xml)) {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(confReader);
+            XmlUtils.beginDocument(parser, "ports");
+
+            while (true) {
+                XmlUtils.nextElement(parser);
+                String entryName = parser.getName();
+                if (!"port".equals(entryName)) {
+                    break;
+                }
+                String inputPort = parser.getAttributeValue(null, "input");
+                String displayPort = parser.getAttributeValue(null, "display");
+                if (TextUtils.isEmpty(inputPort) || TextUtils.isEmpty(displayPort)) {
+                    // This is likely an error by an OEM during device configuration
+                    Slog.wtf(TAG, "Ignoring incomplete entry");
+                    continue;
+                }
+                try {
+                    Integer.parseUnsignedInt(displayPort);
+                } catch (NumberFormatException e) {
+                    Slog.wtf(TAG, "Display port should be an integer");
+                    continue;
+                }
+                associations.add(new Pair<>(inputPort, displayPort));
+            }
+        }
+        return associations;
+    }
+}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 3339a49..d96b6cb 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -64,18 +64,18 @@
 import android.provider.Settings.SettingNotFoundException;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.Xml;
 import android.view.Display;
 import android.view.IInputFilter;
 import android.view.IInputFilterHost;
 import android.view.IWindow;
-import android.view.InputChannel;
 import android.view.InputApplicationHandle;
-import android.view.InputWindowHandle;
+import android.view.InputChannel;
 import android.view.InputDevice;
 import android.view.InputEvent;
+import android.view.InputWindowHandle;
 import android.view.KeyEvent;
 import android.view.PointerIcon;
 import android.view.Surface;
@@ -97,14 +97,13 @@
 import libcore.io.IoUtils;
 import libcore.io.Streams;
 
-import org.xmlpull.v1.XmlPullParser;
-
 import java.io.File;
 import java.io.FileDescriptor;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
-import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -124,6 +123,7 @@
     static final boolean DEBUG = false;
 
     private static final String EXCLUDED_DEVICES_PATH = "etc/excluded-input-devices.xml";
+    private static final String PORT_ASSOCIATIONS_PATH = "etc/input-port-associations.xml";
 
     private static final int MSG_DELIVER_INPUT_DEVICES_CHANGED = 1;
     private static final int MSG_SWITCH_KEYBOARD_LAYOUT = 2;
@@ -1852,11 +1852,9 @@
     }
 
     // Native callback.
-    private String[] getExcludedDeviceNames() {
-        ArrayList<String> names = new ArrayList<String>();
-
+    private static String[] getExcludedDeviceNames() {
+        List<String> names = new ArrayList<>();
         // Read partner-provided list of excluded input devices
-        XmlPullParser parser = null;
         // Environment.getRootDirectory() is a fancy way of saying ANDROID_ROOT or "/system".
         final File[] baseDirs = {
             Environment.getRootDirectory(),
@@ -1864,33 +1862,52 @@
         };
         for (File baseDir: baseDirs) {
             File confFile = new File(baseDir, EXCLUDED_DEVICES_PATH);
-            FileReader confreader = null;
             try {
-                confreader = new FileReader(confFile);
-                parser = Xml.newPullParser();
-                parser.setInput(confreader);
-                XmlUtils.beginDocument(parser, "devices");
-
-                while (true) {
-                    XmlUtils.nextElement(parser);
-                    if (!"device".equals(parser.getName())) {
-                        break;
-                    }
-                    String name = parser.getAttributeValue(null, "name");
-                    if (name != null) {
-                        names.add(name);
-                    }
-                }
+                InputStream stream = new FileInputStream(confFile);
+                names.addAll(ConfigurationProcessor.processExcludedDeviceNames(stream));
             } catch (FileNotFoundException e) {
                 // It's ok if the file does not exist.
             } catch (Exception e) {
-                Slog.e(TAG, "Exception while parsing '" + confFile.getAbsolutePath() + "'", e);
-            } finally {
-                try { if (confreader != null) confreader.close(); } catch (IOException e) { }
+                Slog.e(TAG, "Could not parse '" + confFile.getAbsolutePath() + "'", e);
             }
         }
+        return names.toArray(new String[0]);
+    }
 
-        return names.toArray(new String[names.size()]);
+    /**
+     * Flatten a list of pairs into a list, with value positioned directly next to the key
+     * @return Flattened list
+     */
+    private static <T> List<T> flatten(@NonNull List<Pair<T, T>> pairs) {
+        List<T> list = new ArrayList<>(pairs.size() * 2);
+        for (Pair<T, T> pair : pairs) {
+            list.add(pair.first);
+            list.add(pair.second);
+        }
+        return list;
+    }
+
+    /**
+     * Ports are highly platform-specific, so only allow these to be specified in the vendor
+     * directory.
+     */
+    // Native callback
+    private static String[] getInputPortAssociations() {
+        File baseDir = Environment.getVendorDirectory();
+        File confFile = new File(baseDir, PORT_ASSOCIATIONS_PATH);
+
+        try {
+            InputStream stream = new FileInputStream(confFile);
+            List<Pair<String, String>> associations =
+                    ConfigurationProcessor.processInputPortAssociations(stream);
+            List<String> associationList = flatten(associations);
+            return associationList.toArray(new String[0]);
+        } catch (FileNotFoundException e) {
+            // Most of the time, file will not exist, which is expected.
+        } catch (Exception e) {
+            Slog.e(TAG, "Could not parse '" + confFile.getAbsolutePath() + "'", e);
+        }
+        return new String[0];
     }
 
     // Native callback.
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index fcd9335..b36a8a7 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -32,6 +32,7 @@
 #include <atomic>
 #include <cinttypes>
 #include <limits.h>
+#include <android-base/parseint.h>
 #include <android-base/stringprintf.h>
 #include <android_runtime/AndroidRuntime.h>
 #include <android_runtime/Log.h>
@@ -71,6 +72,7 @@
 
 #define INDENT "  "
 
+using android::base::ParseUint;
 using android::base::StringPrintf;
 
 namespace android {
@@ -81,6 +83,7 @@
 static const float POINTER_SPEED_EXPONENT = 1.0f / 4;
 
 static struct {
+    jclass clazz;
     jmethodID notifyConfigurationChanged;
     jmethodID notifyInputDevicesChanged;
     jmethodID notifySwitch;
@@ -95,6 +98,7 @@
     jmethodID checkInjectEventsPermission;
     jmethodID getVirtualKeyQuietTimeMillis;
     jmethodID getExcludedDeviceNames;
+    jmethodID getInputPortAssociations;
     jmethodID getKeyRepeatTimeout;
     jmethodID getKeyRepeatDelay;
     jmethodID getHoverTapTimeout;
@@ -183,6 +187,13 @@
     WM_ACTION_PASS_TO_USER = 1,
 };
 
+static std::string getStringElementFromJavaArray(JNIEnv* env, jobjectArray array, jsize index) {
+    jstring item = jstring(env->GetObjectArrayElement(array, index));
+    ScopedUtfChars chars(env, item);
+    std::string result(chars.c_str());
+    return result;
+}
+
 
 // --- NativeInputManager ---
 
@@ -452,20 +463,44 @@
     }
 
     outConfig->excludedDeviceNames.clear();
-    jobjectArray excludedDeviceNames = jobjectArray(env->CallObjectMethod(mServiceObj,
-            gServiceClassInfo.getExcludedDeviceNames));
+    jobjectArray excludedDeviceNames = jobjectArray(env->CallStaticObjectMethod(
+            gServiceClassInfo.clazz, gServiceClassInfo.getExcludedDeviceNames));
     if (!checkAndClearExceptionFromCallback(env, "getExcludedDeviceNames") && excludedDeviceNames) {
         jsize length = env->GetArrayLength(excludedDeviceNames);
         for (jsize i = 0; i < length; i++) {
-            jstring item = jstring(env->GetObjectArrayElement(excludedDeviceNames, i));
-            const char* deviceNameChars = env->GetStringUTFChars(item, nullptr);
-            outConfig->excludedDeviceNames.push_back(deviceNameChars);
-            env->ReleaseStringUTFChars(item, deviceNameChars);
-            env->DeleteLocalRef(item);
+            std::string deviceName = getStringElementFromJavaArray(env, excludedDeviceNames, i);
+            outConfig->excludedDeviceNames.push_back(deviceName);
         }
         env->DeleteLocalRef(excludedDeviceNames);
     }
 
+    // Associations between input ports and display ports
+    // The java method packs the information in the following manner:
+    // Original data: [{'inputPort1': '1'}, {'inputPort2': '2'}]
+    // Received data: ['inputPort1', '1', 'inputPort2', '2']
+    // So we unpack accordingly here.
+    outConfig->portAssociations.clear();
+    jobjectArray portAssociations = jobjectArray(env->CallStaticObjectMethod(
+            gServiceClassInfo.clazz, gServiceClassInfo.getInputPortAssociations));
+    if (!checkAndClearExceptionFromCallback(env, "getInputPortAssociations") && portAssociations) {
+        jsize length = env->GetArrayLength(portAssociations);
+        for (jsize i = 0; i < length / 2; i++) {
+            std::string inputPort = getStringElementFromJavaArray(env, portAssociations, 2 * i);
+            std::string displayPortStr =
+                    getStringElementFromJavaArray(env, portAssociations, 2 * i + 1);
+            uint8_t displayPort;
+            // Should already have been validated earlier, but do it here for safety.
+            bool success = ParseUint(displayPortStr, &displayPort);
+            if (!success) {
+                ALOGE("Could not parse entry in port configuration file, received: %s",
+                    displayPortStr.c_str());
+                continue;
+            }
+            outConfig->portAssociations.insert({inputPort, displayPort});
+        }
+        env->DeleteLocalRef(portAssociations);
+    }
+
     jint hoverTapTimeout = env->CallIntMethod(mServiceObj,
             gServiceClassInfo.getHoverTapTimeout);
     if (!checkAndClearExceptionFromCallback(env, "getHoverTapTimeout")) {
@@ -1697,6 +1732,10 @@
         var = env->GetMethodID(clazz, methodName, methodDescriptor); \
         LOG_FATAL_IF(! (var), "Unable to find method " methodName);
 
+#define GET_STATIC_METHOD_ID(var, clazz, methodName, methodDescriptor) \
+        var = env->GetStaticMethodID(clazz, methodName, methodDescriptor); \
+        LOG_FATAL_IF(! (var), "Unable to find static method " methodName);
+
 #define GET_FIELD_ID(var, clazz, fieldName, fieldDescriptor) \
         var = env->GetFieldID(clazz, fieldName, fieldDescriptor); \
         LOG_FATAL_IF(! (var), "Unable to find field " fieldName);
@@ -1711,6 +1750,7 @@
 
     jclass clazz;
     FIND_CLASS(clazz, "com/android/server/input/InputManagerService");
+    gServiceClassInfo.clazz = clazz;
 
     GET_METHOD_ID(gServiceClassInfo.notifyConfigurationChanged, clazz,
             "notifyConfigurationChanged", "(J)V");
@@ -1754,9 +1794,12 @@
     GET_METHOD_ID(gServiceClassInfo.getVirtualKeyQuietTimeMillis, clazz,
             "getVirtualKeyQuietTimeMillis", "()I");
 
-    GET_METHOD_ID(gServiceClassInfo.getExcludedDeviceNames, clazz,
+    GET_STATIC_METHOD_ID(gServiceClassInfo.getExcludedDeviceNames, clazz,
             "getExcludedDeviceNames", "()[Ljava/lang/String;");
 
+    GET_STATIC_METHOD_ID(gServiceClassInfo.getInputPortAssociations, clazz,
+            "getInputPortAssociations", "()[Ljava/lang/String;");
+
     GET_METHOD_ID(gServiceClassInfo.getKeyRepeatTimeout, clazz,
             "getKeyRepeatTimeout", "()I");
 
diff --git a/services/tests/servicestests/res/raw/input_port_associations.xml b/services/tests/servicestests/res/raw/input_port_associations.xml
new file mode 100644
index 0000000..b10d541
--- /dev/null
+++ b/services/tests/servicestests/res/raw/input_port_associations.xml
@@ -0,0 +1,4 @@
+<ports>
+    <port display="0" input="USB1" />
+    <port display="1" input="USB2" />
+</ports>
\ No newline at end of file
diff --git a/services/tests/servicestests/res/raw/input_port_associations_bad_displayport.xml b/services/tests/servicestests/res/raw/input_port_associations_bad_displayport.xml
new file mode 100644
index 0000000..8eeb1f5
--- /dev/null
+++ b/services/tests/servicestests/res/raw/input_port_associations_bad_displayport.xml
@@ -0,0 +1,3 @@
+<ports>
+    <port display="a" input="USB1" />
+</ports>
\ No newline at end of file
diff --git a/services/tests/servicestests/res/raw/input_port_associations_bad_xml.xml b/services/tests/servicestests/res/raw/input_port_associations_bad_xml.xml
new file mode 100644
index 0000000..cf6e124
--- /dev/null
+++ b/services/tests/servicestests/res/raw/input_port_associations_bad_xml.xml
@@ -0,0 +1,3 @@
+<ports>
+    <port Garbage data inside xml>
+</ports>
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/input/ConfigurationProcessorTest.java b/services/tests/servicestests/src/com/android/server/input/ConfigurationProcessorTest.java
new file mode 100644
index 0000000..636aa37
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/input/ConfigurationProcessorTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2018 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.input;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.util.Pair;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * Build/Install/Run:
+ * atest ConfigurationProcessorTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class ConfigurationProcessorTest {
+
+    private Context mContext;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+    }
+
+    @Test
+    public void testGetInputPortAssociations() {
+        final int res = com.android.frameworks.servicestests.R.raw.input_port_associations;
+        InputStream xml = mContext.getResources().openRawResource(res);
+        List<Pair<String, String>> associations = null;
+        try {
+            associations = ConfigurationProcessor.processInputPortAssociations(xml);
+        } catch (Exception e) {
+            fail("Could not process xml file for input associations");
+        }
+        assertNotNull(associations);
+        assertEquals(2, associations.size());
+        assertTrue(associations.contains(Pair.create("USB1", "0")));
+        assertTrue(associations.contains(Pair.create("USB2", "1")));
+    }
+
+    @Test
+    public void testGetInputPortAssociationsBadDisplayport() {
+        final int res =
+                com.android.frameworks.servicestests.R.raw.input_port_associations_bad_displayport;
+        InputStream xml = mContext.getResources().openRawResource(res);
+        List<Pair<String, String>> associations = null;
+        try {
+            associations = ConfigurationProcessor.processInputPortAssociations(xml);
+        } catch (Exception e) {
+            fail("Could not process xml file for input associations");
+        }
+        assertNotNull(associations);
+        assertEquals(0, associations.size());
+    }
+
+    @Test
+    public void testGetInputPortAssociationsEmptyConfig() {
+        final int res = com.android.frameworks.servicestests.R.raw.input_port_associations_bad_xml;
+        InputStream xml = mContext.getResources().openRawResource(res);
+        try {
+            ConfigurationProcessor.processInputPortAssociations(xml);
+            fail("Parsing should fail, because xml contains bad data");
+        } catch (Exception e) {
+            // This is expected
+        }
+    }
+}