Merge "Populates collection info count for A11y toggle feature pages." into main

This commit is contained in:
Treehugger Robot
2024-09-05 21:27:35 +00:00
committed by Android (Google) Code Review
9 changed files with 288 additions and 6 deletions

View File

@@ -37,6 +37,13 @@ flag {
bug: "300302098"
}
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"
@@ -89,8 +96,11 @@ flag {
}
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
}
}

View File

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

View File

@@ -56,6 +56,7 @@ import androidx.annotation.VisibleForTesting;
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 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
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);
}
}

View File

@@ -79,7 +79,8 @@ public class ToggleScreenMagnificationPreferenceFragmentForSetupWizard
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);
}

View File

@@ -68,7 +68,8 @@ public class ToggleScreenReaderPreferenceFragmentForSetupWizard
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);
}

View File

@@ -68,7 +68,8 @@ public class ToggleSelectToSpeakPreferenceFragmentForSetupWizard
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);
}

View File

@@ -23,9 +23,14 @@ import android.content.Context;
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 @@ public class OneHandedSettings extends AccessibilityShortcutPreferenceFragment {
return OneHandedSettingsUtils.isSupportOneHandedMode();
}
};
@Override
public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
RecyclerView recyclerView =
super.onCreateRecyclerView(inflater, parent, savedInstanceState);
return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(recyclerView);
}
}

View File

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

View File

@@ -53,9 +53,12 @@ import android.platform.test.flag.junit.SetFlagsRule;
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 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
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);
}