xref: /aosp_15_r20/cts/tests/tests/view/src/android/view/cts/FocusFinderTest.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
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