Wednesday, 22 November, 2017 UTC


Summary

TL; DR;
Android 8.0 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. Repeatably. Until you uninstall the app programmatically. Most people might need a factory reset. See the image below.
It’s important to force default notification icon using the metadata tags in AndroidManifest.xml.
Intrigued? Read on.
The problem
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 now 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

  1. Create a new Android Studio project, an app is fine. Ensure it the target SDK is 26 or higher.
  2. Include Firebase, add the generated google-services.json , and the firebase-messaging dependency. The steps to follow are described here:
    https://firebase.google.com/docs/android/setup
    https://firebase.google.com/docs/cloud-messaging/android/client
  3. Start an emulator with Android Oreo 8.0 (SDK 26). I used a Pixel Emulator. Install the application, and background once it’s running.
  4. Send a notification to the phone; you can use the Firebase console to compose it: https://console.firebase.google.com/project/_/notification/compose
Compose any notification
  1. 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:
    E/AndroidRuntime: FATAL EXCEPTION: main
                      Process: com.android.systemui, PID: 4555
                      java.lang.IllegalArgumentException: width and height must be > 0
                          at android.graphics.Bitmap.createBitmap(Bitmap.java:989)
                          at android.graphics.Bitmap.createBitmap(Bitmap.java:956)
                          at android.graphics.Bitmap.createBitmap(Bitmap.java:906)
                          at android.graphics.Bitmap.createBitmap(Bitmap.java:867)
                          at android.graphics.drawable.AdaptiveIconDrawable.updateMaskBoundsInternal(AdaptiveIconDrawable.java:333)
                          at android.graphics.drawable.AdaptiveIconDrawable.updateLayerBounds(AdaptiveIconDrawable.java:295)
                          at android.graphics.drawable.AdaptiveIconDrawable.onStateChange(AdaptiveIconDrawable.java:796)
                          at android.graphics.drawable.Drawable.setState(Drawable.java:760)
                          at android.widget.ImageView.drawableStateChanged(ImageView.java:1268)
                          at android.view.View.refreshDrawableState(View.java:19619)
                          at android.view.View.dispatchAttachedToWindow(View.java:17020)
                          at android.view.ViewGroup.addViewInner(ViewGroup.java:4924)
                          at android.view.ViewGroup.addView(ViewGroup.java:4716)
                          at com.android.systemui.statusbar.phone.NotificationIconAreaController.updateIconsForLayout(NotificationIconAreaController.java:204)
                          at com.android.systemui.statusbar.phone.NotificationIconAreaController.updateNotificationIcons(NotificationIconAreaController.java:152)
                          at com.android.systemui.statusbar.phone.StatusBar.updateNotificationShade(StatusBar.java:1900)
                          at com.android.systemui.statusbar.phone.StatusBar.updateNotifications(StatusBar.java:2080)
                          at com.android.systemui.statusbar.phone.StatusBar.addNotificationViews(StatusBar.java:6561)
                          at com.android.systemui.statusbar.phone.StatusBar.addNotification(StatusBar.java:1589)
                          at com.android.systemui.statusbar.phone.StatusBar$23$1.run(StatusBar.java:5534)
                          at android.os.Handler.handleCallback(Handler.java:769)
                          at android.os.Handler.dispatchMessage(Handler.java:98)
                          at android.os.Looper.loop(Looper.java:164)
                          at android.app.ActivityThread.main(ActivityThread.java:6535)
                          at java.lang.reflect.Method.invoke(Native Method)
                          at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
                          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:
    <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
        <background android:drawable="@drawable/ic_launcher_background" />
        <foreground android:drawable="@drawable/ic_launcher_foreground" />
    </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:
    <meta-data
        android:name="com.google.firebase.messaging.default_notification_icon"
        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 application starts.
    internal fun validateApplicationIcon(context: Context) {
      if (targetSdkIsBelowOreo(context)) {
        return
      }
      if (canCreateIconDrawable(context)) {
        return
      }
      if (hasDefaultFCMIconInMetadata(context)) {
        return
      }

      throw IllegalStateException(
        "You are targetting Android Oreo and using adaptive icons without having a fallback drawable set for FCM notifications. \n This can cause a irreversible crash on devices using Oreo. \n " +
          "To learn more about this issue check: https://issuetracker.google.com/issues/68716460")
    }
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 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 measures 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!
The post How to upgrade your app to Android Oreo and avoid a factory reset appeared first on Pusher Blog.