Sanity tests for a11y am: 7436aaff62 am: d0f3cfec7e

Change-Id: Ia3cc0b5097399dd15a0ec515427dad6fd96539d4
diff --git a/bridge/src/com/android/layoutlib/bridge/Bridge.java b/bridge/src/com/android/layoutlib/bridge/Bridge.java
index fa84d28..88298d6 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.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 @@
                     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 2640617..4eaf352 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 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 1a81387..1338c17 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.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 @@
                     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/src/libcore/icu/ICU_Delegate.java b/bridge/src/libcore/icu/ICU_Delegate.java
index 880344f..708cf62 100644
--- a/bridge/src/libcore/icu/ICU_Delegate.java
+++ b/bridge/src/libcore/icu/ICU_Delegate.java
@@ -75,72 +75,6 @@
     }
 
     @LayoutlibDelegate
-    /*package*/ static boolean initLocaleDataNative(String locale, LocaleData result) {
-
-        // Used by Calendar.
-        result.firstDayOfWeek = 1;
-        result.minimalDaysInFirstWeek = 1;
-
-        // Used by DateFormatSymbols.
-        result.amPm = new String[] { "AM", "PM" };
-        result.eras = new String[] { "BC", "AD" };
-
-        result.longMonthNames = new String[] { "January", "February", "March", "April", "May",
-                "June", "July", "August", "September", "October", "November", "December" };
-        result.shortMonthNames = new String[] { "Jan", "Feb", "Mar", "Apr", "May",
-                "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
-        result.longStandAloneMonthNames = result.longMonthNames;
-        result.shortStandAloneMonthNames = result.shortMonthNames;
-
-        // The platform code expects this to begin at index 1, rather than 0. It maps it directly to
-        // the constants from java.util.Calendar.<weekday>
-        result.longWeekdayNames = new String[] {
-                "", "Sunday", "Monday" ,"Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
-        result.shortWeekdayNames = new String[] {
-                "", "Sun", "Mon" ,"Tue", "Wed", "Thu", "Fri", "Sat" };
-        result.tinyWeekdayNames = new String[] {
-                "", "S", "M", "T", "W", "T", "F", "S" };
-
-        result.longStandAloneWeekdayNames = result.longWeekdayNames;
-        result.shortStandAloneWeekdayNames = result.shortWeekdayNames;
-        result.tinyStandAloneWeekdayNames = result.tinyWeekdayNames;
-
-        result.fullTimeFormat = "";
-        result.longTimeFormat = "";
-        result.mediumTimeFormat = "";
-        result.shortTimeFormat = "";
-
-        result.fullDateFormat = "";
-        result.longDateFormat = "";
-        result.mediumDateFormat = "";
-        result.shortDateFormat = "";
-
-        // Used by DecimalFormatSymbols.
-        result.zeroDigit = '0';
-        result.decimalSeparator = '.';
-        result.groupingSeparator = ',';
-        result.patternSeparator = ' ';
-        result.percent = "%";
-        result.perMill = "\u2030";
-        result.monetarySeparator = ' ';
-        result.minusSign = "-";
-        result.exponentSeparator = "e";
-        result.infinity = "\u221E";
-        result.NaN = "NaN";
-        // Also used by Currency.
-        result.currencySymbol = "$";
-        result.internationalCurrencySymbol = "USD";
-
-        // Used by DecimalFormat and NumberFormat.
-        result.numberPattern = "%f";
-        result.integerPattern = "%d";
-        result.currencyPattern = "%s";
-        result.percentPattern = "%f";
-
-        return true;
-    }
-
-    @LayoutlibDelegate
     /*package*/ static void setDefaultLocale(String locale) {
         ICU.setDefaultLocale(locale);
     }
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 0000000..d195080
--- /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 0000000..f578c26
--- /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 0000000..0f20c19
--- /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 19e1ab1..4d3ac6f 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 b7f5bb0..baadc62 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 @@
     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 @@
     }
 
     @NonNull
+    public SessionParamsBuilder enableLayoutValidationImageCheck() {
+        this.enableLayoutValidatorImageCheck = true;
+        return this;
+    }
+
+    @NonNull
     public SessionParams build() {
         assert mFrameworkResources != null;
         assert mProjectResources != null;
@@ -206,6 +213,9 @@
         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 0b26fbb..23e40ff 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 @@
 
         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 8465612..16ad4a2 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.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 @@
             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 @@
             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 @@
             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 @@
             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 @@
             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 @@
     }
 
     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) throws Exception {
+    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();
 
-        render(sBridge, params, -1, verifier);
+        if (enableImageCheck) {
+            params.enableLayoutValidationImageCheck();
+        }
+
+        render(sBridge, params.build(), -1, verifier);
     }
 
     /**
diff --git a/rename_font/build_font.py b/rename_font/build_font.py
index a53ebbc..db0c98a 100755
--- a/rename_font/build_font.py
+++ b/rename_font/build_font.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 # Copyright (C) 2014 The Android Open Source Project
 #
@@ -96,7 +96,7 @@
 
 def convert_font(input_path):
   filename = os.path.basename(input_path)
-  print 'Converting font: ' + filename
+  print('Converting font: ' + filename)
   # the path to the output file. The file name is the fontfilename.ttx
   ttx_path = os.path.join(dest_dir, filename)
   ttx_path = ttx_path[:-1] + 'x'
@@ -116,11 +116,11 @@
     ttx.main(ttx_args)
   except InvalidFontException:
     # In case of invalid fonts, we exit.
-    print filename + ' is not a valid font'
+    print(filename + ' is not a valid font')
     raise
   except Exception as e:
-    print 'Error converting font: ' + filename
-    print e
+    print('Error converting font: ' + filename)
+    print(e)
     # Some fonts are too big to be handled by the ttx library.
     # Just copy paste them.
     shutil.copy(input_path, dest_dir)
@@ -136,7 +136,7 @@
       found in the name table of the font. """
   fonts = []
   font = None
-  last_name_id = sys.maxint
+  last_name_id = sys.maxsize
   for namerecord in tag.iter('namerecord'):
     if 'nameID' in namerecord.attrib:
       name_id = int(namerecord.attrib['nameID'])
@@ -164,14 +164,14 @@
 
 
 def update_tag(tag, fonts):
-  last_name_id = sys.maxint
+  last_name_id = sys.maxsize
   fonts_iterator = fonts.__iter__()
   font = None
   for namerecord in tag.iter('namerecord'):
     if 'nameID' in namerecord.attrib:
       name_id = int(namerecord.attrib['nameID'])
       if name_id <= last_name_id:
-        font = fonts_iterator.next()
+        font = next(fonts_iterator)
         font = update_font_name(font)
       last_name_id = name_id
       if name_id == NAMEID_FAMILY:
@@ -192,7 +192,7 @@
     new_family = font.family + font.version
   else:
     new_family = font.family
-  if font.style is 'Regular' and not font.ends_in_regular:
+  if font.style == 'Regular' and not font.ends_in_regular:
     font.fullname = new_family
   else:
     font.fullname = new_family + ' ' + font.style
@@ -205,7 +205,7 @@
       'Regular' for plain fonts. However, some fonts don't obey this rule. We
       keep the style info, to minimize the diff. """
   string = string.strip().split()[-1]
-  return string is 'Regular'
+  return string == 'Regular'
 
 
 def get_version(string):
diff --git a/rename_font/build_font_single.py b/rename_font/build_font_single.py
index 22d7fdf..b254072 100755
--- a/rename_font/build_font_single.py
+++ b/rename_font/build_font_single.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 # Copyright (C) 2014 The Android Open Source Project
 #
@@ -71,7 +71,7 @@
 
 def main(argv):
   if len(argv) < 2:
-    print 'Incorrect usage: ' + str(argv)
+    print('Incorrect usage: ' + str(argv))
     sys.exit('Usage: build_font_single.py /path/to/input/font.ttf /path/to/out/font.ttf')
   dest_path = argv[-1]
   input_path = argv[0]
@@ -85,7 +85,7 @@
 
 def convert_font(input_path, dest_path):
   filename = os.path.basename(input_path)
-  print 'Converting font: ' + filename
+  print('Converting font: ' + filename)
   # the path to the output file. The file name is the fontfilename.ttx
   ttx_path = dest_path[:-1] + 'x'
   try:
@@ -104,11 +104,11 @@
     ttx.main(ttx_args)
   except InvalidFontException:
     # assume, like for .ttc and .otf that font might be valid, but warn.
-    print 'Family and/or Style nameIDs not found in '+ filename
+    print('Family and/or Style nameIDs not found in '+ filename)
     shutil.copy(input_path, dest_path)
   except Exception as e:
-    print 'Error converting font: ' + filename
-    print e
+    print('Error converting font: ' + filename)
+    print(e)
     # Some fonts are too big to be handled by the ttx library.
     # Just copy paste them.
     shutil.copy(input_path, dest_path)
@@ -124,7 +124,7 @@
       found in the name table of the font. """
   fonts = []
   font = None
-  last_name_id = sys.maxint
+  last_name_id = sys.maxsize
   for namerecord in tag.iter('namerecord'):
     if 'nameID' in namerecord.attrib:
       name_id = int(namerecord.attrib['nameID'])
@@ -155,7 +155,7 @@
 
 
 def update_tag(tag, fonts):
-  last_name_id = sys.maxint
+  last_name_id = sys.maxsize
   fonts_iterator = fonts.__iter__()
   font = None
   for namerecord in tag.iter('namerecord'):
@@ -165,7 +165,7 @@
       if name_id < NAMEID_LIST_MIN or name_id > NAMEID_LIST_MAX:
         continue
       if name_id <= last_name_id:
-        font = fonts_iterator.next()
+        font = next(fonts_iterator)
         font = update_font_name(font)
       last_name_id = name_id
       if name_id == NAMEID_FAMILY:
@@ -186,7 +186,7 @@
     new_family = font.family + font.version
   else:
     new_family = font.family
-  if font.style is 'Regular' and not font.ends_in_regular:
+  if font.style == 'Regular' and not font.ends_in_regular:
     font.fullname = new_family
   else:
     font.fullname = new_family + ' ' + font.style
@@ -199,7 +199,7 @@
       'Regular' for plain fonts. However, some fonts don't obey this rule. We
       keep the style info, to minimize the diff. """
   string = string.strip().split()[-1]
-  return string is 'Regular'
+  return string == 'Regular'
 
 
 def get_version(string):
diff --git a/rename_font/test.py b/rename_font/test.py
index 2ffddf4..cf26ee9 100755
--- a/rename_font/test.py
+++ b/rename_font/test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 """Tests build_font.py by renaming a font.
 
@@ -22,10 +22,10 @@
   def test(self):
     font_name = "Roboto-Regular.ttf"
     srcdir = tempfile.mkdtemp()
-    print "srcdir: " + srcdir
+    print("srcdir: " + srcdir)
     shutil.copy(font_name, srcdir)
     destdir = tempfile.mkdtemp()
-    print "destdir: " + destdir
+    print("destdir: " + destdir)
     self.assertTrue(build_font.main([srcdir, destdir]) is None)
     out_path = os.path.join(destdir, font_name)
     ttx.main([out_path])
diff --git a/validator/src/com/android/tools/idea/validator/LayoutValidator.java b/validator/src/com/android/tools/idea/validator/LayoutValidator.java
index 0312ca9..7ec6f47 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.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 @@
  */
 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 @@
      * @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 79129bc..8cc5c3d 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 @@
 
     @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 @@
         return mIssues;
     }
 
+    /**
+     * @return metric for validation.
+     */
+    public Metric getMetric() {
+        return mMetric;
+    }
+
     @Override
     public String toString() {
         StringBuilder builder = new StringBuilder()
@@ -81,10 +90,55 @@
     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 179bf71..e679750 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.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.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 @@
     /**
      * 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 @@
             issue.mSourceClass = result.getSourceCheckClass().getSimpleName();
             builder.mIssues.add(issue);
         }
+        builder.mMetric.endTimer();
         return builder.build();
     }
 
@@ -126,15 +134,28 @@
 
     @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 0000000..59d20a8
--- /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;
+    }
+}