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