material-components / material-components/material-components-android

[NavigationView] Insets listener set on NavigationView header can be called many times per second in an infinite loop

Open
#5,040 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Java
Stars
17.4k
Forks
3.2k
PR merge metrics
No merged PRs in 30d

Description

Description:

If an insets listener is set on NavigationView's header, it is possible to get into a situation when it will start to be called many times per second in an infinite loop. This happens on Android < 11.

Expected behavior: Insets listener is not called hundreds of times per second.

Minimal sample app repro:

navigation-view-bug.zip

Launch the app. Open the left drawer. Click on "request insets" button. Observe counter going up (the counter shows the number of times insets listener was called).

Android API version: 29

Material Library version: 1.13.0

Device: Any Android 10 device

I believe that the cause of the issue is the combination of two factors:

  1. ScrimInsetsFrameLayout (which NavigationView extends) calls its onInsetsChanged method with insets before consumeSystemWindowInsets(): https://github.com/material-components/material-components-android/blob/889a3910bd9266407bfc2f0fb3abbce3685cad96/lib/java/com/google/android/material/internal/ScrimInsetsFrameLayout.java#L88

These insets are applied directly to the header container in NavigationMenuPresenter: https://github.com/material-components/material-components-android/blob/889a3910bd9266407bfc2f0fb3abbce3685cad96/lib/java/com/google/android/material/internal/NavigationMenuPresenter.java#L469

However when ScrimInsetsFrameLayout's insets listener return insets after consumeSystemWindowInsets(), these insets (which are now different) are then also applied to the header through ViewGroup's insets dispatching.

  1. Now the piece that actually causes the loop is in the ViewCompat.setOnApplyWindowInsetsListener(): https://github.com/androidx/androidx/blob/d0264c461cc7e0eee8003ef0d1de0d5ee496a0d0/core/core/src/main/java/androidx/core/view/ViewCompat.java#L5051

What happens is that when insets are applied again (because of requestApplyInsets() or some other reason), insets from ScrimInsetsFrameLayout.onInsetsChanged and from ViewGroup dispatching arrive in pairs and because they are different (the equals() check fails) it causes wrappedUserListener to repeatedly call requestApplyInsets() which created a self-sustaining loop.

I'm not sure why it doesn't happen immediately on application start though.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the issue with the linked minimal sample on an Android 10/API 29 device: launch it, open the drawer, and click “request insets” while watching the counter. Start by reading ScrimInsetsFrameLayout.java and NavigationMenuPresenter.java, then compare their inset handling with ViewCompat.setOnApplyWindowInsetsListener(). Done means the header listener no longer loops or runs hundreds of times per second.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, java
Domain
mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.