1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.accessibility; 18 19 import android.os.Bundle; 20 import android.text.TextUtils; 21 import android.view.View; 22 import android.view.accessibility.AccessibilityEvent; 23 import android.view.accessibility.AccessibilityNodeInfo; 24 25 import androidx.annotation.NonNull; 26 import androidx.annotation.VisibleForTesting; 27 import androidx.core.view.AccessibilityDelegateCompat; 28 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 29 import androidx.preference.Preference; 30 import androidx.preference.PreferenceGroupAdapter; 31 import androidx.recyclerview.widget.RecyclerView; 32 import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; 33 34 import com.android.settingslib.widget.IllustrationPreference; 35 36 /** Utilities for {@code Settings > Accessibility} fragments. */ 37 public class AccessibilityFragmentUtils { 38 // TODO: b/350782252 - Replace with an official library-provided solution when available. 39 /** 40 * Modifies the existing {@link RecyclerViewAccessibilityDelegate} of the provided 41 * {@link RecyclerView} for this fragment to report the number of visible and important 42 * items on this page via the RecyclerView's {@link AccessibilityNodeInfo}. 43 * 44 * <p><strong>Note:</strong> This is special-cased to the structure of these fragments: 45 * one column, N rows (one per preference, including category titles and header+footer 46 * preferences), <=N 'important' rows (image prefs without content descriptions). This 47 * is not intended for use with generic {@link RecyclerView}s. 48 */ addCollectionInfoToAccessibilityDelegate(RecyclerView recyclerView)49 public static RecyclerView addCollectionInfoToAccessibilityDelegate(RecyclerView recyclerView) { 50 if (!Flags.toggleFeatureFragmentCollectionInfo()) { 51 return recyclerView; 52 } 53 final RecyclerViewAccessibilityDelegate delegate = 54 recyclerView.getCompatAccessibilityDelegate(); 55 if (delegate == null) { 56 // No delegate, so do nothing. This should not occur for real RecyclerViews. 57 return recyclerView; 58 } 59 recyclerView.setAccessibilityDelegateCompat( 60 new RvAccessibilityDelegateWrapper(recyclerView, delegate) { 61 @Override 62 public void onInitializeAccessibilityNodeInfo(@NonNull View host, 63 @NonNull AccessibilityNodeInfoCompat info) { 64 super.onInitializeAccessibilityNodeInfo(host, info); 65 if (!(recyclerView.getAdapter() 66 instanceof final PreferenceGroupAdapter preferenceGroupAdapter)) { 67 return; 68 } 69 final int visibleCount = preferenceGroupAdapter.getItemCount(); 70 int importantCount = 0; 71 for (int i = 0; i < visibleCount; i++) { 72 if (isPreferenceImportantToA11y(preferenceGroupAdapter.getItem(i))) { 73 importantCount++; 74 } 75 } 76 info.unwrap().setCollectionInfo( 77 new AccessibilityNodeInfo.CollectionInfo( 78 /*rowCount=*/visibleCount, 79 /*columnCount=*/1, 80 /*hierarchical=*/false, 81 AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_SINGLE, 82 /*itemCount=*/visibleCount, 83 /*importantForAccessibilityItemCount=*/importantCount)); 84 } 85 }); 86 return recyclerView; 87 } 88 89 /** 90 * Returns whether the preference will be marked as important to accessibility for the sake 91 * of calculating {@link AccessibilityNodeInfo.CollectionInfo} counts. 92 * 93 * <p>The accessibility service itself knows this information for an individual preference 94 * on the screen, but it expects the preference's {@link RecyclerView} to also provide the 95 * same information for its entire set of adapter items. 96 */ 97 @VisibleForTesting isPreferenceImportantToA11y(Preference pref)98 static boolean isPreferenceImportantToA11y(Preference pref) { 99 if ((pref instanceof IllustrationPreference illustrationPref 100 && TextUtils.isEmpty(illustrationPref.getContentDescription())) 101 || pref instanceof PaletteListPreference) { 102 // Illustration preference that is visible but unannounced by accessibility services. 103 return false; 104 } 105 // All other preferences from the PreferenceGroupAdapter are important. 106 return true; 107 } 108 109 /** 110 * Wrapper around a {@link RecyclerViewAccessibilityDelegate} that allows customizing 111 * a subset of methods and while also deferring to the original. All overridden methods 112 * in instantiations of this class should call {@code super}. 113 */ 114 private static class RvAccessibilityDelegateWrapper extends RecyclerViewAccessibilityDelegate { 115 private final RecyclerViewAccessibilityDelegate mOriginal; 116 RvAccessibilityDelegateWrapper(RecyclerView recyclerView, RecyclerViewAccessibilityDelegate original)117 RvAccessibilityDelegateWrapper(RecyclerView recyclerView, 118 RecyclerViewAccessibilityDelegate original) { 119 super(recyclerView); 120 mOriginal = original; 121 } 122 123 @Override performAccessibilityAction(@onNull View host, int action, Bundle args)124 public boolean performAccessibilityAction(@NonNull View host, int action, Bundle args) { 125 return mOriginal.performAccessibilityAction(host, action, args); 126 } 127 128 @Override onInitializeAccessibilityNodeInfo(@onNull View host, @NonNull AccessibilityNodeInfoCompat info)129 public void onInitializeAccessibilityNodeInfo(@NonNull View host, 130 @NonNull AccessibilityNodeInfoCompat info) { 131 mOriginal.onInitializeAccessibilityNodeInfo(host, info); 132 } 133 134 @Override onInitializeAccessibilityEvent(@onNull View host, @NonNull AccessibilityEvent event)135 public void onInitializeAccessibilityEvent(@NonNull View host, 136 @NonNull AccessibilityEvent event) { 137 mOriginal.onInitializeAccessibilityEvent(host, event); 138 } 139 140 @Override 141 @NonNull getItemDelegate()142 public AccessibilityDelegateCompat getItemDelegate() { 143 if (mOriginal == null) { 144 // Needed for super constructor which calls getItemDelegate before mOriginal is 145 // defined, but unused by actual clients of this RecyclerViewAccessibilityDelegate 146 // which invoke getItemDelegate() after the constructor finishes. 147 return new ItemDelegate(this); 148 } 149 return mOriginal.getItemDelegate(); 150 } 151 } 152 } 153