PSA: Android 6.0 Theme.NoDisplay Regression

If you have been using Theme.NoDisplay in one or more activities in your app, and you have not tested them on Android 6.0 yet, I recommend that you do so soon. An undocumented regression in Android 6.0 will cause some of those activities to crash upon launch, if your targetSdkVersion is 23 or higher.

If the activity does all of its work in onCreate() (e.g., starts up a service), then calls finish(), you seem to be OK.

But sometimes the work that needs to be done is a bit more involved than that. In particular, calling startActivityForResult(), with an eye towards calling finish() in onActivityResult(), will cause your app to crash with an IllegalStateException saying that your activity “did not call finish() prior to onResume() completing”. This, apparently, is a new requirement of Theme.NoDisplay activities.

You might wonder why you need a Theme.NoDisplay activity to be calling startActivityForResult() in the first place. The scenario I encountered was trying to use the media projection APIs, specifically to set up recording a screencast. To use the media projection APIs, you need to call startActivityForResult() on a system-supplied Intent, then use the result data to set up a MediaProjection. In the case of this sample app, the user interface was primarily going to be a foreground Notification of a service (plus some command-line scripts), so I had no need for an activity, except as a means of enabling the media projection APIs.

So, I have something like this:

package com.commonsware.android.andcorder;

import android.app.Activity;
import android.content.Intent;
import android.media.projection.MediaProjectionManager;
import android.os.Bundle;

public class MainActivity extends Activity {
  private static final int REQUEST_SCREENSHOT=59706;
  private MediaProjectionManager mgr;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mgr=(MediaProjectionManager)getSystemService(MEDIA_PROJECTION_SERVICE);

    startActivityForResult(mgr.createScreenCaptureIntent(),
      REQUEST_SCREENSHOT);
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode==REQUEST_SCREENSHOT) {
      if (resultCode==RESULT_OK) {
        Intent i=
            new Intent(this, RecorderService.class)
                .putExtra(RecorderService.EXTRA_RESULT_CODE, resultCode)
                .putExtra(RecorderService.EXTRA_RESULT_INTENT, data);

        startService(i);
      }
    }

    finish();
  }
}

This works just fine with Theme.NoDisplay on Android 5.1, passing control to my RecorderService to actually manage starting and stopping the screen recording. On Android 6.0, the app crashes with the aforementioned exception.

Another scenario is using requestPermissions() from the Android 6.0 runtime permission system. Under the covers, requestPermissions() uses startActivityForResult(). If you have an app where the UI is mostly not an activity (e.g., app widget, Notification, wear app), but you need runtime permissions, you might try creating a Theme.NoDisplay activity just to call requestPermissions() and handle onRequestPermissionsResult(). I would expect this to generate the same exception on Android 6.0, though I have not tried this scenario to confirm the theory.

The issue that was filed on the M Developer Preview about this ran into the problem when trying to show a Dialog from onCreate(). The issue went nowhere.

One fix described in that issue — calling setVisible(true) in onStart() — works, but you wind up with a visible activity UI (black background and old-timey gray title bar).

What worked better for me was to use Theme.Translucent.NoTitleBar as the theme on Android 6.0+. If you want to stick with Theme.NoDisplay on older devices, create a custom theme that inherits from Theme.NoDisplay in res/values/ but from Theme.Translucent.NoTitleBar in res/values-v23/, then use your custom theme.

Or, go with targetSdkVersion 22 temporarily. Eventually, something will drive you to using targetSdkVersion of 23 or higher, though.

UPDATE: 2015-11-08 Dianne Hackborn responded to this post, explaining that Theme.NoDisplay is somewhat documented to require a finish() before onResume() and outlines a bit of the rationale for the change to detect this case and fail fast. She and I disagree on what constitutes a regression (IMHO, a substantial change in behavior is a regression, even if documentation hints at the validity of the change), but it is great that we have a more-or-less official source for what is going on.

Many thanks to the Android Weekly newsletter for happening to mention Ms. Hackborn’s post in this week’s issue.