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.accessibility;
18 
19 import com.android.tools.idea.validator.ValidatorData;
20 import com.android.tools.idea.validator.ValidatorData.Fix;
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.Type;
24 import com.android.tools.idea.validator.ValidatorResult;
25 import com.android.tools.idea.validator.ValidatorResult.Metric;
26 import com.android.tools.layoutlib.annotations.NotNull;
27 import com.android.tools.layoutlib.annotations.Nullable;
28 
29 import android.view.View;
30 
31 import java.awt.image.BufferedImage;
32 import java.util.ArrayList;
33 import java.util.EnumSet;
34 import java.util.HashSet;
35 import java.util.List;
36 import java.util.Locale;
37 import java.util.ResourceBundle;
38 import java.util.Set;
39 
40 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset;
41 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
42 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck;
43 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult;
44 import com.google.android.apps.common.testing.accessibility.framework.Parameters;
45 import com.google.android.apps.common.testing.accessibility.framework.strings.StringManager;
46 import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchyAndroid;
47 import com.google.common.collect.BiMap;
48 
49 /**
50  * Validator specific for running Accessibility specific issues.
51  */
52 public class AccessibilityValidator {
53 
54     static {
55         /**
56          * Overriding default ResourceBundle ATF uses. ATF would use generic Java resources
57          * instead of Android's .xml.
58          *
59          * By default ATF generates ResourceBundle to support Android specific env/ classloader,
60          * which is quite different from Layoutlib, which supports multiple classloader depending
61          * on env (testing vs in studio).
62          *
63          * To support ATF in Layoutlib, easiest way is to convert resources from Android xml to
64          * generic Java resources (strings.properties), and have the default ResourceBundle ATF
65          * uses be redirected.
66          */
67         StringManager.setResourceBundleProvider(locale -> ResourceBundle.getBundle("strings"));
68     }
69 
70     /**
71      * Run Accessibility specific validation test and receive results.
72      * @param view the root view
73      * @param image the output image of the view. Null if not available.
74      * @param policy e.g: list of levels to allow
75      * @return results with all the accessibility issues and warnings.
76      */
77     @NotNull
validateAccessibility( @otNull View view, @Nullable BufferedImage image, @NotNull ValidatorData.Policy policy)78     public static ValidatorResult validateAccessibility(
79             @NotNull View view,
80             @Nullable BufferedImage image,
81             @NotNull ValidatorData.Policy policy) {
82 
83         EnumSet<Level> filter = policy.mLevels;
84         ValidatorResult.Builder builder = new ValidatorResult.Builder();
85         builder.mMetric.startTimer();
86         if (!policy.mTypes.contains(Type.ACCESSIBILITY)) {
87             return builder.build();
88         }
89 
90         List<AccessibilityHierarchyCheckResult> results = getHierarchyCheckResults(
91                 builder.mMetric,
92                 view,
93                 builder.mSrcMap,
94                 image,
95                 policy.mChecks);
96 
97         for (AccessibilityHierarchyCheckResult result : results) {
98             String category = getCheckClassCategory(result.getSourceCheckClass());
99 
100             ValidatorData.Level level = convertLevel(result.getType());
101             if (!filter.contains(level)) {
102                 continue;
103             }
104 
105             try {
106                 IssueBuilder issueBuilder = new IssueBuilder()
107                         .setCategory(category)
108                         .setMsg(result.getMessage(Locale.ENGLISH).toString())
109                         .setLevel(level)
110                         .setFix(generateFix(result))
111                         .setSourceClass(result.getSourceCheckClass().getSimpleName());
112                 if (result.getElement() != null) {
113                     issueBuilder.setSrcId(result.getElement().getCondensedUniqueId());
114                 }
115                 AccessibilityHierarchyCheck subclass = AccessibilityCheckPreset
116                         .getHierarchyCheckForClass(result
117                                 .getSourceCheckClass()
118                                 .asSubclass(AccessibilityHierarchyCheck.class));
119                 if (subclass != null) {
120                     issueBuilder.setHelpfulUrl(subclass.getHelpUrl());
121                 }
122                 builder.mIssues.add(issueBuilder.build());
123             } catch (Exception e) {
124                 builder.mIssues.add(new IssueBuilder()
125                         .setCategory(category)
126                         .setType(Type.INTERNAL_ERROR)
127                         .setMsg(e.getMessage())
128                         .setLevel(Level.ERROR)
129                         .setSourceClass("AccessibilityValidator").build());
130             }
131         }
132         builder.mMetric.endTimer();
133         return builder.build();
134     }
135 
136     @NotNull
getCheckClassCategory(@otNull Class<?> checkClass)137     private static String getCheckClassCategory(@NotNull Class<?> checkClass) {
138         try {
139             Class<? extends AccessibilityHierarchyCheck> subClass =
140                     checkClass.asSubclass(AccessibilityHierarchyCheck.class);
141             AccessibilityHierarchyCheck check =
142                     AccessibilityCheckPreset.getHierarchyCheckForClass(subClass);
143             return (check == null) ? "Accessibility" : check.getCategory().name();
144         } catch (ClassCastException e) {
145             return "Accessibility";
146         }
147     }
148 
149     @NotNull
convertLevel(@otNull AccessibilityCheckResultType type)150     private static ValidatorData.Level convertLevel(@NotNull AccessibilityCheckResultType type) {
151         switch (type) {
152             case ERROR:
153                 return Level.ERROR;
154             case WARNING:
155                 return Level.WARNING;
156             case INFO:
157                 return Level.INFO;
158             // TODO: Maybe useful later?
159             case SUPPRESSED:
160             case NOT_RUN:
161             default:
162                 return Level.VERBOSE;
163         }
164     }
165 
166     @Nullable
generateFix(@otNull AccessibilityHierarchyCheckResult result)167     private static ValidatorData.Fix generateFix(@NotNull AccessibilityHierarchyCheckResult result) {
168         // TODO: Once ATF is ready to return us with appropriate fix, build proper fix here.
169         return new Fix("");
170     }
171 
172     @NotNull
getHierarchyCheckResults( @otNull Metric metric, @NotNull View view, @NotNull BiMap<Long, View> originMap, @Nullable BufferedImage image, HashSet<AccessibilityHierarchyCheck> policyChecks)173     private static List<AccessibilityHierarchyCheckResult> getHierarchyCheckResults(
174             @NotNull Metric metric,
175             @NotNull View view,
176             @NotNull BiMap<Long, View> originMap,
177             @Nullable BufferedImage image,
178             HashSet<AccessibilityHierarchyCheck> policyChecks) {
179 
180         @NotNull Set<AccessibilityHierarchyCheck> checks = policyChecks.isEmpty()
181                 ? AccessibilityCheckPreset
182                         .getAccessibilityHierarchyChecksForPreset(AccessibilityCheckPreset.LATEST)
183                 : policyChecks;
184 
185         @NotNull AccessibilityHierarchyAndroid hierarchy = AccessibilityHierarchyAndroid
186                 .newBuilder(view)
187                 .setViewOriginMap(originMap)
188                 .build();
189         ArrayList<AccessibilityHierarchyCheckResult> a11yResults = new ArrayList();
190 
191         Parameters parameters = null;
192         if (image != null) {
193             parameters = new Parameters();
194             parameters.putScreenCapture(new AtfBufferedImage(image, metric));
195         }
196 
197         for (AccessibilityHierarchyCheck check : checks) {
198             a11yResults.addAll(check.runCheckOnHierarchy(hierarchy, null, parameters));
199         }
200 
201         return a11yResults;
202     }
203 }
204