Merge "Add HiddenApiTestApp."
diff --git a/HiddenApiTestApp/Android.bp b/HiddenApiTestApp/Android.bp
new file mode 100644
index 0000000..91a5cf7
--- /dev/null
+++ b/HiddenApiTestApp/Android.bp
@@ -0,0 +1,30 @@
+// Copyright (C) 2020 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.
+
+android_app {
+    name: "HiddenApiTestApp",
+    platform_apis: true,
+    srcs: ["java/**/*.java"],
+    resource_dirs: ["res"],
+    min_sdk_version: "27",
+}
+
+android_test {
+    name: "HiddenApiTestAppTests",
+    srcs: ["javatests/**/*.java"],
+    manifest: "javatests/AndroidManifest.xml",
+    instrumentation_for: "HiddenApiTestApp",
+    sdk_version: "26",
+    min_sdk_version: "21",
+}
diff --git a/HiddenApiTestApp/AndroidManifest.xml b/HiddenApiTestApp/AndroidManifest.xml
new file mode 100644
index 0000000..271020a
--- /dev/null
+++ b/HiddenApiTestApp/AndroidManifest.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.hiddenapi.testapp">
+
+  <application
+      android:allowBackup="true"
+      android:debuggable="true"
+      android:icon="@mipmap/ic_launcher"
+      android:label="@string/app_name"
+      android:roundIcon="@mipmap/ic_launcher_round"
+      android:supportsRtl="true">
+    <activity android:name=".MainActivity"
+        android:process="hiddenapi.testappprocess" >
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN"/>
+        <category android:name="android.intent.category.LAUNCHER"/>
+      </intent-filter>
+    </activity>
+
+    <activity
+        android:name=".NoChecksActivity"
+        android:label="@string/disable_checks_activity_name"
+        android:process=":disablechecks" >
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN"/>
+        <category android:name="android.intent.category.LAUNCHER"/>
+      </intent-filter>
+    </activity>
+
+    <receiver
+      android:name=".ReflectReceiver"
+      android:exported="true" />
+
+    <service
+      android:name=".IsolatedService"
+      android:isolatedProcess="true" />
+
+  </application>
+
+  <!-- Note this doesn't work, as it is a signature permission: -->
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+  <instrumentation
+    android:name=".MainInstrumentation"
+    android:targetPackage="com.google.android.hiddenapi.testapp" />
+
+</manifest>
diff --git a/HiddenApiTestApp/README b/HiddenApiTestApp/README
new file mode 100644
index 0000000..0cf6c1b
--- /dev/null
+++ b/HiddenApiTestApp/README
@@ -0,0 +1,4 @@
+HiddenApiTestApp.apk:
+
+Exercises various ways of accessing hidden APIs, ways of
+disabling the checks.
diff --git a/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/DexField.java b/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/DexField.java
new file mode 100644
index 0000000..7397c52
--- /dev/null
+++ b/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/DexField.java
@@ -0,0 +1,12 @@
+package com.google.android.hiddenapi.testapp;
+
+public class DexField extends DexMember {
+  public DexField(String className, String name, String type) {
+      super(className, name, type);
+  }
+
+  @Override
+  public String toString() {
+      return getJavaType() + " " + getJavaClassName() + "." + getName();
+  }
+}
diff --git a/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/DexMember.java b/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/DexMember.java
new file mode 100644
index 0000000..b26c939
--- /dev/null
+++ b/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/DexMember.java
@@ -0,0 +1,74 @@
+package com.google.android.hiddenapi.testapp;
+
+/**
+ * Represents one class member parsed from the reader of dex signatures.
+ */
+public abstract class DexMember {
+    private final String mName;
+    private final String mClassDescriptor;
+    private final String mType;
+
+    protected DexMember(String className, String name, String type) {
+        mName = name;
+        mClassDescriptor = className;
+        mType = type;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public String getDexClassName() {
+        return mClassDescriptor;
+    }
+
+    public String getJavaClassName() {
+        return dexToJavaType(mClassDescriptor);
+    }
+
+    public String getDexType() {
+        return mType;
+    }
+
+    public String getJavaType() {
+        return dexToJavaType(mType);
+    }
+
+    /**
+     * Converts `type` to a Java type.
+     */
+    public static String dexToJavaType(String type) {
+        String javaDimension = "";
+        while (type.startsWith("[")) {
+            javaDimension += "[]";
+            type = type.substring(1);
+        }
+
+        String javaType = null;
+        if ("V".equals(type)) {
+            javaType = "void";
+        } else if ("Z".equals(type)) {
+            javaType = "boolean";
+        } else if ("B".equals(type)) {
+            javaType = "byte";
+        } else if ("C".equals(type)) {
+            javaType = "char";
+        } else if ("S".equals(type)) {
+            javaType = "short";
+        } else if ("I".equals(type)) {
+            javaType = "int";
+        } else if ("J".equals(type)) {
+            javaType = "long";
+        } else if ("F".equals(type)) {
+            javaType = "float";
+        } else if ("D".equals(type)) {
+            javaType = "double";
+        } else if (type.startsWith("L") && type.endsWith(";")) {
+            javaType = type.substring(1, type.length() - 1).replace('/', '.');
+        } else {
+            throw new IllegalStateException("Unexpected type " + type);
+        }
+
+        return javaType + javaDimension;
+    }
+}
diff --git a/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/DexMethod.java b/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/DexMethod.java
new file mode 100644
index 0000000..5cd7328
--- /dev/null
+++ b/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/DexMethod.java
@@ -0,0 +1,131 @@
+package com.google.android.hiddenapi.testapp;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class DexMethod extends DexMember {
+  private final List<String> mParamTypeList;
+
+  public DexMethod(String className, String name, String signature) {
+      super(className, name, parseDexReturnType(signature));
+      mParamTypeList = parseDexTypeList(signature);
+  }
+
+  public String getDexSignature() {
+      return "(" + String.join("", mParamTypeList) + ")" + getDexType();
+  }
+
+  public List<String> getJavaParameterTypes() {
+      return mParamTypeList.stream().map(DexMember::dexToJavaType).collect(Collectors.toList());
+  }
+
+  public boolean isConstructor() {
+      return "<init>".equals(getName()) && "V".equals(getDexType());
+  }
+
+  public boolean isStaticConstructor() {
+      return "<clinit>".equals(getName()) && "V".equals(getDexType());
+  }
+
+  @Override
+  public String toString() {
+      return getJavaType() + " " + getJavaClassName() + "." + getName()
+              + "(" + String.join(", ", getJavaParameterTypes()) + ")";
+  }
+
+  public static Class<?> getTypeClass(String type) throws Exception {
+    switch (type) {
+      case "V":
+        return Void.TYPE;
+      case "Z":
+        return Boolean.TYPE;
+      case "B":
+        return Byte.TYPE;
+      case "C":
+        return Character.TYPE;
+      case "S":
+        return Short.TYPE;
+      case "I":
+        return Integer.TYPE;
+      case "J":
+        return Long.TYPE;
+      case "F":
+        return Float.TYPE;
+      case "D":
+        return Double.TYPE;
+      default:
+        if (type.startsWith("L")) {
+          return Class.forName(dexToJavaType(type));
+        } else {
+          throw new IllegalArgumentException("Unknown type: " + type);
+        }
+
+    }
+  }
+
+  public Class<?>[] getParameterTypes() throws Exception {
+    Class<?>[] types = new Class<?>[mParamTypeList.size()];
+    for (int i = 0; i < mParamTypeList.size(); ++i) {
+      String type = mParamTypeList.get(i);
+      types[i] = getTypeClass(type);
+    }
+    return types;
+  }
+
+  private static Matcher matchSignature(String signature) {
+      Matcher m = Pattern.compile("^\\((.*)\\)(.*)$").matcher(signature);
+      if (!m.matches()) {
+          throw new RuntimeException("Could not parse method signature: " + signature);
+      }
+      return m;
+  }
+
+  private static String parseDexReturnType(String signature) {
+      return matchSignature(signature).group(2);
+  }
+
+  private static List<String> parseDexTypeList(String signature) {
+      String typeSequence = matchSignature(signature).group(1);
+      List<String> list = new ArrayList<String>();
+      while (!typeSequence.isEmpty()) {
+          String type = firstDexTypeFromList(typeSequence);
+          list.add(type);
+          typeSequence = typeSequence.substring(type.length());
+      }
+      return list;
+  }
+
+  /**
+   * Returns the first dex type in `typeList` or throws a ParserException
+   * if a dex type is not recognized. The input is not changed.
+   */
+  private static String firstDexTypeFromList(String typeList) {
+      String dexDimension = "";
+      while (typeList.startsWith("[")) {
+          dexDimension += "[";
+          typeList = typeList.substring(1);
+      }
+
+      String type = null;
+      if (typeList.startsWith("V")
+              || typeList.startsWith("Z")
+              || typeList.startsWith("B")
+              || typeList.startsWith("C")
+              || typeList.startsWith("S")
+              || typeList.startsWith("I")
+              || typeList.startsWith("J")
+              || typeList.startsWith("F")
+              || typeList.startsWith("D")) {
+          type = typeList.substring(0, 1);
+      } else if (typeList.startsWith("L") && typeList.indexOf(";") > 0) {
+          type = typeList.substring(0, typeList.indexOf(";") + 1);
+      } else {
+          throw new RuntimeException("Unexpected dex type in \"" + typeList + "\"");
+      }
+
+      return dexDimension + type;
+  }
+}
diff --git a/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/MainActivity.java b/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/MainActivity.java
new file mode 100644
index 0000000..af2e174
--- /dev/null
+++ b/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/MainActivity.java
@@ -0,0 +1,175 @@
+package com.google.android.hiddenapi.testapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.StrictMode;
+import android.os.strictmode.Violation;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+import dalvik.system.VMRuntime;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class MainActivity extends Activity implements OnClickListener {
+
+  enum AppStrictMode {
+    OFF,
+    LOG,
+    DEATH,
+    CALLBACK;
+    private static AppStrictMode[] vals = values();
+    public AppStrictMode next()
+    {
+      return vals[(this.ordinal()+1) % vals.length];
+    }
+  }
+
+  private final StrictMode.OnVmViolationListener mViolationListener = (v) -> onVmViolation(v);
+
+  private TextView mTextView;
+  private TextView mCurrentStrictMode;
+  private TextView mStrictModeText;
+
+  private AppStrictMode mCurrentMode = AppStrictMode.OFF;
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.activity_main);
+    mTextView = findViewById(R.id.textView);
+    mCurrentStrictMode = findViewById(R.id.strict_mode);
+    mStrictModeText = findViewById(R.id.strict_mode_text);
+
+    setAllButtonClickListeners(findViewById(R.id.layout));
+
+    setStrictMode(AppStrictMode.OFF);
+  }
+
+  private void setAllButtonClickListeners(View v) {
+    if (v instanceof Button) {
+      v.setOnClickListener(this);
+    } else if (v instanceof ViewGroup) {
+      ViewGroup vg = (ViewGroup) v;
+      for (int i = 0; i < vg.getChildCount(); i++) {
+        setAllButtonClickListeners(vg.getChildAt(i));
+      }
+    }
+  }
+
+  private void onVmViolation(Violation v) {
+    setStrictModeText(toString(v));
+  }
+
+  private void setStrictModeText(String s) {
+    if (TextUtils.isEmpty(s)) {
+      mStrictModeText.setText("");
+      mStrictModeText.setVisibility(View.GONE);
+    } else {
+      mStrictModeText.setText(s);
+      mStrictModeText.setVisibility(View.VISIBLE);
+    }
+
+  }
+  private void setStrictMode(AppStrictMode mode) {
+    StrictMode.VmPolicy policy;
+    switch (mode) {
+      case OFF:
+        policy = new StrictMode.VmPolicy.Builder().permitNonSdkApiUsage().build();
+        break;
+      case LOG:
+        policy = new StrictMode.VmPolicy.Builder().detectNonSdkApiUsage().penaltyLog().build();
+        break;
+      case DEATH:
+        policy = new StrictMode.VmPolicy.Builder().detectNonSdkApiUsage().penaltyDeath().build();
+        break;
+      case CALLBACK:
+        policy = new StrictMode.VmPolicy.Builder()
+            .detectNonSdkApiUsage()
+            .penaltyListener(r -> mStrictModeText.post(r), mViolationListener)
+            .build();
+        break;
+      default:
+        return;
+    }
+    mCurrentMode = mode;
+    StrictMode.setVmPolicy(policy);
+    mCurrentStrictMode.setText("StrictMode: " + mode);
+  }
+
+  @Override
+  public void onClick(View view) {
+    setStrictModeText(null);
+    switch (view.getId()) {
+      case R.id.disable_checks_reflection:
+        try {
+          tryDisableApiChecksReflectively();
+          mTextView.setText("Ok \uD83D\uDE00");
+        } catch (Exception e) {
+          handleException(e);
+        }
+        break;
+      case R.id.disable_checks_linking:
+        try {
+          tryDisableApiChecksLinking();
+        } catch (LinkageError e) {
+          handleException(e);
+        }
+        break;
+      case R.id.strict_mode:
+        setStrictMode(mCurrentMode.next());
+        break;
+      default:
+        String name = ((TextView) view).getText().toString();
+        accessActivityMember(name);
+        break;
+    }
+  }
+
+  private void accessActivityMember(String name) {
+    try {
+      Field f = Activity.class.getDeclaredField(name);
+      f.setAccessible(true);
+      Object o = f.get(this);
+      mTextView.setText(String.format("%s: %s", name, o == null ? "null" : o.toString()));
+    } catch (NoSuchFieldException e) {
+      handleException(e);
+    } catch (IllegalAccessException e) {
+      handleException(e);
+    }
+  }
+
+  private void handleException(Throwable e) {
+    Log.e(getClass().getSimpleName(), "Failed", e);
+    mTextView.setText(toString(e));
+  }
+
+  private static String toString(Throwable t) {
+    StringWriter s = new StringWriter();
+    t.printStackTrace(new PrintWriter(s));
+    return s.toString();
+  }
+
+  private static void tryDisableApiChecksReflectively()
+          throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
+          IllegalAccessException {
+    Class vmRuntimeClass = Class.forName("dalvik.system.VMRuntime");
+    Method getRuntime = vmRuntimeClass.getDeclaredMethod("getRuntime");
+    Object vmRuntime =  getRuntime.invoke(null);
+    Method setExemptions = vmRuntimeClass.getDeclaredMethod(
+            "setHiddenApiExemptions", String[].class);
+    setExemptions.invoke(vmRuntime, (Object) new String[]{"L"});
+  }
+
+  private static void tryDisableApiChecksLinking() {
+    VMRuntime.getRuntime().setHiddenApiExemptions(new String[]{"L"});
+  }
+
+}
diff --git a/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/MainInstrumentation.java b/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/MainInstrumentation.java
new file mode 100644
index 0000000..b1b75ac
--- /dev/null
+++ b/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/MainInstrumentation.java
@@ -0,0 +1,28 @@
+package com.google.android.hiddenapi.testapp;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Looper;
+import android.util.Log;
+
+public class MainInstrumentation extends Instrumentation {
+
+  private static final String TAG = "compat-test";
+
+  @Override
+  public void onCreate(Bundle args) {
+    super.onCreate(args);
+    Log.d(TAG, "MainInstrumentation.onCreate()");
+    start();
+  }
+
+  @Override
+  public void onStart() {
+    Log.d(TAG, "MainInstrumentation.onStart()");
+    Intent i = new Intent(getTargetContext(), MainActivity.class);
+    i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    getContext().startActivity(i);
+    Log.d(TAG, "Started Activity");
+  }
+}
diff --git a/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/NoChecksActivity.java b/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/NoChecksActivity.java
new file mode 100644
index 0000000..39f972e
--- /dev/null
+++ b/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/NoChecksActivity.java
@@ -0,0 +1,69 @@
+package com.google.android.hiddenapi.testapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.TextView;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+
+public class NoChecksActivity extends Activity {
+
+  public static String TAG = "compat-test";
+
+  private TextView mTextView;
+  private final StringBuilder mText = new StringBuilder();
+  private final Runnable mUpdateRunnable = () -> mTextView.setText(mText);
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.activity_nochecks);
+    mTextView = (TextView) findViewById(R.id.textView);
+  }
+
+  @Override
+  public void onStart() {
+    super.onStart();
+    try {
+      Process am = Runtime.getRuntime().exec(
+          "am instrument --no-hidden-api-checks mypackage/.MainInstrumentation");
+      new ReaderThread(am.getErrorStream()).start();
+      new ReaderThread(am.getInputStream()).start();
+    } catch (IOException ioe) {
+      throw new RuntimeException(ioe);
+    }
+    // TODO get proc output, display in text box
+  }
+
+  public synchronized void logLine(String line) {
+    if (line == null) {
+      return;
+    }
+    Log.d(TAG, line);
+    mText.append(line);
+    mTextView.post(mUpdateRunnable);
+  }
+
+  class ReaderThread extends Thread {
+    private final BufferedReader mInput;
+    public ReaderThread(InputStream in) {
+      mInput = new BufferedReader(new InputStreamReader(in));
+    }
+
+    public void run() {
+      try {
+        String line;
+        do {
+          line = mInput.readLine();
+          logLine(line);
+        } while (line != null);
+      } catch (IOException e) {
+        Log.e(TAG, "ReaderThread threw", e);
+      }
+    }
+  }
+
+}
diff --git a/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/ReflectReceiver.java b/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/ReflectReceiver.java
new file mode 100644
index 0000000..8ae32e3
--- /dev/null
+++ b/HiddenApiTestApp/java/com/google/android/hiddenapi/testapp/ReflectReceiver.java
@@ -0,0 +1,99 @@
+package com.google.android.hiddenapi.testapp;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Field;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public class ReflectReceiver extends BroadcastReceiver {
+
+  public static final String EXTRA_CLASS = "class";
+  public static final String EXTRA_SIGNATURE = "signature";
+
+  private static final Pattern REGEX_CLASS = Pattern.compile("^L[^->]*;$");
+  private static final Pattern REGEX_FIELD = Pattern.compile("^(L[^->]*;)->(.*):(.*)$");
+  private static final Pattern REGEX_METHOD = Pattern.compile("^(L[^->]*;)->(.*)(\\(.*\\).*)$");
+
+  public ReflectReceiver() {
+    try {
+      lookupSignature(
+          "Landroid/bluetooth/BluetoothAdapter;->setDiscoverableTimeout(I)V");
+      Log.d(getClass().getSimpleName(), "Successfully accessed method!");
+    } catch (Exception e) {
+      Log.e(getClass().getSimpleName(), "Failed", e);
+    }
+  }
+
+  private void lookupClass(String className) throws Exception {
+    Class<?> clazz= Class.forName(className);
+    setResult(0, "Got class: " + clazz, null);
+  }
+
+  private void lookupSignature(String signature) throws Exception {
+    Matcher matchClass = REGEX_CLASS.matcher(signature);
+    Matcher matchField = REGEX_FIELD.matcher(signature);
+    Matcher matchMethod = REGEX_METHOD.matcher(signature);
+    int matchCount = (matchClass.matches() ? 1 : 0) + (matchField.matches() ? 1 : 0) +
+        (matchMethod.matches() ? 1 : 0);
+    if (matchCount == 0) {
+      setResult(1, "Failed to parse signature: " + signature, null);
+      return;
+    } else if (matchCount > 1) {
+      setResult(1, "Ambiguous signature: " + signature, null);
+    }
+
+    if (matchClass.matches()) {
+      String className = DexMember.dexToJavaType(signature);
+      lookupClass(className);
+      return;
+    } else if (matchField.matches()) {
+      DexField dexField = new DexField(
+          matchField.group(1), matchField.group(2), matchField.group(3));
+      Class<?> clazz = Class.forName(dexField.getJavaClassName());
+      Field field = clazz.getDeclaredField(dexField.getName());
+      setResult(0, "Got field " + field, null);
+    } else if (matchMethod.matches()) {
+      DexMethod dexMethod = new DexMethod(
+          matchMethod.group(1),matchMethod.group(2), matchMethod.group(3));
+      Class<?> clazz = Class.forName(dexMethod.getJavaClassName());
+      Executable method;
+      if (dexMethod.isConstructor()) {
+        method = clazz.getConstructor(dexMethod.getParameterTypes());
+      } else {
+        method = clazz.getMethod(dexMethod.getName(), dexMethod.getParameterTypes());
+      }
+      setResult(0, "Got method " + method, null);
+    }
+
+  }
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    try {
+      String className = intent.getStringExtra(EXTRA_CLASS);
+      if (className != null) {
+        lookupClass(className);
+        return;
+      }
+      String signature = intent.getStringExtra(EXTRA_SIGNATURE);
+      if (signature != null) {
+        lookupSignature(signature);
+        return;
+      }
+
+      setResult(1, "No action specified", null);
+    } catch (Exception e) {
+      StringWriter stack = new StringWriter();
+      e.printStackTrace(new PrintWriter(stack));
+      setResult(1, stack.toString(), null);
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/HiddenApiTestApp/javatests/AndroidManifest.xml b/HiddenApiTestApp/javatests/AndroidManifest.xml
new file mode 100644
index 0000000..413543e
--- /dev/null
+++ b/HiddenApiTestApp/javatests/AndroidManifest.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.hiddenapi.testapp.tests">
+
+  <application >
+  </application>
+
+  <instrumentation
+    android:name=".TestInstrumentation"
+    android:targetPackage="com.google.android.hiddenapi.testapp" />
+
+</manifest>
diff --git a/HiddenApiTestApp/javatests/com/google/android/hiddenapi/testapp/tests/TestInstrumentation.java b/HiddenApiTestApp/javatests/com/google/android/hiddenapi/testapp/tests/TestInstrumentation.java
new file mode 100644
index 0000000..0b78198
--- /dev/null
+++ b/HiddenApiTestApp/javatests/com/google/android/hiddenapi/testapp/tests/TestInstrumentation.java
@@ -0,0 +1,28 @@
+package com.google.android.hiddenapi.testapp.tests;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import com.google.android.hiddenapi.testapp.MainActivity;
+
+public class TestInstrumentation extends Instrumentation {
+
+  private static final String TAG = "compat-test";
+
+  @Override
+  public void onCreate(Bundle args) {
+    super.onCreate(args);
+    Log.d(TAG, "MainInstrumentation.onCreate()");
+    start();
+  }
+
+  @Override
+  public void onStart() {
+    Log.d(TAG, "MainInstrumentation.onStart()");
+    Intent i = new Intent(getTargetContext(), MainActivity.class);
+    i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    getContext().startActivity(i);
+    Log.d(TAG, "Started Activity");
+  }
+}
diff --git a/HiddenApiTestApp/res/drawable-v24/ic_launcher_foreground.xml b/HiddenApiTestApp/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..b1517ed
--- /dev/null
+++ b/HiddenApiTestApp/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportHeight="108"
+    android:viewportWidth="108">
+  <path
+      android:fillType="evenOdd"
+      android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
+      android:strokeColor="#00000000"
+      android:strokeWidth="1">
+    <aapt:attr name="android:fillColor">
+      <gradient
+          android:endX="78.5885"
+          android:endY="90.9159"
+          android:startX="48.7653"
+          android:startY="61.0927"
+          android:type="linear">
+        <item
+            android:color="#44000000"
+            android:offset="0.0"/>
+        <item
+            android:color="#00000000"
+            android:offset="1.0"/>
+      </gradient>
+    </aapt:attr>
+  </path>
+  <path
+      android:fillColor="#FFFFFF"
+      android:fillType="nonZero"
+      android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
+      android:strokeColor="#00000000"
+      android:strokeWidth="1"/>
+</vector>
diff --git a/HiddenApiTestApp/res/drawable/ic_launcher_background.xml b/HiddenApiTestApp/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..88e3187
--- /dev/null
+++ b/HiddenApiTestApp/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportHeight="108"
+    android:viewportWidth="108">
+  <path
+      android:fillColor="#26A69A"
+      android:pathData="M0,0h108v108h-108z"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M9,0L9,108"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M19,0L19,108"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M29,0L29,108"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M39,0L39,108"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M49,0L49,108"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M59,0L59,108"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M69,0L69,108"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M79,0L79,108"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M89,0L89,108"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M99,0L99,108"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M0,9L108,9"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M0,19L108,19"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M0,29L108,29"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M0,39L108,39"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M0,49L108,49"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M0,59L108,59"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M0,69L108,69"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M0,79L108,79"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M0,89L108,89"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M0,99L108,99"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M19,29L89,29"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M19,39L89,39"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M19,49L89,49"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M19,59L89,59"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M19,69L89,69"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M19,79L89,79"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M29,19L29,89"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M39,19L39,89"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M49,19L49,89"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M59,19L59,89"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M69,19L69,89"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+  <path
+      android:fillColor="#00000000"
+      android:pathData="M79,19L79,89"
+      android:strokeColor="#33FFFFFF"
+      android:strokeWidth="0.8"/>
+</vector>
diff --git a/HiddenApiTestApp/res/layout/activity_main.xml b/HiddenApiTestApp/res/layout/activity_main.xml
new file mode 100644
index 0000000..093ae5d
--- /dev/null
+++ b/HiddenApiTestApp/res/layout/activity_main.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fillViewport="true">
+
+  <LinearLayout
+    android:id="@+id/layout"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <Button
+      android:id="@+id/strict_mode"
+      android:text="StrictMode"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content" />
+
+    <TextView
+        android:id="@+id/strict_mode_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:text=""/>
+
+    <GridLayout
+        android:id="@+id/grid_layout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:columnCount="2"
+        android:rowCount="3"
+        android:orientation="horizontal" >
+      <Button
+          android:id="@+id/button1"
+          android:layout_width="0dp"
+          android:layout_height="wrap_content"
+          android:layout_gravity="fill"
+          android:layout_columnWeight="1"
+          android:text="mWindow"/>
+
+      <Button
+          android:id="@+id/button2"
+          android:layout_width="0dp"
+          android:layout_height="wrap_content"
+          android:layout_gravity="fill"
+          android:layout_columnWeight="1"
+          android:text="mCalled"/>
+
+      <Button
+          android:id="@+id/button3"
+          android:layout_width="0dp"
+          android:layout_height="wrap_content"
+          android:layout_gravity="fill"
+          android:layout_columnWeight="1"
+          android:text="mDoReportFullyDrawn"/>
+
+      <Button
+          android:id="@+id/button3"
+          android:layout_width="0dp"
+          android:layout_height="wrap_content"
+          android:layout_gravity="fill"
+          android:layout_columnWeight="1"
+          android:text="mContext"/>
+
+      <Button
+          android:id="@+id/disable_checks_reflection"
+          android:layout_width="0dp"
+          android:layout_height="wrap_content"
+          android:layout_gravity="fill"
+          android:layout_columnWeight="1"
+          android:text="Disable API checks (reflection)"/>
+
+      <Button
+          android:id="@+id/disable_checks_linking"
+          android:layout_width="0dp"
+          android:layout_height="wrap_content"
+          android:layout_gravity="fill"
+          android:layout_columnWeight="1"
+          android:text="Disable API checks (linking)"/>
+
+    </GridLayout>
+
+
+    <TextView
+        android:id="@+id/textView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="Click a button!"/>
+
+  </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/HiddenApiTestApp/res/layout/activity_nochecks.xml b/HiddenApiTestApp/res/layout/activity_nochecks.xml
new file mode 100644
index 0000000..c274c53
--- /dev/null
+++ b/HiddenApiTestApp/res/layout/activity_nochecks.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+  <TextView
+      android:id="@+id/textView"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:text=""/>
+
+</LinearLayout>
diff --git a/HiddenApiTestApp/res/mipmap-anydpi-v26/ic_launcher.xml b/HiddenApiTestApp/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6d5e5d0
--- /dev/null
+++ b/HiddenApiTestApp/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+  <background android:drawable="@drawable/ic_launcher_background"/>
+  <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/HiddenApiTestApp/res/mipmap-anydpi-v26/ic_launcher_round.xml b/HiddenApiTestApp/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6d5e5d0
--- /dev/null
+++ b/HiddenApiTestApp/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+  <background android:drawable="@drawable/ic_launcher_background"/>
+  <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/HiddenApiTestApp/res/mipmap-hdpi/ic_launcher.png b/HiddenApiTestApp/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a2f5908
--- /dev/null
+++ b/HiddenApiTestApp/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/HiddenApiTestApp/res/mipmap-hdpi/ic_launcher_round.png b/HiddenApiTestApp/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1b52399
--- /dev/null
+++ b/HiddenApiTestApp/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/HiddenApiTestApp/res/mipmap-mdpi/ic_launcher.png b/HiddenApiTestApp/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..ff10afd
--- /dev/null
+++ b/HiddenApiTestApp/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/HiddenApiTestApp/res/mipmap-mdpi/ic_launcher_round.png b/HiddenApiTestApp/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..115a4c7
--- /dev/null
+++ b/HiddenApiTestApp/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/HiddenApiTestApp/res/mipmap-xhdpi/ic_launcher.png b/HiddenApiTestApp/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..dcd3cd8
--- /dev/null
+++ b/HiddenApiTestApp/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/HiddenApiTestApp/res/mipmap-xhdpi/ic_launcher_round.png b/HiddenApiTestApp/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..459ca60
--- /dev/null
+++ b/HiddenApiTestApp/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/HiddenApiTestApp/res/mipmap-xxhdpi/ic_launcher.png b/HiddenApiTestApp/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..8ca12fe
--- /dev/null
+++ b/HiddenApiTestApp/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/HiddenApiTestApp/res/mipmap-xxhdpi/ic_launcher_round.png b/HiddenApiTestApp/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8e19b41
--- /dev/null
+++ b/HiddenApiTestApp/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/HiddenApiTestApp/res/mipmap-xxxhdpi/ic_launcher.png b/HiddenApiTestApp/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b824ebd
--- /dev/null
+++ b/HiddenApiTestApp/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/HiddenApiTestApp/res/mipmap-xxxhdpi/ic_launcher_round.png b/HiddenApiTestApp/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..4c19a13
--- /dev/null
+++ b/HiddenApiTestApp/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/HiddenApiTestApp/res/values/colors.xml b/HiddenApiTestApp/res/values/colors.xml
new file mode 100644
index 0000000..5a077b3
--- /dev/null
+++ b/HiddenApiTestApp/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <color name="colorPrimary">#3F51B5</color>
+  <color name="colorPrimaryDark">#303F9F</color>
+  <color name="colorAccent">#FF4081</color>
+</resources>
diff --git a/HiddenApiTestApp/res/values/strings.xml b/HiddenApiTestApp/res/values/strings.xml
new file mode 100644
index 0000000..1296fc4
--- /dev/null
+++ b/HiddenApiTestApp/res/values/strings.xml
@@ -0,0 +1,4 @@
+<resources>
+  <string name="app_name">TestApp</string>
+  <string name="disable_checks_activity_name">TestApp (disable API check)</string>
+</resources>
diff --git a/HiddenApiTestApp/res/values/styles.xml b/HiddenApiTestApp/res/values/styles.xml
new file mode 100644
index 0000000..f11f745
--- /dev/null
+++ b/HiddenApiTestApp/res/values/styles.xml
@@ -0,0 +1,3 @@
+<resources>
+
+</resources>