StrictMode for API Greylist Monitoring

Android P is putting restrictions on apps’ (and libraries’) ability to use hidden stuff, as I covered in January and in March. We were told to monitor LogCat, watching for “blacklist” and “greylist” messages showing what methods we called that were banned.

However, LogCat isn’t a great solution for this sort of thing:

  • LogCat gets flooded with messages, so these messages can get lost in the shuffle

  • These messages are not posted with error severity, so they do not stand out

  • These messages do not provide a stack trace or other concrete indication of where we are calling the banned API

Back in March, I asked for a solution for that third bullet. Much to my shock and amazement, a solution was granted in P DP2: greylist monitoring is now tied into StrictMode, via detectNonSdkApiUsage().

This gives us two main options for raising awareness of where and how we are using banned APIs. The classic solution would be to crash the app, by applying penaltyDeath() to the VmPolicy. This works but is a bit limited. Suppose we find a banned API use in a library. We may not be in position to fix the library ourselves and need to wait for some update to the library that avoids this banned API. However, in the interim, we may not be able to cope with our app crashing when we use the library.

Fortunately, Android P also adds another option to the StrictMode family of “penalties”: we can use penaltyListener() to receive a callback on a StrictMode violation. We are given a Violation object that describes the specific problem (CleartextNetworkViolation, NonSdkApiUsedViolation, etc.). And Violation is a Throwable, from which we can get a stack trace showing exactly where the problem occurred.

For example, this JUnit4 Suite logs each Violation to a file in getExternaCacheDir():

public class GreylistSuite {
  private static final String TAG="GreylistSuite";
  private static final ExecutorService LISTENER_EXECUTOR=Executors.newSingleThreadExecutor();
  private static File logDir=
    new File(InstrumentationRegistry.getTargetContext().getExternalCacheDir(), "__greylist");

  @BeforeClass
  public static void init() {
    if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.P) {
      if (logDir.listFiles()!=null) {
        for (File file : logDir.listFiles()) {
          if (!file.isDirectory()) {
            file.delete();
          }
        }
      }

      StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
        .detectNonSdkApiUsage()
        .penaltyListener(LISTENER_EXECUTOR, GREYLISTENER)
        .build());
    }
  }

  @AfterClass
  public static void term() {
    LISTENER_EXECUTOR.shutdown();

    try {
      LISTENER_EXECUTOR.awaitTermination(5, TimeUnit.SECONDS);
    }
    catch (InterruptedException e) {
      Log.e(TAG, "Saving stack traces took too long!", e);
    }
  }

  private final static StrictMode.OnVmViolationListener GREYLISTENER=
    new StrictMode.OnVmViolationListener() {
      @Override
      public void onVmViolation(Violation violation) {
        logDir.mkdirs();

        String name=Long.toString(SystemClock.uptimeMillis())+".txt";
        File trace=new File(logDir, name);

        try {
          FileOutputStream fos=new FileOutputStream(trace);
          OutputStreamWriter osw=new OutputStreamWriter(fos);
          PrintWriter out=new PrintWriter(osw);

          violation.printStackTrace(out);
          out.flush();
          fos.getFD().sync();
          out.close();
        }
        catch (IOException e) {
          Log.e(TAG, "Exception writing trace", e);
        }
      }
  };
}

A test harness could check that directory for files and fail something if there are logged violations. The suite could even have its own “whitelist” capability, to not log violations matching some pattern (e.g., if Library A called Banned Method B, do not log it, as we know about that problem already).

The biggest limitation of the penaltyListener() approach is that the listener is called on a background thread, via an Executor that you supply. If you throw a RuntimeException of your own from onVmViolation(), it will crash the process, but since that crash is decoupled from the thread running the tests, it will not fail any specific test.

Still, I expect that penaltyListener() will be a powerful tool going forward, for banned API detection and other StrictMode violations. For example, one might imagine a Sonar plugin that surfaces StrictMode violations, complete with stack traces, to help developers catch these problems without totally breaking the flow of the app or test suite.

And, for the purposes of detecting banned API use, penaltyListener() is something to consider integrating into your testing, to be able to catalog all of the banned API uses and where they come from, so you can start figuring out what to do in case Android 9.0 blocks your access to those banned items.