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
This commit is contained in:
Håkon Bertheussen 2024-06-27 20:44:04 +02:00 committed by GitHub
parent e95c0d9c7a
commit 8311e5eca9
2 changed files with 80 additions and 5 deletions

View File

@ -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();
}

View File

@ -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<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
@ -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<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);