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