• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.tools.idea.validator;
18 
19 import com.android.tools.idea.validator.ValidatorData.CompoundFix;
20 import com.android.tools.idea.validator.ValidatorData.Issue;
21 import com.android.tools.idea.validator.ValidatorData.Issue.IssueBuilder;
22 import com.android.tools.idea.validator.ValidatorData.Level;
23 import com.android.tools.idea.validator.ValidatorData.RemoveViewAttributeFix;
24 import com.android.tools.idea.validator.ValidatorData.SetViewAttributeFix;
25 import com.android.tools.idea.validator.ValidatorData.Type;
26 import com.android.tools.idea.validator.ValidatorResult.Builder;
27 import com.android.tools.idea.validator.hierarchy.CustomHierarchyHelper;
28 import com.android.tools.layoutlib.annotations.NotNull;
29 import com.android.tools.layoutlib.annotations.Nullable;
30 
31 import android.view.View;
32 
33 import java.awt.image.BufferedImage;
34 import java.io.PrintWriter;
35 import java.io.StringWriter;
36 import java.util.ArrayList;
37 import java.util.EnumSet;
38 import java.util.HashSet;
39 import java.util.List;
40 import java.util.Locale;
41 import java.util.ResourceBundle;
42 import java.util.Set;
43 import java.util.stream.Collectors;
44 
45 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheck;
46 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset;
47 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
48 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck;
49 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult;
50 import com.google.android.apps.common.testing.accessibility.framework.Parameters;
51 import com.google.android.apps.common.testing.accessibility.framework.checks.EditableContentDescCheck;
52 import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck;
53 import com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck;
54 import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck;
55 import com.google.android.apps.common.testing.accessibility.framework.strings.StringManager;
56 import com.google.android.apps.common.testing.accessibility.framework.suggestions.CompoundFixSuggestions;
57 import com.google.android.apps.common.testing.accessibility.framework.suggestions.FixSuggestion;
58 import com.google.android.apps.common.testing.accessibility.framework.suggestions.FixSuggestionPreset;
59 import com.google.android.apps.common.testing.accessibility.framework.suggestions.RemoveViewAttributeFixSuggestion;
60 import com.google.android.apps.common.testing.accessibility.framework.suggestions.SetViewAttributeFixSuggestion;
61 import com.google.android.apps.common.testing.accessibility.framework.suggestions.ViewAttribute;
62 import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy;
63 import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchyAndroid;
64 import com.google.android.apps.common.testing.accessibility.framework.uielement.CustomViewBuilderAndroid;
65 import com.google.android.apps.common.testing.accessibility.framework.uielement.DefaultCustomViewBuilderAndroid;
66 import com.google.android.apps.common.testing.accessibility.framework.uielement.ViewHierarchyElementAndroid;
67 import com.google.common.collect.ImmutableList;
68 import com.google.common.collect.ImmutableSet;
69 
70 public class ValidatorUtil {
71 
72     static {
73         /**
74          * Overriding default ResourceBundle ATF uses. ATF would use generic Java resources
75          * instead of Android's .xml.
76          *
77          * By default ATF generates ResourceBundle to support Android specific env/ classloader,
78          * which is quite different from Layoutlib, which supports multiple classloader depending
79          * on env (testing vs in studio).
80          *
81          * To support ATF in Layoutlib, easiest way is to convert resources from Android xml to
82          * generic Java resources (strings.properties), and have the default ResourceBundle ATF
83          * uses be redirected.
84          */
85         StringManager.setResourceBundleProvider(locale -> ResourceBundle.getBundle("strings"));
86         // Enable using AccessibilityNodeInfo in addition to View for accessibility testing
87         AccessibilityHierarchyAndroid.viewOverlayEnabled = true;
88     }
89 
90     // Visible for testing.
91     protected static DefaultCustomViewBuilderAndroid sDefaultCustomViewBuilderAndroid =
92             new DefaultCustomViewBuilderAndroid();
93 
94     /**
95      * Fixes could be only provided for a {@link AccessibilityHierarchyCheckResult}s generated by
96      * a predefined set of {@link AccessibilityHierarchyCheck}s.
97      */
98     private final static ImmutableSet<Class<? extends AccessibilityHierarchyCheck>>
99             sAllowedCheckResultClassSet4Fix = ImmutableSet.of(SpeakableTextPresentCheck.class,
100             TextContrastCheck.class, TouchTargetSizeCheck.class,  EditableContentDescCheck.class);
101 
102     /**
103      * The maximum allowed length of the requested text location data is used to avoid the
104      * performance issue caused by obtaining character location data for a view with a long text.
105      */
106     public static final int CHARACTER_LOCATION_ARG_MAX_LENGTH = 100;
107 
108     /**
109      * @param policy policy to apply for the hierarchy
110      * @param view root view to build hierarchy from
111      * @param image screenshot image that matches the view
112      * @param scaleX scaling done via layoutlib in x coord
113      * @param scaleY scaling done via layoutlib in y coord
114      * @return The hierarchical data required for running the ATF checks.
115      */
buildHierarchy( @otNull ValidatorData.Policy policy, @NotNull View view, @Nullable BufferedImage image, float scaleX, float scaleY)116     public static ValidatorHierarchy buildHierarchy(
117             @NotNull ValidatorData.Policy policy,
118             @NotNull View view,
119             @Nullable BufferedImage image,
120             float scaleX,
121             float scaleY) {
122         ValidatorHierarchy hierarchy = new ValidatorHierarchy();
123         if (!policy.mTypes.contains(Type.ACCESSIBILITY)) {
124             return hierarchy;
125         }
126 
127         ValidatorResult.Builder builder = new ValidatorResult.Builder();
128         @Nullable Parameters parameters = null;
129         builder.mMetric.startHierarchyCreationTimer();
130         try {
131             hierarchy.mView = AccessibilityHierarchyAndroid
132                     .newBuilder(view)
133                     .setViewOriginMap(builder.mSrcMap)
134                     .setObtainCharacterLocations(LayoutValidator.obtainCharacterLocations())
135                     .setCharacterLocationArgMaxLength(CHARACTER_LOCATION_ARG_MAX_LENGTH)
136                     .setCustomViewBuilder(new CustomViewBuilderAndroid() {
137                         @Override
138                         public Class<?> getClassByName(
139                                 ViewHierarchyElementAndroid viewHierarchyElementAndroid,
140                                 String className) {
141                             Class<?> toReturn = sDefaultCustomViewBuilderAndroid.getClassByName(
142                                     viewHierarchyElementAndroid, className);
143                             if (toReturn == null) {
144                                 toReturn = CustomHierarchyHelper.getClassByName(className);
145                             }
146                             return toReturn;
147                         }
148 
149                         @Override
150                         public boolean isCheckable(View view) {
151                             return CustomHierarchyHelper.isCheckable(view);
152                         }
153                     }).build();
154             if (image != null) {
155                 parameters = new Parameters();
156                 parameters.putScreenCapture(
157                         new AtfBufferedImage(image, builder.mMetric, scaleX, scaleY));
158             }
159         } finally {
160             builder.mMetric.recordHierarchyCreationTime();
161         }
162 
163         hierarchy.mBuilder = builder;
164         hierarchy.mParameters = parameters;
165         return hierarchy;
166     }
167 
168     /**
169      * @param hierarchy to build result from. If {@link ValidatorHierarchy#isHierarchyBuilt()}
170      * is false, returns a result with an internal error.
171      * @return Returns ValidatorResult with given hierarchical data.
172      */
generateResults( @otNull ValidatorData.Policy policy, @NotNull ValidatorHierarchy hierarchy)173     public static ValidatorResult generateResults(
174             @NotNull ValidatorData.Policy policy,
175             @NotNull ValidatorHierarchy hierarchy) {
176         ValidatorResult.Builder builder = hierarchy.mBuilder;
177         try {
178             if (!hierarchy.isHierarchyBuilt()) {
179                 // Unable to build.
180                 builder = new Builder();
181                 String errorMsg = hierarchy.mErrorMessage != null ? hierarchy.mErrorMessage :
182                         "Hierarchy is not built yet.";
183                 builder.mIssues.add(new IssueBuilder()
184                         .setCategory("Accessibility")
185                         .setType(Type.INTERNAL_ERROR)
186                         .setMsg(errorMsg)
187                         .setLevel(Level.ERROR)
188                         .setSourceClass("ValidatorHierarchy")
189                         .build());
190                 return builder.build();
191             }
192             builder.mMetric.startGenerateResultsTimer();
193 
194             AccessibilityHierarchyAndroid view = hierarchy.mView;
195             Parameters parameters = hierarchy.mParameters;
196 
197             EnumSet<Level> filter = policy.mLevels;
198             ArrayList<AccessibilityHierarchyCheckResult> a11yResults = new ArrayList<>();
199 
200             HashSet<AccessibilityHierarchyCheck> policyChecks = policy.mChecks;
201             @NotNull Set<AccessibilityHierarchyCheck> checks = policyChecks.isEmpty() ?
202                     AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset(
203                             AccessibilityCheckPreset.LATEST) : policyChecks;
204 
205             for (AccessibilityHierarchyCheck check : checks) {
206                 a11yResults.addAll(check.runCheckOnHierarchy(view, null, parameters));
207             }
208 
209             for (AccessibilityHierarchyCheckResult result : a11yResults) {
210                 // TODO: b/183726816 replace this with
211                 // AccessibilityCheckPreset.getHierarchyCheckForClassName(checkClassName)
212                 // .getTitleMessage(Locale.ENGLISH)
213                 String category = ValidatorUtil.getCheckClassCategory(result.getSourceCheckClass());
214 
215                 ValidatorData.Level level = ValidatorUtil.convertLevel(result.getType());
216                 if (!filter.contains(level)) {
217                     continue;
218                 }
219 
220                 try {
221                     IssueBuilder issueBuilder = new IssueBuilder().setCategory(category).setMsg(
222                             result.getMessage(Locale.ENGLISH).toString()).setLevel(level).setFix(
223                             ValidatorUtil.generateFix(result, view, parameters)).setSourceClass(
224                             result.getSourceCheckClass().getSimpleName());
225                     if (result.getElement() != null) {
226                         issueBuilder.setSrcId(result.getElement().getCondensedUniqueId());
227                     }
228                     AccessibilityHierarchyCheck subclass =
229                             AccessibilityCheckPreset.getHierarchyCheckForClass(
230                                     result.getSourceCheckClass().asSubclass(
231                                             AccessibilityHierarchyCheck.class));
232                     if (subclass != null) {
233                         issueBuilder.setHelpfulUrl(subclass.getHelpUrl());
234                     }
235                     builder.mIssues.add(issueBuilder.build());
236                 } catch (Exception e) {
237                     StringWriter sw = new StringWriter();
238                     PrintWriter pw = new PrintWriter(sw);
239                     e.printStackTrace(pw);
240                     builder.mIssues.add(new IssueBuilder()
241                             .setCategory(category)
242                             .setType(Type.INTERNAL_ERROR)
243                             .setMsg(sw.toString())
244                             .setLevel(Level.ERROR)
245                             .setSourceClass("ValidatorHierarchy").build());
246                 }
247             }
248         } finally {
249             builder.mMetric.recordGenerateResultsTime();
250         }
251         return builder.build();
252     }
253 
254     /**
255      * @return the list of internal errors in results. Useful for testing and debugging.
256      */
filterInternalErrors(List<ValidatorData.Issue> results)257     public static List<Issue> filterInternalErrors(List<ValidatorData.Issue> results) {
258         return filterByTypes(results, EnumSet.of(Type.INTERNAL_ERROR));
259     }
260 
261     /**
262      * @return the list filtered by the level. Useful for testing and debugging.
263      */
filter(List<ValidatorData.Issue> results, EnumSet<Level> errors)264     public static List<Issue> filter(List<ValidatorData.Issue> results, EnumSet<Level> errors) {
265         return results.stream().filter(
266                 issue -> errors.contains(issue.mLevel)).collect(Collectors.toList());
267     }
268 
269     /**
270      * @return the list filtered by the source class name. Useful for testing and debugging.
271      */
filter( List<ValidatorData.Issue> results, String sourceClass)272     public static List<Issue> filter(
273             List<ValidatorData.Issue> results, String sourceClass) {
274         return results.stream().filter(
275                 issue -> sourceClass.equals(issue.mSourceClass)).collect(Collectors.toList());
276     }
277 
278     /**
279      * @return the list filtered by the source class name. Useful for testing and debugging.
280      */
filterByTypes( List<ValidatorData.Issue> results, EnumSet<Type> types)281     public static List<Issue> filterByTypes(
282             List<ValidatorData.Issue> results, EnumSet<Type> types) {
283         return results.stream().filter(
284                 issue -> types.contains(issue.mType)).collect(Collectors.toList());
285     }
286 
287     /**
288      * @param checkClass classes expected to extend AccessibilityHierarchyCheck
289      * @return {@link AccessibilityCheck.Category} of the class.
290      */
291     @NotNull
getCheckClassCategory(@otNull Class<?> checkClass)292     private static String getCheckClassCategory(@NotNull Class<?> checkClass) {
293         try {
294             Class<? extends AccessibilityHierarchyCheck> subClass =
295                     checkClass.asSubclass(AccessibilityHierarchyCheck.class);
296             AccessibilityHierarchyCheck check =
297                     AccessibilityCheckPreset.getHierarchyCheckForClass(subClass);
298             return (check == null) ? "Accessibility" : check.getCategory().name();
299         } catch (ClassCastException e) {
300             return "Accessibility";
301         }
302     }
303 
304     /** Convert {@link AccessibilityCheckResultType} to {@link ValidatorData.Level} */
305     @NotNull
convertLevel(@otNull AccessibilityCheckResultType type)306     private static ValidatorData.Level convertLevel(@NotNull AccessibilityCheckResultType type) {
307         switch (type) {
308             case ERROR:
309                 return Level.ERROR;
310             case WARNING:
311                 return Level.WARNING;
312             case INFO:
313                 return Level.INFO;
314             // TODO: Maybe useful later?
315             case SUPPRESSED:
316             case NOT_RUN:
317             default:
318                 return Level.VERBOSE;
319         }
320     }
321 
322     /**
323      * Create a {@link ValidatorData.Fix} for the given result, or {@code null} if there is no
324      * fixes available.
325      *
326      * <p>If there are multiple fixes available, return the first fix which is considered to be the
327      * best fix available.
328      *
329      * @param result to generate a fix from.
330      * @param hierarchy The hierarchy from which the result is generated from.
331      * @param parameters Optional input data or preferences.
332      */
333     @Nullable
generateFix( @otNull AccessibilityHierarchyCheckResult result, @NotNull AccessibilityHierarchy hierarchy, @Nullable Parameters parameters)334     private static ValidatorData.Fix generateFix(
335             @NotNull AccessibilityHierarchyCheckResult result,
336             @NotNull AccessibilityHierarchy hierarchy,
337             @Nullable Parameters parameters) {
338         if (sAllowedCheckResultClassSet4Fix.contains(result.getSourceCheckClass())) {
339             ImmutableList<FixSuggestion> fixSuggestions =
340                     FixSuggestionPreset.provideFixSuggestions(result, hierarchy, parameters);
341             return fixSuggestions.isEmpty() ? null : convertFix(fixSuggestions.get(0));
342         }
343         return null;
344     }
345 
346     /** Convert {@link FixSuggestion} to {@link ValidatorData.Fix} */
347     @Nullable
convertFix(@otNull FixSuggestion fixSuggestion)348     private static ValidatorData.Fix convertFix(@NotNull FixSuggestion fixSuggestion) {
349         if (fixSuggestion instanceof CompoundFixSuggestions) {
350             CompoundFixSuggestions compoundFixSuggestions = (CompoundFixSuggestions)fixSuggestion;
351             List<ValidatorData.Fix> fixes =
352                     compoundFixSuggestions
353                             .getFixSuggestions()
354                             .stream()
355                             .map(ValidatorUtil::convertFix)
356                             .collect(Collectors.toList());
357             return new CompoundFix(
358                     fixes,
359                     compoundFixSuggestions.getDescription(Locale.ENGLISH));
360         } else if (fixSuggestion instanceof RemoveViewAttributeFixSuggestion) {
361             RemoveViewAttributeFixSuggestion removeViewAttributeFix =
362                     (RemoveViewAttributeFixSuggestion)fixSuggestion;
363             return new RemoveViewAttributeFix(
364                     convertViewAttribute(removeViewAttributeFix.getViewAttribute()),
365                     removeViewAttributeFix.getDescription(Locale.ENGLISH));
366         } else if (fixSuggestion instanceof SetViewAttributeFixSuggestion) {
367             SetViewAttributeFixSuggestion setViewAttributeFixSuggestion =
368                     (SetViewAttributeFixSuggestion)fixSuggestion;
369             return new SetViewAttributeFix(
370                     convertViewAttribute(setViewAttributeFixSuggestion.getViewAttribute()),
371                     setViewAttributeFixSuggestion.getSuggestedValue(),
372                     setViewAttributeFixSuggestion.getDescription(Locale.ENGLISH));
373         }
374         return null;
375     }
376 
377     /** Convert {@link ViewAttribute} to {@link ValidatorData.ViewAttribute} */
378     @NotNull
convertViewAttribute( @otNull ViewAttribute viewAttribute)379     private static ValidatorData.ViewAttribute convertViewAttribute(
380             @NotNull ViewAttribute viewAttribute) {
381         return new ValidatorData.ViewAttribute(
382                 viewAttribute.getNamespaceUri(),
383                 viewAttribute.getNamespace(),
384                 viewAttribute.getAttributeName());
385     }
386 }
387