1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.accessibilityservice.cts;
18 
19 import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 import static com.google.common.truth.Truth.assertWithMessage;
23 
24 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
25 import android.accessibilityservice.AccessibilityServiceInfo;
26 import android.accessibilityservice.cts.activities.AccessibilityTestActivity;
27 import android.accessibilityservice.cts.utils.DisplayUtils;
28 import android.app.Instrumentation;
29 import android.app.UiAutomation;
30 import android.graphics.Point;
31 import android.graphics.Rect;
32 import android.hardware.display.DisplayManager;
33 import android.os.Bundle;
34 import android.platform.test.annotations.Presubmit;
35 import android.server.wm.CtsWindowInfoUtils;
36 import android.util.DisplayMetrics;
37 import android.util.Log;
38 import android.view.Display;
39 import android.view.SurfaceControlViewHost;
40 import android.view.SurfaceHolder;
41 import android.view.SurfaceView;
42 import android.view.View;
43 import android.view.accessibility.AccessibilityNodeInfo;
44 
45 import androidx.lifecycle.Lifecycle;
46 import androidx.test.ext.junit.rules.ActivityScenarioRule;
47 import androidx.test.ext.junit.runners.AndroidJUnit4;
48 import androidx.test.platform.app.InstrumentationRegistry;
49 import androidx.test.uiautomator.UiDevice;
50 
51 import com.android.compatibility.common.util.CddTest;
52 
53 import org.junit.AfterClass;
54 import org.junit.Before;
55 import org.junit.BeforeClass;
56 import org.junit.Rule;
57 import org.junit.Test;
58 import org.junit.rules.RuleChain;
59 import org.junit.runner.RunWith;
60 
61 import java.util.List;
62 import java.util.concurrent.TimeoutException;
63 
64 /**
65  * Tests that AccessibilityNodeInfos from an embedded hierarchy that is present to another
66  * hierarchy are properly populated.
67  */
68 @CddTest(requirements = {"3.10/C-1-1,C-1-2"})
69 @RunWith(AndroidJUnit4.class)
70 @Presubmit
71 public class AccessibilityEmbeddedHierarchyTest {
72     private static Instrumentation sInstrumentation;
73     private static UiAutomation sUiAutomation;
74     private static UiDevice sUiDevice;
75 
76     private final ActivityScenarioRule<AccessibilityEmbeddedHierarchyActivity> mActivityRule =
77             new ActivityScenarioRule<>(AccessibilityEmbeddedHierarchyActivity.class);
78 
79     private static final String HOST_PARENT_RESOURCE_NAME =
80             "android.accessibilityservice.cts:id/host_surfaceview";
81     private static final String EMBEDDED_VIEW_RESOURCE_NAME =
82             "android.accessibilityservice.cts:id/embedded_editText";
83 
84     private final AccessibilityDumpOnFailureRule mDumpOnFailureRule =
85             new AccessibilityDumpOnFailureRule();
86 
87     private AccessibilityEmbeddedHierarchyActivity mActivity;
88 
89     @Rule
90     public final RuleChain mRuleChain = RuleChain
91             .outerRule(mActivityRule)
92             .around(mDumpOnFailureRule);
93 
94     @BeforeClass
oneTimeSetup()95     public static void oneTimeSetup() {
96         sInstrumentation = InstrumentationRegistry.getInstrumentation();
97         sInstrumentation.setInTouchMode(false);
98         sUiAutomation = sInstrumentation.getUiAutomation();
99         sUiDevice = UiDevice.getInstance(sInstrumentation);
100         AccessibilityServiceInfo info = sUiAutomation.getServiceInfo();
101         info.flags |=
102                 AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE
103                         | AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
104         sUiAutomation.setServiceInfo(info);
105     }
106 
107     @AfterClass
postTestTearDown()108     public static void postTestTearDown() {
109         sUiAutomation.destroy();
110         sInstrumentation.resetInTouchMode();
111     }
112 
113     @Before
setUp()114     public void setUp() throws Throwable {
115         mActivityRule.getScenario()
116                 .moveToState(Lifecycle.State.RESUMED)
117                 .onActivity(activity -> mActivity = activity);
118         sUiDevice.waitForIdle();
119     }
120 
121     @Test
testEmbeddedViewCanBeFound()122     public void testEmbeddedViewCanBeFound() {
123         final AccessibilityNodeInfo target =
124                 findEmbeddedAccessibilityNodeInfo(sUiAutomation.getRootInActiveWindow());
125         assertThat(target).isNotNull();
126     }
127 
128     @Test
testEmbeddedView_PerformActionTransfersWindowInputFocus()129     public void testEmbeddedView_PerformActionTransfersWindowInputFocus() {
130         final View hostView = mActivity.mInputFocusableView;
131         final View embeddedView = mActivity.mViewHost.getView();
132         final AccessibilityNodeInfo embeddedNode =
133                 findEmbeddedAccessibilityNodeInfo(sUiAutomation.getRootInActiveWindow());
134         assertThat(hostView.isFocusable()).isTrue();
135         assertThat(embeddedView.isFocusable()).isTrue();
136 
137         // Start by ensuring the host-side view has window input focus.
138         hostView.performClick();
139         assertThat(CtsWindowInfoUtils.waitForWindowFocus(hostView, true)).isTrue();
140         assertThat(embeddedView.hasWindowFocus()).isFalse();
141 
142         // ACTION_ACCESSIBILITY_FOCUS should not transfer window input focus.
143         embeddedNode.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
144         assertThat(CtsWindowInfoUtils.waitForWindowFocus(embeddedView, true)).isFalse();
145 
146         // Other actions like ACTION_CLICK should transfer window input focus.
147         embeddedNode.performAction(AccessibilityNodeInfo.ACTION_CLICK);
148         assertThat(CtsWindowInfoUtils.waitForWindowFocus(embeddedView, true)).isTrue();
149         assertThat(hostView.hasWindowFocus()).isFalse();
150     }
151 
152     @Test
testEmbeddedViewCanFindItsHostParent()153     public void testEmbeddedViewCanFindItsHostParent() {
154         final AccessibilityNodeInfo target =
155                 findEmbeddedAccessibilityNodeInfo(sUiAutomation.getRootInActiveWindow());
156         final AccessibilityNodeInfo parent = target.getParent();
157         assertThat(parent.getViewIdResourceName()).isEqualTo(HOST_PARENT_RESOURCE_NAME);
158     }
159 
160     @Test
testEmbeddedViewHasCorrectBound()161     public void testEmbeddedViewHasCorrectBound() {
162         final AccessibilityNodeInfo target =
163                 findEmbeddedAccessibilityNodeInfo(sUiAutomation.getRootInActiveWindow());
164         final AccessibilityNodeInfo parent = target.getParent();
165 
166         final Rect hostViewBoundsInScreen = new Rect();
167         final Rect embeddedViewBoundsInScreen = new Rect();
168         parent.refresh();
169         target.refresh();
170         parent.getBoundsInScreen(hostViewBoundsInScreen);
171         target.getBoundsInScreen(embeddedViewBoundsInScreen);
172 
173         assertWithMessage(
174                 "hostViewBoundsInScreen" + hostViewBoundsInScreen.toShortString()
175                         + " doesn't contain embeddedViewBoundsInScreen"
176                         + embeddedViewBoundsInScreen.toShortString()).that(
177                 DisplayUtils.fuzzyBoundsInScreenContains(
178                         hostViewBoundsInScreen, embeddedViewBoundsInScreen)).isTrue();
179     }
180 
181     @Test
testEmbeddedViewHasCorrectBoundAfterHostViewMove()182     public void testEmbeddedViewHasCorrectBoundAfterHostViewMove() throws TimeoutException {
183         final AccessibilityNodeInfo target =
184                 findEmbeddedAccessibilityNodeInfo(sUiAutomation.getRootInActiveWindow());
185         final AccessibilityNodeInfo parent = target.getParent();
186 
187         final Rect hostViewBoundsInScreen = new Rect();
188         final Rect newEmbeddedViewBoundsInScreen = new Rect();
189         final Rect oldEmbeddedViewBoundsInScreen = new Rect();
190         target.refresh();
191         target.getBoundsInScreen(oldEmbeddedViewBoundsInScreen);
192 
193         // Move the host's SurfaceView away from (0,0).
194         int moveAmountPx = mActivity.getResources().getDimensionPixelSize(
195                 R.dimen.embedded_hierarchy_embedded_layout_movement_size);
196         mActivity.moveSurfaceViewLayoutPosition(moveAmountPx, moveAmountPx, false);
197 
198         parent.refresh();
199         target.refresh();
200         parent.getBoundsInScreen(hostViewBoundsInScreen);
201         target.getBoundsInScreen(newEmbeddedViewBoundsInScreen);
202 
203         assertWithMessage(
204                 "hostViewBoundsInScreen" + hostViewBoundsInScreen.toShortString()
205                         + " doesn't contain newEmbeddedViewBoundsInScreen"
206                         + newEmbeddedViewBoundsInScreen.toShortString()).that(
207                 DisplayUtils.fuzzyBoundsInScreenContains(hostViewBoundsInScreen,
208                         newEmbeddedViewBoundsInScreen)).isTrue();
209         assertWithMessage(
210                 "newEmbeddedViewBoundsInScreen" + newEmbeddedViewBoundsInScreen.toShortString()
211                         + " shouldn't be the same with oldEmbeddedViewBoundsInScreen"
212                         + oldEmbeddedViewBoundsInScreen.toShortString()).that(
213                 newEmbeddedViewBoundsInScreen.equals(oldEmbeddedViewBoundsInScreen)).isFalse();
214     }
215 
216     @Test
testEmbeddedViewIsInvisibleAfterMovingOutOfScreen()217     public void testEmbeddedViewIsInvisibleAfterMovingOutOfScreen() throws TimeoutException {
218         final AccessibilityNodeInfo target =
219                 findEmbeddedAccessibilityNodeInfo(sUiAutomation.getRootInActiveWindow());
220         assertWithMessage("Embedded view should be visible at beginning.").that(
221                 target.isVisibleToUser()).isTrue();
222 
223         // Move Host SurfaceView out of screen
224         final Point screenSize = getScreenSize();
225         mActivity.moveSurfaceViewLayoutPosition(screenSize.x * 2, screenSize.y * 2, true);
226 
227         target.refresh();
228         assertWithMessage("Embedded view should be invisible after moving out of screen.").that(
229                 target.isVisibleToUser()).isFalse();
230     }
231 
findEmbeddedAccessibilityNodeInfo(AccessibilityNodeInfo root)232     private AccessibilityNodeInfo findEmbeddedAccessibilityNodeInfo(AccessibilityNodeInfo root) {
233         final int childCount = root.getChildCount();
234         for (int i = 0; i < childCount; i++) {
235             final AccessibilityNodeInfo info = root.getChild(i);
236             if (info == null) {
237                 continue;
238             }
239             if (EMBEDDED_VIEW_RESOURCE_NAME.equals(info.getViewIdResourceName())) {
240                 return info;
241             }
242             if (info.getChildCount() != 0) {
243                 return findEmbeddedAccessibilityNodeInfo(info);
244             }
245         }
246         return null;
247     }
248 
findHostAccessibilityNodeInfo( AccessibilityNodeInfo root)249     private static AccessibilityNodeInfo findHostAccessibilityNodeInfo(
250             AccessibilityNodeInfo root) {
251         List<AccessibilityNodeInfo> nodes =
252                 root.findAccessibilityNodeInfosByViewId(HOST_PARENT_RESOURCE_NAME);
253         return nodes.isEmpty() ? null : nodes.get(0);
254     }
255 
getScreenSize()256     private Point getScreenSize() {
257         final DisplayManager dm = sInstrumentation.getContext().getSystemService(
258                 DisplayManager.class);
259         final Display display = dm.getDisplay(Display.DEFAULT_DISPLAY);
260         final DisplayMetrics metrics = new DisplayMetrics();
261         display.getRealMetrics(metrics);
262         return new Point(metrics.widthPixels, metrics.heightPixels);
263     }
264 
265     /**
266      * This class is an placeholder {@link android.app.Activity} used to perform embedded hierarchy
267      * testing of the accessibility feature by interaction with the UI widgets.
268      */
269     public static class AccessibilityEmbeddedHierarchyActivity extends
270             AccessibilityTestActivity implements SurfaceHolder.Callback {
271         private SurfaceView mSurfaceView;
272         private View mInputFocusableView;
273         private SurfaceControlViewHost mViewHost;
274 
275         @Override
onCreate(Bundle savedInstanceState)276         protected void onCreate(Bundle savedInstanceState) {
277             super.onCreate(savedInstanceState);
278             setContentView(R.layout.accessibility_embedded_hierarchy_test_host_side);
279             mSurfaceView = findViewById(R.id.host_surfaceview);
280             mSurfaceView.getHolder().addCallback(this);
281             mInputFocusableView = findViewById(R.id.host_editText);
282         }
283 
284         @Override
surfaceCreated(SurfaceHolder holder)285         public void surfaceCreated(SurfaceHolder holder) {
286             mViewHost = new SurfaceControlViewHost(this, this.getDisplay(),
287                     mSurfaceView.getHostToken());
288 
289             mSurfaceView.setChildSurfacePackage(mViewHost.getSurfacePackage());
290 
291             View layout = getLayoutInflater().inflate(
292                     R.layout.accessibility_embedded_hierarchy_test_embedded_side, null);
293             final int viewSizePx = getResources().getDimensionPixelSize(
294                     R.dimen.embedded_hierarchy_embedded_layout_size);
295             mViewHost.setView(layout, viewSizePx, viewSizePx);
296         }
297 
298         @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)299         public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
300             // No-op
301         }
302 
303         @Override
surfaceDestroyed(SurfaceHolder holder)304         public void surfaceDestroyed(SurfaceHolder holder) {
305             // No-op
306         }
307 
moveSurfaceViewLayoutPosition(int x, int y, boolean offScreen)308         private void moveSurfaceViewLayoutPosition(int x, int y, boolean offScreen)
309                 throws TimeoutException {
310             final AccessibilityNodeInfo surfaceViewNode = findHostAccessibilityNodeInfo(
311                     sUiAutomation.getRootInActiveWindow());
312             final Rect expectedBounds = new Rect(), boundsAfter = new Rect();
313             surfaceViewNode.getBoundsInScreen(expectedBounds);
314             expectedBounds.offset(x, y);
315             sUiAutomation.executeAndWaitForEvent(
316                     () -> sInstrumentation.runOnMainSync(() -> {
317                         mSurfaceView.setTranslationX(x);
318                         mSurfaceView.setTranslationY(y);
319                     }),
320                     (event) -> {
321                         surfaceViewNode.refresh();
322                         surfaceViewNode.getBoundsInScreen(boundsAfter);
323                         final boolean hasExpectedPosition;
324                         if (offScreen) {
325                             hasExpectedPosition = !surfaceViewNode.isVisibleToUser();
326                         } else {
327                             hasExpectedPosition = DisplayUtils.fuzzyBoundsInScreenSameOrigin(
328                                     expectedBounds, boundsAfter);
329                         }
330                         if (!hasExpectedPosition) {
331                             Log.i(AccessibilityEmbeddedHierarchyTest.class.getSimpleName(),
332                                     "mSurfaceView expected bounds: " + expectedBounds
333                                             + "\tActual bounds: " + boundsAfter);
334                         }
335                         return hasExpectedPosition;
336                     }, DEFAULT_TIMEOUT_MS);
337             sUiDevice.waitForIdle();
338         }
339     }
340 }
341