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”.

This appendix covers this new version of Android and what it offers to developers… sometimes whether developers like it or not.

Note that Android 8.0 is API Level 26.

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.

What Is a Background Service, Exactly?

Here, “background services” are ones that:

That latter scenario covers cases where your service exposes an API that is bound to by other apps or by core OS processes. This includes custom APIs implemented via AIDL and framework-supplied APIs such as those used by JobService, TileService, and so on.

Certain events, such as having your code triggered by a Notification PendingIntent, or by receiving a broadcast, will also give you a fresh window of time when background services behave normally.

What Happens?

The documentation indicates that when your process moves from the foreground to the background, or when one of the other triggers (e.g., receiving a broadcast) occurs, you have “several minutes” of normal operation. Testing suggests that by “several minutes”, Google actually means “a minute or so”.

At that point:

Your process is still running at this point. However, its importance is the same as if you had no service running, meaning that your process is at risk of being terminated at any point to free up system RAM.

Note that while the documentation suggests that startService() will throw an IllegalStateException when your app is ineligible to start a background service, this behavior varies. If a service calls startService(), no exception is thrown, and the call seems to fail quietly. If another Context is used — such as a BroadcastReceiver — you will get the exception:

Process: com.commonsware.android.service.ouroboros, PID: 27276
java.lang.RuntimeException: Unable to start receiver com.commonsware.android.service.ouroboros.HackReceiver: java.lang.IllegalStateException: Not allowed to start service Intent { cmp=com.commonsware.android.service.ouroboros/.SecondSillyService (has extras) }: app is in background
   at android.app.ActivityThread.handleReceiver(ActivityThread.java:3159)
   at android.app.ActivityThread.-wrap18(Unknown Source:0)
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1621)
   at android.os.Handler.dispatchMessage(Handler.java:102)
   at android.os.Looper.loop(Looper.java:154)
   at android.app.ActivityThread.main(ActivityThread.java:6408)
   at java.lang.reflect.Method.invoke(Native Method)
   at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:232)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:751)
Caused by: java.lang.IllegalStateException: Not allowed to start service Intent { cmp=com.commonsware.android.service.ouroboros/.SecondSillyService (has extras) }: app is in background
   at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1451)
   at android.app.ContextImpl.startService(ContextImpl.java:1405)
   at android.content.ContextWrapper.startService(ContextWrapper.java:630)
   at android.content.ContextWrapper.startService(ContextWrapper.java:630)
   at com.commonsware.android.service.ouroboros.HackReceiver.onReceive(HackReceiver.java:12)
   at android.app.ActivityThread.handleReceiver(ActivityThread.java:3152)
   at android.app.ActivityThread.-wrap18(Unknown Source:0) 
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1621) 
   at android.os.Handler.dispatchMessage(Handler.java:102) 
   at android.os.Looper.loop(Looper.java:154) 
   at android.app.ActivityThread.main(ActivityThread.java:6408) 
   at java.lang.reflect.Method.invoke(Native Method) 
   at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:232) 
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:751)

What Are the Alternatives?

An obvious solution is to use a foreground service. You have three options for doing this:

The latter two approaches work even when your process no longer has the ability to call startService(). However, those methods are new to Android 8.0. Also, despite their method names, they do not actually start your service as a foreground service. Rather, they give you a short reprieve from the normal service-starting limits — your service needs to call startForeground() shortly after being created, or else your service will be destroyed.

A JobService can spend ~10 minutes processing a job, before Android will consider the service to be broken and reduce the process’ priority to lower levels. Hence, using JobScheduler may be an option for you, particularly if the work you are trying to do is periodic in nature, where your JobService does the work.

As this change only affects apps with a targetSdkVersion higher than 25, keeping your targetSdkVersion at 25 or lower will avoid this behavior change.

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

A minor improvement to JobScheduler comes in the form of new constraint methods on JobInfo.Builder: setRequiresBatteryNotLow() and setRequiresStorageNotLow(). If you use these, your jobs will not run when the battery is low or when storage is low, respectively.

However, the bigger change really comes in the roles that JobScheduler plays:

JobIntentService

IntentService still works on Android 8.0, but unless you make it be a foreground service, you will be limited to ~1 minute of runtime before your service is stopped abruptly. JobIntentService is a wrapper around a JobService that offers IntentService-style semantics. On Android 8.0 and higher, when you tell a JobIntentService to do some work, it enqueues that work via JobScheduler. On Android 7.1 and earlier, the JobIntentService behaves more like a regular IntentService, though one that supplies a WakeLock for you (akin to the author’s WakefulIntentService).

The Service/JobIntentService sample project is a clone of an early IntentService sample, where we use the service to download a PDF file. The revised sample swaps out the IntentService with a JobIntentService, which is a fairly easy conversion to make.

First, since a JobIntentService is actually used as a JobService on Android 8.0+, we need to defend it with the android.permission.BIND_JOB_SERVICE permission:

    <service
      android:name="Downloader"
      android:permission="android.permission.BIND_JOB_SERVICE" />

What had been onHandleIntent() in an IntentService turns into onHandleWork() in a JobIntentService:

package com.commonsware.android.downloader;

import android.content.Context;
import android.content.Intent;
import android.os.Environment;
import android.support.v4.app.JobIntentService;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class Downloader extends JobIntentService {
  public static final String ACTION_COMPLETE=
      "com.commonsware.android.downloader.action.COMPLETE";
  private static final int UNIQUE_JOB_ID=1337;

  static void enqueueWork(Context ctxt, Intent i) {
    enqueueWork(ctxt, Downloader.class, UNIQUE_JOB_ID, i);
  }

  @Override
  public void onHandleWork(Intent i) {
    try {
      File root=
          Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);

      root.mkdirs();

      File output=new File(root, i.getData().getLastPathSegment());

      if (output.exists()) {
        output.delete();
      }

      URL url=new URL(i.getData().toString());
      HttpURLConnection c=(HttpURLConnection)url.openConnection();

      FileOutputStream fos=new FileOutputStream(output.getPath());
      BufferedOutputStream out=new BufferedOutputStream(fos);

      try {
        InputStream in=c.getInputStream();
        byte[] buffer=new byte[8192];
        int len=0;

        while ((len=in.read(buffer)) >= 0) {
          out.write(buffer, 0, len);
        }

        out.flush();
      }
      finally {
        fos.getFD().sync();
        out.close();
        c.disconnect();
      }

      LocalBroadcastManager.getInstance(this)
                           .sendBroadcast(new Intent(ACTION_COMPLETE));
    }
    catch (IOException e2) {
      Log.e(getClass().getName(), "Exception in download", e2);
    }
  }
}

The semantics of onHandleWork() are the same as doWakefulWork() with a WakefulIntentService:

Because our code is using a wakelock — by way of a support library — we need to have the WAKE_LOCK permission in the manifest, along with other permissions needed for our business logic:

  <uses-permission android:name="android.permission.WAKE_LOCK" />
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

To arrange to have the service perform some work, with IntentService, you would use startService(), with an Intent identifying the service (and, optionally, passing along details of the work to be done). With JobIntentService, you instead call a static enqueueWork() method defined on JobIntentService. This takes four parameters:

Since the second and third parameters are constants, you can create your own enqueueWork() method that calls the JobService implementation, passing along those constant values:

  private static final int UNIQUE_JOB_ID=1337;

  static void enqueueWork(Context ctxt, Intent i) {
    enqueueWork(ctxt, Downloader.class, UNIQUE_JOB_ID, i);
  }

Then, your code that wishes to have the work performed calls your enqueueWork() method to do so:

  private void doTheDownload() {
    b.setEnabled(false);

    Intent i=new Intent(getActivity(), Downloader.class);

    i.setData(Uri.parse("https://commonsware.com/Android/Android-1_0-CC.pdf"));

    Downloader.enqueueWork(getActivity(), i);
  }

The JobIntentService can spend up to ~10 minutes in onHandleWork() on Android 8.0+, which is a substantial improvement over the ~1 minute a background IntentService has. If, however, there is a substantial chance that the work would exceed 10 minutes, use a foreground IntentService.

JobScheduler as Work Queue

Under the covers, JobIntentService is taking advantage of a new capability in JobScheduler on Android 8.0: a work queue. IntentService had such a queue, after a fashion, in that it would process one Intent at a time through onHandleIntent(), queuing up other Intent objects that arrive while onHandleIntent() is busy. JobScheduler now offers a similar capability for your JobService, where you can post jobs and have your JobService end when the work is completed.

This is covered in detail in the chapter on JobScheduler.

Auto-Fill

The preview of this section is en route to Mars.

Notification Channels

The preview of this section left for Hollywood to appear in a reality TV show.

Other Changes with Notifications

The preview of this section was the victim of a MITM ('Martian in the middle') attack.

Multi-Window Changes

The preview of this section is presently indisposed.

WebView Changes

The preview of this section is sleeping in.

ContentProvider Changes

The preview of this section is off trying to sweet-talk the Khaleesi into providing us with a dragon.

Storage Access Framework Changes

The preview of this section was eaten by a grue.

Package Management

The preview of this section apparently resembled a Pokémon.

Fonts as Resources

The preview of this section is out seeking fame and fortune as the Dread Pirate Roberts.

Other Major Changes in Android 8.0

The preview of this section left for Hollywood to appear in a reality TV show.

Other Minor Changes in Android 8.0

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