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