From 8311e5eca9d50ff1e2f8394764a6bf640ca4da70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Bertheussen?= Date: Thu, 27 Jun 2024 20:44:04 +0200 Subject: [PATCH] Fix AccessibilityFeatures.disableAnimations flag on Android 12+ (flutter/engine#53428) This PR fixes the problem where Flutter would not respect the "remove animation" accessibility setting on Android 12+. Please see this issue for details: https://github.com/flutter/flutter/issues/130976 As [mentioned](https://github.com/flutter/flutter/issues/130976#issuecomment-1931388665) by [horsemankukka](https://github.com/horsemankukka), the problem has to do with reading `Settings.Global.TRANSITION_ANIMATION_SCALE` as a string instead of a float. Flutter would compare it to the string "0" to determine if animations should be disabled. Presumably, this worked because the settings app did indeed use the string "0" or "1" for this setting. But as of Android 12 it's instead written using float representation ("0.0" or "1.0"), at least on Samsung devices. [The documentation](https://developer.android.com/reference/android/provider/Settings.Global#TRANSITION_ANIMATION_SCALE) also states that this setting should be read as a float, which is what this PR does. [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style --- .../io/flutter/view/AccessibilityBridge.java | 18 +++-- .../flutter/view/AccessibilityBridgeTest.java | 67 +++++++++++++++++++ 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java index e6014f4329c..8516d622206 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -126,6 +126,12 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { // Font weight adjustment for bold text. FontWeight.Bold - FontWeight.Normal = w700 - w400 = 300. private static final int BOLD_TEXT_WEIGHT_ADJUSTMENT = 300; + // Default transition animation scale (animations enabled) + private static final float DEFAULT_TRANSITION_ANIMATION_SCALE = 1.0f; + + // Transition animation scale when animations are disabled + private static final float DISABLED_TRANSITION_ANIMATION_SCALE = 0.0f; + /// Value is derived from ACTION_TYPE_MASK in AccessibilityNodeInfo.java private static int FIRST_RESOURCE_ID = 267386881; @@ -399,11 +405,13 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { return; } // Retrieve the current value of TRANSITION_ANIMATION_SCALE from the OS. - String value = - Settings.Global.getString( - contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE); + float value = + Settings.Global.getFloat( + contentResolver, + Settings.Global.TRANSITION_ANIMATION_SCALE, + DEFAULT_TRANSITION_ANIMATION_SCALE); - boolean shouldAnimationsBeDisabled = value != null && value.equals("0"); + boolean shouldAnimationsBeDisabled = value == DISABLED_TRANSITION_ANIMATION_SCALE; if (shouldAnimationsBeDisabled) { accessibilityFeatureFlags |= AccessibilityFeature.DISABLE_ANIMATIONS.value; } else { @@ -560,7 +568,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { if (shouldBold) { accessibilityFeatureFlags |= AccessibilityFeature.BOLD_TEXT.value; } else { - accessibilityFeatureFlags &= AccessibilityFeature.BOLD_TEXT.value; + accessibilityFeatureFlags &= ~AccessibilityFeature.BOLD_TEXT.value; } sendLatestAccessibilityFlagsToFlutter(); } diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index 389c329f96c..f86ea7a3b8d 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -13,6 +13,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -28,8 +29,10 @@ import android.content.ContentResolver; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.database.ContentObserver; import android.graphics.Rect; import android.os.Bundle; +import android.provider.Settings; import android.text.SpannableString; import android.text.SpannedString; import android.text.style.LocaleSpan; @@ -1346,6 +1349,31 @@ public class AccessibilityBridgeTest { /*platformViewsAccessibilityDelegate=*/ null); verify(mockChannel).setAccessibilityFeatures(1 << 3); + reset(mockChannel); + + // Now verify that clearing the BOLD_TEXT flag doesn't touch any of the other flags. + // Ensure the DISABLE_ANIMATION flag will be set + Settings.Global.putFloat(null, "transition_animation_scale", 0.0f); + // Ensure the BOLD_TEXT flag will be cleared + config.fontWeightAdjustment = 0; + + accessibilityBridge = + setUpBridge( + /*rootAccessibilityView=*/ mockRootView, + /*accessibilityChannel=*/ mockChannel, + /*accessibilityManager=*/ mockManager, + /*contentResolver=*/ null, + /*accessibilityViewEmbedder=*/ mockViewEmbedder, + /*platformViewsAccessibilityDelegate=*/ null); + + // setAccessibilityFeatures() will be called multiple times from AccessibilityBridge's + // constructor, verify that the latest argument is correct + ArgumentCaptor captor = ArgumentCaptor.forClass(Integer.class); + verify(mockChannel, atLeastOnce()).setAccessibilityFeatures(captor.capture()); + assertEquals(1 << 2 /* DISABLE_ANIMATION */, captor.getValue().intValue()); + + // Set back to default + Settings.Global.putFloat(null, "transition_animation_scale", 1.0f); } @Test @@ -2012,6 +2040,45 @@ public class AccessibilityBridgeTest { verify(accessibilityViewEmbedder).getRootNode(eq(embeddedView), eq(0), any(Rect.class)); } + @Test + public void testItSetsDisableAnimationsFlagBasedOnTransitionAnimationScale() { + AccessibilityChannel mockChannel = mock(AccessibilityChannel.class); + ContentResolver mockContentResolver = mock(ContentResolver.class); + + AccessibilityBridge accessibilityBridge = + setUpBridge( + /*rootAccessibilityView=*/ null, + /*accessibilityChannel=*/ mockChannel, + /*accessibilityManager=*/ null, + /*contentResolver=*/ mockContentResolver, + /*accessibilityViewEmbedder=*/ null, + /*platformViewsAccessibilityDelegate=*/ null); + + // Capture the observer registered for Settings.Global.TRANSITION_ANIMATION_SCALE + ArgumentCaptor observerCaptor = ArgumentCaptor.forClass(ContentObserver.class); + verify(mockContentResolver) + .registerContentObserver( + eq(Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE)), + eq(false), + observerCaptor.capture()); + ContentObserver observer = observerCaptor.getValue(); + + // Initial state + verify(mockChannel).setAccessibilityFeatures(0); + reset(mockChannel); + + // Animations are disabled + Settings.Global.putFloat(mockContentResolver, "transition_animation_scale", 0.0f); + observer.onChange(false); + verify(mockChannel).setAccessibilityFeatures(1 << 2); + reset(mockChannel); + + // Animations are enabled + Settings.Global.putFloat(mockContentResolver, "transition_animation_scale", 1.0f); + observer.onChange(false); + verify(mockChannel).setAccessibilityFeatures(0); + } + @Test public void releaseDropsChannelMessageHandler() { AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);