The following is the first few sections of a chapter from The Busy Coder's Guide to Android Development, plus headings for the remaining major sections, to give you an idea about the content of the chapter.


Appendix B: Android 8.0

In 2017, Google released Android 8.0, code-named “Oreo”, followed shortly by Android 8.1. Android 8.1 changed very little in the Android SDK for most developers, but Android 8.0 had some significant changes.

This appendix outlines those changes. In some cases, it serves as pointers to where this material is covered elsewhere in the book. For smaller topics, details of the change appear directly in the appendix.

The War on Background Processing, Continued

Starting with Android 6.0, Google has been trying to limit the impacts of background processing on the device, particularly with respect to battery usage and RAM consumption. Since most background work tends to be invisible to the user, users therefore will tend to blame Android for problems that stem from the users’ chosen apps as much, if not more than, from Android itself. As a result, in Android 6.0, Doze mode and app standby were added to curtail periodic work, and Android 7.0 started putting limitations on some types of system broadcasts.

Android 8.0 is furthering Google’s objectives in this area, eliminating significant types of background processing.

Background Service Limitations

For apps that have a targetSdkVersion over 25 and are running on Android 8.0, background services are limited. After a short period of time — as low as one minute — any such services will be stopped and you will be unable to start new ones.

Also, even if your targetSdkVersion is 25 or lower, you might still have these limitations applied to your app. If your app appears on the Battery screen in Settings — indicating that it is using above-average power — the user will have the ability to apply these limitations to your app from there.

This is explored in greater detail in the chapter on services.

WakeLock Limitations

If your service holds a WakeLock, and that WakeLock is not released when the service is stopped, Android will forcibly release the WakeLock.

Leaking an acquired WakeLock was a bad practice, and since your process can be terminated quickly at any point once you no longer have a running service, developers should have been assuming all along that a WakeLock should be released when a service is stopped. Android 8.0 is merely being a bit more aggressive about dealing with these leaks.

Manifest-Registered Broadcast Limitations

For apps that have a targetSdkVersion over 25, another limitation comes into play: you cannot receive implicit broadcasts via a manifest-registered receiver.

In other words, if you have a receiver in the manifest that has an <intent-filter>, there is a very good chance that it will no longer receive broadcasts.

What Is Affected

Implicit broadcasts are broadcasts using an implicit Intent, one that just has an action string (and possibly a Uri, categories, or MIME type), but does not identify a specific BroadcastReceiver. Explicit broadcasts use an explicit Intent, one that does identify a specific BroadcastReceiver.

The Android 8.0 limitation affects:

If your targetSdkVersion is 25 or lower, though, your app will not be affected. Also, if you happen to be the one sending the broadcast, and you are requiring a signature-level permission for that broadcast, your app will not be affected, apparently.

Also note that various Intent actions documented on the Intent class are actually used with explicit broadcasts, not implicit ones. For example, the ACTION_PACKAGE_REPLACED broadcast is an implicit one, but ACTION_MY_PACKAGE_REPLACED is an explicit one, as that one is only sent to the app that was just upgraded.

The Intents/PackageLogger sample project is a very simple app, dominated by an OnPackageChangeReceiver that registers for a few Intent actions in the manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.commonsware.android.sysevents.pkg"
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:versionCode="1"
  android:versionName="1.0">

  <application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:allowBackup="false">
    <receiver android:name=".OnPackageChangeReceiver">
      <intent-filter>
        <action android:name="android.intent.action.PACKAGE_ADDED" />
        <action android:name="android.intent.action.PACKAGE_REPLACED" />
        <action android:name="android.intent.action.PACKAGE_REMOVED" />

        <data android:scheme="package" />
      </intent-filter>
    </receiver>

    <activity
      android:name="BootstrapActivity"
      android:theme="@android:style/Theme.Translucent.NoTitleBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>

</manifest>

…and logs their occurrences to Logcat:

package com.commonsware.android.sysevents.pkg;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class OnPackageChangeReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    Log.d(getClass().getSimpleName(),
      intent.getAction()+" for "+intent.getData().toString());
  }
}

On Android 7.1 and lower devices, this app dutifully logs those events (e.g., when the user installs an app). On Android 8.0, instead, the receiver does not get control, and the following message is recorded to Logcat:

W/BroadcastQueue: Background execution not allowed: receiving Intent { act=android.intent.action.PACKAGE_REMOVED dat=package:com.commonsware.cwac.cam2.demo flg=0x4000010 (has extras) } to com.commonsware.android.sysevents.pkg/.OnPackageChangeReceiver

Why This Ban Was Added

You might think that the concern was tied to the battery, as this seems like another front in the ongoing “war on background processing” that has been going on since Doze mode was introduced in Android 6.0.

As it turns out, battery is of secondary importance. The real reason is process churn.

Quoting a Google engineer:

To help understand what is going on, I need to clarify that the purpose of this change is not directly related to battery use, but rather to address long-standing issues we have had in the platform where devices that are under memory pressure can get in to bad thrashing states. Very often these states are due to broadcasts: some broadcast or broadcasts are being sent relatively frequently, which a lot of applications are listening to through their manifest (so need to be launched to receive it), but there is not enough RAM to keep all of those app proceses [sic] in cache, so the system ends up continually thrashing through processes each time the broadcast is sent.

This is an issue regardless of whether the device is currently plugged in to power. In fact, this can more frequently be an issue on Android TV devices (which are always plugged in to power) because they tend to be fairly tight on RAM!

Workarounds for Senders

If you are using broadcasts for communicating between app components within a single process, switch to using LocalBroadcastManager.

If you are using broadcasts for communicating between app components within multiple processes of your own, switch to using explicit broadcasts.

Beyond that, if you are sending implicit broadcasts, you can break through the ban by finding the receivers and sending individual explicit broadcasts instead.

This, and the overall ban, is illustrated in the Intents/Fanout sample project. As with some of the event bus samples, this app has a UI that consists of a transcript-mode ListView, to which we will append events as they arrive. In this case, the events are broadcasts that we are sending, using different approaches for sending them based on an overflow menu item.

If the user taps the “Explicit” overflow menu item, we create an explicit Intent identifying our TestReceiver and send that using sendBroadcast(). This works, even for an app like this one that has targetSdkVersion 'O', and the broadcast shows up in the list.

If the user taps the “Implicit” overflow menu item, we create an implicit Intent tied to the action string used by the <intent-filter> of the TestReceiver:

<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.commonsware.android.broadcast.fanout"
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:versionCode="1"
  android:versionName="1.0">

  <application
    android:allowBackup="false"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme">
    <activity
      android:name=".MainActivity"
      android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>

    <receiver android:name=".TestReceiver">
      <intent-filter>
        <action android:name="${applicationId}.TEST" />
      </intent-filter>
    </receiver>
  </application>

</manifest>

However, sending that implicit Intent fails, with this warning message showing up in Logcat:

W/BroadcastQueue: Background execution not allowed: receiving Intent { act=com.commonsware.android.broadcast.fanout.TEST flg=0x10 (has extras) } to com.commonsware.android.broadcast.fanout/.TestReceiver

If the user taps the “Fanout” overflow menu item, we create the same implicit Intent as before (though we tuck an extra onto it to identify it as the “fanout” case instead of the regular implicit case). And this time, it works. The reason why it works is that rather than sending one implicit broadcast, we send N explicit broadcasts, one for each registered receiver:

  private static void sendImplicitBroadcast(Context ctxt, Intent i) {
    PackageManager pm=ctxt.getPackageManager();
    List<ResolveInfo> matches=pm.queryBroadcastReceivers(i, 0);

    for (ResolveInfo resolveInfo : matches) {
      Intent explicit=new Intent(i);
      ComponentName cn=
        new ComponentName(resolveInfo.activityInfo.applicationInfo.packageName,
          resolveInfo.activityInfo.name);

      explicit.setComponent(cn);
      ctxt.sendBroadcast(explicit);
    }
  }

Unfortunately, this brings back the process churn, and if lots of developers do this, there may be reprisals from Google. You might try introducing some delay between the broadcasts, inside the loop, to spread out the impact. However, this starts to get tricky if you spread it out over more than a few seconds (e.g., do you now need an IntentService and a WakeLock? what if your process is terminated before the broadcast loop is completed?).

Google recommends that you have the user agree to which of these components should receive the broadcast, perhaps through some sort of MultiSelectListPreference. Then, instead of broadcasting to all that match your implicit broadcast, you only broadcast to those that the user has chosen. How practical this is will depend on the app and the desired user experience.

Workarounds for Receivers

If you are receiving system-sent implicit broadcasts (e.g., ACTION_PACKAGE_ADDED), keep your targetSdkVersion at 25 or lower, until we figure out better workarounds that (hopefully) do not involve polling.

If you are receiving implicit broadcasts from another app, ask the developer of that app what the plan is for Android 8.0. Perhaps they will use the above technique, or perhaps they will switch to some alternative communications pattern.

Background Location Limitations

Background apps — principally, those that do not have the foreground UI and are not a foreground service — will receive fewer location updates than before, whether using LocationManager or the Play Services fused location API. The documentation says that background apps will receive location information “only a few times each hour”.

Note that this affects all apps, not just those with a targetSdkVersion over 25.

Besides putting your app in the foreground, you can:

JobScheduler Enhancements

The preview of this section is presently indisposed.

Auto-Fill

The preview of this section was accidentally identified as an Android 'tasty treat' by the Cookie Monster.

Notification Channels

The preview of this section did not survive Thanos's finger snap.

Other Changes with Notifications

The preview of this section was lost in the sofa cushions.

Multi-Window Changes

The preview of this section was traded for a bag of magic beans.

WebView Changes

The preview of this section was last seen in the Bermuda Triangle.

ContentProvider Changes

The preview of this section was traded for a bag of magic beans.

Storage Access Framework Changes

The preview of this section was lost due to a rupture in the space-time continuum.

Package Management

The preview of this section was abducted by space aliens.

Fonts as Resources

The preview of this section was fed to a gremlin, after midnight.

Other Major Changes in Android 8.0

The preview of this section is in the process of being translated from its native Klingon.

Other Minor Changes in Android 8.0

The preview of this section was whisked away by a shark-infested tornado.