• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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