From 394d5e0010392c5ffe6edd4a8b8fdfbb214c7492 Mon Sep 17 00:00:00 2001 From: Toni Barzic Date: Wed, 5 Mar 2025 17:01:01 +0000 Subject: [PATCH] Improve KQS accessibility Adds accessible pane name, and content description for keyboard quick switch view. Improves content description for task views, by having it provide the task position in the task list. Also, sets a task content description immediately when it gets associated with a task, even if the task description is not available at the time. Accessibility focus for a task may be requested as the KQS view is shown, so the task view should have a meaningful content description from the start. While here, update KQS to focus first task by default when opened from taskbar. Bug: 399032757 Test: Manual, with Talkback enabled - open KQS via taskbar overflow view, and Alt+Tab, and verify Talkback announcements are informative (e.g. no "Pixel launcher" utterance). Also atest NexusLauncherTests:TaplTestsKeyboardQuickSwitch Flag: EXEMPT bug fix Change-Id: I3688954ab383035c1db35c5eb7a0ddf9a39a123f --- .../res/layout/keyboard_quick_switch_view.xml | 2 + quickstep/res/values/strings.xml | 8 +++ .../taskbar/KeyboardQuickSwitchTaskView.java | 64 +++++++++++++++---- .../taskbar/KeyboardQuickSwitchView.java | 13 +++- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml index 885bdb9f3b..2dea79c8a2 100644 --- a/quickstep/res/layout/keyboard_quick_switch_view.xml +++ b/quickstep/res/layout/keyboard_quick_switch_view.xml @@ -18,6 +18,8 @@ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/keyboard_quick_switch_view" + android:contentDescription="@string/quick_switch_content_description" + android:accessibilityPaneTitle="@string/quick_switch_pane_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/keyboard_quick_switch_margin_top" diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml index 65f4b3cd2e..7578bd546f 100644 --- a/quickstep/res/values/strings.xml +++ b/quickstep/res/values/strings.xml @@ -325,6 +325,12 @@ Open app as a bubble + + Recent apps + + + Recent app list + {count, plural, =1{more app} @@ -336,6 +342,8 @@ %1$s and %2$s + + %1$s, item %2$d of %3$d diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java index f80dc909f8..3cc9e7f7fd 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java @@ -64,6 +64,11 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout { @Nullable private ImageView mIcon2; @Nullable private View mContent; + // Describe the task position in the parent container. Used to add information about the task's + // position in a task list to the task view's content description. + private int mIndexInParent = -1; + private int mTotalTasksInParent = -1; + public KeyboardQuickSwitchTaskView(@NonNull Context context) { this(context, null); } @@ -153,36 +158,51 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout { applyThumbnail(mThumbnailView1, task1, thumbnailUpdateFunction); applyThumbnail(mThumbnailView2, task2, thumbnailUpdateFunction); + // Update content description, even in cases task icons, and content descriptions need to be + // loaded asynchronously to ensure that the task has non empty description (assuming task + // position information was set), as KeyboardQuickSwitch view may request accessibility + // focus to be moved to the task when the quick switch UI gets shown. The description will + // be updated once the task metadata has been loaded - the delay should be very short, and + // the content description when task titles are not available still gives some useful + // information to the user (the task's position in the list). + updateContentDesctiptionForTasks(task1, task2); + if (iconUpdateFunction == null) { applyIcon(mIcon1, task1); applyIcon(mIcon2, task2); - setContentDescription(task2 == null - ? task1.titleDescription - : getContext().getString( - R.string.quick_switch_split_task, - task1.titleDescription, - task2.titleDescription)); return; } + iconUpdateFunction.updateIconInBackground(task1, t -> { applyIcon(mIcon1, task1); if (task2 != null) { return; } - setContentDescription(task1.titleDescription); + updateContentDesctiptionForTasks(task1, null); }); + if (task2 == null) { return; } iconUpdateFunction.updateIconInBackground(task2, t -> { applyIcon(mIcon2, task2); - setContentDescription(getContext().getString( - R.string.quick_switch_split_task, - task1.titleDescription, - task2.titleDescription)); + updateContentDesctiptionForTasks(task1, task2); }); } + /** + * Initializes information about the task's position within the parent container context - used + * to add position information to the view's content description. + * Should be called before associating the view with tasks. + * + * @param index The view's 0-based index within the parent task container. + * @param totalTasks The total number of tasks in the parent task container. + */ + protected void setPositionInformation(int index, int totalTasks) { + mIndexInParent = index; + mTotalTasksInParent = totalTasks; + } + protected void setThumbnailsForSplitTasks( @NonNull Task task1, @Nullable Task task2, @@ -281,6 +301,28 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout { constantState.newDrawable(getResources(), getContext().getTheme())); } + /** + * Updates the task view's content description to reflect tasks represented by the view. + */ + private void updateContentDesctiptionForTasks(@NonNull Task task1, @Nullable Task task2) { + String tasksDescription = task1.titleDescription == null || task2 == null + ? task1.titleDescription + : getContext().getString( + R.string.quick_switch_split_task, + task1.titleDescription, + task2.titleDescription); + if (mIndexInParent < 0) { + setContentDescription(tasksDescription); + return; + } + + setContentDescription( + getContext().getString(R.string.quick_switch_task_with_position_in_parent, + tasksDescription != null ? tasksDescription : "", + mIndexInParent + 1, + mTotalTasksInParent)); + } + protected interface ThumbnailUpdateFunction { void updateThumbnailInBackground(Task task, Consumer callback); diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java index 336ef48c64..92a51e403b 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java @@ -300,6 +300,7 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { continue; } + currentTaskView.setPositionInformation(i, tasksToDisplay); currentTaskView.setThumbnailsForSplitTasks( task1, task2, @@ -547,6 +548,9 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { ViewOutlineProvider outlineProvider = getOutlineProvider(); + int defaultFocusedTaskIndex = Math.min( + getTaskCount() - 1, + currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride); mOpenAnimation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { @@ -600,9 +604,7 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { }); } - animateFocusMove(-1, Math.min( - getTaskCount() - 1, - currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride)); + animateFocusMove(-1, defaultFocusedTaskIndex); displayedContent.setVisibility(VISIBLE); setVisibility(VISIBLE); requestFocus(); @@ -622,6 +624,11 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { invalidateOutline(); mOpenAnimation = null; InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN); + + View focusedTask = getTaskAt(defaultFocusedTaskIndex); + if (focusedTask != null) { + focusedTask.requestAccessibilityFocus(); + } } });