Mike Korcha

Blog

This post has a continuation article here!

Experiences in Early-Adopting Android "O" in Development

I decided that with the Android O release I would actually be ahead of the curve with my apps and ensure full compatibility beforehand. Little did I know how weird things could get with the use of a release-candidate SDK. This post serves as a log of me updating my pet project Media Button Overlay.

SDK Updates

I started by following the Android O migration guide by Google. This involves getting appropriate system images and testing for current and the current release candidate of Android. The initial build-and-run on O (with the current SDK) showed no signs of anything working incorrectly. So the next test is updating build.gradle to use the O SDK (note that this requires the canary branch of Android Studio):

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"

    // ...

    defaultConfig {
        // ...
        minSdkVersion 14
        targetSdkVersion "O"
        // ...
    }
}

After a compile/run cycle, I ran into an error:

java.lang.RuntimeException: Unable to create service com.mikekorcha.mediabuttonoverlay.services.OverlayService: android.view.WindowManager$BadTokenException: Unable to add window [email protected] -- permission denied for window type 2002

Now this is strange, wasn't it just working? After already being aware of appropriate behavior changes to overlays, which require an overlay to use a different layout type, while deprecating several old ones for the new API for non-system apps, which causes an error, even with appropriate permissions (see the API documentation for O). I'm no stranger to checking the available SDK version for feature availability, so the next thing to do is get the next SDK version set up:

android {
    compileSdkVersion "android-O"
    buildToolsVersion "26.0.0-rc1"

    // ...

    defaultConfig {
        // ...
        minSdkVersion 14
        targetSdkVersion "O"
        // ...
    }
}

// ...

dependencies {
    // ...
    compile 'com.android.support:appcompat-v7:26.0.0-alpha1'
    // ...
}

Then, do a typical check for the SDK version:

int overlayType = WindowManager.LayoutParams.TYPE_PHONE;
if(Build.VERSION.SDK_INT > 25) {
    overlayType = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
}

After compiling and running, I still got the same error! Interestingly, according to the documentation on the release timeline, the API changes aren't finalized until preview 3, and the SDK_INT isn't incremented. This is confirmed when inspecting Build.VERSION.SDK_INT in the debugger. Not incrementing the SDK version seems strange to me, as for development purposes this requires me to use something not available in previous versions. I suppose this is fine, since publishing with the O SDK isn't available until then anyways. It does require me to do this to have my overlay launch in development, however, until the SDK version changes:

int overlayType = WindowManager.LayoutParams./*TYPE_PHONE*/TYPE_APPLICATION_OVERLAY;

Broadcast Changes

We once again have the overlay working, but now none of the intent actions seem to be working! Documentation says that there are changes to background processing, to include implicit broadcasts, background services, and location updates to improve battery life. In particular, we are worried about the service and the broadcasts. According the docs, we can no longer register implicit receivers in the manifest (except certain system ones), and instead must register them at rutime. This has the benefit to the user that apps aren't catching intents when they aren't active, but does require some level of rewriting MBO to work properly again.

There are two ways to handle this, depending on the kind of receiver you wish to have. To keep it an implicit receiver, you can just "convert" the manifest declartion into a runtime declaration, which will accept the implicit broadcast while running. For example, this:

<receiver
    android:name=".services.OverlayService$StopReceiver"
    android:enabled="true"
    android:exported="false">

    <intent-filter>
        <action android:name="com.mikekorcha.mediabuttonoverlay.STOP" />
    </intent-filter>
</receiver>

becomes this:

stopReciever = new StopReciever();
// ...
registerReceiver(stopReciever, new IntentFilter("com.mikekorcha.mediabuttonoverlay.STOP"));
// ... and in onDestroy, to prevent leaking the reciever
unregisterReceiver(stopReciever);

The other way is to send explicit broadcasts to the classes that handle them. Depending on the use case for your broadcasts, this may be the better choice. As an example, this:

context.sendBroadcast(new Intent(context.getPackageName() + ".STOP"));

would become this:

context.sendBroadcast(new Intent(context, StopReceiver.class));

Some intent recievers are excepted from the new rules. Media Button Overlay has a notification feature that starts on boot, using BOOT_COMPLETED, which does not need to be converted (at least, not at this time). A list of them exists, though there aren't many. These actions are those that cannot be done with a JobScheduler, or are so uncommonly fired and recieved that it wouldn't make sense to require changing them.

Service Changes

There is a distinction between foreground and background apps, which will soon modify how the services they create will behave and terminate. Apps that are not considered foreground will have a limited window for background services to be created and run before they are terminated.

My app never promoted the service to the foreground in the first place, as the expected use case was one where the device wouldn't really be in use (so it wouldn't get pushed out of memory). However, it seems the service handling will be more aggressive when the app isn't considered foreground. I also should have treated it as foreground because it is supposed to be for apps that are noticeable to the user (creating an overlay on screen is pretty noticeable). This seems like a good time to change that.

First, we'll update the service to promote itself to the foreground, by adding a notification to the onStart method, and calling startForeground:

Intent notificationIntent = new Intent(this, StopReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);

Notification notification = new NotificationCompat.Builder(this)
        .setSmallIcon(R.drawable.ic_notification)
        .setContentTitle(getResources().getString(R.string.app_name))
        .setContentText(getResources().getString(R.string.notification_running))
        .setContentIntent(pendingIntent)
        .setAutoCancel(true)
        .build();

startForeground(NOTIFICATION_ID, notification);

Note here, you can have the pending intent do a number of other things when it's tapped, such as start an activity. Next, we have to add the SDK-26-which-is-really-25 code, which is used when the service is actually started instead of in the service itself:

Intent service = new Intent(this, OverlayService.class);
if(Build.VERSION.SDK_INT > 25) {
    NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

    Intent notificationIntent = new Intent(this, OverlayService.StopReceiver.class);
    PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0,
            notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);

    Notification notification = new NotificationCompat.Builder(this)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(getResources().getString(R.string.app_name))
            .setContentText(getResources().getString(R.string.notification_running))
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)
            .build();

    nm.startServiceInForeground(service, NOTIFICATION_ID, notification)
}
else {
    this.startService(new Intent(this, OverlayService.class));
}

Then, we wrap the call for the old one to check for an SDK under 26. I ended up adding a few functions to make the code a bit cleaner after implementing the changes.

Other Stuff

Apprently you don't need to cast a call to findViewById now, though a lot of people now seem to be using data bindings for their views instead. Worth mentioning, nonetheless.

Putting it all Together

This was my experience in upgrading an app of mine to work for Android O. Steps and experiences may be different from one person to another (I'm sure someone who has been developing for Android more consistently won't have as many problems). For inspiration, you can look at this diff (ignore the IDE files) for the project to see what I had to do. Disregard some bad-style things I've done in the project - it was one of my first Android projects and I did things that worked, without much thought on other stuff. But hey, now it's fully O compatible, so when the third preview comes around I can start playing with new features!


  • development
  • android
  • media-button-overlay
Written 2017-03-26