Merge "Populates collection info count for A11y toggle feature pages." into main
This commit is contained in:
committed by
Android (Google) Code Review
commit
dcc2530964
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user