aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorandroid-build-team Robot <android-build-team-robot@google.com>2020-05-29 01:13:34 +0000
committerandroid-build-team Robot <android-build-team-robot@google.com>2020-05-29 01:13:34 +0000
commit4ff5a059f09b6f914c81f6e793168f09d3e1a4ff (patch)
tree55c15177d3690858f9d018a8ab04f4c3897edecf
parenta7a501800991f552e3120ccc2e0379a78d5f4814 (diff)
parent19c8a852af8128c26533d0da6cd9fee14b4a8f79 (diff)
downloadlayoutlib-4ff5a059f09b6f914c81f6e793168f09d3e1a4ff.tar.gz
Snap for 6538275 from 19c8a852af8128c26533d0da6cd9fee14b4a8f79 to rvc-d1-release
Change-Id: I6d2b00920dade62de32c5fda5605857864c71325
-rw-r--r--bridge/bridge.iml9
-rw-r--r--bridge/src/com/android/layoutlib/bridge/Bridge.java10
-rw-r--r--bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java14
-rw-r--r--bridge/src/com/android/layoutlib/bridge/android/RenderParamsFlags.java7
-rw-r--r--bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java32
-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_dup_clickable_bounds.xml46
-rw-r--r--bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_duplicate_speakable.xml49
-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_redundant_desc.xml52
-rw-r--r--bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_speakable_text_present.xml37
-rw-r--r--bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_speakable_text_present2.xml30
-rw-r--r--bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_speakable_text_present3.xml29
-rw-r--r--bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_text_contrast.xml60
-rw-r--r--bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_touch_target_size.xml58
-rw-r--r--bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java3
-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.java338
-rw-r--r--validator/src/com/android/tools/idea/validator/LayoutValidator.java16
-rw-r--r--validator/src/com/android/tools/idea/validator/ValidatorData.java5
-rw-r--r--validator/src/com/android/tools/idea/validator/ValidatorResult.java58
-rw-r--r--validator/src/com/android/tools/idea/validator/accessibility/AccessibilityValidator.java36
-rw-r--r--validator/src/com/android/tools/idea/validator/accessibility/AtfBufferedImage.java98
25 files changed, 1010 insertions, 30 deletions
diff --git a/bridge/bridge.iml b/bridge/bridge.iml
index 5d437cb167..9358a54f3c 100644
--- a/bridge/bridge.iml
+++ b/bridge/bridge.iml
@@ -93,5 +93,14 @@
</library>
</orderEntry>
<orderEntry type="module" module-name="validator" />
+ <orderEntry type="module-library" scope="TEST">
+ <library>
+ <CLASSES>
+ <root url="jar://$MODULE_DIR$/../../../prebuilts/misc/common/atf/atf_classes.jar!/" />
+ </CLASSES>
+ <JAVADOC />
+ <SOURCES />
+ </library>
+ </orderEntry>
</component>
</module> \ No newline at end of file
diff --git a/bridge/src/com/android/layoutlib/bridge/Bridge.java b/bridge/src/com/android/layoutlib/bridge/Bridge.java
index 7632b2e015..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,14 +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()) {
- // TODO: Once session can hold ValidatorResult stop using data.
- ValidatorResult validatorResult = LayoutValidator.validate(((View) scene.getViewInfos().get(0).getViewObject()));
- lastResult = lastResult.getCopyWithData(validatorResult);
- }
}
} finally {
scene.release();
diff --git a/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java b/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java
index 6a78f3df46..442a77bdcd 100644
--- a/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java
+++ b/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java
@@ -108,15 +108,11 @@ public class BridgeRenderSession extends RenderSession {
try {
Bridge.prepareThread();
mLastResult = mSession.acquire(timeout);
- // If there was any data to preserve from inflate.
- // TODO: Remove this once session is able to hold data from inflate.
- Object data = mLastResult.getData();
if (mLastResult.isSuccess()) {
if (forceMeasure) {
mSession.invalidateRenderingSize();
}
- Result result = mSession.render(false /*freshRender*/);
- mLastResult = (data == null) ? result : result.getCopyWithData(data);
+ mLastResult = mSession.render(false /*freshRender*/);
}
} finally {
mSession.release();
@@ -158,4 +154,12 @@ public class BridgeRenderSession extends RenderSession {
}
mLastResult = lastResult;
}
+
+ @Override
+ public Object getValidationData() {
+ if (mSession != null) {
+ return mSession.getValidatorResult();
+ }
+ return null;
+ }
}
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 e7904abcc4..1338c1700d 100644
--- a/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java
+++ b/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java
@@ -47,6 +47,10 @@ import com.android.layoutlib.bridge.android.support.SupportPreferencesUtil;
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;
@@ -127,6 +131,7 @@ public class RenderSessionImpl extends RenderAction<SessionParams> {
private List<ViewInfo> mSystemViewInfoList;
private Layout.Builder mLayoutBuilder;
private boolean mNewRenderSize;
+ @Nullable private ValidatorResult mValidatorResult = null;
private static final class PostInflateException extends Exception {
private static final long serialVersionUID = 1L;
@@ -569,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) {
@@ -1130,6 +1153,15 @@ public class RenderSessionImpl extends RenderAction<SessionParams> {
return getContext().getDefaultNamespacedStyles();
}
+ @Nullable
+ public ValidatorResult getValidatorResult() {
+ return mValidatorResult;
+ }
+
+ public void setValidatorResult(ValidatorResult result) {
+ mValidatorResult = result;
+ }
+
public void setScene(RenderSession session) {
mScene = session;
}
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_dup_clickable_bounds.xml b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_dup_clickable_bounds.xml
new file mode 100644
index 0000000000..cbc04ac269
--- /dev/null
+++ b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_dup_clickable_bounds.xml
@@ -0,0 +1,46 @@
+<?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.
+ -->
+
+<!-- Accessibility test with decoy clickable item -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/clickable_wrapper_for_button"
+ android:orientation="horizontal"
+ android:clickable="true">
+
+ <Button
+ android:text="Button in clickable parent"
+ android:id="@+id/button_in_clickable_parent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minHeight="48dp"/>
+ </LinearLayout>
+
+ <Button
+ android:text="Button on its own"
+ android:id="@+id/button_on_its_own"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minHeight="48dp"/>
+
+</LinearLayout>
diff --git a/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_duplicate_speakable.xml b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_duplicate_speakable.xml
new file mode 100644
index 0000000000..676a2d4e32
--- /dev/null
+++ b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_duplicate_speakable.xml
@@ -0,0 +1,49 @@
+<?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:id="@+id/layout_across_top"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/first_news_text"
+ android:text="News"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:textSize="32dp"/>
+ <Button
+ android:id="@+id/first_print_button"
+ android:text="Print"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <TextView
+ android:id="@+id/first_news_text1"
+ android:text="News"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:textSize="32dp"/>
+ <Button
+ android:id="@+id/first_print_button2"
+ android:text="Print"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+</LinearLayout>
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_redundant_desc.xml b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_redundant_desc.xml
new file mode 100644
index 0000000000..ff99a3547c
--- /dev/null
+++ b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_redundant_desc.xml
@@ -0,0 +1,52 @@
+<?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:orientation="vertical">
+
+ <Button
+ android:id="@+id/button_with_label_containing_button"
+ android:text="OK"
+ android:contentDescription="OK button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp" />
+
+ <TextView
+ android:id="@+id/oath"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:text="I hereby declare, on oath, that I absolutely and entirely renounce and abjure all allegiance
+ and fidelity to any foreign prince, potentate, state, or sovereignty of whom or which I have
+ heretofore been a subject or citizen; that I will support and defend the Constitution and
+ laws of the United States of America against all enemies, foreign and domestic; that I will
+ bear true faith and allegiance to the same; that I will bear arms on behalf of the United
+ States when required by the law; that I will perform noncombatant service in the
+ Armed Forces of the United States when required by the law; that I will perform work of
+ national importance under civilian direction when required by the law; and that I take this
+ obligation freely without any mental reservation or purpose of evasion; so help me God."/>
+
+ <Button
+ android:text="Good Button"
+ android:id="@+id/bottom_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp" />
+
+</LinearLayout>
diff --git a/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_speakable_text_present.xml b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_speakable_text_present.xml
new file mode 100644
index 0000000000..64c2873043
--- /dev/null
+++ b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_speakable_text_present.xml
@@ -0,0 +1,37 @@
+<?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.
+ -->
+
+<!-- Accessibility test with label -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <Button
+ android:id="@+id/button_labeled_by_textview"
+ android:layout_width="48dp"
+ android:layout_height="48dp" />
+
+ <TextView
+ android:id="@+id/textview_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:labelFor="@id/button_labeled_by_textview"
+ android:minHeight="48dp"
+ android:text="Howdy"/>
+
+</LinearLayout>
diff --git a/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_speakable_text_present2.xml b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_speakable_text_present2.xml
new file mode 100644
index 0000000000..c26197982a
--- /dev/null
+++ b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_speakable_text_present2.xml
@@ -0,0 +1,30 @@
+<?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.
+ -->
+
+<!-- Accessibility test with nothing important error msg -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <Button
+ android:id="@+id/unlabeled_button"
+ android:importantForAccessibility="no"
+ android:layout_width="48dp"
+ android:layout_height="48dp" />
+
+</LinearLayout>
diff --git a/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_speakable_text_present3.xml b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_speakable_text_present3.xml
new file mode 100644
index 0000000000..6f9741b10d
--- /dev/null
+++ b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_speakable_text_present3.xml
@@ -0,0 +1,29 @@
+<?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.
+ -->
+
+<!-- Accessibility test with nothing important error msg -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <Button
+ android:id="@+id/unlabeled_button2"
+ android:layout_width="48dp"
+ android:layout_height="48dp" />
+
+</LinearLayout>
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
new file mode 100644
index 0000000000..4d3ac6f26a
--- /dev/null
+++ b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_text_contrast.xml
@@ -0,0 +1,60 @@
+<?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">
+ <!-- Low contrast, with slightly transparent text -->
+ <Button
+ android:id="@+id/low_contrast_button1"
+ android:text="Bad Button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_margin="8dp"
+ android:layout_marginLeft="0dp"
+ android:background="@android:color/holo_green_dark"
+ android:textColor="#fe0099cc" />
+ <!-- ATF bypasses transparent views / colors unless image is available. -->
+ <Button
+ android:id="@+id/low_contrast_button2"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:background="@android:color/holo_green_dark"
+ android:text="Button B"
+ android:textColor="@android:color/holo_blue_dark"/>
+ <EditText
+ android:id="@+id/low_contrast_edit_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:inputType="textPersonName"
+ android:minHeight="48dp"
+ android:text="Edit B"
+ android:background="@android:color/holo_green_dark"
+ android:textColor="@android:color/holo_blue_dark"
+ />
+ <CheckBox
+ android:id="@+id/low_contrast_check_box"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@android:color/holo_green_dark"
+ android:minHeight="48dp"
+ android:text="CheckBox B"
+ android:textColor="@android:color/holo_blue_dark"/>
+</LinearLayout>
diff --git a/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_touch_target_size.xml b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_touch_target_size.xml
new file mode 100644
index 0000000000..58885ac86b
--- /dev/null
+++ b/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_touch_target_size.xml
@@ -0,0 +1,58 @@
+<?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">
+
+ <Button
+ android:id="@+id/ok_top_left_corner_button1"
+ android:text="E"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ />
+
+ <Button
+ android:id="@+id/thin_top_button2"
+ android:text="F"
+ android:layout_width="32dp"
+ android:layout_height="48dp"
+ />
+
+ <Button
+ android:id="@+id/ok_top_button3"
+ android:text="G"
+ android:layout_width="48dp"
+ android:layout_height="32dp"
+ />
+
+ <Button
+ android:id="@+id/short_top_button4"
+ android:text="H"
+ android:layout_width="48dp"
+ android:layout_height="31dp"
+ />
+
+ <Button
+ android:id="@+id/short_top_right_corner_button5"
+ android:text="I"
+ android:layout_width="32dp"
+ android:layout_height="31dp"
+ />
+
+</LinearLayout>
diff --git a/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java b/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java
index 9229d0dbe9..e43531a09d 100644
--- a/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java
+++ b/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java
@@ -23,6 +23,7 @@ import com.android.layoutlib.bridge.android.BridgeXmlBlockParserTest;
import com.android.layoutlib.bridge.impl.LayoutParserWrapperTest;
import com.android.layoutlib.bridge.impl.ResourceHelperTest;
import com.android.tools.idea.validator.LayoutValidatorTests;
+import com.android.tools.idea.validator.accessibility.AccessibilityValidatorTests;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@@ -46,7 +47,7 @@ import android.util.imagepool.ImagePoolImplTest;
BridgeRenderSessionTest.class, ResourceHelperTest.class, BridgeContextTest.class,
Resources_DelegateTest.class, Color_DelegateTest.class, ImagePoolHelperTest.class,
ImagePoolImplTest.class, HighQualityShadowsRenderTests.class,
- LayoutValidatorTests.class
+ LayoutValidatorTests.class, AccessibilityValidatorTests.class
})
public class Main {
}
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
new file mode 100644
index 0000000000..16ad4a2061
--- /dev/null
+++ b/bridge/tests/src/com/android/tools/idea/validator/accessibility/AccessibilityValidatorTests.java
@@ -0,0 +1,338 @@
+/*
+ * 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.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.SessionParams;
+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 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 the following:
+ * - ClassNameCheck
+ * - ClickableSpanCheck
+ * - EditableContentDescCheck
+ * - LinkPurposeUnclearCheck
+ * As these require more complex UI for testing.
+ *
+ * It's also missing:
+ * - TraversalOrderCheck
+ * 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 {
+
+ @Test
+ public void testDuplicateClickableBoundsCheck() throws Exception {
+ render("a11y_test_dup_clickable_bounds.xml", session -> {
+ ValidatorResult result = getRenderResult(session);
+ List<Issue> dupBounds = filter(result.getIssues(), "DuplicateClickableBoundsCheck");
+
+ ExpectedLevels expectedLevels = new ExpectedLevels();
+ expectedLevels.expectedErrors = 1;
+ expectedLevels.check(dupBounds);
+ });
+ }
+
+ @Test
+ public void testDuplicateSpeakableTextsCheck() throws Exception {
+ render("a11y_test_duplicate_speakable.xml", session -> {
+ ValidatorResult result = getRenderResult(session);
+ List<Issue> duplicateSpeakableTexts = filter(result.getIssues(),
+ "DuplicateSpeakableTextCheck");
+
+ ExpectedLevels expectedLevels = new ExpectedLevels();
+ expectedLevels.expectedInfos = 1;
+ expectedLevels.expectedWarnings = 1;
+ expectedLevels.check(duplicateSpeakableTexts);
+ });
+ }
+
+ @Test
+ public void testRedundantDescriptionCheck() throws Exception {
+ render("a11y_test_redundant_desc.xml", session -> {
+ ValidatorResult result = getRenderResult(session);
+ List<Issue> redundant = filter(result.getIssues(), "RedundantDescriptionCheck");
+
+ ExpectedLevels expectedLevels = new ExpectedLevels();
+ expectedLevels.expectedVerboses = 3;
+ expectedLevels.expectedWarnings = 1;
+ expectedLevels.check(redundant);
+ });
+ }
+
+ @Test
+ public void testLabelFor() throws Exception {
+ render("a11y_test_speakable_text_present.xml", session -> {
+ ValidatorResult result = getRenderResult(session);
+ List<Issue> speakableCheck = filter(result.getIssues(), "SpeakableTextPresentCheck");
+
+ // Post-JB MR2 support labelFor, so SpeakableTextPresentCheck does not need to find any
+ // speakable text. Expected 1 verbose result saying something along the line of
+ // didn't run or not important for a11y.
+ ExpectedLevels expectedLevels = new ExpectedLevels();
+ expectedLevels.expectedVerboses = 1;
+ expectedLevels.check(speakableCheck);
+ });
+ }
+
+ @Test
+ public void testImportantForAccessibility() throws Exception {
+ render("a11y_test_speakable_text_present2.xml", session -> {
+ ValidatorResult result = getRenderResult(session);
+ List<Issue> speakableCheck = filter(result.getIssues(), "SpeakableTextPresentCheck");
+
+ // Post-JB MR2 support importantForAccessibility, so SpeakableTextPresentCheck
+ // does not need to find any speakable text. Expected 2 verbose results.
+ ExpectedLevels expectedLevels = new ExpectedLevels();
+ expectedLevels.expectedVerboses = 2;
+ expectedLevels.check(speakableCheck);
+ });
+ }
+
+ @Test
+ public void testSpeakableTextPresentCheck() throws Exception {
+ render("a11y_test_speakable_text_present3.xml", session -> {
+ ValidatorResult result = getRenderResult(session);
+ List<Issue> speakableCheck = filter(result.getIssues(), "SpeakableTextPresentCheck");
+
+ ExpectedLevels expectedLevels = new ExpectedLevels();
+ expectedLevels.expectedVerboses = 1;
+ expectedLevels.expectedErrors = 1;
+ expectedLevels.check(speakableCheck);
+
+ // Make sure no other errors in the system.
+ speakableCheck = filter(speakableCheck, EnumSet.of(Level.ERROR));
+ assertEquals(1, speakableCheck.size());
+ List<Issue> allErrors = filter(
+ result.getIssues(), EnumSet.of(Level.ERROR, Level.WARNING, Level.INFO));
+ checkEquals(speakableCheck, allErrors);
+ });
+ }
+
+ @Test
+ public void testTextContrastCheck() 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.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;
+ 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);
+ }, 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);
+ List<Issue> targetSizes = filter(result.getIssues(), "TouchTargetSizeCheck");
+
+ ExpectedLevels expectedLevels = new ExpectedLevels();
+ expectedLevels.expectedErrors = 5;
+ expectedLevels.expectedVerboses = 1;
+ expectedLevels.check(targetSizes);
+
+ // Make sure no other errors in the system.
+ targetSizes = filter(targetSizes, EnumSet.of(Level.ERROR));
+ List<Issue> filtered = filter(result.getIssues(), EnumSet.of(Level.ERROR));
+ checkEquals(filtered, targetSizes);
+ });
+ }
+
+ private void checkEquals(List<Issue> list1, List<Issue> list2) {
+ assertEquals(list1.size(), list2.size());
+ for (int i = 0; i < list1.size(); i++) {
+ assertEquals(list1.get(i), list2.get(i));
+ }
+ }
+
+ private List<Issue> filter(List<ValidatorData.Issue> results, EnumSet<Level> errors) {
+ return results.stream().filter(
+ issue -> errors.contains(issue.mLevel)).collect(Collectors.toList());
+ }
+
+ private List<Issue> filter(
+ List<ValidatorData.Issue> results, String sourceClass) {
+ return results.stream().filter(
+ issue -> sourceClass.equals(issue.mSourceClass)).collect(Collectors.toList());
+ }
+
+ private ValidatorResult getRenderResult(RenderSession session) {
+ 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();
+ SessionParamsBuilder params = getSessionParamsBuilder()
+ .setParser(parser)
+ .setConfigGenerator(ConfigGenerator.NEXUS_5)
+ .setCallback(layoutLibCallback)
+ .disableDecoration()
+ .enableLayoutValidation();
+
+ if (enableImageCheck) {
+ params.enableLayoutValidationImageCheck();
+ }
+
+ render(sBridge, params.build(), -1, verifier);
+ }
+
+ /**
+ * Helper class that checks the list of issues..
+ */
+ private static class ExpectedLevels {
+ // Number of errors expected
+ public int expectedErrors = 0;
+ // Number of warnings expected
+ public int expectedWarnings = 0;
+ // Number of infos expected
+ public int expectedInfos = 0;
+ // Number of verboses expected
+ public int expectedVerboses = 0;
+
+ public void check(List<Issue> issues) {
+ int errors = 0;
+ int warnings = 0;
+ int infos = 0;
+ int verboses = 0;
+
+ for (Issue issue : issues) {
+ switch (issue.mLevel) {
+ case ERROR:
+ errors++;
+ break;
+ case WARNING:
+ warnings++;
+ break;
+ case INFO:
+ infos++;
+ break;
+ case VERBOSE:
+ verboses++;
+ break;
+ }
+ }
+
+ assertEquals("Number of expected errors", expectedErrors, errors);
+ assertEquals("Number of expected warnings",expectedWarnings, warnings);
+ assertEquals("Number of expected infos", expectedInfos, infos);
+ assertEquals("Number of expected verboses", expectedVerboses, verboses);
+
+ int size = expectedErrors + expectedWarnings + expectedInfos + expectedVerboses;
+ assertEquals("expected size", size, issues.size());
+ }
+ };
+}
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/ValidatorData.java b/validator/src/com/android/tools/idea/validator/ValidatorData.java
index f2112a59af..06974720a6 100644
--- a/validator/src/com/android/tools/idea/validator/ValidatorData.java
+++ b/validator/src/com/android/tools/idea/validator/ValidatorData.java
@@ -41,7 +41,8 @@ public class ValidatorData {
ERROR,
WARNING,
INFO,
- VERBOSE
+ /** The test not ran or suppressed. */
+ VERBOSE,
}
/**
@@ -77,6 +78,8 @@ public class ValidatorData {
@NotNull public final Level mLevel;
@Nullable public final Long mSrcId;
@Nullable public final Fix mFix;
+ // Used for debugging.
+ @Nullable public String mSourceClass;
public Issue(
@NotNull Type type,
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 3f59ff925a..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());
@@ -95,8 +102,10 @@ public class AccessibilityValidator {
level,
srcId,
fix);
+ issue.mSourceClass = result.getSourceCheckClass().getSimpleName();
builder.mIssues.add(issue);
}
+ builder.mMetric.endTimer();
return builder.build();
}
@@ -125,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;
+ }
+}