diff options
| author | 2023-10-20 13:46:06 +0000 | |
|---|---|---|
| committer | 2023-10-20 15:01:38 +0000 | |
| commit | 173f325f4478783990ea60082ddf08485e213ead (patch) | |
| tree | 757995f870180b3519cce2471dd32748aec6b1cd | |
| parent | 57cb73f29ba5a5c7f9dcd8cf7e0029bcef9be213 (diff) | |
Fix accessibility for connected display status bar icon
Now it has a content description, and every time the green icon animates in "Display connected" is announced.
The icon is now selectable as well.
Fixes: 306525994
Fixes: 306522117
Fixes: 306527238
Test: SystemStatusAnimationSchedulerImplTest
Change-Id: I072daedb3e5873d640ae6da2ea319619a5bb32d1
8 files changed, 103 insertions, 2 deletions
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 7a6d29ab3c1f..9e2ebf60fe8f 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3224,6 +3224,9 @@ <!--- Label of the dismiss button of the dialog appearing when an external display is connected [CHAR LIMIT=NONE]--> <string name="dismiss_dialog">Dismiss</string> + <!--- Content description of the connected display status bar icon that appears every time a display is connected [CHAR LIMIT=NONE]--> + <string name="connected_display_icon_desc">Display connected</string> + <!-- Title of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=30] --> <string name="privacy_dialog_title">Microphone & Camera</string> <!-- Subtitle of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt index bde298d7a33d..ea1d7820c79c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt @@ -36,6 +36,11 @@ interface StatusEvent { val showAnimation: Boolean val viewCreator: ViewCreator var contentDescription: String? + /** + * When true, an accessibility event with [contentDescription] is announced when the view + * becomes visible. + */ + val shouldAnnounceAccessibilityEvent: Boolean // Update this event with values from another event. fun updateFromEvent(other: StatusEvent?) { @@ -76,6 +81,7 @@ class BatteryEvent(@IntRange(from = 0, to = 100) val batteryLevel: Int) : Status override var forceVisible = false override val showAnimation = true override var contentDescription: String? = "" + override val shouldAnnounceAccessibilityEvent: Boolean = false override val viewCreator: ViewCreator = { context -> BatteryStatusChip(context).apply { @@ -95,6 +101,7 @@ class ConnectedDisplayEvent : StatusEvent { override var forceVisible = false override val showAnimation = true override var contentDescription: String? = "" + override val shouldAnnounceAccessibilityEvent: Boolean = true override val viewCreator: ViewCreator = { context -> ConnectedDisplayChip(context) @@ -110,6 +117,7 @@ open class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEven override var contentDescription: String? = null override val priority = 100 override var forceVisible = true + override val shouldAnnounceAccessibilityEvent: Boolean = false var privacyItems: List<PrivacyItem> = listOf() private var privacyChip: OngoingPrivacyChip? = null diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt index dccc23f61092..73c0bfec69a1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt @@ -273,6 +273,11 @@ class SystemEventChipAnimationController @Inject constructor( }) } + /** Announces [contentDescriptions] for accessibility. */ + fun announceForAccessibility(contentDescriptions: String) { + currentAnimatedView?.view?.announceForAccessibility(contentDescriptions) + } + private fun updateDimens(contentArea: Rect) { val lp = animationWindowView.layoutParams as FrameLayout.LayoutParams lp.height = contentArea.height() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt index 7d866df73da9..8ee1ade7a185 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt @@ -100,9 +100,12 @@ constructor( } private fun startConnectedDisplayCollection() { + val connectedDisplayEvent = ConnectedDisplayEvent().apply { + contentDescription = context.getString(R.string.connected_display_icon_desc) + } connectedDisplayCollectionJob = onDisplayConnectedFlow - .onEach { scheduler.onStatusEvent(ConnectedDisplayEvent()) } + .onEach { scheduler.onStatusEvent(connectedDisplayEvent) } .launchIn(appScope) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt index a3bc00248362..f0e60dd2ce54 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt @@ -254,11 +254,18 @@ constructor( currentlyRunningAnimationJob = coroutineScope.launch { runChipAppearAnimation() + announceForAccessibilityIfNeeded(event) delay(APPEAR_ANIMATION_DURATION + DISPLAY_LENGTH) runChipDisappearAnimation() } } + private fun announceForAccessibilityIfNeeded(event: StatusEvent) { + val description = event.contentDescription ?: return + if (!event.shouldAnnounceAccessibilityEvent) return + chipAnimationController.announceForAccessibility(description) + } + /** * 1. Define a total budget for the chip animation (1500ms) * 2. Send out callbacks to listeners so that they can generate animations locally diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index 54d81b83197e..5a8b636e54fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -300,7 +300,8 @@ public class PhoneStatusBarPolicy mIconController.setIconVisibility(mSlotCast, false); // connected display - mIconController.setIcon(mSlotConnectedDisplay, R.drawable.stat_sys_connected_display, null); + mIconController.setIcon(mSlotConnectedDisplay, R.drawable.stat_sys_connected_display, + mResources.getString(R.string.connected_display_icon_desc)); mIconController.setIconVisibility(mSlotConnectedDisplay, false); // hotspot diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt index 839770267c74..df8afde1b9a3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt @@ -26,6 +26,7 @@ class FakeStatusEvent( override var forceVisible: Boolean = false, override val showAnimation: Boolean = true, override var contentDescription: String? = "", + override val shouldAnnounceAccessibilityEvent: Boolean = false ) : StatusEvent class FakePrivacyStatusEvent( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt index 4fcccf887e58..fee8b82a3038 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt @@ -33,6 +33,8 @@ import com.android.systemui.statusbar.BatteryStatusChip import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider import com.android.systemui.statusbar.window.StatusBarWindowController import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import junit.framework.Assert.assertEquals @@ -370,6 +372,63 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { } @Test + fun testAccessibilityAnnouncement_announced() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + val accessibilityDesc = "Some desc" + val mockView = mock<View>() + val mockAnimatableView = + mock<BackgroundAnimatableView> { whenever(view).thenReturn(mockView) } + + scheduleFakeEventWithView( + accessibilityDesc, + mockAnimatableView, + shouldAnnounceAccessibilityEvent = true + ) + fastForwardAnimationToState(ANIMATING_OUT) + + verify(mockView).announceForAccessibility(eq(accessibilityDesc)) + } + + @Test + fun testAccessibilityAnnouncement_nullDesc_noAnnouncement() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + val accessibilityDesc = null + val mockView = mock<View>() + val mockAnimatableView = + mock<BackgroundAnimatableView> { whenever(view).thenReturn(mockView) } + + scheduleFakeEventWithView( + accessibilityDesc, + mockAnimatableView, + shouldAnnounceAccessibilityEvent = true + ) + fastForwardAnimationToState(ANIMATING_OUT) + + verify(mockView, never()).announceForAccessibility(any()) + } + + @Test + fun testAccessibilityAnnouncement_notNeeded_noAnnouncement() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + val accessibilityDesc = "something" + val mockView = mock<View>() + val mockAnimatableView = + mock<BackgroundAnimatableView> { whenever(view).thenReturn(mockView) } + + scheduleFakeEventWithView( + accessibilityDesc, + mockAnimatableView, + shouldAnnounceAccessibilityEvent = false + ) + fastForwardAnimationToState(ANIMATING_OUT) + + verify(mockView, never()).announceForAccessibility(any()) + } + + @Test fun testPrivacyDot_isRemovedDuringChipAnimation() = runTest { // Instantiate class under test with TestScope from runTest initializeSystemStatusAnimationScheduler(testScope = this) @@ -572,6 +631,20 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { return privacyChip } + private fun scheduleFakeEventWithView( + desc: String?, + view: BackgroundAnimatableView, + shouldAnnounceAccessibilityEvent: Boolean + ) { + val fakeEvent = + FakeStatusEvent( + viewCreator = { view }, + contentDescription = desc, + shouldAnnounceAccessibilityEvent = shouldAnnounceAccessibilityEvent + ) + systemStatusAnimationScheduler.onStatusEvent(fakeEvent) + } + private fun createAndScheduleFakeBatteryEvent(): BatteryStatusChip { val batteryChip = BatteryStatusChip(mContext) val fakeBatteryEvent = |