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.