Sometimes the worst bugs to track down are the ones that seem to be impossible to reproduce. Or worse, inconsistent performance problems that can cause "Application Not Responding" errors in Android apps. No matter how much test code you write, these types of errors seem to have a way of sneaking into your app and causing problems for your users. However, with some clever use of Android's StrictMode API, alongside Firebase Test Lab for Android, you can find out about these problems before they reach production!
Over the years since Android was first available, a number of best practices have been observed for writing solid apps. For example, you shouldn't be performing blocking I/O on the main thread, and you shouldn't store references to Activity objects that are held after the Activity is destroyed. While it's likely that no one will force you to observe these practices (and you may never see a problem during development), enabling StrictMode in your app will let you know where you've made a mistake. These are called "policy violations", and you configure these policies with code in your app.
Personally, I don't want any of my code to cause any StrictMode violations. In fact, I'd like to consider them bugs that are just as serious as a regular crash, because they could crash my app in some cases! And, in fact, I can configure all StrictMode violations to crash the app. The Java code for that looks like this:
private static final StrictMode.ThreadPolicy FATAL_THREAD_POLICY = new StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyLog() .penaltyDeath() .build(); private static final StrictMode.VmPolicy FATAL_VM_POLICY = new StrictMode.VmPolicy.Builder() .detectAll() .penaltyLog() .penaltyDeath() .build(); public static void enableFatalStrictMode() { if (shouldEnableFatalStrictMode()) { StrictMode.setThreadPolicy(FATAL_THREAD_POLICY); StrictMode.setVmPolicy(FATAL_VM_POLICY); } }
Here I have both a ThreadPolicy (for the main thread) and a VmPolicy (for the entire app) that look for all known violations, and will both log that violation and crash the app. Take a look through the javadoc for those builders to learn about all the situations they can catch.
Also notice that I'm enabling the policies conditionally. You probably don't want an accidental violation to crash on your users in production, so the method shouldEnableFatalStrictMode() typically looks like this, which activates it for your debug builds only:
shouldEnableFatalStrictMode()
private static boolean shouldEnableFatalStrictMode() { return BuildConfig.DEBUG; }
What I'd like to do is take this further and have StrictMode crash my app also when it's running in Firebase Test Lab. This is handy because the crash will fail the test, and I'll get a report about that in the test results. For that to happen, I can change the method to query a system setting on the device that's unique to devices in Test Lab. Note that this requires an Android Context to obtain a ContentResolver:
private static boolean shouldEnableFatalStrictMode() { ContentResolver resolver = context.getContentResolver(); String isInTestLab = Settings.System.getString(resolver, "firebase.test.lab"); return BuildConfig.DEBUG || "true".equals(isInTestLab); }
Now, my instrumented tests (and the automated Robo test) will crash with any StrictMode violations, and I'll see those very clearly in my Test Lab report. Here's an app that crashed in Test Lab with a StrictMode violation during a Robo test:
This doesn't tell you exactly what happened, though. Digging into the logs in the test report, I see the details of the violation right before the crash:
D/StrictMode(6996): StrictMode policy violation; ~duration=80 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=327743 violation=2 D/StrictMode(6996): at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1415) D/StrictMode(6996): at java.io.UnixFileSystem.checkAccess(UnixFileSystem.java:251) D/StrictMode(6996): at java.io.File.exists(File.java:807) D/StrictMode(6996): at android.app.ContextImpl.getDataDir(ContextImpl.java:2167) D/StrictMode(6996): at android.app.ContextImpl.getPreferencesDir(ContextImpl.java:498) D/StrictMode(6996): at android.app.ContextImpl.getSharedPreferencesPath(ContextImpl.java:692) D/StrictMode(6996): at android.app.ContextImpl.getSharedPreferences(ContextImpl.java:360) D/StrictMode(6996): at android.content.ContextWrapper.getSharedPreferences(ContextWrapper.java:167) D/StrictMode(6996): at com.google.firebasesandbox.MainActivity.onCreate(MainActivity.java:25) D/StrictMode(6996): at android.app.Activity.performCreate(Activity.java:6982) D/StrictMode(6996): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1213) D/StrictMode(6996): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2770) D/StrictMode(6996): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2892) D/StrictMode(6996): at android.app.ActivityThread.-wrap11(Unknown Source:0) D/StrictMode(6996): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1593) D/StrictMode(6996): at android.os.Handler.dispatchMessage(Handler.java:105) D/StrictMode(6996): at android.os.Looper.loop(Looper.java:164) D/StrictMode(6996): at android.app.ActivityThread.main(ActivityThread.java:6541) D/StrictMode(6996): at java.lang.reflect.Method.invoke(Native Method) D/StrictMode(6996): at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) D/StrictMode(6996): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
So we've discovered here that an activity's onCreate() called getSharedPreferences(), and this method ends up reading the device filesystem. Blocking the main thread like this could be the source of some jank in the app, so it's worth figuring out a good way to move that I/O to another thread.
onCreate()
getSharedPreferences()
Now we have a way to check for StrictMode violations during a test, but there's still a few more details to work out.
If you do this immediately when the app launches (in an Application subclass or a ContentProvider), there is a subtle bug in some versions of Android that may need to be worked around in order to avoid losing your StrictMode policies when the first Activity appears.
There might be some times when you know there is a violation, but you can't fix the code right away for whatever reason (for example, it's in a library you don't control). In that case, you might want to temporarily suspend the crashing behavior and simply log it instead. To temporarily suspend the crash, you can define a couple other non-fatal policies and swap them in where you know there's an issue to ignore:
private static final StrictMode.ThreadPolicy NONFATAL_THREAD_POLICY = new StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyLog() .build(); private static final StrictMode.VmPolicy NONFATAL_VM_POLICY = new StrictMode.VmPolicy.Builder() .detectAll() .penaltyLog() .build(); public static void disableStrictModeFatality() { StrictMode.setThreadPolicy(NONFATAL_THREAD_POLICY); StrictMode.setVmPolicy(NONFATAL_VM_POLICY); }
Similarly, you may not want to track all the possible violations all the time. In that case, you might want to instruct the policy builder to only detect a subset of the potential issues.
While StrictMode violations are not always the worst thing for your users, I've always found it educational to enable StrictMode in order to find out where my app might be causing problems. In combination with Firebase Test lab, it's a great tool for maximizing the quality of your app.