aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsaac Chai <ichai@google.com>2020-05-28 19:25:19 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2020-05-28 19:25:19 +0000
commit19c8a852af8128c26533d0da6cd9fee14b4a8f79 (patch)
tree55c15177d3690858f9d018a8ab04f4c3897edecf
parentd0f3cfec7e42a8d8334b3a7023726841141b7919 (diff)
parent03a9aabbe7cd204496d5954524c9b647ed02c463 (diff)
downloadlayoutlib-19c8a852af8128c26533d0da6cd9fee14b4a8f79.tar.gz
Add image contrast capability + metrics am: 03a9aabbe7
Change-Id: I30f27d22867aefbdb2f6dfcf83dc2ef5419b9805
-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;
+ }
+}