Android Nougat and rate limiting of notification updates

While working on a feature for downloading images in my app, I ran into a strange issue where progress notifications were never reaching 100%. Knowing that the framework is usually not wrong, I started suspecting my own skills (as usual). To make matters worse, nobody was able to reproduce the issue in a sample project I shared with other developers.

It took me a baffling/embarrassing amount of time to realize that the notifications were only getting stuck on Nougat and above. And the reason nobody was able to reproduce it was because they were testing on M and lower versions. This was a huge hint. I removed my logcat filters to check if the system was printing any logs and there they were:

E/NotificationService: Package enqueue rate is 10.062265. Shedding events. package=me.saket.notifications

Turns out, Android has a rate-limiting system in place for notification updates to prevent apps from DDOS-ing the notification shade. Although it has existed since long, the limit was significantly reduced in Nougat from 50 updates to 10 updates every second to improve performance:

Updates to progress bars are the main culprit in system
performance events caused by apps spamming the notification
service. Rate-limiting only updates allows us to set a lower
threshold without the worry of mistakenly dropping bursts of
notifications being quickly posted after a network sync.

The git commits can be read here here and here.

Although I had no intentions of spamming, I never thought that updating a progress bar will be so expensive for the notification shade. The shade is perhaps more complex than I can fathom. An engineer who works on notifications on Android further suggested 5 updates every second is a good rate to aim for instead of 10.

RxJava to the rescue!

As usual, there’s an Rx operator for that: Observable#sample(), which emits the most recently emitted item within periodic time intervals.

// Avoid sampling the last "success" emission 
// before terminating the stream.
boolean includeLastItem = true;

streamDownloadProgress()
.sample(200, TimeUnit.MILLISECONDS, includeLastItem)
 .subscribe(progress -> {
   updateProgressNotification(progress, NOTIFICATION_ID);
 });

Small caveat

The problem sadly isn’t fully solved yet. The rate limit is enforced on a per package level and not per notification ID (please feel free to correct me if this is wrong), which means that there has to be a global check in place somewhere in your app. I haven’t figured out any solution, but aiming for ~5 updates a second seems to be working for me.

A sample project demonstrating this problem can be seen here on Github.