CVE-2024-0041 AI-generated analysis
platform/frameworks/base, SystemUI status bar events (SystemStatusAnimationSchedulerImpl, privacy/persistent dot)
Rewrites SystemStatusAnimationSchedulerImpl.removePersistentDot() so that, when the status chip is ANIMATING_OUT, the hide-dot listeners are notified immediately. Before, the notification came from a coroutine that waited for a state change under a DISAPPEAR_ANIMATION_DURATION timeout, and it was silently dropped when the animator callback arrived late.
Root cause
In packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt, removePersistentDot() handled the ANIMATING_OUT state by launching a coroutine on coroutineScope. That coroutine ran withTimeout(DISAPPEAR_ANIMATION_DURATION) { animationState.first { SHOWING_PERSISTENT_DOT || IDLE || ANIMATION_QUEUED }; notifyHidePersistentDot() }. The state transition it waited for is driven by the animator's onAnimationEnd callback. If that callback arrived more than DISAPPEAR_ANIMATION_DURATION after the removal request, the timeout expired and notifyHidePersistentDot() was never called. Listeners were then never told to hide the dot, so the persistent dot was not removed even though hasPersistentDot had been set to false. The new test testDotIsRemoved_evenIfAnimatorCallbackIsDelayed reproduces this case: it advances the test scheduler past DISAPPEAR_ANIMATION_DURATION before the animator fires, which the test comment says caused the old code to miss the callback. In addition, notifyHidePersistentDot() itself changed animationState (to ANIMATION_QUEUED or IDLE) when the state was SHOWING_PERSISTENT_DOT.
How the fix works
- SystemStatusAnimationSchedulerImpl.kt:removePersistentDot(): the ANIMATING_OUT branch no longer launches a coroutine with withTimeout. It calls notifyHidePersistentDot() synchronously (the function starts with Assert.isMainThread()). The comment says animationState will be updated later by the onAnimationEnd callback, so the hide notification no longer depends on animator timing.
- SystemStatusAnimationSchedulerImpl.kt:removePersistentDot(): the SHOWING_PERSISTENT_DOT branch now calls notifyHidePersistentDot() and then sets animationState to ANIMATION_QUEUED if scheduledEvent.value != null, otherwise IDLE. The two separate ifs became an if / else if chain.
- SystemStatusAnimationSchedulerImpl.kt:notifyHidePersistentDot(): the block that changed animationState when in SHOWING_PERSISTENT_DOT was removed. The function now only collects the listener animators (listeners.mapNotNull { it.onHidePersistentDot() }) and plays them. Updating the state is now the job of removePersistentDot().
- SystemStatusAnimationSchedulerImplTest.kt: two existing tests drop testScheduler.runCurrent() after removePersistentDot(). One of them now verifies onHidePersistentDot() right after the call. The new test testDotIsRemoved_evenIfAnimatorCallbackIsDelayed advances the coroutine scheduler by DISAPPEAR_ANIMATION_DURATION + 1 before advancing the animator, then asserts that the callback fires once and that the state ends as IDLE.
Completeness
- The fix covers both states in which a dot is visible or about to appear when removePersistentDot() is called: SHOWING_PERSISTENT_DOT (hide and update the state synchronously) and ANIMATING_OUT (hide synchronously, state left to onAnimationEnd).
- States before ANIMATING_OUT get no explicit hide call. Per the in-code comment, they rely on hasPersistentDot = false so that the disappear animation removes the chip without turning it into a dot. The fix does not change this path, and the test testPrivacyDot_isRemovedDuringChipAnimation covers it.
- The coroutine and timeout are removed entirely rather than given a longer timeout, so this path no longer depends on DISAPPEAR_ANIMATION_DURATION.
- In the ANIMATING_OUT branch, the fix relies on the onAnimationEnd callback (not shown in the diff) to move animationState out of ANIMATING_OUT. The diff does not change that callback.
Behavior changes
- When the chip is ANIMATING_OUT, listener onHidePersistentDot() is now invoked synchronously inside removePersistentDot(), before the disappear animation finishes, instead of after the state change.
- removePersistentDot() no longer launches a coroutine.
- notifyHidePersistentDot() no longer changes animationState. In the diff, removePersistentDot() updates the state itself. Any other callers of notifyHidePersistentDot() are not shown in the material.
- Test expectations changed: in testPrivacyDot_isRemoved, onHidePersistentDot is verified right after removePersistentDot() without running the test scheduler.
Per major
- Android 14: Same fix: commit d6f718877340, applied in android-security-14.0.0_r5. It is a cherry-pick of internal commit d027036adf63, Change-Id I7380cb956b48c38bdd540b297287786cf35b94f2. At android-security-14.0.0_r29, 6/8 lines of the patched region are present, and the context shows the new if/else-if structure in removePersistentDot().
- Android 15: Not affected per the bulletin. At android-security-15.0.0_r17, 6/8 lines of the patched region are present.
- Android 16: Not affected per the bulletin. At android-security-16.0.0_r8, 5/8 lines of the patched region are present.
- Android 17: Not affected per the bulletin. At android-security-17.0.0_r1, 5/8 lines of the patched region are present.
Unknowns
- The material does not show who calls removePersistentDot() or under what runtime conditions the animator callback is delayed.
- The onAnimationEnd handler that the ANIMATING_OUT branch now relies on to update animationState is not in the diff or the context.
- The material does not show whether notifyHidePersistentDot() has callers other than removePersistentDot().
- The material does not explain how a stale persistent dot maps to the bulletin's EoP classification.
- The material does not show why 15, 16 and 17 are not listed by the bulletin, or whether the fix is fully present there.