diff options
-rw-r--r-- | Android.bp | 1 | ||||
-rw-r--r-- | bridge/tests/src/com/android/tools/idea/validator/LayoutValidatorTests.java | 128 | ||||
-rw-r--r-- | create/src/com/android/tools/layoutlib/create/Main.java | 2 | ||||
-rw-r--r-- | validator/Android.bp | 1 | ||||
-rw-r--r-- | validator/src/android/os/Build.java | 35 | ||||
-rw-r--r-- | validator/src/com/android/tools/idea/validator/LayoutValidator.java | 6 | ||||
-rw-r--r-- | validator/src/com/android/tools/idea/validator/ValidatorData.java | 95 | ||||
-rw-r--r-- | validator/src/com/android/tools/idea/validator/accessibility/AccessibilityValidator.java | 61 | ||||
-rw-r--r-- | validator/validator.iml | 9 |
9 files changed, 242 insertions, 96 deletions
diff --git a/Android.bp b/Android.bp index acdbffdbfa..b38baa2b8a 100644 --- a/Android.bp +++ b/Android.bp @@ -25,6 +25,7 @@ java_genrule_host { tools: ["layoutlib_create"], out: ["temp_layoutlib.jar"], srcs: [ + ":atf-prebuilt{.jar}", ":core-icu4j{.jar}", ":core-libart{.jar}", ":framework-all{.jar}", diff --git a/bridge/tests/src/com/android/tools/idea/validator/LayoutValidatorTests.java b/bridge/tests/src/com/android/tools/idea/validator/LayoutValidatorTests.java index 23e40ff094..ec91e17560 100644 --- a/bridge/tests/src/com/android/tools/idea/validator/LayoutValidatorTests.java +++ b/bridge/tests/src/com/android/tools/idea/validator/LayoutValidatorTests.java @@ -29,7 +29,15 @@ import org.junit.Test; import android.view.View; +import java.util.EnumSet; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset; +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck; + import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class LayoutValidatorTests extends RenderTestBase { @@ -52,19 +60,7 @@ public class LayoutValidatorTests extends RenderTestBase { @Test public void testValidation() throws Exception { - LayoutPullParser parser = createParserFromPath("a11y_test1.xml"); - LayoutLibTestCallback layoutLibCallback = - new LayoutLibTestCallback(getLogger(), mDefaultClassLoader); - layoutLibCallback.initResources(); - SessionParams params = getSessionParamsBuilder() - .setParser(parser) - .setConfigGenerator(ConfigGenerator.NEXUS_5) - .setCallback(layoutLibCallback) - .disableDecoration() - .enableLayoutValidation() - .build(); - - render(sBridge, params, -1, session -> { + render(sBridge, generateParams(), -1, session -> { ValidatorResult result = LayoutValidator .validate(((View) session.getRootViews().get(0).getViewObject()), null); assertEquals(3, result.getIssues().size()); @@ -73,16 +69,114 @@ public class LayoutValidatorTests extends RenderTestBase { assertEquals(Level.ERROR, issue.mLevel); } + Issue first = result.getIssues().get(0); assertEquals("This item may not have a label readable by screen readers.", - result.getIssues().get(0).mMsg); + first.mMsg); + assertEquals("https://support.google.com/accessibility/android/answer/7158690", + first.mHelpfulUrl); + assertEquals("SpeakableTextPresentCheck", first.mSourceClass); + + Issue second = result.getIssues().get(1); assertEquals("This item's size is 10dp x 10dp. Consider making this touch target " + "48dp wide and 48dp high or larger.", - result.getIssues().get(1).mMsg); + second.mMsg); + assertEquals("https://support.google.com/accessibility/android/answer/7101858", + second.mHelpfulUrl); + assertEquals("TouchTargetSizeCheck", second.mSourceClass); + + Issue third = result.getIssues().get(2); assertEquals("The item's text contrast ratio is 1.00. This ratio is based on a text color " + "of #000000 and background color of #000000. Consider increasing this item's" + " text contrast ratio to 4.50 or greater.", - result.getIssues().get(2).mMsg); - // TODO: It should recognize 10dp x 10dp button. Investigate why it's not. + third.mMsg); + assertEquals("https://support.google.com/accessibility/android/answer/7158390", + third.mHelpfulUrl); + assertEquals("TextContrastCheck", third.mSourceClass); }); } + + @Test + public void testValidationPolicyType() throws Exception { + try { + ValidatorData.Policy newPolicy = new ValidatorData.Policy( + EnumSet.of(Type.RENDER), + EnumSet.of(Level.ERROR, Level.WARNING)); + LayoutValidator.updatePolicy(newPolicy); + + render(sBridge, generateParams(), -1, session -> { + ValidatorResult result = LayoutValidator.validate( + ((View) session.getRootViews().get(0).getViewObject()), null); + assertTrue(result.getIssues().isEmpty()); + }); + } finally { + LayoutValidator.updatePolicy(LayoutValidator.DEFAULT_POLICY); + } + } + + @Test + public void testValidationPolicyLevel() throws Exception { + try { + ValidatorData.Policy newPolicy = new ValidatorData.Policy( + EnumSet.of(Type.ACCESSIBILITY, Type.RENDER), + EnumSet.of(Level.VERBOSE)); + LayoutValidator.updatePolicy(newPolicy); + + render(sBridge, generateParams(), -1, session -> { + ValidatorResult result = LayoutValidator.validate( + ((View) session.getRootViews().get(0).getViewObject()), null); + assertEquals(27, result.getIssues().size()); + result.getIssues().forEach(issue ->assertEquals(Level.VERBOSE, issue.mLevel)); + }); + } finally { + LayoutValidator.updatePolicy(LayoutValidator.DEFAULT_POLICY); + } + } + + @Test + public void testValidationPolicyChecks() throws Exception { + Set<AccessibilityHierarchyCheck> allChecks = + AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset( + AccessibilityCheckPreset.LATEST); + Set<AccessibilityHierarchyCheck> filtered =allChecks + .stream() + .filter(it -> it.getClass().getSimpleName().equals("TextContrastCheck")) + .collect(Collectors.toSet()); + try { + ValidatorData.Policy newPolicy = new ValidatorData.Policy( + EnumSet.of(Type.ACCESSIBILITY, Type.RENDER), + EnumSet.of(Level.ERROR)); + newPolicy.mChecks.addAll(filtered); + LayoutValidator.updatePolicy(newPolicy); + + render(sBridge, generateParams(), -1, session -> { + ValidatorResult result = LayoutValidator.validate( + ((View) session.getRootViews().get(0).getViewObject()), null); + assertEquals(1, result.getIssues().size()); + Issue textCheck = result.getIssues().get(0); + assertEquals("The item's text contrast ratio is 1.00. This ratio is based on a text color " + + "of #000000 and background color of #000000. Consider increasing this item's" + + " text contrast ratio to 4.50 or greater.", + textCheck.mMsg); + assertEquals("https://support.google.com/accessibility/android/answer/7158390", + textCheck.mHelpfulUrl); + assertEquals("TextContrastCheck", textCheck.mSourceClass); + }); + } finally { + LayoutValidator.updatePolicy(LayoutValidator.DEFAULT_POLICY); + } + } + + private SessionParams generateParams() throws Exception { + LayoutPullParser parser = createParserFromPath("a11y_test1.xml"); + LayoutLibTestCallback layoutLibCallback = + new LayoutLibTestCallback(getLogger(), mDefaultClassLoader); + layoutLibCallback.initResources(); + return getSessionParamsBuilder() + .setParser(parser) + .setConfigGenerator(ConfigGenerator.NEXUS_5) + .setCallback(layoutLibCallback) + .disableDecoration() + .enableLayoutValidation() + .build(); + } } diff --git a/create/src/com/android/tools/layoutlib/create/Main.java b/create/src/com/android/tools/layoutlib/create/Main.java index 4636a1c4c5..b56416e5aa 100644 --- a/create/src/com/android/tools/layoutlib/create/Main.java +++ b/create/src/com/android/tools/layoutlib/create/Main.java @@ -134,6 +134,8 @@ public class Main { "android.annotation.Nullable", // annotations "com.android.internal.transition.EpicenterTranslateClipReveal", "com.android.internal.graphics.drawable.AnimationScaleListDrawable", + "com.google.android.apps.common.testing.accessibility.**", + "com.google.android.libraries.accessibility.**", }, info.getExcludedClasses(), new String[] { diff --git a/validator/Android.bp b/validator/Android.bp index 0a93158511..0eff99b19e 100644 --- a/validator/Android.bp +++ b/validator/Android.bp @@ -28,7 +28,6 @@ java_library_host { ], static_libs: [ - "atf-prebuilt-jars", "hamcrest", "jsoup-1.6.3", "protobuf-lite", diff --git a/validator/src/android/os/Build.java b/validator/src/android/os/Build.java deleted file mode 100644 index 971f4667aa..0000000000 --- a/validator/src/android/os/Build.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.os; - -public class Build { - public static class VERSION { - public static int SDK_INT = _Original_Build.VERSION.SDK_INT; - } - - public static class VERSION_CODES { - public final static int Q = _Original_Build.VERSION_CODES.Q; - public final static int N = _Original_Build.VERSION_CODES.N; - public final static int LOLLIPOP_MR1 = _Original_Build.VERSION_CODES.LOLLIPOP_MR1; - public final static int LOLLIPOP = _Original_Build.VERSION_CODES.LOLLIPOP; - - public final static int JELLY_BEAN = _Original_Build.VERSION_CODES.JELLY_BEAN; - public final static int HONEYCOMB = _Original_Build.VERSION_CODES.HONEYCOMB; - public final static int JELLY_BEAN_MR2 = _Original_Build.VERSION_CODES.JELLY_BEAN_MR2; - public final static int JELLY_BEAN_MR1 = _Original_Build.VERSION_CODES.JELLY_BEAN_MR1; - } -} diff --git a/validator/src/com/android/tools/idea/validator/LayoutValidator.java b/validator/src/com/android/tools/idea/validator/LayoutValidator.java index 7ec6f47188..dc34e908ff 100644 --- a/validator/src/com/android/tools/idea/validator/LayoutValidator.java +++ b/validator/src/com/android/tools/idea/validator/LayoutValidator.java @@ -33,10 +33,12 @@ import java.util.EnumSet; */ public class LayoutValidator { - private static ValidatorData.Policy sPolicy = new Policy( + public static final ValidatorData.Policy DEFAULT_POLICY = new Policy( EnumSet.of(Type.ACCESSIBILITY, Type.RENDER), EnumSet.of(Level.ERROR, Level.WARNING)); + private static ValidatorData.Policy sPolicy = DEFAULT_POLICY; + /** * Validate the layout using the default policy. * Precondition: View must be attached to the window. @@ -46,7 +48,7 @@ public class LayoutValidator { @NotNull public static ValidatorResult validate(@NotNull View view, @Nullable BufferedImage image) { if (view.isAttachedToWindow()) { - return AccessibilityValidator.validateAccessibility(view, image, sPolicy.mLevels); + return AccessibilityValidator.validateAccessibility(view, image, sPolicy); } // TODO: Add non-a11y layout validation later. return new ValidatorResult.Builder().build(); diff --git a/validator/src/com/android/tools/idea/validator/ValidatorData.java b/validator/src/com/android/tools/idea/validator/ValidatorData.java index 06974720a6..6d9d6b6422 100644 --- a/validator/src/com/android/tools/idea/validator/ValidatorData.java +++ b/validator/src/com/android/tools/idea/validator/ValidatorData.java @@ -20,6 +20,9 @@ import com.android.tools.layoutlib.annotations.NotNull; import com.android.tools.layoutlib.annotations.Nullable; import java.util.EnumSet; +import java.util.HashSet; + +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck; /** * Data used for layout validation. @@ -32,6 +35,7 @@ public class ValidatorData { public enum Type { ACCESSIBILITY, RENDER, + INTERNAL_ERROR } /** @@ -49,8 +53,9 @@ public class ValidatorData { * Determine what types and levels of validation to run. */ public static class Policy { - @NotNull final EnumSet<Type> mTypes; - @NotNull final EnumSet<Level> mLevels; + @NotNull public final EnumSet<Type> mTypes; + @NotNull public final EnumSet<Level> mLevels; + @NotNull public final HashSet<AccessibilityHierarchyCheck> mChecks = new HashSet(); public Policy(@NotNull EnumSet<Type> types, @NotNull EnumSet<Level> levels) { mTypes = types; @@ -72,26 +77,90 @@ public class ValidatorData { /** * Issue describing the layout problem. */ - public static class Issue{ - @NotNull public final Type mType; - @NotNull public final String mMsg; - @NotNull public final Level mLevel; - @Nullable public final Long mSrcId; - @Nullable public final Fix mFix; - // Used for debugging. - @Nullable public String mSourceClass; - - public Issue( + public static class Issue { + @NotNull + public final Type mType; + @NotNull + public final String mMsg; + @NotNull + public final Level mLevel; + @Nullable + public final Long mSrcId; + @Nullable + public final Fix mFix; + @NotNull + public final String mSourceClass; + @Nullable + public final String mHelpfulUrl; + + private Issue( @NotNull Type type, @NotNull String msg, @NotNull Level level, @Nullable Long srcId, - @Nullable Fix fix) { + @Nullable Fix fix, + @NotNull String sourceClass, + @Nullable String helpfulUrl) { mType = type; mMsg = msg; mLevel = level; mSrcId = srcId; mFix = fix; + mSourceClass = sourceClass; + mHelpfulUrl = helpfulUrl; + } + + public static class IssueBuilder { + private Type mType = Type.ACCESSIBILITY; + private String mMsg; + private Level mLevel; + private Long mSrcId; + private Fix mFix; + private String mSourceClass; + private String mHelpfulUrl; + + public IssueBuilder setType(Type type) { + mType = type; + return this; + } + + public IssueBuilder setMsg(String msg) { + mMsg = msg; + return this; + } + + public IssueBuilder setLevel(Level level) { + mLevel = level; + return this; + } + + public IssueBuilder setSrcId(Long srcId) { + mSrcId = srcId; + return this; + } + + public IssueBuilder setFix(Fix fix) { + mFix = fix; + return this; + } + + public IssueBuilder setSourceClass(String sourceClass) { + mSourceClass = sourceClass; + return this; + } + + public IssueBuilder setHelpfulUrl(String url) { + mHelpfulUrl = url; + return this; + } + + public Issue build() { + assert(mType != null); + assert(mMsg != null); + assert(mLevel != null); + assert(mSourceClass != null); + return new Issue(mType, mMsg, mLevel, mSrcId, mFix, mSourceClass, mHelpfulUrl); + } } } } diff --git a/validator/src/com/android/tools/idea/validator/accessibility/AccessibilityValidator.java b/validator/src/com/android/tools/idea/validator/accessibility/AccessibilityValidator.java index e679750321..a2ec2c45a8 100644 --- a/validator/src/com/android/tools/idea/validator/accessibility/AccessibilityValidator.java +++ b/validator/src/com/android/tools/idea/validator/accessibility/AccessibilityValidator.java @@ -18,7 +18,7 @@ package com.android.tools.idea.validator.accessibility; import com.android.tools.idea.validator.ValidatorData; import com.android.tools.idea.validator.ValidatorData.Fix; -import com.android.tools.idea.validator.ValidatorData.Issue; +import com.android.tools.idea.validator.ValidatorData.Issue.IssueBuilder; import com.android.tools.idea.validator.ValidatorData.Level; import com.android.tools.idea.validator.ValidatorData.Type; import com.android.tools.idea.validator.ValidatorResult; @@ -31,6 +31,7 @@ import android.view.View; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.EnumSet; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.ResourceBundle; @@ -70,20 +71,28 @@ public class AccessibilityValidator { * Run Accessibility specific validation test and receive results. * @param view the root view * @param image the output image of the view. Null if not available. - * @param filter list of levels to allow + * @param policy e.g: list of levels to allow * @return results with all the accessibility issues and warnings. */ @NotNull public static ValidatorResult validateAccessibility( - @NotNull View view, @Nullable BufferedImage image, @NotNull EnumSet<Level> filter) { + @NotNull View view, + @Nullable BufferedImage image, + @NotNull ValidatorData.Policy policy) { + + EnumSet<Level> filter = policy.mLevels; ValidatorResult.Builder builder = new ValidatorResult.Builder(); builder.mMetric.startTimer(); + if (!policy.mTypes.contains(Type.ACCESSIBILITY)) { + return builder.build(); + } List<AccessibilityHierarchyCheckResult> results = getHierarchyCheckResults( builder.mMetric, view, builder.mSrcMap, - image); + image, + policy.mChecks); for (AccessibilityHierarchyCheckResult result : results) { ValidatorData.Level level = convertLevel(result.getType()); @@ -91,19 +100,30 @@ public class AccessibilityValidator { continue; } - ValidatorData.Fix fix = generateFix(result); - Long srcId = null; - if (result.getElement() != null) { - srcId = result.getElement().getCondensedUniqueId(); + try { + IssueBuilder issueBuilder = new IssueBuilder() + .setMsg(result.getMessage(Locale.ENGLISH).toString()) + .setLevel(level) + .setFix(generateFix(result)) + .setSourceClass(result.getSourceCheckClass().getSimpleName()); + if (result.getElement() != null) { + issueBuilder.setSrcId(result.getElement().getCondensedUniqueId()); + } + AccessibilityHierarchyCheck subclass = AccessibilityCheckPreset + .getHierarchyCheckForClass(result + .getSourceCheckClass() + .asSubclass(AccessibilityHierarchyCheck.class)); + if (subclass != null) { + issueBuilder.setHelpfulUrl(subclass.getHelpUrl()); + } + builder.mIssues.add(issueBuilder.build()); + } catch (Exception e) { + builder.mIssues.add(new IssueBuilder() + .setType(Type.INTERNAL_ERROR) + .setMsg(e.getMessage()) + .setLevel(Level.ERROR) + .setSourceClass("AccessibilityValidator").build()); } - Issue issue = new Issue( - Type.ACCESSIBILITY, - result.getMessage(Locale.ENGLISH).toString(), - level, - srcId, - fix); - issue.mSourceClass = result.getSourceCheckClass().getSimpleName(); - builder.mIssues.add(issue); } builder.mMetric.endTimer(); return builder.build(); @@ -137,10 +157,13 @@ public class AccessibilityValidator { @NotNull Metric metric, @NotNull View view, @NotNull BiMap<Long, View> originMap, - @Nullable BufferedImage image) { + @Nullable BufferedImage image, + HashSet<AccessibilityHierarchyCheck> policyChecks) { - @NotNull Set<AccessibilityHierarchyCheck> checks = AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset( - AccessibilityCheckPreset.LATEST); + @NotNull Set<AccessibilityHierarchyCheck> checks = policyChecks.isEmpty() + ? AccessibilityCheckPreset + .getAccessibilityHierarchyChecksForPreset(AccessibilityCheckPreset.LATEST) + : policyChecks; @NotNull AccessibilityHierarchyAndroid hierarchy = AccessibilityHierarchyAndroid .newBuilder(view) diff --git a/validator/validator.iml b/validator/validator.iml index e75d99a918..f580aa31c0 100644 --- a/validator/validator.iml +++ b/validator/validator.iml @@ -41,14 +41,5 @@ <SOURCES /> </library> </orderEntry> - <orderEntry type="module-library"> - <library> - <CLASSES> - <root url="jar://$MODULE_DIR$/../../../prebuilts/misc/common/atf/atf_classes.jar!/" /> - </CLASSES> - <JAVADOC /> - <SOURCES /> - </library> - </orderEntry> </component> </module>
\ No newline at end of file |