aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsaac Chai <ichai@google.com>2020-05-20 18:18:27 -0700
committerIsaac Chai <ichai@google.com>2020-05-28 11:06:20 -0700
commit03a9aabbe7cd204496d5954524c9b647ed02c463 (patch)
tree55c15177d3690858f9d018a8ab04f4c3897edecf
parent7436aaff62cac594864998ab89b2c965822a69ae (diff)
downloadlayoutlib-03a9aabbe7cd204496d5954524c9b647ed02c463.tar.gz
Add image contrast capability + metrics
CL contains: 1) Adding image input to atf - now it can run image contrast check and more accurate text contrast checks 2) Fix some tests render path 3) Added separate control flags for image input (in case we need to turn this feature off without shutting down atf integration) 4) Added additional metrics so we can monitor the perf stress due to validator. Bug: 150331000 Test: Able to pass all the unit tests. Change-Id: I34f7d51fdba7551bef55b29e82291b4558b31947 (cherry picked from commit 98190e755d5422c50d6fe811e533f7de1cdc8ee8)
-rw-r--r--bridge/src/com/android/layoutlib/bridge/Bridge.java9
-rw-r--r--bridge/src/com/android/layoutlib/bridge/android/RenderParamsFlags.java7
-rw-r--r--bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java21
-rw-r--r--bridge/tests/res/testApp/MyApplication/src/main/res/drawable/eye_chart.pngbin0 -> 3332 bytes
-rw-r--r--bridge/tests/res/testApp/MyApplication/src/main/res/drawable/eye_chart_low_contrast.jpgbin0 -> 1527 bytes
-rw-r--r--bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_image_contrast.xml41
-rw-r--r--bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_text_contrast.xml4
-rw-r--r--bridge/tests/src/com/android/layoutlib/bridge/intensive/util/SessionParamsBuilder.java10
-rw-r--r--bridge/tests/src/com/android/tools/idea/validator/LayoutValidatorTests.java2
-rw-r--r--bridge/tests/src/com/android/tools/idea/validator/accessibility/AccessibilityValidatorTests.java123
-rw-r--r--validator/src/com/android/tools/idea/validator/LayoutValidator.java16
-rw-r--r--validator/src/com/android/tools/idea/validator/ValidatorResult.java58
-rw-r--r--validator/src/com/android/tools/idea/validator/accessibility/AccessibilityValidator.java35
-rw-r--r--validator/src/com/android/tools/idea/validator/accessibility/AtfBufferedImage.java98
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
new file mode 100644
index 0000000000..d1950807e3
--- /dev/null
+++ b/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/eye_chart.png
Binary files differ
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
new file mode 100644
index 0000000000..f578c263ad
--- /dev/null
+++ b/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/eye_chart_low_contrast.jpg
Binary files differ
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;
+ }
+}