How to upgrade your app to Android Oreo and avoid a factory reset

upgrade-app-android-oreo-avoid-factory-reset-header.png

Android Oreo has adaptive icons that can’t be inflated by the system UI into a notification. System UI crashes when a notification is received without a specified icon. Repeatedly. Until you uninstall the app programmatically. Most people might need a factory reset. See the image below. It’s important to force the default notification icon using \[…\]

Introduction

Android Oreo has adaptive icons that can’t be inflated by the system UI into a notification. System UI crashes when a notification is received without a specified icon. Repeatedly. Until you uninstall the app programmatically. Most people might need a factory reset.

See the image below. It’s important to force the default notification icon using the metadata tags in AndroidManifest.xml.

Intrigued? Read on.

The problem with Android Oreo

We discovered an intriguing, somewhat scary issue with the latest version of Android – 8.0 aka Oreo. It can, and will, cause your system to go into a nasty crash loop involving the system. The culprit? A push notification.

The nasty bit about it is that you might not know about it until your app has already been installed by your users, and you didn’t try sending a push notification to it. After all, who tests for Android Oreo that 0.3% of people have installed, right? :trollface: (as of November 2017)
Here you go, reproduced in a friendly emulator near you:

The loop of Death

Now if this happens in an emulator, you’re probably a developer, and just a single adb uninstall com.your.application.id from solving the problem.

But what if it happens to your user? The last thing you want is having to tell your users to factory reset the device. It happened to cause a major uproar with at least one application – Swipe for Facebook. Various publications have published stories about it: https://wccftech.com/android-oreo-adaptive-icons-bug/

We discovered this problem when building the new Pusher Push Notifications SDK. After some digging, we found a way around it and learned a bunch of stuff about adaptive notifications in the process.

Steps to reproduce

Compose any notification

  • Observe the crash and the crash log. Note that you’ll need to select “no filters” if viewing the logs in Android Studio, as it’s caused by the System UI trying to inflate the view.

The crash log is below:

1E/AndroidRuntime: FATAL EXCEPTION: main
2                      Process: com.android.systemui, PID: 4555
3                      java.lang.IllegalArgumentException: width and height must be > 0
4                          at android.graphics.Bitmap.createBitmap(Bitmap.java:989)
5                          at android.graphics.Bitmap.createBitmap(Bitmap.java:956)
6                          at android.graphics.Bitmap.createBitmap(Bitmap.java:906)
7                          at android.graphics.Bitmap.createBitmap(Bitmap.java:867)
8                          at android.graphics.drawable.AdaptiveIconDrawable.updateMaskBoundsInternal(AdaptiveIconDrawable.java:333)
9                          at android.graphics.drawable.AdaptiveIconDrawable.updateLayerBounds(AdaptiveIconDrawable.java:295)
10                          at android.graphics.drawable.AdaptiveIconDrawable.onStateChange(AdaptiveIconDrawable.java:796)
11                          at android.graphics.drawable.Drawable.setState(Drawable.java:760)
12                          at android.widget.ImageView.drawableStateChanged(ImageView.java:1268)
13                          at android.view.View.refreshDrawableState(View.java:19619)
14                          at android.view.View.dispatchAttachedToWindow(View.java:17020)
15                          at android.view.ViewGroup.addViewInner(ViewGroup.java:4924)
16                          at android.view.ViewGroup.addView(ViewGroup.java:4716)
17                          at com.android.systemui.statusbar.phone.NotificationIconAreaController.updateIconsForLayout(NotificationIconAreaController.java:204)
18                          at com.android.systemui.statusbar.phone.NotificationIconAreaController.updateNotificationIcons(NotificationIconAreaController.java:152)
19                          at com.android.systemui.statusbar.phone.StatusBar.updateNotificationShade(StatusBar.java:1900)
20                          at com.android.systemui.statusbar.phone.StatusBar.updateNotifications(StatusBar.java:2080)
21                          at com.android.systemui.statusbar.phone.StatusBar.addNotificationViews(StatusBar.java:6561)
22                          at com.android.systemui.statusbar.phone.StatusBar.addNotification(StatusBar.java:1589)
23                          at com.android.systemui.statusbar.phone.StatusBar$23$1.run(StatusBar.java:5534)
24                          at android.os.Handler.handleCallback(Handler.java:769)
25                          at android.os.Handler.dispatchMessage(Handler.java:98)
26                          at android.os.Looper.loop(Looper.java:164)
27                          at android.app.ActivityThread.main(ActivityThread.java:6535)
28                          at java.lang.reflect.Method.invoke(Native Method)
29                          at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
30                          at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)

Digging deeper…

We can see from the logs, that the crash is caused by view inflation when a notification is being created, and one specifically related to the new Adaptive Icons that were introduced in Oreo.

Adaptive icons are a new thing introduced in Android Oreo. They can change the icon shape, based on the device’s own preferences. That means icons will look differently on Samsung, Pixel, OnePlus, and other devices by different manufacturers, yet still in the line with the rest of the UI. They also support visual effects, presumably so Michael Bay can make them explode. For more information about them, feel free to read the official guide on d.android.com.

To create an adaptive effect, we can’t rely on just resources in mipmaps anymore. The adaptive icons live in mipmap-anydpi-v26 folder and are defined in XML, as you can see from the image below. On devices running SDK 25 and lower the icons will just be taken from the corresponding mipmap-xdpi folder.

Resource hierarchy, as generated by Android Studio 3.0

SDK 26 and above use the following drawable element, defined by the adaptive-icon node:

ic_launcher_round.xml:

1<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
2        <background android:drawable="@drawable/ic_launcher_background" />
3        <foreground android:drawable="@drawable/ic_launcher_foreground" />
4    </adaptive-icon>

Solving the problem

The easiest thing to do would be to target SDK 25 or avoid using Adaptive Icons for now, until a fix is issued by Google.

A better way would be to add metadata tags to your Android Manifest, that specify a new default drawable for notifications created by FCM. That drawable must not be an adaptive icon.

In your Application tags in Manifest you could put something like this:

1<meta-data
2        android:name="com.google.firebase.messaging.default_notification_icon"
3        android:resource="@drawable/your_non_adaptive_drawable" />

Lastly, you could ensure that all the notifications are sent with the icon payload that specifies the desired drawable (must be non-adaptive).
Note that you cannot do this from the Firebase Notifications console. You can either use the API directly or a third-party notifications provider such as Pusher.

How we are helping our customers avoid this problem

We wrote this blog post for starters to raise awareness. ?

We are adding a validation function to our new SDK that will clearly alert our app developers to this problem when the application starts.

1internal fun validateApplicationIcon(context: Context) {
2      if (targetSdkIsBelowOreo(context)) {
3        return
4      }
5      if (canCreateIconDrawable(context)) {
6        return
7      }
8      if (hasDefaultFCMIconInMetadata(context)) {
9        return
10      }
11
12      throw IllegalStateException(
13        "You are targetting Android Oreo and using adaptive icons without having a fallback drawable set for FCM notifications. \n This can cause an irreversible crash on devices using Oreo. \n " +
14          "To learn more about this issue check: https://issuetracker.google.com/issues/68716460")
15    }

The function checks for 3 things:
First, it checks whether the target SDK used is Android Oreo or newer, then whether the launcher icon is adaptive, and lastly if default drawable metadata is set in the Manifest.
If any of these things are true, it’s all good – otherwise, we throw an IllegalStateException with a message explaining how to avoid the potential issue.
You can find the entire validation logic in a gist we published on Github.

Google already issued a fix for the crash in Android 8.1, which is currently available on the Beta channel.
Some existing Pixel devices will probably still be on Oreo for the time being, however. We believe that they will also put some preventive measures in the Firebase SDK, likely defaulting to a fallback bitmap when Firebase Notifications are added, possibly injected directly to the Manifest via their Gradle plugin.

Until then, you have Pusher. Happy coding! ?