Merge "Populates collection info count for A11y toggle feature pages." into main
diff --git a/aconfig/accessibility/accessibility_flags.aconfig b/aconfig/accessibility/accessibility_flags.aconfig
index 5c81cc9..1871172 100644
--- a/aconfig/accessibility/accessibility_flags.aconfig
+++ b/aconfig/accessibility/accessibility_flags.aconfig
@@ -38,6 +38,13 @@
}
flag {
+ name: "enable_color_contrast_control"
+ namespace: "accessibility"
+ description: "Allows users to control color contrast in the Accessibility settings page."
+ bug: "246577325"
+}
+
+flag {
name: "enable_hearing_aid_preset_control"
namespace: "accessibility"
description: "Allows users to control hearing aid preset in the Bluetooth device details page."
@@ -89,8 +96,11 @@
}
flag {
- name: "enable_color_contrast_control"
+ name: "toggle_feature_fragment_collection_info"
namespace: "accessibility"
- description: "Allows users to control color contrast in the Accessibility settings page."
- bug: "246577325"
+ description: "Provides custom CollectionInfo for ToggleFeaturePreferenceFragment"
+ bug: "318607873"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
diff --git a/src/com/android/settings/accessibility/AccessibilityFragmentUtils.java b/src/com/android/settings/accessibility/AccessibilityFragmentUtils.java
new file mode 100644
index 0000000..34e17c0
--- /dev/null
+++ b/src/com/android/settings/accessibility/AccessibilityFragmentUtils.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.accessibility;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.view.AccessibilityDelegateCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroupAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
+
+import com.android.settingslib.widget.IllustrationPreference;
+
+/** Utilities for {@code Settings > Accessibility} fragments. */
+public class AccessibilityFragmentUtils {
+ // TODO: b/350782252 - Replace with an official library-provided solution when available.
+ /**
+ * Modifies the existing {@link RecyclerViewAccessibilityDelegate} of the provided
+ * {@link RecyclerView} for this fragment to report the number of visible and important
+ * items on this page via the RecyclerView's {@link AccessibilityNodeInfo}.
+ *
+ * <p><strong>Note:</strong> This is special-cased to the structure of these fragments:
+ * one column, N rows (one per preference, including category titles and header+footer
+ * preferences), <=N 'important' rows (image prefs without content descriptions). This
+ * is not intended for use with generic {@link RecyclerView}s.
+ */
+ public static RecyclerView addCollectionInfoToAccessibilityDelegate(RecyclerView recyclerView) {
+ if (!Flags.toggleFeatureFragmentCollectionInfo()) {
+ return recyclerView;
+ }
+ final RecyclerViewAccessibilityDelegate delegate =
+ recyclerView.getCompatAccessibilityDelegate();
+ if (delegate == null) {
+ // No delegate, so do nothing. This should not occur for real RecyclerViews.
+ return recyclerView;
+ }
+ recyclerView.setAccessibilityDelegateCompat(
+ new RvAccessibilityDelegateWrapper(recyclerView, delegate) {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(@NonNull View host,
+ @NonNull AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ if (!(recyclerView.getAdapter()
+ instanceof final PreferenceGroupAdapter preferenceGroupAdapter)) {
+ return;
+ }
+ final int visibleCount = preferenceGroupAdapter.getItemCount();
+ int importantCount = 0;
+ for (int i = 0; i < visibleCount; i++) {
+ if (isPreferenceImportantToA11y(preferenceGroupAdapter.getItem(i))) {
+ importantCount++;
+ }
+ }
+ info.unwrap().setCollectionInfo(
+ new AccessibilityNodeInfo.CollectionInfo(
+ /*rowCount=*/visibleCount,
+ /*columnCount=*/1,
+ /*hierarchical=*/false,
+ AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_SINGLE,
+ /*itemCount=*/visibleCount,
+ /*importantForAccessibilityItemCount=*/importantCount));
+ }
+ });
+ return recyclerView;
+ }
+
+ /**
+ * Returns whether the preference will be marked as important to accessibility for the sake
+ * of calculating {@link AccessibilityNodeInfo.CollectionInfo} counts.
+ *
+ * <p>The accessibility service itself knows this information for an individual preference
+ * on the screen, but it expects the preference's {@link RecyclerView} to also provide the
+ * same information for its entire set of adapter items.
+ */
+ @VisibleForTesting
+ static boolean isPreferenceImportantToA11y(Preference pref) {
+ if ((pref instanceof IllustrationPreference illustrationPref
+ && TextUtils.isEmpty(illustrationPref.getContentDescription()))
+ || pref instanceof PaletteListPreference) {
+ // Illustration preference that is visible but unannounced by accessibility services.
+ return false;
+ }
+ // All other preferences from the PreferenceGroupAdapter are important.
+ return true;
+ }
+
+ /**
+ * Wrapper around a {@link RecyclerViewAccessibilityDelegate} that allows customizing
+ * a subset of methods and while also deferring to the original. All overridden methods
+ * in instantiations of this class should call {@code super}.
+ */
+ private static class RvAccessibilityDelegateWrapper extends RecyclerViewAccessibilityDelegate {
+ private final RecyclerViewAccessibilityDelegate mOriginal;
+
+ RvAccessibilityDelegateWrapper(RecyclerView recyclerView,
+ RecyclerViewAccessibilityDelegate original) {
+ super(recyclerView);
+ mOriginal = original;
+ }
+
+ @Override
+ public boolean performAccessibilityAction(@NonNull View host, int action, Bundle args) {
+ return mOriginal.performAccessibilityAction(host, action, args);
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(@NonNull View host,
+ @NonNull AccessibilityNodeInfoCompat info) {
+ mOriginal.onInitializeAccessibilityNodeInfo(host, info);
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(@NonNull View host,
+ @NonNull AccessibilityEvent event) {
+ mOriginal.onInitializeAccessibilityEvent(host, event);
+ }
+
+ @Override
+ @NonNull
+ public AccessibilityDelegateCompat getItemDelegate() {
+ if (mOriginal == null) {
+ // Needed for super constructor which calls getItemDelegate before mOriginal is
+ // defined, but unused by actual clients of this RecyclerViewAccessibilityDelegate
+ // which invoke getItemDelegate() after the constructor finishes.
+ return new ItemDelegate(this);
+ }
+ return mOriginal.getItemDelegate();
+ }
+ }
+}
diff --git a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java
index 0ac29bc..9c61e5c 100644
--- a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java
+++ b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java
@@ -56,6 +56,7 @@
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
+import androidx.recyclerview.widget.RecyclerView;
import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
@@ -871,4 +872,12 @@
return PreferredShortcuts.retrieveUserShortcutType(
getPrefContext(), mComponentName.flattenToString(), getDefaultShortcutTypes());
}
+
+ @Override
+ public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent,
+ Bundle savedInstanceState) {
+ RecyclerView recyclerView =
+ super.onCreateRecyclerView(inflater, parent, savedInstanceState);
+ return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(recyclerView);
+ }
}
diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java
index 97405d2..52d75c1 100644
--- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java
+++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java
@@ -79,7 +79,8 @@
Bundle savedInstanceState) {
if (parent instanceof GlifPreferenceLayout) {
final GlifPreferenceLayout layout = (GlifPreferenceLayout) parent;
- return layout.onCreateRecyclerView(inflater, parent, savedInstanceState);
+ return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(
+ layout.onCreateRecyclerView(inflater, parent, savedInstanceState));
}
return super.onCreateRecyclerView(inflater, parent, savedInstanceState);
}
diff --git a/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java
index 4309b1d..10813a7 100644
--- a/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java
+++ b/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java
@@ -68,7 +68,8 @@
Bundle savedInstanceState) {
if (parent instanceof GlifPreferenceLayout) {
final GlifPreferenceLayout layout = (GlifPreferenceLayout) parent;
- return layout.onCreateRecyclerView(inflater, parent, savedInstanceState);
+ return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(
+ layout.onCreateRecyclerView(inflater, parent, savedInstanceState));
}
return super.onCreateRecyclerView(inflater, parent, savedInstanceState);
}
diff --git a/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java
index 8d26785..10796b5 100644
--- a/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java
+++ b/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java
@@ -68,7 +68,8 @@
Bundle savedInstanceState) {
if (parent instanceof GlifPreferenceLayout) {
final GlifPreferenceLayout layout = (GlifPreferenceLayout) parent;
- return layout.onCreateRecyclerView(inflater, parent, savedInstanceState);
+ return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(
+ layout.onCreateRecyclerView(inflater, parent, savedInstanceState));
}
return super.onCreateRecyclerView(inflater, parent, savedInstanceState);
}
diff --git a/src/com/android/settings/gestures/OneHandedSettings.java b/src/com/android/settings/gestures/OneHandedSettings.java
index c84b9ea..0a1ab64 100644
--- a/src/com/android/settings/gestures/OneHandedSettings.java
+++ b/src/com/android/settings/gestures/OneHandedSettings.java
@@ -23,9 +23,14 @@
import android.os.Bundle;
import android.os.UserHandle;
import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.RecyclerView;
import com.android.internal.accessibility.AccessibilityShortcutController;
import com.android.settings.R;
+import com.android.settings.accessibility.AccessibilityFragmentUtils;
import com.android.settings.accessibility.AccessibilityShortcutPreferenceFragment;
import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType;
import com.android.settings.search.BaseSearchIndexProvider;
@@ -176,4 +181,12 @@
return OneHandedSettingsUtils.isSupportOneHandedMode();
}
};
+
+ @Override
+ public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent,
+ Bundle savedInstanceState) {
+ RecyclerView recyclerView =
+ super.onCreateRecyclerView(inflater, parent, savedInstanceState);
+ return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(recyclerView);
+ }
}
diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilityFragmentUtilsTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilityFragmentUtilsTest.java
new file mode 100644
index 0000000..cd4ee89
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilityFragmentUtilsTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.accessibility;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.preference.Preference;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settingslib.widget.IllustrationPreference;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link AccessibilityFragmentUtils} */
+@RunWith(RobolectricTestRunner.class)
+public class AccessibilityFragmentUtilsTest {
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+
+ @Test
+ public void isPreferenceImportantToA11y_basicPreference_isImportant() {
+ final Preference pref = new ShortcutPreference(mContext, /* attrs= */ null);
+
+ assertThat(AccessibilityFragmentUtils.isPreferenceImportantToA11y(pref)).isTrue();
+ }
+
+ @Test
+ public void isPreferenceImportantToA11y_illustrationPreference_hasContentDesc_isImportant() {
+ final IllustrationPreference pref =
+ new IllustrationPreference(mContext, /* attrs= */ null);
+ pref.setContentDescription("content desc");
+
+ assertThat(AccessibilityFragmentUtils.isPreferenceImportantToA11y(pref)).isTrue();
+ }
+
+ @Test
+ public void isPreferenceImportantToA11y_illustrationPreference_noContentDesc_notImportant() {
+ final IllustrationPreference pref =
+ new IllustrationPreference(mContext, /* attrs= */ null);
+ pref.setContentDescription(null);
+
+ assertThat(AccessibilityFragmentUtils.isPreferenceImportantToA11y(pref)).isFalse();
+ }
+
+ @Test
+ public void isPreferenceImportantToA11y_paletteListPreference_notImportant() {
+ final PaletteListPreference pref =
+ new PaletteListPreference(mContext, /* attrs= */ null);
+
+ assertThat(AccessibilityFragmentUtils.isPreferenceImportantToA11y(pref)).isFalse();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java
index 22bb266..038672f 100644
--- a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java
@@ -53,9 +53,12 @@
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.preference.Preference;
import androidx.preference.TwoStatePreference;
+import androidx.recyclerview.widget.RecyclerView;
import androidx.test.core.app.ApplicationProvider;
import com.android.server.accessibility.Flags;
@@ -1000,6 +1003,28 @@
assertThat(summary).isEqualTo(expected);
}
+ @Test
+ @EnableFlags(
+ com.android.settings.accessibility.Flags.FLAG_TOGGLE_FEATURE_FRAGMENT_COLLECTION_INFO)
+ public void fragmentRecyclerView_getCollectionInfo_hasCorrectCounts() {
+ ToggleScreenMagnificationPreferenceFragment fragment =
+ mFragController.create(R.id.main_content, /* bundle= */
+ null).start().resume().get();
+ RecyclerView rv = fragment.getListView();
+
+ AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain();
+ rv.getCompatAccessibilityDelegate().onInitializeAccessibilityNodeInfo(rv, node);
+ AccessibilityNodeInfo.CollectionInfo collectionInfo = node.unwrap().getCollectionInfo();
+
+ // Asserting against specific item counts will be brittle to changes to the preferences
+ // included on this page, so instead just check some properties of these counts.
+ assertThat(collectionInfo.getColumnCount()).isEqualTo(1);
+ assertThat(collectionInfo.getRowCount()).isEqualTo(collectionInfo.getItemCount());
+ assertThat(collectionInfo.getItemCount())
+ // One unimportant item: the illustration preference
+ .isEqualTo(collectionInfo.getImportantForAccessibilityItemCount() + 1);
+ }
+
private void putStringIntoSettings(String key, String componentName) {
Settings.Secure.putString(mContext.getContentResolver(), key, componentName);
}