Set up a test for hidden API enforcement

Submitting the test first for easier review. The expected outcome
is that all currently class members are discoverable and accessible.
Future CLs will implement the enforcement and change the expected
outcome.

The test itself has two JARs - parent declares classes and child
tries to access them using reflection, JNI and static linking.
The test driver ("Main" class) loads these JARs as follows:
(a) both with class loaders
(b) parent in boot class path, child with class loader
(c) both in boot class path

In (a), there should be no enforcement as the JAR does not have
hidden API access flags (would not load otherwise). In situation(b),
child should only be allowed to access parent's public, non-hidden
members. And in (c), parent contains hidden API access flags but
child is exempt from access checks.

Bug: 64382372
Test: art/test.py -b -r -t 674-hiddenapi
Change-Id: I19f5f7c30c0c7913703209817d36006b161c6778
diff --git a/test/674-hiddenapi/api-blacklist.txt b/test/674-hiddenapi/api-blacklist.txt
new file mode 100644
index 0000000..d43360c
--- /dev/null
+++ b/test/674-hiddenapi/api-blacklist.txt
@@ -0,0 +1,25 @@
+LNullaryConstructorBlacklist;-><init>()V
+LParentClass;->fieldPublicBlacklist:I
+LParentClass;->fieldPackageBlacklist:I
+LParentClass;->fieldProtectedBlacklist:I
+LParentClass;->fieldPrivateBlacklist:I
+LParentClass;->fieldPublicStaticBlacklist:I
+LParentClass;->fieldPackageStaticBlacklist:I
+LParentClass;->fieldProtectedStaticBlacklist:I
+LParentClass;->fieldPrivateStaticBlacklist:I
+LParentClass;->methodPublicBlacklist()I
+LParentClass;->methodPackageBlacklist()I
+LParentClass;->methodProtectedBlacklist()I
+LParentClass;->methodPrivateBlacklist()I
+LParentClass;->methodPublicStaticBlacklist()I
+LParentClass;->methodPackageStaticBlacklist()I
+LParentClass;->methodProtectedStaticBlacklist()I
+LParentClass;->methodPrivateStaticBlacklist()I
+LParentClass;-><init>(IC)V
+LParentClass;-><init>(FC)V
+LParentClass;-><init>(JC)V
+LParentClass;-><init>(DC)V
+LParentInterface;->fieldPublicStaticBlacklist:I
+LParentInterface;->methodPublicBlacklist()I
+LParentInterface;->methodPublicStaticBlacklist()I
+LParentInterface;->methodPublicDefaultBlacklist()I
\ No newline at end of file
diff --git a/test/674-hiddenapi/api-dark-greylist.txt b/test/674-hiddenapi/api-dark-greylist.txt
new file mode 100644
index 0000000..d0f35f6
--- /dev/null
+++ b/test/674-hiddenapi/api-dark-greylist.txt
@@ -0,0 +1,25 @@
+LNullaryConstructorDarkGreylist;-><init>()V
+LParentClass;->fieldPublicDarkGreylist:I
+LParentClass;->fieldPackageDarkGreylist:I
+LParentClass;->fieldProtectedDarkGreylist:I
+LParentClass;->fieldPrivateDarkGreylist:I
+LParentClass;->fieldPublicStaticDarkGreylist:I
+LParentClass;->fieldPackageStaticDarkGreylist:I
+LParentClass;->fieldProtectedStaticDarkGreylist:I
+LParentClass;->fieldPrivateStaticDarkGreylist:I
+LParentClass;->methodPublicDarkGreylist()I
+LParentClass;->methodPackageDarkGreylist()I
+LParentClass;->methodProtectedDarkGreylist()I
+LParentClass;->methodPrivateDarkGreylist()I
+LParentClass;->methodPublicStaticDarkGreylist()I
+LParentClass;->methodPackageStaticDarkGreylist()I
+LParentClass;->methodProtectedStaticDarkGreylist()I
+LParentClass;->methodPrivateStaticDarkGreylist()I
+LParentClass;-><init>(IB)V
+LParentClass;-><init>(FB)V
+LParentClass;-><init>(JB)V
+LParentClass;-><init>(DB)V
+LParentInterface;->fieldPublicStaticDarkGreylist:I
+LParentInterface;->methodPublicDarkGreylist()I
+LParentInterface;->methodPublicStaticDarkGreylist()I
+LParentInterface;->methodPublicDefaultDarkGreylist()I
\ No newline at end of file
diff --git a/test/674-hiddenapi/api-light-greylist.txt b/test/674-hiddenapi/api-light-greylist.txt
new file mode 100644
index 0000000..2809025
--- /dev/null
+++ b/test/674-hiddenapi/api-light-greylist.txt
@@ -0,0 +1,25 @@
+LNullaryConstructorLightGreylist;-><init>()V
+LParentClass;->fieldPublicLightGreylist:I
+LParentClass;->fieldPackageLightGreylist:I
+LParentClass;->fieldProtectedLightGreylist:I
+LParentClass;->fieldPrivateLightGreylist:I
+LParentClass;->fieldPublicStaticLightGreylist:I
+LParentClass;->fieldPackageStaticLightGreylist:I
+LParentClass;->fieldProtectedStaticLightGreylist:I
+LParentClass;->fieldPrivateStaticLightGreylist:I
+LParentClass;->methodPublicLightGreylist()I
+LParentClass;->methodPackageLightGreylist()I
+LParentClass;->methodProtectedLightGreylist()I
+LParentClass;->methodPrivateLightGreylist()I
+LParentClass;->methodPublicStaticLightGreylist()I
+LParentClass;->methodPackageStaticLightGreylist()I
+LParentClass;->methodProtectedStaticLightGreylist()I
+LParentClass;->methodPrivateStaticLightGreylist()I
+LParentClass;-><init>(IZ)V
+LParentClass;-><init>(FZ)V
+LParentClass;-><init>(JZ)V
+LParentClass;-><init>(DZ)V
+LParentInterface;->fieldPublicStaticLightGreylist:I
+LParentInterface;->methodPublicLightGreylist()I
+LParentInterface;->methodPublicStaticLightGreylist()I
+LParentInterface;->methodPublicDefaultLightGreylist()I
\ No newline at end of file
diff --git a/test/674-hiddenapi/build b/test/674-hiddenapi/build
new file mode 100644
index 0000000..330a6de
--- /dev/null
+++ b/test/674-hiddenapi/build
@@ -0,0 +1,38 @@
+#!/bin/bash
+#
+# Copyright 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.
+
+set -e
+
+# Build the jars twice. First with applying hiddenapi, creating a boot jar, then
+# a second time without to create a normal jar. We need to do this because we
+# want to load the jar once as an app module and once as a member of the boot
+# class path. The DexFileVerifier would fail on the former as it does not allow
+# hidden API access flags in dex files. DexFileVerifier is not invoked on boot
+# class path dex files, so the boot jar loads fine in the latter case.
+
+export USE_HIDDENAPI=true
+./default-build "$@"
+
+# Move the jar file into the resource folder to be bundled with the test.
+mkdir res
+mv ${TEST_NAME}.jar res/boot.jar
+
+# Clear all intermediate files otherwise default-build would either skip
+# compilation or fail rebuilding.
+rm -rf classes*
+
+export USE_HIDDENAPI=false
+./default-build "$@"
diff --git a/test/674-hiddenapi/check b/test/674-hiddenapi/check
new file mode 100644
index 0000000..660643f
--- /dev/null
+++ b/test/674-hiddenapi/check
@@ -0,0 +1,20 @@
+#!/bin/bash
+#
+# 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.
+
+# Remove pid and date from the log messages.
+grep -vE '^dalvikvm(32|64) E [^]]+]' "$2" | grep -v JNI_OnUnload > "$2.tmp"
+
+./default-check "$1" "$2.tmp"
diff --git a/test/674-hiddenapi/expected.txt b/test/674-hiddenapi/expected.txt
new file mode 100644
index 0000000..727a061
--- /dev/null
+++ b/test/674-hiddenapi/expected.txt
@@ -0,0 +1,5 @@
+JNI_OnLoad called
+JNI_OnLoad called
+JNI_OnLoad called
+JNI_OnLoad called
+Done
diff --git a/test/674-hiddenapi/hiddenapi.cc b/test/674-hiddenapi/hiddenapi.cc
new file mode 100644
index 0000000..672079b
--- /dev/null
+++ b/test/674-hiddenapi/hiddenapi.cc
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#include "class_linker.h"
+#include "dex/art_dex_file_loader.h"
+#include "jni.h"
+#include "runtime.h"
+#include "scoped_thread_state_change-inl.h"
+#include "thread.h"
+#include "ti-agent/scoped_utf_chars.h"
+
+namespace art {
+namespace Test674HiddenApi {
+
+extern "C" JNIEXPORT void JNICALL Java_Main_appendToBootClassLoader(
+    JNIEnv* env, jclass, jstring jpath) {
+  ScopedUtfChars utf(env, jpath);
+  const char* path = utf.c_str();
+  if (path == nullptr) {
+    return;
+  }
+
+  ArtDexFileLoader dex_loader;
+  std::string error_msg;
+  std::vector<std::unique_ptr<const DexFile>> dex_files;
+  if (!dex_loader.Open(path,
+                       path,
+                       /* verify */ false,
+                       /* verify_checksum */ true,
+                       &error_msg,
+                       &dex_files)) {
+    LOG(FATAL) << "Could not open " << path << " for boot classpath extension: " << error_msg;
+    UNREACHABLE();
+  }
+
+  ScopedObjectAccess soa(Thread::Current());
+  for (std::unique_ptr<const DexFile>& dex_file : dex_files) {
+    Runtime::Current()->GetClassLinker()->AppendToBootClassPath(
+        Thread::Current(), *dex_file.release());
+  }
+}
+
+static jobject NewInstance(JNIEnv* env, jclass klass) {
+  jmethodID constructor = env->GetMethodID(klass, "<init>", "()V");
+  if (constructor == NULL) {
+    return NULL;
+  }
+  return env->NewObject(klass, constructor);
+}
+
+extern "C" JNIEXPORT jboolean JNICALL Java_JNI_canDiscoverField(
+    JNIEnv* env, jclass, jclass klass, jstring name, jboolean is_static) {
+  ScopedUtfChars utf_name(env, name);
+  jfieldID field = is_static ? env->GetStaticFieldID(klass, utf_name.c_str(), "I")
+                             : env->GetFieldID(klass, utf_name.c_str(), "I");
+  if (field == NULL) {
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+
+  return JNI_TRUE;
+}
+
+extern "C" JNIEXPORT jboolean JNICALL Java_JNI_canGetField(
+    JNIEnv* env, jclass, jclass klass, jstring name, jboolean is_static) {
+  ScopedUtfChars utf_name(env, name);
+  jfieldID field = is_static ? env->GetStaticFieldID(klass, utf_name.c_str(), "I")
+                             : env->GetFieldID(klass, utf_name.c_str(), "I");
+  if (field == NULL) {
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+  if (is_static) {
+    env->GetStaticIntField(klass, field);
+  } else {
+    jobject obj = NewInstance(env, klass);
+    if (obj == NULL) {
+      env->ExceptionDescribe();
+      env->ExceptionClear();
+      return JNI_FALSE;
+    }
+    env->GetIntField(obj, field);
+  }
+
+  if (env->ExceptionOccurred()) {
+    env->ExceptionDescribe();
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+
+  return JNI_TRUE;
+}
+
+extern "C" JNIEXPORT jboolean JNICALL Java_JNI_canSetField(
+    JNIEnv* env, jclass, jclass klass, jstring name, jboolean is_static) {
+  ScopedUtfChars utf_name(env, name);
+  jfieldID field = is_static ? env->GetStaticFieldID(klass, utf_name.c_str(), "I")
+                             : env->GetFieldID(klass, utf_name.c_str(), "I");
+  if (field == NULL) {
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+  if (is_static) {
+    env->SetStaticIntField(klass, field, 42);
+  } else {
+    jobject obj = NewInstance(env, klass);
+    if (obj == NULL) {
+      env->ExceptionDescribe();
+      env->ExceptionClear();
+      return JNI_FALSE;
+    }
+    env->SetIntField(obj, field, 42);
+  }
+
+  if (env->ExceptionOccurred()) {
+    env->ExceptionDescribe();
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+
+  return JNI_TRUE;
+}
+
+extern "C" JNIEXPORT jboolean JNICALL Java_JNI_canDiscoverMethod(
+    JNIEnv* env, jclass, jclass klass, jstring name, jboolean is_static) {
+  ScopedUtfChars utf_name(env, name);
+  jmethodID method = is_static ? env->GetStaticMethodID(klass, utf_name.c_str(), "()I")
+                               : env->GetMethodID(klass, utf_name.c_str(), "()I");
+  if (method == NULL) {
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+
+  return JNI_TRUE;
+}
+
+extern "C" JNIEXPORT jboolean JNICALL Java_JNI_canInvokeMethodA(
+    JNIEnv* env, jclass, jclass klass, jstring name, jboolean is_static) {
+  ScopedUtfChars utf_name(env, name);
+  jmethodID method = is_static ? env->GetStaticMethodID(klass, utf_name.c_str(), "()I")
+                               : env->GetMethodID(klass, utf_name.c_str(), "()I");
+  if (method == NULL) {
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+
+  if (is_static) {
+    env->CallStaticIntMethodA(klass, method, nullptr);
+  } else {
+    jobject obj = NewInstance(env, klass);
+    if (obj == NULL) {
+      env->ExceptionDescribe();
+      env->ExceptionClear();
+      return JNI_FALSE;
+    }
+    env->CallIntMethodA(obj, method, nullptr);
+  }
+
+  if (env->ExceptionOccurred()) {
+    env->ExceptionDescribe();
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+
+  return JNI_TRUE;
+}
+
+extern "C" JNIEXPORT jboolean JNICALL Java_JNI_canInvokeMethodV(
+    JNIEnv* env, jclass, jclass klass, jstring name, jboolean is_static) {
+  ScopedUtfChars utf_name(env, name);
+  jmethodID method = is_static ? env->GetStaticMethodID(klass, utf_name.c_str(), "()I")
+                               : env->GetMethodID(klass, utf_name.c_str(), "()I");
+  if (method == NULL) {
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+
+  if (is_static) {
+    env->CallStaticIntMethod(klass, method);
+  } else {
+    jobject obj = NewInstance(env, klass);
+    if (obj == NULL) {
+      env->ExceptionDescribe();
+      env->ExceptionClear();
+      return JNI_FALSE;
+    }
+    env->CallIntMethod(obj, method);
+  }
+
+  if (env->ExceptionOccurred()) {
+    env->ExceptionDescribe();
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+
+  return JNI_TRUE;
+}
+
+static constexpr size_t kConstructorSignatureLength = 5;  // e.g. (IZ)V
+static constexpr size_t kNumConstructorArgs = kConstructorSignatureLength - 3;
+
+extern "C" JNIEXPORT jboolean JNICALL Java_JNI_canDiscoverConstructor(
+    JNIEnv* env, jclass, jclass klass, jstring args) {
+  ScopedUtfChars utf_args(env, args);
+  jmethodID constructor = env->GetMethodID(klass, "<init>", utf_args.c_str());
+  if (constructor == NULL) {
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+
+  return JNI_TRUE;
+}
+
+extern "C" JNIEXPORT jboolean JNICALL Java_JNI_canInvokeConstructorA(
+    JNIEnv* env, jclass, jclass klass, jstring args) {
+  ScopedUtfChars utf_args(env, args);
+  jmethodID constructor = env->GetMethodID(klass, "<init>", utf_args.c_str());
+  if (constructor == NULL) {
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+
+  // CheckJNI won't allow out-of-range values, so just zero everything.
+  CHECK_EQ(strlen(utf_args.c_str()), kConstructorSignatureLength);
+  size_t initargs_size = sizeof(jvalue) * kNumConstructorArgs;
+  jvalue *initargs = reinterpret_cast<jvalue*>(alloca(initargs_size));
+  memset(initargs, 0, initargs_size);
+
+  env->NewObjectA(klass, constructor, initargs);
+  if (env->ExceptionOccurred()) {
+    env->ExceptionDescribe();
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+
+  return JNI_TRUE;
+}
+
+extern "C" JNIEXPORT jboolean JNICALL Java_JNI_canInvokeConstructorV(
+    JNIEnv* env, jclass, jclass klass, jstring args) {
+  ScopedUtfChars utf_args(env, args);
+  jmethodID constructor = env->GetMethodID(klass, "<init>", utf_args.c_str());
+  if (constructor == NULL) {
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+
+  // CheckJNI won't allow out-of-range values, so just zero everything.
+  CHECK_EQ(strlen(utf_args.c_str()), kConstructorSignatureLength);
+  size_t initargs_size = sizeof(jvalue) * kNumConstructorArgs;
+  jvalue *initargs = reinterpret_cast<jvalue*>(alloca(initargs_size));
+  memset(initargs, 0, initargs_size);
+
+  static_assert(kNumConstructorArgs == 2, "Change the varargs below if you change the constant");
+  env->NewObject(klass, constructor, initargs[0], initargs[1]);
+  if (env->ExceptionOccurred()) {
+    env->ExceptionDescribe();
+    env->ExceptionClear();
+    return JNI_FALSE;
+  }
+
+  return JNI_TRUE;
+}
+
+extern "C" JNIEXPORT jint JNICALL Java_Reflection_getHiddenApiAccessFlags(JNIEnv*, jclass) {
+  return static_cast<jint>(kAccHiddenApiBits);
+}
+
+extern "C" JNIEXPORT jboolean JNICALL Java_ChildClass_hasPendingWarning(JNIEnv*, jclass) {
+  return false;
+}
+
+extern "C" JNIEXPORT void JNICALL Java_ChildClass_clearWarning(JNIEnv*, jclass) {
+  return;
+}
+
+}  // namespace Test674HiddenApi
+}  // namespace art
diff --git a/test/674-hiddenapi/info.txt b/test/674-hiddenapi/info.txt
new file mode 100644
index 0000000..25ac6ae
--- /dev/null
+++ b/test/674-hiddenapi/info.txt
@@ -0,0 +1,15 @@
+Test whether hidden API access flags are being enforced. The test is composed of
+two JARs. The first (parent) defines methods and fields and the second (child)
+tries to access them with reflection/JNI or link against them. Note that the
+first is compiled twice - once with and once without hidden access flags.
+
+The test then proceeds to exercise the following combinations of class loading:
+(a) Both parent and child dex loaded with PathClassLoader, parent's class loader
+    is the child's class loader's parent. Access flags should not be enforced as
+    the parent does not belong to boot class path.
+(b) Parent is appended to boot class path, child is loaded with PathClassLoader.
+    In this situation child should not be able to access hidden methods/fields
+    of the parent.
+(c) Both parent and child are appended to boot class path. Restrictions should
+    not apply as hidden APIs are accessible within the boundaries of the boot
+    class path.
diff --git a/test/674-hiddenapi/src-art/Main.java b/test/674-hiddenapi/src-art/Main.java
new file mode 100644
index 0000000..9773e0a
--- /dev/null
+++ b/test/674-hiddenapi/src-art/Main.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+import dalvik.system.InMemoryDexClassLoader;
+import dalvik.system.PathClassLoader;
+import java.io.File;
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class Main {
+  public static void main(String[] args) throws Exception {
+    System.loadLibrary(args[0]);
+    prepareNativeLibFileName(args[0]);
+
+    // Run test with both parent and child dex files loaded with class loaders.
+    // The expectation is that hidden members in parent should be visible to
+    // the child.
+    doTest(false, false);
+
+    // Now append parent dex file to boot class path and run again. This time
+    // the child dex file should not be able to access private APIs of the parent.
+    appendToBootClassLoader(DEX_PARENT_BOOT);
+    doTest(true, false);
+
+    // And finally append to child to boot class path as well. With both in the
+    // boot class path, access should be granted.
+    appendToBootClassLoader(DEX_CHILD);
+    doTest(true, true);
+
+    System.out.println("Done");
+  }
+
+  private static void doTest(boolean parentInBoot, boolean childInBoot) throws Exception {
+    // Load parent dex if it is not in boot class path.
+    ClassLoader parentLoader = null;
+    if (parentInBoot) {
+      parentLoader = BOOT_CLASS_LOADER;
+    } else {
+      parentLoader = new PathClassLoader(DEX_PARENT, ClassLoader.getSystemClassLoader());
+    }
+
+    // Load child dex if it is not in boot class path.
+    ClassLoader childLoader = null;
+    if (childInBoot) {
+      if (parentLoader != BOOT_CLASS_LOADER) {
+        throw new IllegalStateException(
+            "DeclaringClass must be in parent class loader of CallingClass");
+      }
+      childLoader = BOOT_CLASS_LOADER;
+    } else {
+      childLoader = new InMemoryDexClassLoader(readDexFile(DEX_CHILD), parentLoader);
+    }
+
+    // Create a unique copy of the native library. Each shared library can only
+    // be loaded once, but for some reason even classes from a class loader
+    // cannot register their native methods against symbols in a shared library
+    // loaded by their parent class loader.
+    String nativeLibCopy = createNativeLibCopy(parentInBoot, childInBoot);
+
+    // Invoke ChildClass.runTest
+    Class.forName("ChildClass", true, childLoader)
+        .getDeclaredMethod("runTest", String.class, Boolean.TYPE, Boolean.TYPE)
+            .invoke(null, nativeLibCopy, parentInBoot, childInBoot);
+  }
+
+  // Routine which tries to figure out the absolute path of our native library.
+  private static void prepareNativeLibFileName(String arg) throws Exception {
+    String libName = System.mapLibraryName(arg);
+    Method libPathsMethod = Runtime.class.getDeclaredMethod("getLibPaths");
+    libPathsMethod.setAccessible(true);
+    String[] libPaths = (String[]) libPathsMethod.invoke(Runtime.getRuntime());
+    nativeLibFileName = null;
+    for (String p : libPaths) {
+      String candidate = p + libName;
+      if (new File(candidate).exists()) {
+        nativeLibFileName = candidate;
+        break;
+      }
+    }
+    if (nativeLibFileName == null) {
+      throw new IllegalStateException("Didn't find " + libName + " in " +
+          Arrays.toString(libPaths));
+    }
+  }
+
+  // Helper to read dex file into memory.
+  private static ByteBuffer readDexFile(String jarFileName) throws Exception {
+    ZipFile zip = new ZipFile(new File(jarFileName));
+    ZipEntry entry = zip.getEntry("classes.dex");
+    InputStream is = zip.getInputStream(entry);
+    int offset = 0;
+    int size = (int) entry.getSize();
+    ByteBuffer buffer = ByteBuffer.allocate(size);
+    while (is.available() > 0) {
+      is.read(buffer.array(), offset, size - offset);
+    }
+    is.close();
+    zip.close();
+    return buffer;
+  }
+
+  // Copy native library to a new file with a unique name so it does not conflict
+  // with other loaded instance of the same binary file.
+  private static String createNativeLibCopy(boolean parentInBoot, boolean childInBoot)
+      throws Exception {
+    String tempFileName = System.mapLibraryName(
+        "hiddenapitest_" + (parentInBoot ? "1" : "0") + (childInBoot ? "1" : "0"));
+    File tempFile = new File(System.getenv("DEX_LOCATION"), tempFileName);
+    Files.copy(new File(nativeLibFileName).toPath(), tempFile.toPath());
+    return tempFile.getAbsolutePath();
+  }
+
+  private static String nativeLibFileName;
+
+  private static final String DEX_PARENT =
+      new File(System.getenv("DEX_LOCATION"), "674-hiddenapi.jar").getAbsolutePath();
+  private static final String DEX_PARENT_BOOT =
+      new File(new File(System.getenv("DEX_LOCATION"), "res"), "boot.jar").getAbsolutePath();
+  private static final String DEX_CHILD =
+      new File(System.getenv("DEX_LOCATION"), "674-hiddenapi-ex.jar").getAbsolutePath();
+
+  private static ClassLoader BOOT_CLASS_LOADER = Object.class.getClassLoader();
+
+  private static native void appendToBootClassLoader(String dexPath);
+}
diff --git a/test/674-hiddenapi/src-ex/ChildClass.java b/test/674-hiddenapi/src-ex/ChildClass.java
new file mode 100644
index 0000000..af615bf
--- /dev/null
+++ b/test/674-hiddenapi/src-ex/ChildClass.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeVisitor;
+
+public class ChildClass {
+  enum PrimitiveType {
+    TInteger('I', Integer.TYPE, Integer.valueOf(0)),
+    TLong('J', Long.TYPE, Long.valueOf(0)),
+    TFloat('F', Float.TYPE, Float.valueOf(0)),
+    TDouble('D', Double.TYPE, Double.valueOf(0)),
+    TBoolean('Z', Boolean.TYPE, Boolean.valueOf(false)),
+    TByte('B', Byte.TYPE, Byte.valueOf((byte) 0)),
+    TShort('S', Short.TYPE, Short.valueOf((short) 0)),
+    TCharacter('C', Character.TYPE, Character.valueOf('0'));
+
+    PrimitiveType(char shorty, Class klass, Object value) {
+      mShorty = shorty;
+      mClass = klass;
+      mDefaultValue = value;
+    }
+
+    public char mShorty;
+    public Class mClass;
+    public Object mDefaultValue;
+  }
+
+  enum Hiddenness {
+    Whitelist(PrimitiveType.TShort),
+    LightGreylist(PrimitiveType.TBoolean),
+    DarkGreylist(PrimitiveType.TByte),
+    Blacklist(PrimitiveType.TCharacter);
+
+    Hiddenness(PrimitiveType type) { mAssociatedType = type; }
+    public PrimitiveType mAssociatedType;
+  }
+
+  enum Visibility {
+    Public(PrimitiveType.TInteger),
+    Package(PrimitiveType.TFloat),
+    Protected(PrimitiveType.TLong),
+    Private(PrimitiveType.TDouble);
+
+    Visibility(PrimitiveType type) { mAssociatedType = type; }
+    public PrimitiveType mAssociatedType;
+  }
+
+  enum Behaviour {
+    Granted,
+    Warning,
+    Denied,
+  }
+
+  private static final boolean booleanValues[] = new boolean[] { false, true };
+
+  public static void runTest(String libFileName, boolean expectedParentInBoot,
+      boolean expectedChildInBoot) throws Exception {
+    System.load(libFileName);
+
+    // Check expectations about loading into boot class path.
+    isParentInBoot = (ParentClass.class.getClassLoader().getParent() == null);
+    if (isParentInBoot != expectedParentInBoot) {
+      throw new RuntimeException("Expected ParentClass " +
+                                 (expectedParentInBoot ? "" : "not ") + "in boot class path");
+    }
+    isChildInBoot = (ChildClass.class.getClassLoader().getParent() == null);
+    if (isChildInBoot != expectedChildInBoot) {
+      throw new RuntimeException("Expected ChildClass " + (expectedChildInBoot ? "" : "not ") +
+                                 "in boot class path");
+    }
+
+    // Run meaningful combinations of access flags.
+    for (Hiddenness hiddenness : Hiddenness.values()) {
+      final Behaviour expected = Behaviour.Granted;
+
+      for (boolean isStatic : booleanValues) {
+        String suffix = (isStatic ? "Static" : "") + hiddenness.name();
+
+        for (Visibility visibility : Visibility.values()) {
+          // Test reflection and JNI on methods and fields
+          for (Class klass : new Class<?>[] { ParentClass.class, ParentInterface.class }) {
+            String baseName = visibility.name() + suffix;
+            checkField(klass, "field" + baseName, isStatic, visibility, expected);
+            checkMethod(klass, "method" + baseName, isStatic, visibility, expected);
+          }
+
+          // Check whether one can use a class constructor.
+          checkConstructor(ParentClass.class, visibility, hiddenness, expected);
+
+          // Check whether you can use an interface default method.
+          String name = "method" + visibility.name() + "Default" + hiddenness.name();
+          checkMethod(ParentInterface.class, name, /*isStatic*/ false, visibility, expected);
+        }
+
+        // Test whether static linking succeeds.
+        checkLinking("LinkFieldGet" + suffix, /*takesParameter*/ false, expected);
+        checkLinking("LinkFieldSet" + suffix, /*takesParameter*/ true, expected);
+        checkLinking("LinkMethod" + suffix, /*takesParameter*/ false, expected);
+      }
+
+      // Check whether Class.newInstance succeeds.
+      checkNullaryConstructor(Class.forName("NullaryConstructor" + hiddenness.name()), expected);
+    }
+  }
+
+  private static void checkField(Class<?> klass, String name, boolean isStatic,
+      Visibility visibility, Behaviour behaviour) throws Exception {
+
+    boolean isPublic = (visibility == Visibility.Public);
+    boolean canDiscover = (behaviour != Behaviour.Denied);
+    boolean setsWarning = (behaviour == Behaviour.Warning);
+
+    if (klass.isInterface() && (!isStatic || !isPublic)) {
+      // Interfaces only have public static fields.
+      return;
+    }
+
+    // Test discovery with reflection.
+
+    if (Reflection.canDiscoverWithGetDeclaredField(klass, name) != canDiscover) {
+      throwDiscoveryException(klass, name, true, "getDeclaredField()", canDiscover);
+    }
+
+    if (Reflection.canDiscoverWithGetDeclaredFields(klass, name) != canDiscover) {
+      throwDiscoveryException(klass, name, true, "getDeclaredFields()", canDiscover);
+    }
+
+    if (Reflection.canDiscoverWithGetField(klass, name) != (canDiscover && isPublic)) {
+      throwDiscoveryException(klass, name, true, "getField()", (canDiscover && isPublic));
+    }
+
+    if (Reflection.canDiscoverWithGetFields(klass, name) != (canDiscover && isPublic)) {
+      throwDiscoveryException(klass, name, true, "getFields()", (canDiscover && isPublic));
+    }
+
+    // Test discovery with JNI.
+
+    if (JNI.canDiscoverField(klass, name, isStatic) != canDiscover) {
+      throwDiscoveryException(klass, name, true, "JNI", canDiscover);
+    }
+
+    // Finish here if we could not discover the field.
+
+    if (!canDiscover) {
+      return;
+    }
+
+    // Test that modifiers are unaffected.
+
+    if (Reflection.canObserveFieldHiddenAccessFlags(klass, name)) {
+      throwModifiersException(klass, name, true);
+    }
+
+    // Test getters and setters when meaningful.
+
+    clearWarning();
+    if (!Reflection.canGetField(klass, name)) {
+      throwAccessException(klass, name, true, "Field.getInt()");
+    }
+    if (hasPendingWarning() != setsWarning) {
+      throwWarningException(klass, name, true, "Field.getInt()", setsWarning);
+    }
+
+    clearWarning();
+    if (!Reflection.canSetField(klass, name)) {
+      throwAccessException(klass, name, true, "Field.setInt()");
+    }
+    if (hasPendingWarning() != setsWarning) {
+      throwWarningException(klass, name, true, "Field.setInt()", setsWarning);
+    }
+
+    clearWarning();
+    if (!JNI.canGetField(klass, name, isStatic)) {
+      throwAccessException(klass, name, true, "getIntField");
+    }
+    if (hasPendingWarning() != setsWarning) {
+      throwWarningException(klass, name, true, "getIntField", setsWarning);
+    }
+
+    clearWarning();
+    if (!JNI.canSetField(klass, name, isStatic)) {
+      throwAccessException(klass, name, true, "setIntField");
+    }
+    if (hasPendingWarning() != setsWarning) {
+      throwWarningException(klass, name, true, "setIntField", setsWarning);
+    }
+  }
+
+  private static void checkMethod(Class<?> klass, String name, boolean isStatic,
+      Visibility visibility, Behaviour behaviour) throws Exception {
+
+    boolean isPublic = (visibility == Visibility.Public);
+    if (klass.isInterface() && !isPublic) {
+      // All interface members are public.
+      return;
+    }
+
+    boolean canDiscover = (behaviour != Behaviour.Denied);
+    boolean setsWarning = (behaviour == Behaviour.Warning);
+
+    // Test discovery with reflection.
+
+    if (Reflection.canDiscoverWithGetDeclaredMethod(klass, name) != canDiscover) {
+      throwDiscoveryException(klass, name, false, "getDeclaredMethod()", canDiscover);
+    }
+
+    if (Reflection.canDiscoverWithGetDeclaredMethods(klass, name) != canDiscover) {
+      throwDiscoveryException(klass, name, false, "getDeclaredMethods()", canDiscover);
+    }
+
+    if (Reflection.canDiscoverWithGetMethod(klass, name) != (canDiscover && isPublic)) {
+      throwDiscoveryException(klass, name, false, "getMethod()", (canDiscover && isPublic));
+    }
+
+    if (Reflection.canDiscoverWithGetMethods(klass, name) != (canDiscover && isPublic)) {
+      throwDiscoveryException(klass, name, false, "getMethods()", (canDiscover && isPublic));
+    }
+
+    // Test discovery with JNI.
+
+    if (JNI.canDiscoverMethod(klass, name, isStatic) != canDiscover) {
+      throwDiscoveryException(klass, name, false, "JNI", canDiscover);
+    }
+
+    // Finish here if we could not discover the field.
+
+    if (!canDiscover) {
+      return;
+    }
+
+    // Test that modifiers are unaffected.
+
+    if (Reflection.canObserveMethodHiddenAccessFlags(klass, name)) {
+      throwModifiersException(klass, name, false);
+    }
+
+    // Test whether we can invoke the method. This skips non-static interface methods.
+
+    if (!klass.isInterface() || isStatic) {
+      clearWarning();
+      if (!Reflection.canInvokeMethod(klass, name)) {
+        throwAccessException(klass, name, false, "invoke()");
+      }
+      if (hasPendingWarning() != setsWarning) {
+        throwWarningException(klass, name, false, "invoke()", setsWarning);
+      }
+
+      clearWarning();
+      if (!JNI.canInvokeMethodA(klass, name, isStatic)) {
+        throwAccessException(klass, name, false, "CallMethodA");
+      }
+      if (hasPendingWarning() != setsWarning) {
+        throwWarningException(klass, name, false, "CallMethodA()", setsWarning);
+      }
+
+      clearWarning();
+      if (!JNI.canInvokeMethodV(klass, name, isStatic)) {
+        throwAccessException(klass, name, false, "CallMethodV");
+      }
+      if (hasPendingWarning() != setsWarning) {
+        throwWarningException(klass, name, false, "CallMethodV()", setsWarning);
+      }
+    }
+  }
+
+  private static void checkConstructor(Class<?> klass, Visibility visibility, Hiddenness hiddenness,
+      Behaviour behaviour) throws Exception {
+
+    boolean isPublic = (visibility == Visibility.Public);
+    String signature = "(" + visibility.mAssociatedType.mShorty +
+                             hiddenness.mAssociatedType.mShorty + ")V";
+    String fullName = "<init>" + signature;
+    Class<?> args[] = new Class[] { visibility.mAssociatedType.mClass,
+                                    hiddenness.mAssociatedType.mClass };
+    Object initargs[] = new Object[] { visibility.mAssociatedType.mDefaultValue,
+                                       hiddenness.mAssociatedType.mDefaultValue };
+
+    boolean canDiscover = (behaviour != Behaviour.Denied);
+    boolean setsWarning = (behaviour == Behaviour.Warning);
+
+    // Test discovery with reflection.
+
+    if (Reflection.canDiscoverWithGetDeclaredConstructor(klass, args) != canDiscover) {
+      throwDiscoveryException(klass, fullName, false, "getDeclaredConstructor()", canDiscover);
+    }
+
+    if (Reflection.canDiscoverWithGetDeclaredConstructors(klass, args) != canDiscover) {
+      throwDiscoveryException(klass, fullName, false, "getDeclaredConstructors()", canDiscover);
+    }
+
+    if (Reflection.canDiscoverWithGetConstructor(klass, args) != (canDiscover && isPublic)) {
+      throwDiscoveryException(
+          klass, fullName, false, "getConstructor()", (canDiscover && isPublic));
+    }
+
+    if (Reflection.canDiscoverWithGetConstructors(klass, args) != (canDiscover && isPublic)) {
+      throwDiscoveryException(
+          klass, fullName, false, "getConstructors()", (canDiscover && isPublic));
+    }
+
+    // Test discovery with JNI.
+
+    if (JNI.canDiscoverConstructor(klass, signature) != canDiscover) {
+      throwDiscoveryException(klass, fullName, false, "JNI", canDiscover);
+    }
+
+    // Finish here if we could not discover the field.
+
+    if (!canDiscover) {
+      return;
+    }
+
+    // Test whether we can invoke the constructor.
+
+    clearWarning();
+    if (!Reflection.canInvokeConstructor(klass, args, initargs)) {
+      throwAccessException(klass, fullName, false, "invoke()");
+    }
+    if (hasPendingWarning() != setsWarning) {
+      throwWarningException(klass, fullName, false, "invoke()", setsWarning);
+    }
+
+    clearWarning();
+    if (!JNI.canInvokeConstructorA(klass, signature)) {
+      throwAccessException(klass, fullName, false, "NewObjectA");
+    }
+    if (hasPendingWarning() != setsWarning) {
+      throwWarningException(klass, fullName, false, "NewObjectA", setsWarning);
+    }
+
+    clearWarning();
+    if (!JNI.canInvokeConstructorV(klass, signature)) {
+      throwAccessException(klass, fullName, false, "NewObjectV");
+    }
+    if (hasPendingWarning() != setsWarning) {
+      throwWarningException(klass, fullName, false, "NewObjectV", setsWarning);
+    }
+  }
+
+  private static void checkNullaryConstructor(Class<?> klass, Behaviour behaviour)
+      throws Exception {
+    boolean canAccess = (behaviour != Behaviour.Denied);
+    boolean setsWarning = (behaviour == Behaviour.Warning);
+
+    clearWarning();
+    if (Reflection.canUseNewInstance(klass) != canAccess) {
+      throw new RuntimeException("Expected to " + (canAccess ? "" : "not ") +
+          "be able to construct " + klass.getName() + ". " +
+          "isParentInBoot = " + isParentInBoot + ", " + "isChildInBoot = " + isChildInBoot);
+    }
+    if (canAccess && hasPendingWarning() != setsWarning) {
+      throwWarningException(klass, "nullary constructor", false, "newInstance", setsWarning);
+    }
+  }
+
+  private static void checkLinking(String className, boolean takesParameter, Behaviour behaviour)
+      throws Exception {
+    boolean canAccess = (behaviour != Behaviour.Denied);
+    boolean setsWarning = (behaviour == Behaviour.Warning);
+
+    clearWarning();
+    if (Linking.canAccess(className, takesParameter) != canAccess) {
+      throw new RuntimeException("Expected to " + (canAccess ? "" : "not ") +
+          "be able to verify " + className + "." +
+          "isParentInBoot = " + isParentInBoot + ", " + "isChildInBoot = " + isChildInBoot);
+    }
+    if (canAccess && hasPendingWarning() != setsWarning) {
+      throwWarningException(
+          Class.forName(className), "access", false, "static linking", setsWarning);
+    }
+  }
+
+  private static void throwDiscoveryException(Class<?> klass, String name, boolean isField,
+      String fn, boolean canAccess) {
+    throw new RuntimeException("Expected " + (isField ? "field " : "method ") + klass.getName() +
+        "." + name + " to " + (canAccess ? "" : "not ") + "be discoverable with " + fn + ". " +
+        "isParentInBoot = " + isParentInBoot + ", " + "isChildInBoot = " + isChildInBoot);
+  }
+
+  private static void throwAccessException(Class<?> klass, String name, boolean isField,
+      String fn) {
+    throw new RuntimeException("Expected to be able to access " + (isField ? "field " : "method ") +
+        klass.getName() + "." + name + " using " + fn + ". " +
+        "isParentInBoot = " + isParentInBoot + ", " + "isChildInBoot = " + isChildInBoot);
+  }
+
+  private static void throwWarningException(Class<?> klass, String name, boolean isField,
+      String fn, boolean setsWarning) {
+    throw new RuntimeException("Expected access to " + (isField ? "field " : "method ") +
+        klass.getName() + "." + name + " using " + fn + " to " + (setsWarning ? "" : "not ") +
+        "set the warning flag. " +
+        "isParentInBoot = " + isParentInBoot + ", " + "isChildInBoot = " + isChildInBoot);
+  }
+
+  private static void throwModifiersException(Class<?> klass, String name, boolean isField) {
+    throw new RuntimeException("Expected " + (isField ? "field " : "method ") + klass.getName() +
+        "." + name + " to not expose hidden modifiers");
+  }
+
+  private static boolean isParentInBoot;
+  private static boolean isChildInBoot;
+
+  private static native boolean hasPendingWarning();
+  private static native void clearWarning();
+}
diff --git a/test/674-hiddenapi/src-ex/JNI.java b/test/674-hiddenapi/src-ex/JNI.java
new file mode 100644
index 0000000..5dfb296
--- /dev/null
+++ b/test/674-hiddenapi/src-ex/JNI.java
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+public class JNI {
+  public static native boolean canDiscoverField(Class<?> klass, String name, boolean isStatic);
+  public static native boolean canGetField(Class<?> klass, String name, boolean isStatic);
+  public static native boolean canSetField(Class<?> klass, String name, boolean isStatic);
+
+  public static native boolean canDiscoverMethod(Class<?> klass, String name, boolean isStatic);
+  public static native boolean canInvokeMethodA(Class<?> klass, String name, boolean isStatic);
+  public static native boolean canInvokeMethodV(Class<?> klass, String name, boolean isStatic);
+
+  public static native boolean canDiscoverConstructor(Class<?> klass, String signature);
+  public static native boolean canInvokeConstructorA(Class<?> klass, String signature);
+  public static native boolean canInvokeConstructorV(Class<?> klass, String signature);
+}
diff --git a/test/674-hiddenapi/src-ex/Linking.java b/test/674-hiddenapi/src-ex/Linking.java
new file mode 100644
index 0000000..c6735d8
--- /dev/null
+++ b/test/674-hiddenapi/src-ex/Linking.java
@@ -0,0 +1,193 @@
+/*
+ * 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.
+ */
+
+import java.lang.reflect.InvocationTargetException;
+
+public class Linking {
+  public static boolean canAccess(String className, boolean takesParameter) throws Exception {
+    try {
+      Class<?> c = Class.forName(className);
+      if (takesParameter) {
+        c.getDeclaredMethod("access", Integer.TYPE).invoke(null, 42);
+      } else {
+        c.getDeclaredMethod("access").invoke(null);
+      }
+      return true;
+    } catch (InvocationTargetException ex) {
+      if (ex.getCause() instanceof IllegalAccessError) {
+        return false;
+      } else {
+        throw ex;
+      }
+    }
+  }
+}
+
+// INSTANCE FIELD GET
+
+class LinkFieldGetWhitelist {
+  public static int access() {
+    return new ParentClass().fieldPublicWhitelist;
+  }
+}
+
+class LinkFieldGetLightGreylist {
+  public static int access() {
+    return new ParentClass().fieldPublicLightGreylist;
+  }
+}
+
+class LinkFieldGetDarkGreylist {
+  public static int access() {
+    return new ParentClass().fieldPublicDarkGreylist;
+  }
+}
+
+class LinkFieldGetBlacklist {
+  public static int access() {
+    return new ParentClass().fieldPublicBlacklist;
+  }
+}
+
+// INSTANCE FIELD SET
+
+class LinkFieldSetWhitelist {
+  public static void access(int x) {
+    new ParentClass().fieldPublicWhitelist = x;
+  }
+}
+
+class LinkFieldSetLightGreylist {
+  public static void access(int x) {
+    new ParentClass().fieldPublicLightGreylist = x;
+  }
+}
+
+class LinkFieldSetDarkGreylist {
+  public static void access(int x) {
+    new ParentClass().fieldPublicDarkGreylist = x;
+  }
+}
+
+class LinkFieldSetBlacklist {
+  public static void access(int x) {
+    new ParentClass().fieldPublicBlacklist = x;
+  }
+}
+
+// STATIC FIELD GET
+
+class LinkFieldGetStaticWhitelist {
+  public static int access() {
+    return ParentClass.fieldPublicStaticWhitelist;
+  }
+}
+
+class LinkFieldGetStaticLightGreylist {
+  public static int access() {
+    return ParentClass.fieldPublicStaticLightGreylist;
+  }
+}
+
+class LinkFieldGetStaticDarkGreylist {
+  public static int access() {
+    return ParentClass.fieldPublicStaticDarkGreylist;
+  }
+}
+
+class LinkFieldGetStaticBlacklist {
+  public static int access() {
+    return ParentClass.fieldPublicStaticBlacklist;
+  }
+}
+
+// STATIC FIELD SET
+
+class LinkFieldSetStaticWhitelist {
+  public static void access(int x) {
+    ParentClass.fieldPublicStaticWhitelist = x;
+  }
+}
+
+class LinkFieldSetStaticLightGreylist {
+  public static void access(int x) {
+    ParentClass.fieldPublicStaticLightGreylist = x;
+  }
+}
+
+class LinkFieldSetStaticDarkGreylist {
+  public static void access(int x) {
+    ParentClass.fieldPublicStaticDarkGreylist = x;
+  }
+}
+
+class LinkFieldSetStaticBlacklist {
+  public static void access(int x) {
+    ParentClass.fieldPublicStaticBlacklist = x;
+  }
+}
+
+// INVOKE INSTANCE METHOD
+
+class LinkMethodWhitelist {
+  public static int access() {
+    return new ParentClass().methodPublicWhitelist();
+  }
+}
+
+class LinkMethodLightGreylist {
+  public static int access() {
+    return new ParentClass().methodPublicLightGreylist();
+  }
+}
+
+class LinkMethodDarkGreylist {
+  public static int access() {
+    return new ParentClass().methodPublicDarkGreylist();
+  }
+}
+
+class LinkMethodBlacklist {
+  public static int access() {
+    return new ParentClass().methodPublicBlacklist();
+  }
+}
+
+// INVOKE STATIC METHOD
+
+class LinkMethodStaticWhitelist {
+  public static int access() {
+    return ParentClass.methodPublicStaticWhitelist();
+  }
+}
+
+class LinkMethodStaticLightGreylist {
+  public static int access() {
+    return ParentClass.methodPublicStaticLightGreylist();
+  }
+}
+
+class LinkMethodStaticDarkGreylist {
+  public static int access() {
+    return ParentClass.methodPublicStaticDarkGreylist();
+  }
+}
+
+class LinkMethodStaticBlacklist {
+  public static int access() {
+    return ParentClass.methodPublicStaticBlacklist();
+  }
+}
diff --git a/test/674-hiddenapi/src-ex/Reflection.java b/test/674-hiddenapi/src-ex/Reflection.java
new file mode 100644
index 0000000..3667e91
--- /dev/null
+++ b/test/674-hiddenapi/src-ex/Reflection.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+
+public class Reflection {
+  public static boolean canDiscoverWithGetDeclaredField(Class<?> klass, String name) {
+    try {
+      klass.getDeclaredField(name);
+      return true;
+    } catch (NoSuchFieldException ex) {
+      return false;
+    }
+  }
+
+  public static boolean canDiscoverWithGetDeclaredFields(Class<?> klass, String name) {
+    for (Field f : klass.getDeclaredFields()) {
+      if (f.getName().equals(name)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static boolean canDiscoverWithGetField(Class<?> klass, String name) {
+    try {
+      klass.getField(name);
+      return true;
+    } catch (NoSuchFieldException ex) {
+      return false;
+    }
+  }
+
+  public static boolean canDiscoverWithGetFields(Class<?> klass, String name) {
+    for (Field f : klass.getFields()) {
+      if (f.getName().equals(name)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static boolean canGetField(Class<?> klass, String name) {
+    try {
+      Field f = klass.getDeclaredField(name);
+      f.setAccessible(true);
+      f.getInt(Modifier.isStatic(f.getModifiers()) ? null : klass.newInstance());
+      return true;
+    } catch (Exception ex) {
+      ex.printStackTrace();
+      return false;
+    }
+  }
+
+  public static boolean canSetField(Class<?> klass, String name) {
+    try {
+      Field f = klass.getDeclaredField(name);
+      f.setAccessible(true);
+      f.setInt(Modifier.isStatic(f.getModifiers()) ? null : klass.newInstance(), 42);
+      return true;
+    } catch (Exception ex) {
+      ex.printStackTrace();
+      return false;
+    }
+  }
+
+  public static boolean canDiscoverWithGetDeclaredMethod(Class<?> klass, String name) {
+    try {
+      klass.getDeclaredMethod(name);
+      return true;
+    } catch (NoSuchMethodException ex) {
+      return false;
+    }
+  }
+
+  public static boolean canDiscoverWithGetDeclaredMethods(Class<?> klass, String name) {
+    for (Method m : klass.getDeclaredMethods()) {
+      if (m.getName().equals(name)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static boolean canDiscoverWithGetMethod(Class<?> klass, String name) {
+    try {
+      klass.getMethod(name);
+      return true;
+    } catch (NoSuchMethodException ex) {
+      return false;
+    }
+  }
+
+  public static boolean canDiscoverWithGetMethods(Class<?> klass, String name) {
+    for (Method m : klass.getMethods()) {
+      if (m.getName().equals(name)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static boolean canInvokeMethod(Class<?> klass, String name) {
+    try {
+      Method m = klass.getDeclaredMethod(name);
+      m.setAccessible(true);
+      m.invoke(klass.isInterface() ? null : klass.newInstance());
+      return true;
+    } catch (Exception ex) {
+      ex.printStackTrace();
+      return false;
+    }
+  }
+
+  public static boolean canDiscoverWithGetDeclaredConstructor(Class<?> klass, Class<?> args[]) {
+    try {
+      klass.getDeclaredConstructor(args);
+      return true;
+    } catch (NoSuchMethodException ex) {
+      return false;
+    }
+  }
+
+  public static boolean canDiscoverWithGetDeclaredConstructors(Class<?> klass, Class<?> args[]) {
+    for (Constructor c : klass.getDeclaredConstructors()) {
+      if (Arrays.equals(c.getParameterTypes(), args)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static boolean canDiscoverWithGetConstructor(Class<?> klass, Class<?> args[]) {
+    try {
+      klass.getConstructor(args);
+      return true;
+    } catch (NoSuchMethodException ex) {
+      return false;
+    }
+  }
+
+  public static boolean canDiscoverWithGetConstructors(Class<?> klass, Class<?> args[]) {
+    for (Constructor c : klass.getConstructors()) {
+      if (Arrays.equals(c.getParameterTypes(), args)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static boolean canInvokeConstructor(Class<?> klass, Class<?> args[], Object[] initargs) {
+    try {
+      Constructor c = klass.getDeclaredConstructor(args);
+      c.setAccessible(true);
+      c.newInstance(initargs);
+      return true;
+    } catch (Exception ex) {
+      ex.printStackTrace();
+      return false;
+    }
+  }
+
+  public static boolean canUseNewInstance(Class<?> klass) throws IllegalAccessException {
+    try {
+      klass.newInstance();
+      return true;
+    } catch (InstantiationException ex) {
+      return false;
+    }
+  }
+
+  private static native int getHiddenApiAccessFlags();
+
+  public static boolean canObserveFieldHiddenAccessFlags(Class<?> klass, String name)
+      throws Exception {
+    return (klass.getDeclaredField(name).getModifiers() & getHiddenApiAccessFlags()) != 0;
+  }
+
+  public static boolean canObserveMethodHiddenAccessFlags(Class<?> klass, String name)
+      throws Exception {
+    return (klass.getDeclaredMethod(name).getModifiers() & getHiddenApiAccessFlags()) != 0;
+  }
+
+  public static boolean canObserveConstructorHiddenAccessFlags(Class<?> klass, Class<?> args[])
+      throws Exception {
+    return (klass.getConstructor(args).getModifiers() & getHiddenApiAccessFlags()) != 0;
+  }
+}
diff --git a/test/674-hiddenapi/src/NullaryConstructorBlacklist.java b/test/674-hiddenapi/src/NullaryConstructorBlacklist.java
new file mode 100644
index 0000000..5bf6278
--- /dev/null
+++ b/test/674-hiddenapi/src/NullaryConstructorBlacklist.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+public class NullaryConstructorBlacklist {
+  public NullaryConstructorBlacklist() { x = 22; }
+  public NullaryConstructorBlacklist(int y) { x = y; }
+  protected int x;
+}
diff --git a/test/674-hiddenapi/src/NullaryConstructorDarkGreylist.java b/test/674-hiddenapi/src/NullaryConstructorDarkGreylist.java
new file mode 100644
index 0000000..c25a767
--- /dev/null
+++ b/test/674-hiddenapi/src/NullaryConstructorDarkGreylist.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+public class NullaryConstructorDarkGreylist {
+  public NullaryConstructorDarkGreylist() { x = 22; }
+  public NullaryConstructorDarkGreylist(int y) { x = y; }
+  protected int x;
+}
diff --git a/test/674-hiddenapi/src/NullaryConstructorLightGreylist.java b/test/674-hiddenapi/src/NullaryConstructorLightGreylist.java
new file mode 100644
index 0000000..d5dac8b
--- /dev/null
+++ b/test/674-hiddenapi/src/NullaryConstructorLightGreylist.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+public class NullaryConstructorLightGreylist {
+  public NullaryConstructorLightGreylist() { x = 22; }
+  public NullaryConstructorLightGreylist(int y) { x = y; }
+  protected int x;
+}
diff --git a/test/674-hiddenapi/src/NullaryConstructorWhitelist.java b/test/674-hiddenapi/src/NullaryConstructorWhitelist.java
new file mode 100644
index 0000000..d101907
--- /dev/null
+++ b/test/674-hiddenapi/src/NullaryConstructorWhitelist.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+public class NullaryConstructorWhitelist {
+  public NullaryConstructorWhitelist() { x = 22; }
+  public NullaryConstructorWhitelist(int y) { x = y; }
+  protected int x;
+}
diff --git a/test/674-hiddenapi/src/ParentClass.java b/test/674-hiddenapi/src/ParentClass.java
new file mode 100644
index 0000000..edad02d
--- /dev/null
+++ b/test/674-hiddenapi/src/ParentClass.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+public class ParentClass {
+  public ParentClass() {}
+
+  // INSTANCE FIELD
+
+  public int fieldPublicWhitelist = 211;
+  int fieldPackageWhitelist = 212;
+  protected int fieldProtectedWhitelist = 213;
+  private int fieldPrivateWhitelist = 214;
+
+  public int fieldPublicLightGreylist = 221;
+  int fieldPackageLightGreylist = 222;
+  protected int fieldProtectedLightGreylist = 223;
+  private int fieldPrivateLightGreylist = 224;
+
+  public int fieldPublicDarkGreylist = 231;
+  int fieldPackageDarkGreylist = 232;
+  protected int fieldProtectedDarkGreylist = 233;
+  private int fieldPrivateDarkGreylist = 234;
+
+  public int fieldPublicBlacklist = 241;
+  int fieldPackageBlacklist = 242;
+  protected int fieldProtectedBlacklist = 243;
+  private int fieldPrivateBlacklist = 244;
+
+  // STATIC FIELD
+
+  public static int fieldPublicStaticWhitelist = 111;
+  static int fieldPackageStaticWhitelist = 112;
+  protected static int fieldProtectedStaticWhitelist = 113;
+  private static int fieldPrivateStaticWhitelist = 114;
+
+  public static int fieldPublicStaticLightGreylist = 121;
+  static int fieldPackageStaticLightGreylist = 122;
+  protected static int fieldProtectedStaticLightGreylist = 123;
+  private static int fieldPrivateStaticLightGreylist = 124;
+
+  public static int fieldPublicStaticDarkGreylist = 131;
+  static int fieldPackageStaticDarkGreylist = 132;
+  protected static int fieldProtectedStaticDarkGreylist = 133;
+  private static int fieldPrivateStaticDarkGreylist = 134;
+
+  public static int fieldPublicStaticBlacklist = 141;
+  static int fieldPackageStaticBlacklist = 142;
+  protected static int fieldProtectedStaticBlacklist = 143;
+  private static int fieldPrivateStaticBlacklist = 144;
+
+  // INSTANCE METHOD
+
+  public int methodPublicWhitelist() { return 411; }
+  int methodPackageWhitelist() { return 412; }
+  protected int methodProtectedWhitelist() { return 413; }
+  private int methodPrivateWhitelist() { return 414; }
+
+  public int methodPublicLightGreylist() { return 421; }
+  int methodPackageLightGreylist() { return 422; }
+  protected int methodProtectedLightGreylist() { return 423; }
+  private int methodPrivateLightGreylist() { return 424; }
+
+  public int methodPublicDarkGreylist() { return 431; }
+  int methodPackageDarkGreylist() { return 432; }
+  protected int methodProtectedDarkGreylist() { return 433; }
+  private int methodPrivateDarkGreylist() { return 434; }
+
+  public int methodPublicBlacklist() { return 441; }
+  int methodPackageBlacklist() { return 442; }
+  protected int methodProtectedBlacklist() { return 443; }
+  private int methodPrivateBlacklist() { return 444; }
+
+  // STATIC METHOD
+
+  public static int methodPublicStaticWhitelist() { return 311; }
+  static int methodPackageStaticWhitelist() { return 312; }
+  protected static int methodProtectedStaticWhitelist() { return 313; }
+  private static int methodPrivateStaticWhitelist() { return 314; }
+
+  public static int methodPublicStaticLightGreylist() { return 321; }
+  static int methodPackageStaticLightGreylist() { return 322; }
+  protected static int methodProtectedStaticLightGreylist() { return 323; }
+  private static int methodPrivateStaticLightGreylist() { return 324; }
+
+  public static int methodPublicStaticDarkGreylist() { return 331; }
+  static int methodPackageStaticDarkGreylist() { return 332; }
+  protected static int methodProtectedStaticDarkGreylist() { return 333; }
+  private static int methodPrivateStaticDarkGreylist() { return 334; }
+
+  public static int methodPublicStaticBlacklist() { return 341; }
+  static int methodPackageStaticBlacklist() { return 342; }
+  protected static int methodProtectedStaticBlacklist() { return 343; }
+  private static int methodPrivateStaticBlacklist() { return 344; }
+
+  // CONSTRUCTOR
+
+  // Whitelist
+  public ParentClass(int x, short y) {}
+  ParentClass(float x, short y) {}
+  protected ParentClass(long x, short y) {}
+  private ParentClass(double x, short y) {}
+
+  // Light greylist
+  public ParentClass(int x, boolean y) {}
+  ParentClass(float x, boolean y) {}
+  protected ParentClass(long x, boolean y) {}
+  private ParentClass(double x, boolean y) {}
+
+  // Dark greylist
+  public ParentClass(int x, byte y) {}
+  ParentClass(float x, byte y) {}
+  protected ParentClass(long x, byte y) {}
+  private ParentClass(double x, byte y) {}
+
+  // Blacklist
+  public ParentClass(int x, char y) {}
+  ParentClass(float x, char y) {}
+  protected ParentClass(long x, char y) {}
+  private ParentClass(double x, char y) {}
+}
diff --git a/test/674-hiddenapi/src/ParentInterface.java b/test/674-hiddenapi/src/ParentInterface.java
new file mode 100644
index 0000000..e36fe0e
--- /dev/null
+++ b/test/674-hiddenapi/src/ParentInterface.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+public interface ParentInterface {
+  // STATIC FIELD
+  static int fieldPublicStaticWhitelist = 11;
+  static int fieldPublicStaticLightGreylist = 12;
+  static int fieldPublicStaticDarkGreylist = 13;
+  static int fieldPublicStaticBlacklist = 14;
+
+  // INSTANCE METHOD
+  int methodPublicWhitelist();
+  int methodPublicBlacklist();
+  int methodPublicLightGreylist();
+  int methodPublicDarkGreylist();
+
+  // STATIC METHOD
+  static int methodPublicStaticWhitelist() { return 21; }
+  static int methodPublicStaticLightGreylist() { return 22; }
+  static int methodPublicStaticDarkGreylist() { return 23; }
+  static int methodPublicStaticBlacklist() { return 24; }
+
+  // DEFAULT METHOD
+  default int methodPublicDefaultWhitelist() { return 31; }
+  default int methodPublicDefaultLightGreylist() { return 32; }
+  default int methodPublicDefaultDarkGreylist() { return 33; }
+  default int methodPublicDefaultBlacklist() { return 34; }
+}
diff --git a/test/Android.bp b/test/Android.bp
index 49a34a1..470a68f 100644
--- a/test/Android.bp
+++ b/test/Android.bp
@@ -389,6 +389,7 @@
         "661-oat-writer-layout/oat_writer_layout.cc",
         "664-aget-verifier/aget-verifier.cc",
         "667-jit-jni-stub/jit_jni_stub_test.cc",
+        "674-hiddenapi/hiddenapi.cc",
         "708-jit-cache-churn/jit.cc",
     ],
     shared_libs: [
diff --git a/test/Android.run-test.mk b/test/Android.run-test.mk
index 2cada76..2df0cc6 100644
--- a/test/Android.run-test.mk
+++ b/test/Android.run-test.mk
@@ -20,6 +20,7 @@
 # Dependencies for actually running a run-test.
 TEST_ART_RUN_TEST_DEPENDENCIES := \
   $(HOST_OUT_EXECUTABLES)/dx \
+  $(HOST_OUT_EXECUTABLES)/hiddenapi \
   $(HOST_OUT_EXECUTABLES)/jasmin \
   $(HOST_OUT_EXECUTABLES)/smali \
   $(HOST_OUT_EXECUTABLES)/dexmerger \
diff --git a/test/etc/default-build b/test/etc/default-build
index 5c8257f..4dc2393 100755
--- a/test/etc/default-build
+++ b/test/etc/default-build
@@ -81,6 +81,17 @@
   HAS_SRC_DEX2OAT_UNRESOLVED=false
 fi
 
+if [ -f api-light-greylist.txt -o -f api-dark-greylist.txt -o -f api-blacklist.txt ]; then
+  HAS_HIDDENAPI_SPEC=true
+else
+  HAS_HIDDENAPI_SPEC=false
+fi
+
+# USE_HIDDENAPI=false run-test... will disable hiddenapi.
+if [ -z "${USE_HIDDENAPI}" ]; then
+  USE_HIDDENAPI=true
+fi
+
 # DESUGAR=false run-test... will disable desugar.
 if [[ "$DESUGAR" == false ]]; then
   USE_DESUGAR=false
@@ -321,6 +332,24 @@
   ${DXMERGER} "$dst_file" "${dex_files_to_merge[@]}"
 }
 
+function make_hiddenapi() {
+  local args=()
+  while [[ $# -gt 0 ]]; do
+    args+=("--dex=$1")
+    shift
+  done
+  if [ -f api-light-greylist.txt ]; then
+    args+=("--light-greylist=api-light-greylist.txt")
+  fi
+  if [ -f api-dark-greylist.txt ]; then
+    args+=("--dark-greylist=api-dark-greylist.txt")
+  fi
+  if [ -f api-blacklist.txt ]; then
+    args+=("--blacklist=api-blacklist.txt")
+  fi
+  ${HIDDENAPI} "${args[@]}"
+}
+
 # Print the directory name only if it exists.
 function maybe_dir() {
   local dirname="$1"
@@ -334,6 +363,13 @@
   exit 0
 fi
 
+# Helper function for a common test. Evaluate with $(has_mutlidex).
+function has_multidex() {
+  echo [ ${HAS_SRC_MULTIDEX} = "true" \
+         -o ${HAS_JASMIN_MULTIDEX} = "true" \
+         -o ${HAS_SMALI_MULTIDEX} = "true" ]
+}
+
 if [ ${HAS_SRC_DEX2OAT_UNRESOLVED} = "true" ]; then
   mkdir classes
   mkdir classes-ex
@@ -501,9 +537,18 @@
   fi
 fi
 
+# Apply hiddenapi on the dex files if the test has API list file(s).
+if [ ${NEED_DEX} = "true" -a ${USE_HIDDENAPI} = "true" -a ${HAS_HIDDENAPI_SPEC} = "true" ]; then
+  if $(has_multidex); then
+    make_hiddenapi classes.dex classes2.dex
+  else
+    make_hiddenapi classes.dex
+  fi
+fi
+
 # Create a single dex jar with two dex files for multidex.
 if [ ${NEED_DEX} = "true" ]; then
-  if [ ${HAS_SRC_MULTIDEX} = "true" ] || [ ${HAS_JASMIN_MULTIDEX} = "true" ] || [ ${HAS_SMALI_MULTIDEX} = "true" ]; then
+  if $(has_multidex); then
     zip $TEST_NAME.jar classes.dex classes2.dex
   else
     zip $TEST_NAME.jar classes.dex
diff --git a/test/run-test b/test/run-test
index 75fe15c..bde3aac 100755
--- a/test/run-test
+++ b/test/run-test
@@ -113,6 +113,11 @@
 fi
 export ZIPALIGN
 
+# If hiddenapi was not set by the environment variable, assume it is in the path.
+if [ -z "$HIDDENAPI" ]; then
+  export HIDDENAPI="hiddenapi"
+fi
+
 info="info.txt"
 build="build"
 run="run"