1 /* 2 * Copyright (C) 2008 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.view.cts; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertNotNull; 21 import static org.junit.Assert.assertSame; 22 import static org.junit.Assert.assertTrue; 23 24 import android.Manifest; 25 import android.graphics.Rect; 26 import android.platform.test.annotations.AppModeSdkSandbox; 27 import android.view.FocusFinder; 28 import android.view.LayoutInflater; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.widget.Button; 32 import android.widget.FrameLayout; 33 34 import androidx.test.InstrumentationRegistry; 35 import androidx.test.filters.MediumTest; 36 import androidx.test.rule.ActivityTestRule; 37 import androidx.test.runner.AndroidJUnit4; 38 39 import com.android.compatibility.common.util.AdoptShellPermissionsRule; 40 41 import org.junit.Before; 42 import org.junit.Rule; 43 import org.junit.Test; 44 import org.junit.runner.RunWith; 45 46 @MediumTest 47 @RunWith(AndroidJUnit4.class) 48 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).") 49 public class FocusFinderTest { 50 private FocusFinder mFocusFinder; 51 private ViewGroup mLayout; 52 private Button mTopLeft; 53 private Button mTopRight; 54 private Button mBottomLeft; 55 private Button mBottomRight; 56 57 @Rule(order = 0) 58 public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule( 59 androidx.test.platform.app.InstrumentationRegistry 60 .getInstrumentation().getUiAutomation(), 61 Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); 62 63 @Rule(order = 1) 64 public ActivityTestRule<FocusFinderCtsActivity> mActivityRule = 65 new ActivityTestRule<>(FocusFinderCtsActivity.class); 66 67 @Before setup()68 public void setup() { 69 FocusFinderCtsActivity activity = mActivityRule.getActivity(); 70 71 mFocusFinder = FocusFinder.getInstance(); 72 mLayout = activity.layout; 73 mTopLeft = activity.topLeftButton; 74 mTopRight = activity.topRightButton; 75 mBottomLeft = activity.bottomLeftButton; 76 mBottomRight = activity.bottomRightButton; 77 mTopLeft.setNextFocusLeftId(View.NO_ID); 78 mTopRight.setNextFocusLeftId(View.NO_ID); 79 mBottomLeft.setNextFocusLeftId(View.NO_ID); 80 mBottomRight.setNextFocusLeftId(View.NO_ID); 81 } 82 83 @Test testGetInstance()84 public void testGetInstance() { 85 assertNotNull(mFocusFinder); 86 } 87 88 @Test testFindNextFocus()89 public void testFindNextFocus() throws Throwable { 90 /* 91 * Go clockwise around the buttons from the top left searching for focus. 92 * 93 * +---+---+ 94 * | 1 | 2 | 95 * +---+---+ 96 * | 3 | 4 | 97 * +---+---+ 98 */ 99 verifyNextFocus(mTopLeft, View.FOCUS_RIGHT, mTopRight); 100 verifyNextFocus(mTopRight, View.FOCUS_DOWN, mBottomRight); 101 verifyNextFocus(mBottomRight, View.FOCUS_LEFT, mBottomLeft); 102 verifyNextFocus(mBottomLeft, View.FOCUS_UP, mTopLeft); 103 104 verifyNextFocus(null, View.FOCUS_RIGHT, mTopLeft); 105 verifyNextFocus(null, View.FOCUS_DOWN, mTopLeft); 106 verifyNextFocus(null, View.FOCUS_LEFT, mBottomRight); 107 verifyNextFocus(null, View.FOCUS_UP, mBottomRight); 108 109 // Check that left/right traversal works when top/bottom borders are equal. 110 verifyNextFocus(mTopRight, View.FOCUS_LEFT, mTopLeft); 111 verifyNextFocus(mBottomLeft, View.FOCUS_RIGHT, mBottomRight); 112 113 // Edge-case where root has focus 114 mActivityRule.runOnUiThread(() -> { 115 mLayout.setFocusableInTouchMode(true); 116 verifyNextFocus(mLayout, View.FOCUS_FORWARD, mTopLeft); 117 }); 118 } 119 verifyNextFocus(View currentFocus, int direction, View expectedNextFocus)120 private void verifyNextFocus(View currentFocus, int direction, View expectedNextFocus) { 121 View actualNextFocus = mFocusFinder.findNextFocus(mLayout, currentFocus, direction); 122 assertEquals(expectedNextFocus, actualNextFocus); 123 } 124 125 @Test testFindNextFocusFromRect()126 public void testFindNextFocusFromRect() { 127 /* 128 * Create a small rectangle on the border between the top left and top right buttons. 129 * 130 * +---+---+ 131 * | [ ] | 132 * +---+---+ 133 * | | | 134 * +---+---+ 135 */ 136 int buttonHalfWidth = mTopLeft.getWidth() / 2; 137 Rect topRect = new Rect(mTopLeft.getLeft() + buttonHalfWidth, 138 mTopLeft.getTop(), 139 mTopLeft.getRight() + buttonHalfWidth, 140 mTopLeft.getBottom()); 141 142 verifytNextFocusFromRect(topRect, View.FOCUS_LEFT, mTopLeft); 143 verifytNextFocusFromRect(topRect, View.FOCUS_RIGHT, mTopRight); 144 145 /* 146 * Create a small rectangle on the border between the top left and bottom left buttons. 147 * 148 * +---+---+ 149 * | | | 150 * +[ ]+---+ 151 * | | | 152 * +---+---+ 153 */ 154 int buttonHalfHeight = mTopLeft.getHeight() / 2; 155 Rect leftRect = new Rect(mTopLeft.getLeft(), 156 mTopLeft.getTop() + buttonHalfHeight, 157 mTopLeft.getRight(), 158 mTopLeft.getBottom() + buttonHalfHeight); 159 160 verifytNextFocusFromRect(leftRect, View.FOCUS_UP, mTopLeft); 161 verifytNextFocusFromRect(leftRect, View.FOCUS_DOWN, mBottomLeft); 162 } 163 verifytNextFocusFromRect(Rect rect, int direction, View expectedNextFocus)164 private void verifytNextFocusFromRect(Rect rect, int direction, View expectedNextFocus) { 165 View actualNextFocus = mFocusFinder.findNextFocusFromRect(mLayout, rect, direction); 166 assertEquals(expectedNextFocus, actualNextFocus); 167 } 168 169 @Test testFindNearestTouchable()170 public void testFindNearestTouchable() { 171 /* 172 * Table layout with two rows and coordinates are relative to those parent rows. 173 * Lines outside the box signify touch points used in the tests. 174 * | 175 * +---+---+ 176 * | 1 | 2 |-- 177 * +---+---+ 178 * --| 3 | 4 | 179 * +---+---+ 180 * | 181 */ 182 183 // 1 184 int x = mTopLeft.getWidth() / 2 - 5; 185 int y = 0; 186 int[] deltas = new int[2]; 187 View view = mFocusFinder.findNearestTouchable(mLayout, x, y, View.FOCUS_DOWN, deltas); 188 assertEquals(mTopLeft, view); 189 assertEquals(0, deltas[0]); 190 assertEquals(0, deltas[1]); 191 192 // 2 193 deltas = new int[2]; 194 x = mTopRight.getRight(); 195 y = mTopRight.getBottom() / 2; 196 view = mFocusFinder.findNearestTouchable(mLayout, x, y, View.FOCUS_LEFT, deltas); 197 assertEquals(mTopRight, view); 198 assertEquals(-1, deltas[0]); 199 assertEquals(0, deltas[1]); 200 201 // 3 202 deltas = new int[2]; 203 x = 0; 204 y = mTopLeft.getBottom() + mBottomLeft.getHeight() / 2; 205 view = mFocusFinder.findNearestTouchable(mLayout, x, y, View.FOCUS_RIGHT, deltas); 206 assertEquals(mBottomLeft, view); 207 assertEquals(0, deltas[0]); 208 assertEquals(0, deltas[1]); 209 210 // 4 211 deltas = new int[2]; 212 x = mBottomRight.getRight(); 213 y = mTopRight.getBottom() + mBottomRight.getBottom(); 214 view = mFocusFinder.findNearestTouchable(mLayout, x, y, View.FOCUS_UP, deltas); 215 assertEquals(mBottomRight, view); 216 assertEquals(0, deltas[0]); 217 assertEquals(-1, deltas[1]); 218 } 219 220 @Test testFindNextAndPrevFocusAvoidingChain()221 public void testFindNextAndPrevFocusAvoidingChain() { 222 mBottomRight.setNextFocusForwardId(mBottomLeft.getId()); 223 mBottomLeft.setNextFocusForwardId(mTopRight.getId()); 224 // Follow the chain 225 verifyNextFocus(mBottomRight, View.FOCUS_FORWARD, mBottomLeft); 226 verifyNextFocus(mBottomLeft, View.FOCUS_FORWARD, mTopRight); 227 verifyNextFocus(mTopRight, View.FOCUS_BACKWARD, mBottomLeft); 228 verifyNextFocus(mBottomLeft, View.FOCUS_BACKWARD, mBottomRight); 229 230 // Now go to the one not in the chain 231 verifyNextFocus(mTopRight, View.FOCUS_FORWARD, mTopLeft); 232 verifyNextFocus(mBottomRight, View.FOCUS_BACKWARD, mTopLeft); 233 234 // Now go back to the top of the chain 235 verifyNextFocus(mTopLeft, View.FOCUS_FORWARD, mBottomRight); 236 verifyNextFocus(mTopLeft, View.FOCUS_BACKWARD, mTopRight); 237 238 // Now make the chain a circle -- this is the pathological case 239 mTopRight.setNextFocusForwardId(mBottomRight.getId()); 240 // Fall back to the next one in a chain. 241 verifyNextFocus(mTopLeft, View.FOCUS_FORWARD, mTopRight); 242 verifyNextFocus(mTopLeft, View.FOCUS_BACKWARD, mBottomRight); 243 244 //Now do branching focus changes 245 mTopRight.setNextFocusForwardId(View.NO_ID); 246 mBottomRight.setNextFocusForwardId(mTopRight.getId()); 247 verifyNextFocus(mBottomRight, View.FOCUS_FORWARD, mTopRight); 248 verifyNextFocus(mBottomLeft, View.FOCUS_FORWARD, mTopRight); 249 // From the tail, it jumps out of the chain 250 verifyNextFocus(mTopRight, View.FOCUS_FORWARD, mTopLeft); 251 252 // Back from the head of a tree goes out of the tree 253 // We don't know which is the head of the focus chain since it is branching. 254 View prevFocus1 = mFocusFinder.findNextFocus(mLayout, mBottomLeft, View.FOCUS_BACKWARD); 255 View prevFocus2 = mFocusFinder.findNextFocus(mLayout, mBottomRight, View.FOCUS_BACKWARD); 256 assertTrue(prevFocus1 == mTopLeft || prevFocus2 == mTopLeft); 257 258 // From outside, it chooses an arbitrary head of the chain 259 View nextFocus = mFocusFinder.findNextFocus(mLayout, mTopLeft, View.FOCUS_FORWARD); 260 assertTrue(nextFocus == mBottomRight || nextFocus == mBottomLeft); 261 262 // Going back from the tail of the split chain, it chooses an arbitrary head 263 nextFocus = mFocusFinder.findNextFocus(mLayout, mTopRight, View.FOCUS_BACKWARD); 264 assertTrue(nextFocus == mBottomRight || nextFocus == mBottomLeft); 265 } 266 267 @Test(timeout = 500) testChainVisibility()268 public void testChainVisibility() { 269 mBottomRight.setNextFocusForwardId(mBottomLeft.getId()); 270 mBottomLeft.setNextFocusForwardId(mTopRight.getId()); 271 mBottomLeft.setVisibility(View.INVISIBLE); 272 View next = mFocusFinder.findNextFocus(mLayout, mBottomRight, View.FOCUS_FORWARD); 273 assertSame(mTopRight, next); 274 275 mBottomLeft.setNextFocusForwardId(View.NO_ID); 276 next = mFocusFinder.findNextFocus(mLayout, mBottomRight, View.FOCUS_FORWARD); 277 assertSame(mTopLeft, next); 278 279 // This shouldn't go into an infinite loop 280 mBottomRight.setNextFocusForwardId(mTopRight.getId()); 281 mTopLeft.setNextFocusForwardId(mTopRight.getId()); 282 mTopRight.setNextFocusForwardId(mBottomLeft.getId()); 283 mBottomLeft.setNextFocusForwardId(mTopLeft.getId()); 284 mActivityRule.getActivity().runOnUiThread(() -> { 285 mTopLeft.setVisibility(View.INVISIBLE); 286 mTopRight.setVisibility(View.INVISIBLE); 287 mBottomLeft.setVisibility(View.INVISIBLE); 288 mBottomRight.setVisibility(View.INVISIBLE); 289 }); 290 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 291 mFocusFinder.findNextFocus(mLayout, mBottomRight, View.FOCUS_FORWARD); 292 } 293 verifyNextCluster(View currentCluster, int direction, View expectedNextCluster)294 private void verifyNextCluster(View currentCluster, int direction, View expectedNextCluster) { 295 View actualNextCluster = mFocusFinder.findNextKeyboardNavigationCluster( 296 mLayout, currentCluster, direction); 297 assertEquals(expectedNextCluster, actualNextCluster); 298 } 299 verifyNextClusterView(View currentCluster, int direction, View expectedNextView)300 private void verifyNextClusterView(View currentCluster, int direction, View expectedNextView) { 301 View actualNextView = mFocusFinder.findNextKeyboardNavigationCluster( 302 mLayout, currentCluster, direction); 303 if (actualNextView == mLayout) { 304 actualNextView = 305 mFocusFinder.findNextKeyboardNavigationCluster(mLayout, null, direction); 306 } 307 assertEquals(expectedNextView, actualNextView); 308 } 309 310 @Test testNoClusters()311 public void testNoClusters() { 312 // No views are marked as clusters, so next cluster is always null. 313 verifyNextCluster(mTopRight, View.FOCUS_FORWARD, null); 314 verifyNextCluster(mTopRight, View.FOCUS_BACKWARD, null); 315 } 316 317 @Test testFindNextCluster()318 public void testFindNextCluster() { 319 // Cluster navigation from all possible starting points in all directions. 320 mTopLeft.setKeyboardNavigationCluster(true); 321 mTopRight.setKeyboardNavigationCluster(true); 322 mBottomLeft.setKeyboardNavigationCluster(true); 323 324 verifyNextCluster(null, View.FOCUS_FORWARD, mTopLeft); 325 verifyNextCluster(mTopLeft, View.FOCUS_FORWARD, mTopRight); 326 verifyNextCluster(mTopRight, View.FOCUS_FORWARD, mBottomLeft); 327 verifyNextCluster(mBottomLeft, View.FOCUS_FORWARD, mLayout); 328 verifyNextCluster(mBottomRight, View.FOCUS_FORWARD, mLayout); 329 330 verifyNextCluster(null, View.FOCUS_BACKWARD, mBottomLeft); 331 verifyNextCluster(mTopLeft, View.FOCUS_BACKWARD, mLayout); 332 verifyNextCluster(mTopRight, View.FOCUS_BACKWARD, mTopLeft); 333 verifyNextCluster(mBottomLeft, View.FOCUS_BACKWARD, mTopRight); 334 verifyNextCluster(mBottomRight, View.FOCUS_BACKWARD, mLayout); 335 } 336 337 @Test testFindNextAndPrevClusterAvoidingChain()338 public void testFindNextAndPrevClusterAvoidingChain() { 339 // Basically a duplicate of normal focus test above. The same logic should be used for both. 340 mTopLeft.setKeyboardNavigationCluster(true); 341 mTopRight.setKeyboardNavigationCluster(true); 342 mBottomLeft.setKeyboardNavigationCluster(true); 343 mBottomRight.setKeyboardNavigationCluster(true); 344 mBottomRight.setNextClusterForwardId(mBottomLeft.getId()); 345 mBottomLeft.setNextClusterForwardId(mTopRight.getId()); 346 // Follow the chain 347 verifyNextCluster(mBottomRight, View.FOCUS_FORWARD, mBottomLeft); 348 verifyNextCluster(mBottomLeft, View.FOCUS_FORWARD, mTopRight); 349 verifyNextCluster(mTopRight, View.FOCUS_BACKWARD, mBottomLeft); 350 verifyNextCluster(mBottomLeft, View.FOCUS_BACKWARD, mBottomRight); 351 352 // Now go to the one not in the chain 353 verifyNextClusterView(mTopRight, View.FOCUS_FORWARD, mTopLeft); 354 verifyNextClusterView(mBottomRight, View.FOCUS_BACKWARD, mTopLeft); 355 356 // Now go back to the top of the chain 357 verifyNextClusterView(mTopLeft, View.FOCUS_FORWARD, mBottomRight); 358 verifyNextClusterView(mTopLeft, View.FOCUS_BACKWARD, mTopRight); 359 360 // Now make the chain a circle -- this is the pathological case 361 mTopRight.setNextClusterForwardId(mBottomRight.getId()); 362 // Fall back to the next one in a chain. 363 verifyNextClusterView(mTopLeft, View.FOCUS_FORWARD, mTopRight); 364 verifyNextClusterView(mTopLeft, View.FOCUS_BACKWARD, mBottomRight); 365 366 //Now do branching focus changes 367 mTopRight.setNextClusterForwardId(View.NO_ID); 368 mBottomRight.setNextClusterForwardId(mTopRight.getId()); 369 assertEquals(mBottomRight.getNextClusterForwardId(), mTopRight.getId()); 370 verifyNextClusterView(mBottomRight, View.FOCUS_FORWARD, mTopRight); 371 verifyNextClusterView(mBottomLeft, View.FOCUS_FORWARD, mTopRight); 372 // From the tail, it jumps out of the chain 373 verifyNextClusterView(mTopRight, View.FOCUS_FORWARD, mTopLeft); 374 375 // Back from the head of a tree goes out of the tree 376 // We don't know which is the head of the focus chain since it is branching. 377 View prevFocus1 = mFocusFinder.findNextKeyboardNavigationCluster(mLayout, mBottomLeft, 378 View.FOCUS_BACKWARD); 379 View prevFocus2 = mFocusFinder.findNextKeyboardNavigationCluster(mLayout, mBottomRight, 380 View.FOCUS_BACKWARD); 381 assertTrue(prevFocus1 == mTopLeft || prevFocus2 == mTopLeft); 382 383 // From outside, it chooses an arbitrary head of the chain 384 View nextFocus = mFocusFinder.findNextKeyboardNavigationCluster(mLayout, mTopLeft, 385 View.FOCUS_FORWARD); 386 assertTrue(nextFocus == mBottomRight || nextFocus == mBottomLeft); 387 388 // Going back from the tail of the split chain, it chooses an arbitrary head 389 nextFocus = mFocusFinder.findNextKeyboardNavigationCluster(mLayout, mTopRight, 390 View.FOCUS_BACKWARD); 391 assertTrue(nextFocus == mBottomRight || nextFocus == mBottomLeft); 392 } 393 394 @Test testDuplicateId()395 public void testDuplicateId() throws Throwable { 396 LayoutInflater inflater = mActivityRule.getActivity().getLayoutInflater(); 397 mLayout = (ViewGroup) mActivityRule.getActivity().findViewById(R.id.inflate_layout); 398 View[] buttons = new View[3]; 399 View[] boxes = new View[3]; 400 mActivityRule.runOnUiThread(() -> { 401 for (int i = 0; i < 3; ++i) { 402 View item = inflater.inflate(R.layout.focus_finder_sublayout, mLayout, false); 403 buttons[i] = item.findViewById(R.id.itembutton); 404 boxes[i] = item.findViewById(R.id.itembox); 405 mLayout.addView(item); 406 } 407 }); 408 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 409 410 verifyNextFocus(buttons[0], View.FOCUS_FORWARD, boxes[0]); 411 verifyNextFocus(boxes[0], View.FOCUS_FORWARD, buttons[1]); 412 verifyNextFocus(buttons[1], View.FOCUS_FORWARD, boxes[1]); 413 verifyNextFocus(boxes[1], View.FOCUS_FORWARD, buttons[2]); 414 } 415 416 @Test testBasicFocusOrder()417 public void testBasicFocusOrder() { 418 // Initial check to make sure sorter is behaving 419 FrameLayout layout = new FrameLayout(mLayout.getContext()); 420 Button button1 = new Button(mLayout.getContext()); 421 Button button2 = new Button(mLayout.getContext()); 422 setViewBox(button1, 0, 0, 10, 10); 423 setViewBox(button2, 0, 0, 10, 10); 424 layout.addView(button1); 425 layout.addView(button2); 426 View[] views = new View[]{button2, button1}; 427 // empty shouldn't crash or anything 428 FocusFinder.sort(views, 0, 0, layout, false); 429 // one view should work 430 FocusFinder.sort(views, 0, 1, layout, false); 431 assertEquals(button2, views[0]); 432 // exactly overlapping views should remain in original order 433 FocusFinder.sort(views, 0, 2, layout, false); 434 assertEquals(button2, views[0]); 435 assertEquals(button1, views[1]); 436 // make sure it will actually mutate input array. 437 setViewBox(button2, 20, 0, 30, 10); 438 FocusFinder.sort(views, 0, 2, layout, false); 439 assertEquals(button1, views[0]); 440 assertEquals(button2, views[1]); 441 442 // While we don't want to test details, we should at least verify basic correctness 443 // like "left-to-right" ordering in well-behaved layouts 444 verifyNextFocus(mTopLeft, View.FOCUS_FORWARD, mTopRight); 445 verifyNextFocus(mTopRight, View.FOCUS_FORWARD, mBottomLeft); 446 verifyNextFocus(mBottomLeft, View.FOCUS_FORWARD, mBottomRight); 447 448 // Should still work intuitively even if some views are slightly shorter. 449 mBottomLeft.setBottom(mBottomLeft.getBottom() - 3); 450 mBottomLeft.offsetTopAndBottom(3); 451 verifyNextFocus(mTopLeft, View.FOCUS_FORWARD, mTopRight); 452 verifyNextFocus(mTopRight, View.FOCUS_FORWARD, mBottomLeft); 453 verifyNextFocus(mBottomLeft, View.FOCUS_FORWARD, mBottomRight); 454 455 // RTL layout should work right-to-left 456 mActivityRule.getActivity().runOnUiThread( 457 () -> mLayout.setLayoutDirection(View.LAYOUT_DIRECTION_RTL)); 458 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 459 verifyNextFocus(mTopLeft, View.FOCUS_FORWARD, mTopRight); 460 verifyNextFocus(mTopRight, View.FOCUS_FORWARD, mBottomLeft); 461 verifyNextFocus(mBottomLeft, View.FOCUS_FORWARD, mBottomRight); 462 } 463 setViewBox(View view, int left, int top, int right, int bottom)464 private void setViewBox(View view, int left, int top, int right, int bottom) { 465 view.setLeft(left); 466 view.setTop(top); 467 view.setRight(right); 468 view.setBottom(bottom); 469 } 470 } 471