diff options
author | Isaac Chai <ichai@google.com> | 2020-05-28 19:25:19 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2020-05-28 19:25:19 +0000 |
commit | 19c8a852af8128c26533d0da6cd9fee14b4a8f79 (patch) | |
tree | 55c15177d3690858f9d018a8ab04f4c3897edecf | |
parent | d0f3cfec7e42a8d8334b3a7023726841141b7919 (diff) | |
parent | 03a9aabbe7cd204496d5954524c9b647ed02c463 (diff) | |
download | layoutlib-19c8a852af8128c26533d0da6cd9fee14b4a8f79.tar.gz |
Add image contrast capability + metrics am: 03a9aabbe7
Change-Id: I30f27d22867aefbdb2f6dfcf83dc2ef5419b9805
14 files changed, 367 insertions, 57 deletions
diff --git a/bridge/src/com/android/layoutlib/bridge/Bridge.java b/bridge/src/com/android/layoutlib/bridge/Bridge.java index fa84d2852b..88298d69e3 100644 --- a/bridge/src/com/android/layoutlib/bridge/Bridge.java +++ b/bridge/src/com/android/layoutlib/bridge/Bridge.java @@ -30,8 +30,6 @@ import com.android.layoutlib.bridge.impl.RenderSessionImpl; import com.android.layoutlib.bridge.util.DynamicIdMap; import com.android.ninepatch.NinePatchChunk; import com.android.resources.ResourceType; -import com.android.tools.idea.validator.LayoutValidator; -import com.android.tools.idea.validator.ValidatorResult; import com.android.tools.layoutlib.annotations.Nullable; import com.android.tools.layoutlib.create.MethodAdapter; import com.android.tools.layoutlib.create.OverrideMethod; @@ -380,13 +378,6 @@ public final class Bridge extends com.android.ide.common.rendering.api.Bridge { if (lastResult.isSuccess() && !doNotRenderOnCreate) { lastResult = scene.render(true /*freshRender*/); } - - boolean enableLayoutValidation = Boolean.TRUE.equals( - params.getFlag(RenderParamsFlags.FLAG_ENABLE_LAYOUT_VALIDATOR)); - if (enableLayoutValidation && !scene.getViewInfos().isEmpty()) { - ValidatorResult validatorResult = LayoutValidator.validate(((View) scene.getViewInfos().get(0).getViewObject())); - scene.setValidatorResult(validatorResult); - } } } finally { scene.release(); diff --git a/bridge/src/com/android/layoutlib/bridge/android/RenderParamsFlags.java b/bridge/src/com/android/layoutlib/bridge/android/RenderParamsFlags.java index 2640617d85..4eaf352aa3 100644 --- a/bridge/src/com/android/layoutlib/bridge/android/RenderParamsFlags.java +++ b/bridge/src/com/android/layoutlib/bridge/android/RenderParamsFlags.java @@ -93,6 +93,13 @@ public final class RenderParamsFlags { public static final Key<Boolean> FLAG_ENABLE_LAYOUT_VALIDATOR = new Key<>("enableLayoutValidator", Boolean.class); + /** + * Enables image-related validation checks within layout validation. + * {@link FLAG_ENABLE_LAYOUT_VALIDATOR} must be enabled before this can be effective. + */ + public static final Key<Boolean> FLAG_ENABLE_LAYOUT_VALIDATOR_IMAGE_CHECK = + new Key<>("enableLayoutValidatorImageCheck", Boolean.class); + // Disallow instances. private RenderParamsFlags() {} } diff --git a/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java b/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java index 1a813877e4..1338c1700d 100644 --- a/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java +++ b/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java @@ -48,6 +48,9 @@ import com.android.layoutlib.bridge.impl.binding.FakeAdapter; import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter; import com.android.tools.layoutlib.java.System_Delegate; import com.android.tools.idea.validator.ValidatorResult; +import com.android.tools.idea.validator.LayoutValidator; +import com.android.tools.idea.validator.ValidatorResult; +import com.android.tools.idea.validator.ValidatorResult.Builder; import com.android.util.Pair; import android.annotation.NonNull; @@ -571,6 +574,24 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { visitAllChildren(mViewRoot, 0, 0, params.getExtendedViewInfoMode(), false); + try { + boolean enableLayoutValidation = Boolean.TRUE.equals(params.getFlag(RenderParamsFlags.FLAG_ENABLE_LAYOUT_VALIDATOR)); + boolean enableLayoutValidationImageCheck = Boolean.TRUE.equals( + params.getFlag(RenderParamsFlags.FLAG_ENABLE_LAYOUT_VALIDATOR_IMAGE_CHECK)); + + if (enableLayoutValidation && !getViewInfos().isEmpty()) { + BufferedImage imageToPass = + enableLayoutValidationImageCheck ? getImage() : null; + ValidatorResult validatorResult = + LayoutValidator.validate(((View) getViewInfos().get(0).getViewObject()), imageToPass); + setValidatorResult(validatorResult); + } + } catch (Throwable e) { + ValidatorResult.Builder builder = new Builder(); + builder.mMetric.mErrorMessage = e.getMessage(); + setValidatorResult(builder.build()); + } + // success! return renderResult; } catch (Throwable e) { diff --git a/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/eye_chart.png b/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/eye_chart.png Binary files differnew file mode 100644 index 0000000000..d1950807e3 --- /dev/null +++ b/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/eye_chart.png diff --git a/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/eye_chart_low_contrast.jpg b/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/eye_chart_low_contrast.jpg Binary files differnew file mode 100644 index 0000000000..f578c263ad --- /dev/null +++ b/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/eye_chart_low_contrast.jpg diff --git a/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_image_contrast.xml b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_image_contrast.xml new file mode 100644 index 0000000000..0f20c194bf --- /dev/null +++ b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_image_contrast.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="top|center_horizontal" + android:orientation="vertical"> + + <ImageView + android:layout_width="97dp" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:adjustViewBounds="true" + android:src="@drawable/eye_chart" + android:contentDescription="Eye Chart" + android:clickable="true" /> + + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:layout_marginTop="0dp" + android:src="@drawable/eye_chart_low_contrast" + android:contentDescription="Eye Chart with Low Contrast" + android:clickable="true" /> +</LinearLayout>
\ No newline at end of file diff --git a/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_text_contrast.xml b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_text_contrast.xml index 19e1ab1098..4d3ac6f26a 100644 --- a/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_text_contrast.xml +++ b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_text_contrast.xml @@ -30,7 +30,7 @@ android:layout_marginLeft="0dp" android:background="@android:color/holo_green_dark" android:textColor="#fe0099cc" /> - <!-- fails : fe0099cc passes : ff0098cb --> + <!-- ATF bypasses transparent views / colors unless image is available. --> <Button android:id="@+id/low_contrast_button2" android:layout_width="wrap_content" @@ -57,4 +57,4 @@ android:minHeight="48dp" android:text="CheckBox B" android:textColor="@android:color/holo_blue_dark"/> -</LinearLayout>
\ No newline at end of file +</LinearLayout> diff --git a/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/SessionParamsBuilder.java b/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/SessionParamsBuilder.java index b7f5bb0bb2..baadc621ed 100644 --- a/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/SessionParamsBuilder.java +++ b/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/SessionParamsBuilder.java @@ -64,6 +64,7 @@ public class SessionParamsBuilder { private boolean enableShadows = true; private boolean highQualityShadows = true; private boolean enableLayoutValidator = false; + private boolean enableLayoutValidatorImageCheck = false; @NonNull public SessionParamsBuilder setParser(@NonNull LayoutPullParser layoutParser) { @@ -183,6 +184,12 @@ public class SessionParamsBuilder { } @NonNull + public SessionParamsBuilder enableLayoutValidationImageCheck() { + this.enableLayoutValidatorImageCheck = true; + return this; + } + + @NonNull public SessionParams build() { assert mFrameworkResources != null; assert mProjectResources != null; @@ -206,6 +213,9 @@ public class SessionParamsBuilder { params.setFlag(RenderParamsFlags.FLAG_ENABLE_SHADOW, enableShadows); params.setFlag(RenderParamsFlags.FLAG_RENDER_HIGH_QUALITY_SHADOW, highQualityShadows); params.setFlag(RenderParamsFlags.FLAG_ENABLE_LAYOUT_VALIDATOR, enableLayoutValidator); + params.setFlag( + RenderParamsFlags.FLAG_ENABLE_LAYOUT_VALIDATOR_IMAGE_CHECK, + enableLayoutValidatorImageCheck); if (mImageFactory != null) { params.setImageFactory(mImageFactory); } 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 0b26fbbec1..23e40ff094 100644 --- a/bridge/tests/src/com/android/tools/idea/validator/LayoutValidatorTests.java +++ b/bridge/tests/src/com/android/tools/idea/validator/LayoutValidatorTests.java @@ -66,7 +66,7 @@ public class LayoutValidatorTests extends RenderTestBase { render(sBridge, params, -1, session -> { ValidatorResult result = LayoutValidator - .validate(((View) session.getRootViews().get(0).getViewObject())); + .validate(((View) session.getRootViews().get(0).getViewObject()), null); assertEquals(3, result.getIssues().size()); for (Issue issue : result.getIssues()) { assertEquals(Type.ACCESSIBILITY, issue.mType); diff --git a/bridge/tests/src/com/android/tools/idea/validator/accessibility/AccessibilityValidatorTests.java b/bridge/tests/src/com/android/tools/idea/validator/accessibility/AccessibilityValidatorTests.java index 84656124fc..16ad4a2061 100644 --- a/bridge/tests/src/com/android/tools/idea/validator/accessibility/AccessibilityValidatorTests.java +++ b/bridge/tests/src/com/android/tools/idea/validator/accessibility/AccessibilityValidatorTests.java @@ -22,32 +22,37 @@ import com.android.layoutlib.bridge.intensive.RenderTestBase; import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator; import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback; import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser; +import com.android.layoutlib.bridge.intensive.util.SessionParamsBuilder; +import com.android.tools.idea.validator.LayoutValidator; import com.android.tools.idea.validator.ValidatorData; import com.android.tools.idea.validator.ValidatorData.Issue; import com.android.tools.idea.validator.ValidatorData.Level; +import com.android.tools.idea.validator.ValidatorData.Policy; +import com.android.tools.idea.validator.ValidatorData.Type; import com.android.tools.idea.validator.ValidatorResult; import org.junit.Test; -import android.view.View; - import java.util.EnumSet; import java.util.List; import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; /** - * Sanity check for a11y checks. For now it lacks checking: + * Sanity check for a11y checks. For now it lacks checking the following: * - ClassNameCheck * - ClickableSpanCheck * - EditableContentDescCheck * - LinkPurposeUnclearCheck - * - ImageContrastCheck - * ATF cannot grab images from Layoutlib generated output. + * As these require more complex UI for testing. + * + * It's also missing: * - TraversalOrderCheck - * In Layoutlib test env, traversalBefore/after attributes seems to be lost. Tested on - * studio and it seems to work ok. + * Because in Layoutlib test env, traversalBefore/after attributes seems to be lost. Tested on + * studio and it seems to work ok. */ public class AccessibilityValidatorTests extends RenderTestBase { @@ -60,10 +65,6 @@ public class AccessibilityValidatorTests extends RenderTestBase { ExpectedLevels expectedLevels = new ExpectedLevels(); expectedLevels.expectedErrors = 1; expectedLevels.check(dupBounds); - - // Make sure no other errors are given. - List<Issue> allErrors = filter(result.getIssues(), EnumSet.of(Level.ERROR, Level.WARNING)); - checkEquals(dupBounds, allErrors); }); } @@ -78,12 +79,6 @@ public class AccessibilityValidatorTests extends RenderTestBase { expectedLevels.expectedInfos = 1; expectedLevels.expectedWarnings = 1; expectedLevels.check(duplicateSpeakableTexts); - - // Make sure no other errors are given. - List<Issue> allErrors = filter( - result.getIssues(), - EnumSet.of(Level.WARNING, Level.INFO)); - checkEquals(duplicateSpeakableTexts, allErrors); }); } @@ -97,13 +92,6 @@ public class AccessibilityValidatorTests extends RenderTestBase { expectedLevels.expectedVerboses = 3; expectedLevels.expectedWarnings = 1; expectedLevels.check(redundant); - - // Make sure no other warnings nor errors unexpectedly thrown. - redundant = filter(redundant, EnumSet.of(Level.WARNING)); - List<Issue> allWarnings = filter( - result.getIssues(), - EnumSet.of(Level.WARNING, Level.ERROR)); - checkEquals(allWarnings, redundant); }); } @@ -162,7 +150,27 @@ public class AccessibilityValidatorTests extends RenderTestBase { ValidatorResult result = getRenderResult(session); List<Issue> textContrast = filter(result.getIssues(), "TextContrastCheck"); - // Expected. ATF doesn't count alpha values. + // ATF doesn't count alpha values unless image is passed. + ExpectedLevels expectedLevels = new ExpectedLevels(); + expectedLevels.expectedErrors = 3; + expectedLevels.expectedWarnings = 1; // This is true only if image is passed. + expectedLevels.expectedVerboses = 2; + expectedLevels.check(textContrast); + + // Make sure no other errors in the system. + textContrast = filter(textContrast, EnumSet.of(Level.ERROR)); + List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR)); + checkEquals(filtered, textContrast); + }); + } + + @Test + public void testTextContrastCheckNoImage() throws Exception { + render("a11y_test_text_contrast.xml", session -> { + ValidatorResult result = getRenderResult(session); + List<Issue> textContrast = filter(result.getIssues(), "TextContrastCheck"); + + // ATF doesn't count alpha values unless image is passed. ExpectedLevels expectedLevels = new ExpectedLevels(); expectedLevels.expectedErrors = 3; expectedLevels.expectedVerboses = 3; @@ -172,10 +180,45 @@ public class AccessibilityValidatorTests extends RenderTestBase { textContrast = filter(textContrast, EnumSet.of(Level.ERROR)); List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR)); checkEquals(filtered, textContrast); + }, false); + } + + @Test + public void testImageContrastCheck() throws Exception { + render("a11y_test_image_contrast.xml", session -> { + ValidatorResult result = getRenderResult(session); + List<Issue> imageContrast = filter(result.getIssues(), "ImageContrastCheck"); + + ExpectedLevels expectedLevels = new ExpectedLevels(); + expectedLevels.expectedWarnings = 1; + expectedLevels.expectedVerboses = 1; + expectedLevels.check(imageContrast); + + // Make sure no other errors in the system. + imageContrast = filter(imageContrast, EnumSet.of(Level.ERROR, Level.WARNING)); + List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR, Level.WARNING)); + checkEquals(filtered, imageContrast); }); } @Test + public void testImageContrastCheckNoImage() throws Exception { + render("a11y_test_image_contrast.xml", session -> { + ValidatorResult result = getRenderResult(session); + List<Issue> imageContrast = filter(result.getIssues(), "ImageContrastCheck"); + + ExpectedLevels expectedLevels = new ExpectedLevels(); + expectedLevels.expectedVerboses = 3; + expectedLevels.check(imageContrast); + + // Make sure no other errors in the system. + imageContrast = filter(imageContrast, EnumSet.of(Level.ERROR, Level.WARNING)); + List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR, Level.WARNING)); + checkEquals(filtered, imageContrast); + }, false); + } + + @Test public void testTouchTargetSizeCheck() throws Exception { render("a11y_test_touch_target_size.xml", session -> { ValidatorResult result = getRenderResult(session); @@ -212,25 +255,39 @@ public class AccessibilityValidatorTests extends RenderTestBase { } private ValidatorResult getRenderResult(RenderSession session) { - View view = (View) session.getRootViews().get(0).getViewObject(); - return AccessibilityValidator.validateAccessibility(view, EnumSet.of(Level.ERROR, - Level.WARNING, Level.INFO, Level.VERBOSE)); + Object validationData = session.getValidationData(); + assertNotNull(validationData); + assertTrue(validationData instanceof ValidatorResult); + return (ValidatorResult) validationData; } - private void render(String fileName, RenderSessionListener verifier) throws Exception { + render(fileName, verifier, true); + } + + private void render( + String fileName, + RenderSessionListener verifier, + boolean enableImageCheck) throws Exception { + LayoutValidator.updatePolicy(new Policy( + EnumSet.of(Type.ACCESSIBILITY, Type.RENDER), + EnumSet.of(Level.ERROR, Level.WARNING, Level.INFO, Level.VERBOSE))); + LayoutPullParser parser = createParserFromPath(fileName); LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger(), mDefaultClassLoader); layoutLibCallback.initResources(); - SessionParams params = getSessionParamsBuilder() + SessionParamsBuilder params = getSessionParamsBuilder() .setParser(parser) .setConfigGenerator(ConfigGenerator.NEXUS_5) .setCallback(layoutLibCallback) .disableDecoration() - .enableLayoutValidation() - .build(); + .enableLayoutValidation(); + + if (enableImageCheck) { + params.enableLayoutValidationImageCheck(); + } - render(sBridge, params, -1, verifier); + render(sBridge, params.build(), -1, verifier); } /** diff --git a/validator/src/com/android/tools/idea/validator/LayoutValidator.java b/validator/src/com/android/tools/idea/validator/LayoutValidator.java index 0312ca9c48..7ec6f47188 100644 --- a/validator/src/com/android/tools/idea/validator/LayoutValidator.java +++ b/validator/src/com/android/tools/idea/validator/LayoutValidator.java @@ -21,9 +21,11 @@ import com.android.tools.idea.validator.ValidatorData.Policy; import com.android.tools.idea.validator.ValidatorData.Type; import com.android.tools.idea.validator.accessibility.AccessibilityValidator; import com.android.tools.layoutlib.annotations.NotNull; +import com.android.tools.layoutlib.annotations.Nullable; import android.view.View; +import java.awt.image.BufferedImage; import java.util.EnumSet; /** @@ -31,7 +33,7 @@ import java.util.EnumSet; */ public class LayoutValidator { - private static final ValidatorData.Policy DEFAULT_POLICY = new Policy( + private static ValidatorData.Policy sPolicy = new Policy( EnumSet.of(Type.ACCESSIBILITY, Type.RENDER), EnumSet.of(Level.ERROR, Level.WARNING)); @@ -42,11 +44,19 @@ public class LayoutValidator { * @return The validation results. If no issue is found it'll return empty result. */ @NotNull - public static ValidatorResult validate(@NotNull View view) { + public static ValidatorResult validate(@NotNull View view, @Nullable BufferedImage image) { if (view.isAttachedToWindow()) { - return AccessibilityValidator.validateAccessibility(view, DEFAULT_POLICY.mLevels); + return AccessibilityValidator.validateAccessibility(view, image, sPolicy.mLevels); } // TODO: Add non-a11y layout validation later. return new ValidatorResult.Builder().build(); } + + /** + * Update the policy with which to run the validation call. + * @param policy new policy. + */ + public static void updatePolicy(@NotNull ValidatorData.Policy policy) { + sPolicy = policy; + } } diff --git a/validator/src/com/android/tools/idea/validator/ValidatorResult.java b/validator/src/com/android/tools/idea/validator/ValidatorResult.java index 79129bc23a..8cc5c3d1df 100644 --- a/validator/src/com/android/tools/idea/validator/ValidatorResult.java +++ b/validator/src/com/android/tools/idea/validator/ValidatorResult.java @@ -36,13 +36,15 @@ public class ValidatorResult { @NotNull private final ImmutableBiMap<Long, View> mSrcMap; @NotNull private final ArrayList<Issue> mIssues; + @NotNull private final Metric mMetric; /** * Please use {@link Builder} for creating results. */ - private ValidatorResult(BiMap<Long, View> srcMap, ArrayList<Issue> issues) { + private ValidatorResult(BiMap<Long, View> srcMap, ArrayList<Issue> issues, Metric metric) { mSrcMap = ImmutableBiMap.<Long, View>builder().putAll(srcMap).build(); mIssues = issues; + mMetric = metric; } /** @@ -59,6 +61,13 @@ public class ValidatorResult { return mIssues; } + /** + * @return metric for validation. + */ + public Metric getMetric() { + return mMetric; + } + @Override public String toString() { StringBuilder builder = new StringBuilder() @@ -81,10 +90,55 @@ public class ValidatorResult { public static class Builder { @NotNull public final BiMap<Long, View> mSrcMap = HashBiMap.create(); @NotNull public final ArrayList<Issue> mIssues = new ArrayList<>(); + @NotNull public final Metric mMetric = new Metric(); public ValidatorResult build() { - return new ValidatorResult(mSrcMap, mIssues); + return new ValidatorResult(mSrcMap, mIssues, mMetric); + } + } + + /** + * Contains metric specific data. + */ + public static class Metric { + /** Error message. If null no error was thrown. */ + public String mErrorMessage = null; + + /** Records how long validation took */ + public long mElapsedMs = 0; + + /** How many new memories (bytes) validator creates for images. */ + public long mImageMemoryBytes = 0; + + private long mStart; + + private Metric() { } + + public void startTimer() { + mStart = System.currentTimeMillis(); } + public void endTimer() { + mElapsedMs = System.currentTimeMillis() - mStart; + } + + @Override + public String toString() { + return "Validation result metric: { elapsed=" + mElapsedMs + + "ms, image memory=" + readableBytes() + " }"; + } + + private String readableBytes() { + if (mImageMemoryBytes > 1000000000) { + return mImageMemoryBytes / 1000000000 + "gb"; + } + else if (mImageMemoryBytes > 1000000) { + return mImageMemoryBytes / 1000000 + "mb"; + } + else if (mImageMemoryBytes > 1000) { + return mImageMemoryBytes / 1000 + "kb"; + } + return mImageMemoryBytes + "bytes"; + } } } 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 179bf71ec2..e679750321 100644 --- a/validator/src/com/android/tools/idea/validator/accessibility/AccessibilityValidator.java +++ b/validator/src/com/android/tools/idea/validator/accessibility/AccessibilityValidator.java @@ -22,11 +22,13 @@ import com.android.tools.idea.validator.ValidatorData.Issue; import com.android.tools.idea.validator.ValidatorData.Level; import com.android.tools.idea.validator.ValidatorData.Type; import com.android.tools.idea.validator.ValidatorResult; +import com.android.tools.idea.validator.ValidatorResult.Metric; import com.android.tools.layoutlib.annotations.NotNull; import com.android.tools.layoutlib.annotations.Nullable; import android.view.View; +import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; @@ -38,6 +40,7 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType; import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck; import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; +import com.google.android.apps.common.testing.accessibility.framework.Parameters; import com.google.android.apps.common.testing.accessibility.framework.strings.StringManager; import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchyAndroid; import com.google.common.collect.BiMap; @@ -66,17 +69,21 @@ 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 * @return results with all the accessibility issues and warnings. */ @NotNull public static ValidatorResult validateAccessibility( - @NotNull View view, - @NotNull EnumSet<Level> filter) { + @NotNull View view, @Nullable BufferedImage image, @NotNull EnumSet<Level> filter) { ValidatorResult.Builder builder = new ValidatorResult.Builder(); + builder.mMetric.startTimer(); - List<AccessibilityHierarchyCheckResult> results = getHierarchyCheckResults(view, - builder.mSrcMap); + List<AccessibilityHierarchyCheckResult> results = getHierarchyCheckResults( + builder.mMetric, + view, + builder.mSrcMap, + image); for (AccessibilityHierarchyCheckResult result : results) { ValidatorData.Level level = convertLevel(result.getType()); @@ -98,6 +105,7 @@ public class AccessibilityValidator { issue.mSourceClass = result.getSourceCheckClass().getSimpleName(); builder.mIssues.add(issue); } + builder.mMetric.endTimer(); return builder.build(); } @@ -126,15 +134,28 @@ public class AccessibilityValidator { @NotNull private static List<AccessibilityHierarchyCheckResult> getHierarchyCheckResults( + @NotNull Metric metric, @NotNull View view, - @NotNull BiMap<Long, View> originMap) { + @NotNull BiMap<Long, View> originMap, + @Nullable BufferedImage image) { + @NotNull Set<AccessibilityHierarchyCheck> checks = AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset( AccessibilityCheckPreset.LATEST); - @NotNull AccessibilityHierarchyAndroid hierarchy = AccessibilityHierarchyAndroid.newBuilder(view).setViewOriginMap(originMap).build(); + + @NotNull AccessibilityHierarchyAndroid hierarchy = AccessibilityHierarchyAndroid + .newBuilder(view) + .setViewOriginMap(originMap) + .build(); ArrayList<AccessibilityHierarchyCheckResult> a11yResults = new ArrayList(); + Parameters parameters = null; + if (image != null) { + parameters = new Parameters(); + parameters.putScreenCapture(new AtfBufferedImage(image, metric)); + } + for (AccessibilityHierarchyCheck check : checks) { - a11yResults.addAll(check.runCheckOnHierarchy(hierarchy)); + a11yResults.addAll(check.runCheckOnHierarchy(hierarchy, null, parameters)); } return a11yResults; diff --git a/validator/src/com/android/tools/idea/validator/accessibility/AtfBufferedImage.java b/validator/src/com/android/tools/idea/validator/accessibility/AtfBufferedImage.java new file mode 100644 index 0000000000..59d20a8f92 --- /dev/null +++ b/validator/src/com/android/tools/idea/validator/accessibility/AtfBufferedImage.java @@ -0,0 +1,98 @@ +/* + * 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 com.android.tools.idea.validator.accessibility; + +import com.android.tools.idea.validator.ValidatorResult.Metric; +import com.android.tools.layoutlib.annotations.NotNull; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.awt.image.WritableRaster; + +import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.Image; + +import static java.awt.image.BufferedImage.TYPE_INT_ARGB; + +/** + * Image implementation to be used in Accessibility Test Framework. + */ +public class AtfBufferedImage implements Image { + + // The source buffered image, expected to contain the full screen rendered image of the layout. + @NotNull private final BufferedImage mBufferedImage; + // Metrics to be returned + @NotNull private final Metric mMetric; + + private final int mLeft; + private final int mTop; + private final int mWidth; + private final int mHeight; + + AtfBufferedImage(@NotNull BufferedImage image, @NotNull Metric metric) { + assert(image.getType() == TYPE_INT_ARGB); + mBufferedImage = image; + mMetric = metric; + mWidth = mBufferedImage.getWidth(); + mHeight = mBufferedImage.getHeight(); + mLeft = 0; + mTop = 0; + } + + private AtfBufferedImage( + @NotNull BufferedImage image, + @NotNull Metric metric, + int left, + int top, + int width, + int height) { + mBufferedImage = image; + mMetric = metric; + mLeft = left; + mTop = top; + mWidth = width; + mHeight = height; + } + + @Override + public int getHeight() { + return mHeight; + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + @NotNull + public Image crop(int left, int top, int width, int height) { + return new AtfBufferedImage(mBufferedImage, mMetric, left, top, width, height); + } + + @Override + @NotNull + public int[] getPixels() { + // ATF unfortunately writes in-place on returned int[] for color analysis. + // It must return copied list otherwise it won't work. + BufferedImage cropped = mBufferedImage.getSubimage(mLeft, mTop, mWidth, mHeight); + WritableRaster raster = cropped.copyData( + cropped.getRaster().createCompatibleWritableRaster()); + int[] toReturn = ((DataBufferInt) raster.getDataBuffer()).getData(); + mMetric.mImageMemoryBytes += toReturn.length * 4; + return toReturn; + } +} |