diff options
author | android-build-team Robot <android-build-team-robot@google.com> | 2020-05-29 01:13:34 +0000 |
---|---|---|
committer | android-build-team Robot <android-build-team-robot@google.com> | 2020-05-29 01:13:34 +0000 |
commit | 4ff5a059f09b6f914c81f6e793168f09d3e1a4ff (patch) | |
tree | 55c15177d3690858f9d018a8ab04f4c3897edecf | |
parent | a7a501800991f552e3120ccc2e0379a78d5f4814 (diff) | |
parent | 19c8a852af8128c26533d0da6cd9fee14b4a8f79 (diff) | |
download | layoutlib-4ff5a059f09b6f914c81f6e793168f09d3e1a4ff.tar.gz |
Snap for 6538275 from 19c8a852af8128c26533d0da6cd9fee14b4a8f79 to rvc-d1-release
Change-Id: I6d2b00920dade62de32c5fda5605857864c71325
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 Binary files differnew file mode 100644 index 0000000000..d1950807e3 --- /dev/null +++ b/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/eye_chart.png diff --git a/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/eye_chart_low_contrast.jpg b/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/eye_chart_low_contrast.jpg Binary files differnew file mode 100644 index 0000000000..f578c263ad --- /dev/null +++ b/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/eye_chart_low_contrast.jpg diff --git a/bridge/tests/res/testApp/MyApplication/src/main/res/layout/a11y_test_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; + } +} |