Fix AccessibilityFeatures.disableAnimations flag on Android 12+ (#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
diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java
index e6014f4..8516d62 100644
--- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java
+++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java
@@ -126,6 +126,12 @@
   // 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 @@
             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 @@
     if (shouldBold) {
       accessibilityFeatureFlags |= AccessibilityFeature.BOLD_TEXT.value;
     } else {
-      accessibilityFeatureFlags &= AccessibilityFeature.BOLD_TEXT.value;
+      accessibilityFeatureFlags &= ~AccessibilityFeature.BOLD_TEXT.value;
     }
     sendLatestAccessibilityFlagsToFlutter();
   }
diff --git a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java
index 389c329..f86ea7a 100644
--- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java
+++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java
@@ -13,6 +13,7 @@
 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.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 @@
             /*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<Integer> 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
@@ -2013,6 +2041,45 @@
   }
 
   @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<ContentObserver> 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);
     AccessibilityManager mockManager = mock(AccessibilityManager.class);