Merge "Deprecate isConnectedToHdmiSwitch"
diff --git a/core/java/android/hardware/hdmi/HdmiUtils.java b/core/java/android/hardware/hdmi/HdmiUtils.java
index 3081738..8c94b78 100644
--- a/core/java/android/hardware/hdmi/HdmiUtils.java
+++ b/core/java/android/hardware/hdmi/HdmiUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2014 The Android Open Source Project
+ * 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.
@@ -16,14 +16,18 @@
 
 package android.hardware.hdmi;
 
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
- * Various utilities to handle HDMI CEC messages.
+ * Various utilities related to HDMI CEC.
  *
  * TODO(b/110094868): unhide for Q
  * @hide
  */
-public class HdmiUtils {
-
+public final class HdmiUtils {
     /**
      * Return value of {@link #getLocalPortFromPhysicalAddress(int, int)}
      */
@@ -78,4 +82,164 @@
         }
         return port;
     }
+
+    /**
+     * TODO(b/110094868): unhide for Q
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({HDMI_RELATIVE_POSITION_UNKNOWN, HDMI_RELATIVE_POSITION_DIRECTLY_BELOW,
+            HDMI_RELATIVE_POSITION_BELOW, HDMI_RELATIVE_POSITION_SAME,
+            HDMI_RELATIVE_POSITION_DIRECTLY_ABOVE, HDMI_RELATIVE_POSITION_ABOVE,
+            HDMI_RELATIVE_POSITION_SIBLING, HDMI_RELATIVE_POSITION_DIFFERENT_BRANCH})
+    public @interface HdmiAddressRelativePosition {}
+    /**
+     * HDMI relative position is not determined.
+     * TODO(b/110094868): unhide for Q
+     * @hide
+     */
+    public static final int HDMI_RELATIVE_POSITION_UNKNOWN = 0;
+    /**
+     * HDMI relative position: directly blow the device.
+     * TODO(b/110094868): unhide for Q
+     * @hide
+     */
+    public static final int HDMI_RELATIVE_POSITION_DIRECTLY_BELOW = 1;
+    /**
+     * HDMI relative position: indirectly below the device.
+     * TODO(b/110094868): unhide for Q
+     * @hide
+     */
+    public static final int HDMI_RELATIVE_POSITION_BELOW = 2;
+    /**
+     * HDMI relative position: the same device.
+     * TODO(b/110094868): unhide for Q
+     * @hide
+     */
+    public static final int HDMI_RELATIVE_POSITION_SAME = 3;
+    /**
+     * HDMI relative position: directly above the device.
+     * TODO(b/110094868): unhide for Q
+     * @hide
+     */
+    public static final int HDMI_RELATIVE_POSITION_DIRECTLY_ABOVE = 4;
+    /**
+     * HDMI relative position: indirectly above the device.
+     * TODO(b/110094868): unhide for Q
+     * @hide
+     */
+    public static final int HDMI_RELATIVE_POSITION_ABOVE = 5;
+    /**
+     * HDMI relative position: directly below a same device.
+     * TODO(b/110094868): unhide for Q
+     * @hide
+     */
+    public static final int HDMI_RELATIVE_POSITION_SIBLING = 6;
+    /**
+     * HDMI relative position: different branch.
+     * TODO(b/110094868): unhide for Q
+     * @hide
+     */
+    public static final int HDMI_RELATIVE_POSITION_DIFFERENT_BRANCH = 7;
+
+    private static final int NPOS = -1;
+
+    /**
+     * Check if the given physical address is valid.
+     *
+     * @param address physical address
+     * @return {@code true} if the given address is valid
+     */
+    public static boolean isValidPhysicalAddress(int address) {
+        if (address < 0 || address >= 0xFFFF) {
+            return false;
+        }
+        int mask = 0xF000;
+        boolean hasZero = false;
+        for (int i = 0; i < 4; i++) {
+            if ((address & mask) == 0) {
+                hasZero = true;
+            } else if (hasZero) {
+                // only 0s are valid after a 0.
+                // e.g. 0x1012 is not valid.
+                return false;
+            }
+            mask >>= 4;
+        }
+        return true;
+    }
+
+
+    /**
+     * Returns the relative position of two physical addresses.
+     */
+    @HdmiAddressRelativePosition
+    public static int getHdmiAddressRelativePosition(int src, int dest) {
+        if (src == 0xFFFF || dest == 0xFFFF) {
+            // address not assigned
+            return HDMI_RELATIVE_POSITION_UNKNOWN;
+        }
+        try {
+            int firstDiffPos = physicalAddressFirstDifferentDigitPos(src, dest);
+            if (firstDiffPos == NPOS) {
+                return HDMI_RELATIVE_POSITION_SAME;
+            }
+            int mask = (0xF000 >> (firstDiffPos * 4));
+            int nextPos = firstDiffPos + 1;
+            if ((src & mask) == 0) {
+                // src is above dest
+                if (nextPos == 4) {
+                    // last digits are different
+                    return HDMI_RELATIVE_POSITION_DIRECTLY_ABOVE;
+                }
+                if (((0xF000 >> (nextPos * 4)) & dest) == 0) {
+                    // next digit is 0
+                    return HDMI_RELATIVE_POSITION_DIRECTLY_ABOVE;
+                }
+                return HDMI_RELATIVE_POSITION_ABOVE;
+            }
+
+            if ((dest & mask) == 0) {
+                // src is below dest
+                if (nextPos == 4) {
+                    // last digits are different
+                    return HDMI_RELATIVE_POSITION_DIRECTLY_BELOW;
+                }
+                if (((0xF000 >> (nextPos * 4)) & src) == 0) {
+                    // next digit is 0
+                    return HDMI_RELATIVE_POSITION_DIRECTLY_BELOW;
+                }
+                return HDMI_RELATIVE_POSITION_BELOW;
+            }
+            if (nextPos == 4) {
+                // last digits are different
+                return HDMI_RELATIVE_POSITION_SIBLING;
+            }
+            if (((0xF000 >> (nextPos * 4)) & src) == 0 && ((0xF000 >> (nextPos * 4)) & dest) == 0) {
+                return HDMI_RELATIVE_POSITION_SIBLING;
+            }
+            return HDMI_RELATIVE_POSITION_DIFFERENT_BRANCH;
+        } catch (IllegalArgumentException e) {
+            // invalid address
+            return HDMI_RELATIVE_POSITION_UNKNOWN;
+        }
+    }
+
+    private static int physicalAddressFirstDifferentDigitPos(int address1, int address2)
+            throws IllegalArgumentException {
+        if (!isValidPhysicalAddress(address1)) {
+            throw new IllegalArgumentException(address1 + " is not a valid address.");
+        }
+        if (!isValidPhysicalAddress(address2)) {
+            throw new IllegalArgumentException(address2 + " is not a valid address.");
+        }
+        int mask = 0xF000;
+        for (int i = 0; i < 4; i++) {
+            if ((address1 & mask) != (address2 & mask)) {
+                return i;
+            }
+            mask = mask >> 4;
+        }
+        return NPOS;
+    }
 }
diff --git a/core/tests/hdmitests/Android.mk b/core/tests/hdmitests/Android.mk
index 2ca31a6..f155feb 100644
--- a/core/tests/hdmitests/Android.mk
+++ b/core/tests/hdmitests/Android.mk
@@ -20,7 +20,7 @@
 # Include all test java files
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules frameworks-base-testutils
+LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules frameworks-base-testutils truth-prebuilt
 
 LOCAL_JAVA_LIBRARIES := android.test.runner
 LOCAL_PACKAGE_NAME := HdmiCecTests
diff --git a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiUtilsTest.java b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiUtilsTest.java
new file mode 100644
index 0000000..fdc6b84
--- /dev/null
+++ b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiUtilsTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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 android.hardware.hdmi;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+/**
+ * Tests for {@link HdmiUtils}.
+ */
+@RunWith(JUnit4.class)
+@SmallTest
+public class HdmiUtilsTest {
+    @Test
+    public void testInvalidAddress() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0, -1))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_UNKNOWN);
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0xFFFF, 0xFFFF))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_UNKNOWN);
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0xFFFFF, 0))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_UNKNOWN);
+    }
+
+    @Test
+    public void testSameAddress() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0x1000, 0x1000))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_SAME);
+    }
+
+    @Test
+    public void testDirectlyAbove() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0x1000, 0x1200))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_DIRECTLY_ABOVE);
+    }
+
+    @Test
+    public void testDirectlyAbove_rootDevice() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0x0000, 0x2000))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_DIRECTLY_ABOVE);
+    }
+
+    @Test
+    public void testDirectlyAbove_leafDevice() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0x1240, 0x1245))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_DIRECTLY_ABOVE);
+    }
+
+    @Test
+    public void testAbove() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0x1000, 0x1210))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_ABOVE);
+    }
+
+    @Test
+    public void testAbove_rootDevice() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0x0000, 0x1200))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_ABOVE);
+    }
+
+    @Test
+    public void testDirectlyBelow() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0x2250, 0x2200))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_DIRECTLY_BELOW);
+    }
+
+    @Test
+    public void testDirectlyBelow_rootDevice() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0x5000, 0x0000))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_DIRECTLY_BELOW);
+    }
+
+    @Test
+    public void testDirectlyBelow_leafDevice() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0x3249, 0x3240))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_DIRECTLY_BELOW);
+    }
+
+    @Test
+    public void testBelow() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0x5143, 0x5100))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_BELOW);
+    }
+
+    @Test
+    public void testBelow_rootDevice() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0x3420, 0x0000))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_BELOW);
+    }
+
+    @Test
+    public void testSibling() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0x4000, 0x5000))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_SIBLING);
+    }
+
+    @Test
+    public void testSibling_leafDevice() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0x798A, 0x798F))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_SIBLING);
+    }
+
+    @Test
+    public void testDifferentBranch() {
+        assertThat(HdmiUtils.getHdmiAddressRelativePosition(0x798A, 0x7970))
+                .isEqualTo(HdmiUtils.HDMI_RELATIVE_POSITION_DIFFERENT_BRANCH);
+    }
+
+    @Test
+    public void isValidPysicalAddress_true() {
+        assertThat(HdmiUtils.isValidPhysicalAddress(0)).isTrue();
+        assertThat(HdmiUtils.isValidPhysicalAddress(0xFFFE)).isTrue();
+        assertThat(HdmiUtils.isValidPhysicalAddress(0x1200)).isTrue();
+    }
+
+    @Test
+    public void isValidPysicalAddress_outOfRange() {
+        assertThat(HdmiUtils.isValidPhysicalAddress(-1)).isFalse();
+        assertThat(HdmiUtils.isValidPhysicalAddress(0xFFFF)).isFalse();
+        assertThat(HdmiUtils.isValidPhysicalAddress(0x10000)).isFalse();
+    }
+
+    @Test
+    public void isValidPysicalAddress_nonTrailingZeros() {
+        assertThat(HdmiUtils.isValidPhysicalAddress(0x0001)).isFalse();
+        assertThat(HdmiUtils.isValidPhysicalAddress(0x0213)).isFalse();
+    }
+}
diff --git a/media/java/android/media/tv/TvInputInfo.java b/media/java/android/media/tv/TvInputInfo.java
index 5cb8fb8..30907a5 100644
--- a/media/java/android/media/tv/TvInputInfo.java
+++ b/media/java/android/media/tv/TvInputInfo.java
@@ -33,7 +33,10 @@
 import android.content.res.XmlResourceParser;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
+import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.HdmiDeviceInfo;
+import android.hardware.hdmi.HdmiUtils;
+import android.hardware.hdmi.HdmiUtils.HdmiAddressRelativePosition;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Parcel;
@@ -49,7 +52,6 @@
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.annotation.Retention;
@@ -145,6 +147,8 @@
     // Attributes specific to HDMI
     private final HdmiDeviceInfo mHdmiDeviceInfo;
     private final boolean mIsConnectedToHdmiSwitch;
+    @HdmiAddressRelativePosition
+    private final int mHdmiConnectionRelativePosition;
     private final String mParentId;
 
     private final Bundle mExtras;
@@ -260,7 +264,9 @@
     private TvInputInfo(ResolveInfo service, String id, int type, boolean isHardwareInput,
             CharSequence label, int labelResId, Icon icon, Icon iconStandby, Icon iconDisconnected,
             String setupActivity, boolean canRecord, int tunerCount, HdmiDeviceInfo hdmiDeviceInfo,
-            boolean isConnectedToHdmiSwitch, String parentId, Bundle extras) {
+            boolean isConnectedToHdmiSwitch,
+            @HdmiAddressRelativePosition int hdmiConnectionRelativePosition, String parentId,
+            Bundle extras) {
         mService = service;
         mId = id;
         mType = type;
@@ -275,6 +281,7 @@
         mTunerCount = tunerCount;
         mHdmiDeviceInfo = hdmiDeviceInfo;
         mIsConnectedToHdmiSwitch = isConnectedToHdmiSwitch;
+        mHdmiConnectionRelativePosition = hdmiConnectionRelativePosition;
         mParentId = parentId;
         mExtras = extras;
     }
@@ -419,6 +426,7 @@
     /**
      * Returns {@code true}, if a CEC device for this TV input is connected to an HDMI switch, i.e.,
      * the device isn't directly connected to a HDMI port.
+     * TODO(b/110094868): add @Deprecated for Q
      * @hide
      */
     @SystemApi
@@ -427,6 +435,16 @@
     }
 
     /**
+     * Returns the relative position of this HDMI input.
+     * TODO(b/110094868): unhide for Q
+     * @hide
+     */
+    @HdmiAddressRelativePosition
+    public int getHdmiConnectionRelativePosition() {
+        return mHdmiConnectionRelativePosition;
+    }
+
+    /**
      * Checks if this TV input is marked hidden by the user in the settings.
      *
      * @param context Supplies a {@link Context} used to check if this TV input is hidden.
@@ -555,6 +573,7 @@
                 && mTunerCount == obj.mTunerCount
                 && Objects.equals(mHdmiDeviceInfo, obj.mHdmiDeviceInfo)
                 && mIsConnectedToHdmiSwitch == obj.mIsConnectedToHdmiSwitch
+                && mHdmiConnectionRelativePosition == obj.mHdmiConnectionRelativePosition
                 && TextUtils.equals(mParentId, obj.mParentId)
                 && Objects.equals(mExtras, obj.mExtras);
     }
@@ -589,6 +608,7 @@
         dest.writeInt(mTunerCount);
         dest.writeParcelable(mHdmiDeviceInfo, flags);
         dest.writeByte(mIsConnectedToHdmiSwitch ? (byte) 1 : 0);
+        dest.writeInt(mHdmiConnectionRelativePosition);
         dest.writeString(mParentId);
         dest.writeBundle(mExtras);
     }
@@ -630,6 +650,7 @@
         mTunerCount = in.readInt();
         mHdmiDeviceInfo = in.readParcelable(null);
         mIsConnectedToHdmiSwitch = in.readByte() == 1;
+        mHdmiConnectionRelativePosition = in.readInt();
         mParentId = in.readString();
         mExtras = in.readBundle();
     }
@@ -883,12 +904,17 @@
             int type;
             boolean isHardwareInput = false;
             boolean isConnectedToHdmiSwitch = false;
+            @HdmiAddressRelativePosition
+            int hdmiConnectionRelativePosition = HdmiUtils.HDMI_RELATIVE_POSITION_UNKNOWN;
 
             if (mHdmiDeviceInfo != null) {
                 id = generateInputId(componentName, mHdmiDeviceInfo);
                 type = TYPE_HDMI;
                 isHardwareInput = true;
-                isConnectedToHdmiSwitch = (mHdmiDeviceInfo.getPhysicalAddress() & 0x0FFF) != 0;
+                hdmiConnectionRelativePosition = getRelativePosition(mContext, mHdmiDeviceInfo);
+                isConnectedToHdmiSwitch =
+                        hdmiConnectionRelativePosition
+                                != HdmiUtils.HDMI_RELATIVE_POSITION_DIRECTLY_BELOW;
             } else if (mTvInputHardwareInfo != null) {
                 id = generateInputId(componentName, mTvInputHardwareInfo);
                 type = sHardwareTypeToTvInputType.get(mTvInputHardwareInfo.getType(), TYPE_TUNER);
@@ -901,7 +927,8 @@
             return new TvInputInfo(mResolveInfo, id, type, isHardwareInput, mLabel, mLabelResId,
                     mIcon, mIconStandby, mIconDisconnected, mSetupActivity,
                     mCanRecord == null ? false : mCanRecord, mTunerCount == null ? 0 : mTunerCount,
-                    mHdmiDeviceInfo, isConnectedToHdmiSwitch, mParentId, mExtras);
+                    mHdmiDeviceInfo, isConnectedToHdmiSwitch, hdmiConnectionRelativePosition,
+                    mParentId, mExtras);
         }
 
         private static String generateInputId(ComponentName name) {
@@ -923,6 +950,16 @@
                     + tvInputHardwareInfo.getDeviceId();
         }
 
+        private static int getRelativePosition(Context context, HdmiDeviceInfo info) {
+            HdmiControlManager hcm =
+                    (HdmiControlManager) context.getSystemService(Context.HDMI_CONTROL_SERVICE);
+            if (hcm == null) {
+                return HdmiUtils.HDMI_RELATIVE_POSITION_UNKNOWN;
+            }
+            return HdmiUtils.getHdmiAddressRelativePosition(
+                    info.getPhysicalAddress(), hcm.getPhysicalAddress());
+        }
+
         private void parseServiceMetadata(int inputType) {
             ServiceInfo si = mResolveInfo.serviceInfo;
             PackageManager pm = mContext.getPackageManager();