Test for hidden api enforcement killswitch

Currently, simply verifies that we can access VMRuntime.THE_ONE, which
doesn't prove anything as there is no blacklisting enabled yet.

Refactor JNI parts of hidden API test into cts-signature-common to it can
be used in this test.

(cherry picked from commit d635f8936bb3baad00c4bf5658e3bc7f420a3421 in
master)

Bug: 64382372
Test: $ cts/tests/signature/runSignatureTests.sh
Change-Id: I8c566136dea2a6b5d41d81981d0f1ef88a0a1a5f
diff --git a/tests/signature/Android.mk b/tests/signature/Android.mk
index fec47b6..84d7cec 100644
--- a/tests/signature/Android.mk
+++ b/tests/signature/Android.mk
@@ -24,6 +24,8 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
+LOCAL_JNI_SHARED_LIBRARIES := libcts_dexchecker
+
 LOCAL_MODULE := cts-signature-common
 
 LOCAL_SDK_VERSION := current
@@ -36,7 +38,7 @@
 include $(CLEAR_VARS)
 
 # These files are for device-side only, so filter-out for host library
-LOCAL_DEVICE_ONLY_SOURCES := %/CurrentApi.java %/ApiDocumentParser.java
+LOCAL_DEVICE_ONLY_SOURCES := %/CurrentApi.java %/ApiDocumentParser.java %/DexMemberChecker.java
 
 LOCAL_SRC_FILES := $(filter-out $(LOCAL_DEVICE_ONLY_SOURCES), $(call all-java-files-under, src))
 
diff --git a/tests/signature/api-check/Android.mk b/tests/signature/api-check/Android.mk
index 4462393..94b59ba 100644
--- a/tests/signature/api-check/Android.mk
+++ b/tests/signature/api-check/Android.mk
@@ -26,7 +26,7 @@
 
 LOCAL_MODULE := cts-api-signature-test
 
-LOCAL_SDK_VERSION := current
+LOCAL_SDK_VERSION := test_current
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
     cts-signature-common \
diff --git a/tests/signature/api-check/hidden-api-killswitch/Android.mk b/tests/signature/api-check/hidden-api-killswitch/Android.mk
new file mode 100644
index 0000000..2e1ee05
--- /dev/null
+++ b/tests/signature/api-check/hidden-api-killswitch/Android.mk
@@ -0,0 +1,30 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_PACKAGE_NAME := CtsHiddenApiKillswitchTestCases
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+LOCAL_MULTILIB := both
+LOCAL_JNI_SHARED_LIBRARIES := libcts_dexchecker
+LOCAL_NDK_STL_VARIANT := c++_static
+
+# Tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts vts general-tests
+LOCAL_SDK_VERSION := current
+LOCAL_STATIC_JAVA_LIBRARIES := cts-api-signature-test
+
+include $(BUILD_CTS_PACKAGE)
diff --git a/tests/signature/api-check/hidden-api-killswitch/AndroidManifest.xml b/tests/signature/api-check/hidden-api-killswitch/AndroidManifest.xml
new file mode 100644
index 0000000..8699d18
--- /dev/null
+++ b/tests/signature/api-check/hidden-api-killswitch/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.signature.cts.api.killswitch">
+
+    <application/>
+
+    <instrumentation android:name="repackaged.android.test.InstrumentationTestRunner"
+                     android:targetPackage="android.signature.cts.api.killswitch"
+                     android:label="Hidden API Killswitch Test"/>
+</manifest>
diff --git a/tests/signature/api-check/hidden-api-killswitch/AndroidTest.xml b/tests/signature/api-check/hidden-api-killswitch/AndroidTest.xml
new file mode 100644
index 0000000..2d99a73
--- /dev/null
+++ b/tests/signature/api-check/hidden-api-killswitch/AndroidTest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Config for CTS Hidden API Signature test cases">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="systems" />
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <!-- Enable the killswitch before running the test, then disable it afterwards. The test
+             is intended to verify the behaviour when the killswitch is enabled. -->
+        <option name="run-command" value="settings put global hidden_api_blacklist_exemptions \*" />
+        <option name="teardown-command" value="settings delete global hidden_api_blacklist_exemptions" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsHiddenApiKillswitchTestCases.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.signature.cts.api.killswitch" />
+        <option name="runner" value="repackaged.android.test.InstrumentationTestRunner" />
+        <option name="class" value="android.signature.cts.api.KillswitchTest" />
+        <option name="runtime-hint" value="30s" />
+    </test>
+</configuration>
diff --git a/tests/signature/api-check/hidden-api/Android.mk b/tests/signature/api-check/hidden-api/Android.mk
index a5bc737..f039032 100644
--- a/tests/signature/api-check/hidden-api/Android.mk
+++ b/tests/signature/api-check/hidden-api/Android.mk
@@ -24,18 +24,8 @@
 $(eval $(call copy-one-file,$(INTERNAL_PLATFORM_HIDDENAPI_BLACKLIST),$(LOCAL_BUILT_MODULE)))
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := libcts_hiddenapi
-LOCAL_MODULE_TAGS := optional
-LOCAL_SRC_FILES := hidden-api.cpp
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_C_INCLUDES := $(JNI_H_INCLUDE)
-LOCAL_SDK_VERSION := current
-LOCAL_NDK_STL_VARIANT := c++_static
-include $(BUILD_SHARED_LIBRARY)
-
-include $(CLEAR_VARS)
 LOCAL_PACKAGE_NAME := CtsHiddenApiDiscoveryTestCases
 LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
 LOCAL_SIGNATURE_API_FILES := blacklist.api
-LOCAL_JNI_SHARED_LIBRARIES := libcts_hiddenapi
+LOCAL_JNI_SHARED_LIBRARIES := libcts_dexchecker
 include $(LOCAL_PATH)/../build_signature_apk.mk
diff --git a/tests/signature/api-check/src/java/android/signature/cts/api/HiddenApiTest.java b/tests/signature/api-check/src/java/android/signature/cts/api/HiddenApiTest.java
index e1da5ac..9673fd2 100644
--- a/tests/signature/api-check/src/java/android/signature/cts/api/HiddenApiTest.java
+++ b/tests/signature/api-check/src/java/android/signature/cts/api/HiddenApiTest.java
@@ -21,13 +21,17 @@
 import android.signature.cts.DexApiDocumentParser.DexField;
 import android.signature.cts.DexApiDocumentParser.DexMember;
 import android.signature.cts.DexApiDocumentParser.DexMethod;
+import android.signature.cts.DexMemberChecker;
 import android.signature.cts.FailureType;
+import android.signature.cts.ResultObserver;
+
 import java.io.File;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Set;
 import java.util.stream.Stream;
 import java.text.ParseException;
 
@@ -52,136 +56,59 @@
      * Will check the entire API, and then report the complete list of failures
      */
     public void testSignature() {
-        System.loadLibrary("cts_hiddenapi");
+        DexMemberChecker.init();
         runWithTestResultObserver(resultObserver -> {
+            DexMemberChecker.Observer observer = new DexMemberChecker.Observer() {
+                @Override
+                public void classAccessible(boolean accessible, DexMember member) {
+                }
+
+                @Override
+                public void fieldAccessibleViaReflection(boolean accessible, DexField field) {
+                    if (accessible) {
+                        resultObserver.notifyFailure(
+                                FailureType.EXTRA_FIELD,
+                                field.toString(),
+                                "Hidden field accessible through reflection");
+                    }
+                }
+
+                @Override
+                public void fieldAccessibleViaJni(boolean accessible, DexField field) {
+                    if (accessible) {
+                        resultObserver.notifyFailure(
+                                FailureType.EXTRA_FIELD,
+                                field.toString(),
+                                "Hidden field accessible through JNI");
+                    }
+                }
+
+                @Override
+                public void methodAccessibleViaReflection(boolean accessible, DexMethod method) {
+                    if (accessible) {
+                        resultObserver.notifyFailure(
+                                FailureType.EXTRA_METHOD,
+                                method.toString(),
+                                "Hidden method accessible through reflection");
+                    }
+                }
+
+                @Override
+                public void methodAccessibleViaJni(boolean accessible, DexMethod method) {
+                    if (accessible) {
+                        resultObserver.notifyFailure(
+                                FailureType.EXTRA_METHOD,
+                                method.toString(),
+                                "Hidden method accessible through JNI");
+                    }
+                }
+            };
             parseDexApiFilesAsStream(hiddenApiFiles).forEach(dexMember -> {
-                checkSingleMember(dexMember, resultObserver);
+                DexMemberChecker.checkSingleMember(dexMember, observer);
             });
         });
     }
 
-    /**
-     * Check that a DexMember cannot be discovered with reflection or JNI, and
-     * record results in the result
-     */
-    private void checkSingleMember(DexMember dexMember, TestResultObserver resultObserver) {
-        Class<?> klass = findClass(dexMember);
-        if (klass == null) {
-            // Class not found. Therefore its members are not visible.
-            return;
-        }
-
-        if (dexMember instanceof DexField) {
-            if (hasMatchingField_Reflection(klass, (DexField) dexMember)) {
-                resultObserver.notifyFailure(
-                        FailureType.EXTRA_FIELD,
-                        dexMember.toString(),
-                        "Hidden field accessible through reflection");
-            }
-            if (hasMatchingField_JNI(klass, (DexField) dexMember)) {
-                resultObserver.notifyFailure(
-                        FailureType.EXTRA_FIELD,
-                        dexMember.toString(),
-                        "Hidden field accessible through JNI");
-            }
-        } else if (dexMember instanceof DexMethod) {
-            if (hasMatchingMethod_Reflection(klass, (DexMethod) dexMember)) {
-                resultObserver.notifyFailure(
-                        FailureType.EXTRA_METHOD,
-                        dexMember.toString(),
-                        "Hidden method accessible through reflection");
-            }
-            if (hasMatchingMethod_JNI(klass, (DexMethod) dexMember)) {
-                resultObserver.notifyFailure(
-                        FailureType.EXTRA_METHOD,
-                        dexMember.toString(),
-                        "Hidden method accessible through JNI");
-            }
-        } else {
-            throw new IllegalStateException("Unexpected type of dex member");
-        }
-    }
-
-    private boolean typesMatch(Class<?>[] classes, List<String> typeNames) {
-        if (classes.length != typeNames.size()) {
-            return false;
-        }
-        for (int i = 0; i < classes.length; ++i) {
-            if (!classes[i].getTypeName().equals(typeNames.get(i))) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private Class<?> findClass(DexMember dexMember) {
-        Class<?> klass = null;
-        try {
-            return Class.forName(dexMember.getJavaClassName());
-        } catch (ClassNotFoundException ex) {
-            return null;
-        }
-    }
-
-    private static boolean hasMatchingField_Reflection(Class<?> klass, DexField dexField) {
-        try {
-            klass.getDeclaredField(dexField.getName());
-            return true;
-        } catch (NoSuchFieldException ex) {
-            return false;
-        }
-    }
-
-    private static boolean hasMatchingField_JNI(Class<?> klass, DexField dexField) {
-        try {
-            getField_JNI(klass, dexField.getName(), dexField.getDexType());
-            return true;
-        } catch (NoSuchFieldError ex) {
-        }
-        try {
-            getStaticField_JNI(klass, dexField.getName(), dexField.getDexType());
-            return true;
-        } catch (NoSuchFieldError ex) {
-        }
-        return false;
-    }
-
-    private boolean hasMatchingMethod_Reflection(Class<?> klass, DexMethod dexMethod) {
-        List<String> methodParams = dexMethod.getJavaParameterTypes();
-
-        if (dexMethod.isConstructor()) {
-            for (Constructor constructor : klass.getDeclaredConstructors()) {
-                if (typesMatch(constructor.getParameterTypes(), methodParams)) {
-                    return true;
-                }
-            }
-        } else {
-            for (Method method : klass.getDeclaredMethods()) {
-                if (method.getName().equals(dexMethod.getName())
-                        && typesMatch(method.getParameterTypes(), methodParams)) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    private static boolean hasMatchingMethod_JNI(Class<?> klass, DexMethod dexMethod) {
-        try {
-            getMethod_JNI(klass, dexMethod.getName(), dexMethod.getDexSignature());
-            return true;
-        } catch (NoSuchMethodError ex) {
-        }
-        if (!dexMethod.isConstructor()) {
-            try {
-                getStaticMethod_JNI(klass, dexMethod.getName(), dexMethod.getDexSignature());
-                return true;
-            } catch (NoSuchMethodError ex) {
-            }
-        }
-        return false;
-    }
-
     private static Stream<DexMember> parseDexApiFilesAsStream(String[] apiFiles) {
         DexApiDocumentParser dexApiDocumentParser = new DexApiDocumentParser();
         return Stream.of(apiFiles)
@@ -190,9 +117,4 @@
                 .flatMap(stream -> dexApiDocumentParser.parseAsStream(stream));
     }
 
-    private static native boolean getField_JNI(Class<?> klass, String name, String type);
-    private static native boolean getStaticField_JNI(Class<?> klass, String name, String type);
-    private static native boolean getMethod_JNI(Class<?> klass, String name, String signature);
-    private static native boolean getStaticMethod_JNI(Class<?> klass, String name,
-            String signature);
 }
diff --git a/tests/signature/api-check/src/java/android/signature/cts/api/KillswitchTest.java b/tests/signature/api-check/src/java/android/signature/cts/api/KillswitchTest.java
new file mode 100644
index 0000000..37ff746
--- /dev/null
+++ b/tests/signature/api-check/src/java/android/signature/cts/api/KillswitchTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.signature.cts.api;
+
+import android.provider.Settings;
+import android.signature.cts.DexApiDocumentParser;
+import android.signature.cts.DexMemberChecker;
+import android.signature.cts.FailureType;
+
+public class KillswitchTest extends AbstractApiTest {
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        DexMemberChecker.init();
+        // precondition check: make sure global setting has been configured properly.
+        // This should be done via an adb command, configured in AndroidTest.xml.
+        assertEquals("Global setting " + Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS,
+                "*",
+                Settings.Global.getString(
+                        getInstrumentation().getContext().getContentResolver(),
+                        Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS));
+    }
+
+    private static final String ERROR_MESSAGE_APPENDIX =
+            " when global setting hidden_api_blacklist_exemptions is set to *";
+
+    public void testKillswitch() {
+        // for now, just verify that we can access something *not* on the light grey or white list
+        DexApiDocumentParser.DexMember member = new DexApiDocumentParser.DexField(
+                "Ldalvik/system/VMRuntime;", "THE_ONE", "Ldalvik/system/VMRuntime;");
+        runWithTestResultObserver(resultObserver ->
+                DexMemberChecker.checkSingleMember(member, new DexMemberChecker.Observer() {
+                    @Override
+                    public void classAccessible(boolean accessible,
+                            DexApiDocumentParser.DexMember member) {
+                        if (!accessible) {
+                            resultObserver.notifyFailure(
+                                    FailureType.MISSING_CLASS,
+                                    member.toString(),
+                                    "Class from boot classpath is not accessible"
+                                            + ERROR_MESSAGE_APPENDIX);
+                        }
+                    }
+
+                    @Override
+                    public void fieldAccessibleViaReflection(boolean accessible,
+                            DexApiDocumentParser.DexField field) {
+                        if (!accessible) {
+                            resultObserver.notifyFailure(
+                                    FailureType.MISSING_FIELD,
+                                    field.toString(),
+                                    "Field from boot classpath is not accessible via reflection"
+                                            + ERROR_MESSAGE_APPENDIX);
+                        }
+                    }
+
+                    @Override
+                    public void fieldAccessibleViaJni(boolean accessible,
+                            DexApiDocumentParser.DexField field) {
+                        if (!accessible) {
+                            resultObserver.notifyFailure(
+                                    FailureType.MISSING_FIELD,
+                                    field.toString(),
+                                    "Field from boot classpath is not accessible via JNI"
+                                            + ERROR_MESSAGE_APPENDIX);
+                        }
+                    }
+
+                    @Override
+                    public void methodAccessibleViaReflection(boolean accessible,
+                            DexApiDocumentParser.DexMethod method) {
+                        if (!accessible) {
+                            resultObserver.notifyFailure(
+                                    FailureType.MISSING_METHOD,
+                                    method.toString(),
+                                    "Method from boot classpath is not accessible via reflection"
+                                            + ERROR_MESSAGE_APPENDIX);
+                        }
+                    }
+
+                    @Override
+                    public void methodAccessibleViaJni(boolean accessible,
+                            DexApiDocumentParser.DexMethod method) {
+                        if (!accessible) {
+                            resultObserver.notifyFailure(
+                                    FailureType.MISSING_METHOD,
+                                    method.toString(),
+                                    "Method from boot classpath is not accessible via JNI"
+                                            + ERROR_MESSAGE_APPENDIX);
+                        }
+                    }
+
+                }));
+    }
+}
diff --git a/tests/signature/dex-checker/Android.mk b/tests/signature/dex-checker/Android.mk
new file mode 100644
index 0000000..8a86a36
--- /dev/null
+++ b/tests/signature/dex-checker/Android.mk
@@ -0,0 +1,25 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := libcts_dexchecker
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES := dex-checker.cpp
+LOCAL_CFLAGS := -Wall -Werror
+LOCAL_C_INCLUDES := $(JNI_H_INCLUDE)
+LOCAL_SDK_VERSION := current
+LOCAL_NDK_STL_VARIANT := c++_static
+include $(BUILD_SHARED_LIBRARY)
diff --git a/tests/signature/api-check/hidden-api/hidden-api.cpp b/tests/signature/dex-checker/dex-checker.cpp
similarity index 90%
rename from tests/signature/api-check/hidden-api/hidden-api.cpp
rename to tests/signature/dex-checker/dex-checker.cpp
index fd2a94d..dc6757e 100644
--- a/tests/signature/api-check/hidden-api/hidden-api.cpp
+++ b/tests/signature/dex-checker/dex-checker.cpp
@@ -43,7 +43,7 @@
 };
 
 extern "C" JNIEXPORT void JNICALL
-Java_android_signature_cts_api_HiddenApiTest_getField_1JNI(
+Java_android_signature_cts_DexMemberChecker_getField_1JNI(
     JNIEnv* env, jclass, jclass klass, jstring name, jstring type) {
   ScopedUtfChars utf_name(env, name);
   ScopedUtfChars utf_type(env, type);
@@ -53,7 +53,7 @@
 }
 
 extern "C" JNIEXPORT void JNICALL
-Java_android_signature_cts_api_HiddenApiTest_getStaticField_1JNI(
+Java_android_signature_cts_DexMemberChecker_getStaticField_1JNI(
     JNIEnv* env, jclass, jclass klass, jstring name, jstring type) {
   ScopedUtfChars utf_name(env, name);
   ScopedUtfChars utf_type(env, type);
@@ -63,7 +63,7 @@
 }
 
 extern "C" JNIEXPORT void JNICALL
-Java_android_signature_cts_api_HiddenApiTest_getMethod_1JNI(
+Java_android_signature_cts_DexMemberChecker_getMethod_1JNI(
     JNIEnv* env, jclass, jclass klass, jstring name, jstring signature) {
   ScopedUtfChars utf_name(env, name);
   ScopedUtfChars utf_signature(env, signature);
@@ -73,7 +73,7 @@
 }
 
 extern "C" JNIEXPORT void JNICALL
-Java_android_signature_cts_api_HiddenApiTest_getStaticMethod_1JNI(
+Java_android_signature_cts_DexMemberChecker_getStaticMethod_1JNI(
     JNIEnv* env, jclass, jclass klass, jstring name, jstring signature) {
   ScopedUtfChars utf_name(env, name);
   ScopedUtfChars utf_signature(env, signature);
diff --git a/tests/signature/runSignatureTests.sh b/tests/signature/runSignatureTests.sh
index 1350297..d3f1546 100755
--- a/tests/signature/runSignatureTests.sh
+++ b/tests/signature/runSignatureTests.sh
@@ -4,6 +4,8 @@
 #
 # Builds and runs signature APK tests.
 
+set -e
+
 if [ -z "$ANDROID_BUILD_TOP" ]; then
     echo "Missing environment variables. Did you run build/envsetup.sh and lunch?" >&2
     exit 1
@@ -24,6 +26,7 @@
 CtsSystemApiAnnotationTestCases
 
 CtsHiddenApiDiscoveryTestCases
+CtsHiddenApiKillswitchTestCases
 "
 else
     PACKAGES=${1+"$@"}
diff --git a/tests/signature/src/android/signature/cts/DexMemberChecker.java b/tests/signature/src/android/signature/cts/DexMemberChecker.java
new file mode 100644
index 0000000..3800682
--- /dev/null
+++ b/tests/signature/src/android/signature/cts/DexMemberChecker.java
@@ -0,0 +1,160 @@
+/*
+ * 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.signature.cts;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.List;
+
+public class DexMemberChecker {
+
+    public interface Observer {
+        void classAccessible(boolean accessible, DexApiDocumentParser.DexMember member);
+        void fieldAccessibleViaReflection(boolean accessible, DexApiDocumentParser.DexField field);
+        void fieldAccessibleViaJni(boolean accessible, DexApiDocumentParser.DexField field);
+        void methodAccessibleViaReflection(boolean accessible,
+                DexApiDocumentParser.DexMethod method);
+        void methodAccessibleViaJni(boolean accessible, DexApiDocumentParser.DexMethod method);
+    }
+
+    public static void init() {
+        System.loadLibrary("cts_dexchecker");
+    }
+
+    public static void checkSingleMember(DexApiDocumentParser.DexMember dexMember,
+            DexMemberChecker.Observer observer) {
+        Class<?> klass = findClass(dexMember);
+        if (klass == null) {
+            // Class not found. Therefore its members are not visible.
+            observer.classAccessible(false, dexMember);
+            return;
+        }
+        observer.classAccessible(true, dexMember);
+
+        StringBuilder error = new StringBuilder("Hidden ");
+        if (dexMember instanceof DexApiDocumentParser.DexField) {
+            DexApiDocumentParser.DexField field = (DexApiDocumentParser.DexField) dexMember;
+            observer.fieldAccessibleViaReflection(
+                    hasMatchingField_Reflection(klass, field),
+                    field);
+            observer.fieldAccessibleViaJni(
+                    hasMatchingField_JNI(klass, field),
+                    field);
+        } else if (dexMember instanceof DexApiDocumentParser.DexMethod) {
+            DexApiDocumentParser.DexMethod method = (DexApiDocumentParser.DexMethod) dexMember;
+            observer.methodAccessibleViaReflection(
+                    hasMatchingMethod_Reflection(klass, method),
+                    method);
+            observer.methodAccessibleViaJni(
+                    hasMatchingMethod_JNI(klass, method),
+                    method);
+        } else {
+            throw new IllegalStateException("Unexpected type of dex member");
+        }
+    }
+
+    private static boolean typesMatch(Class<?>[] classes, List<String> typeNames) {
+        if (classes.length != typeNames.size()) {
+            return false;
+        }
+        for (int i = 0; i < classes.length; ++i) {
+            if (!classes[i].getTypeName().equals(typeNames.get(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static Class<?> findClass(DexApiDocumentParser.DexMember dexMember) {
+        Class<?> klass = null;
+        try {
+            return Class.forName(dexMember.getJavaClassName());
+        } catch (ClassNotFoundException ex) {
+            return null;
+        }
+    }
+
+    private static boolean hasMatchingField_Reflection(Class<?> klass,
+            DexApiDocumentParser.DexField dexField) {
+        try {
+            klass.getDeclaredField(dexField.getName());
+            return true;
+        } catch (NoSuchFieldException ex) {
+            return false;
+        }
+    }
+
+    private static boolean hasMatchingField_JNI(Class<?> klass,
+            DexApiDocumentParser.DexField dexField) {
+        try {
+            getField_JNI(klass, dexField.getName(), dexField.getDexType());
+            return true;
+        } catch (NoSuchFieldError ex) {
+        }
+        try {
+            getStaticField_JNI(klass, dexField.getName(), dexField.getDexType());
+            return true;
+        } catch (NoSuchFieldError ex) {
+        }
+        return false;
+    }
+
+    private static boolean hasMatchingMethod_Reflection(Class<?> klass,
+            DexApiDocumentParser.DexMethod dexMethod) {
+        List<String> methodParams = dexMethod.getJavaParameterTypes();
+
+        if (dexMethod.isConstructor()) {
+            for (Constructor constructor : klass.getDeclaredConstructors()) {
+                if (typesMatch(constructor.getParameterTypes(), methodParams)) {
+                    return true;
+                }
+            }
+        } else {
+            for (Method method : klass.getDeclaredMethods()) {
+                if (method.getName().equals(dexMethod.getName())
+                        && typesMatch(method.getParameterTypes(), methodParams)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private static boolean hasMatchingMethod_JNI(Class<?> klass,
+            DexApiDocumentParser.DexMethod dexMethod) {
+        try {
+            getMethod_JNI(klass, dexMethod.getName(), dexMethod.getDexSignature());
+            return true;
+        } catch (NoSuchMethodError ex) {
+        }
+        if (!dexMethod.isConstructor()) {
+            try {
+                getStaticMethod_JNI(klass, dexMethod.getName(), dexMethod.getDexSignature());
+                return true;
+            } catch (NoSuchMethodError ex) {
+            }
+        }
+        return false;
+    }
+
+    private static native boolean getField_JNI(Class<?> klass, String name, String type);
+    private static native boolean getStaticField_JNI(Class<?> klass, String name, String type);
+    private static native boolean getMethod_JNI(Class<?> klass, String name, String signature);
+    private static native boolean getStaticMethod_JNI(Class<?> klass, String name,
+            String signature);
+
+}