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
17package android.view;
18
19import android.graphics.Rect;
20import android.test.AndroidTestCase;
21import android.test.suitebuilder.annotation.SmallTest;
22
23public class FocusFinderTest extends AndroidTestCase {
24
25    private FocusFinderHelper mFocusFinder;
26
27    @Override
28    protected void setUp() throws Exception {
29        super.setUp();
30
31        mFocusFinder = new FocusFinderHelper(FocusFinder.getInstance());
32    }
33
34    @SmallTest
35    public void testPreconditions() {
36        assertNotNull("focus finder instance", mFocusFinder);
37    }
38
39    @SmallTest
40    public void testBelowNotCandidateForDirectionUp() {
41        assertIsNotCandidate(View.FOCUS_UP,
42                new Rect(0, 30, 10, 40),  // src  (left, top, right, bottom)
43                new Rect(0, 50, 10, 60));  // dest (left, top, right, bottom)
44    }
45
46    @SmallTest
47    public void testAboveShareEdgeEdgeOkForDirectionUp() {
48        final Rect src = new Rect(0, 30, 10, 40);
49
50        final Rect dest = new Rect(src);
51        dest.offset(0, -src.height());
52        assertEquals(src.top, dest.bottom);
53
54        assertDirectionIsCandidate(View.FOCUS_UP, src, dest);
55    }
56
57    @SmallTest
58    public void testCompletelyContainedNotCandidate() {
59        assertIsNotCandidate(
60                View.FOCUS_DOWN,
61                //       L  T   R   B
62                new Rect(0, 0,  50, 50),
63                new Rect(0, 1,  50, 49));
64    }
65
66    @SmallTest
67    public void testContinaedWithCommonBottomNotCandidate() {
68        assertIsNotCandidate(
69                View.FOCUS_DOWN,
70                //       L  T   R   B
71                new Rect(0, 0,  50, 50),
72                new Rect(0, 1,  50, 50));
73    }
74
75    @SmallTest
76    public void testOverlappingIsCandidateWhenBothEdgesAreInDirection() {
77        assertDirectionIsCandidate(
78                View.FOCUS_DOWN,
79                //       L  T   R   B
80                new Rect(0, 0,  50, 50),
81                new Rect(0, 1,  50, 51));
82    }
83
84    @SmallTest
85    public void testTopEdgeOfDestAtOrAboveTopOfSrcNotCandidateForDown() {
86        assertIsNotCandidate(
87                View.FOCUS_DOWN,
88                //       L  T   R   B
89                new Rect(0, 0,  50, 50),
90                new Rect(0, 0,  50, 51));
91        assertIsNotCandidate(
92                View.FOCUS_DOWN,
93                //       L  T   R   B
94                new Rect(0, 0,  50, 50),
95                new Rect(0, -1, 50, 51));
96    }
97
98    @SmallTest
99    public void testSameRectBeamsOverlap() {
100        final Rect rect = new Rect(0, 0, 20, 20);
101
102        assertBeamsOverlap(View.FOCUS_LEFT, rect, rect);
103        assertBeamsOverlap(View.FOCUS_RIGHT, rect, rect);
104        assertBeamsOverlap(View.FOCUS_UP, rect, rect);
105        assertBeamsOverlap(View.FOCUS_DOWN, rect, rect);
106    }
107
108    @SmallTest
109    public void testOverlapBeamsRightLeftUpToEdge() {
110        final Rect rect1 = new Rect(0, 0, 20, 20);
111        final Rect rect2 = new Rect(rect1);
112
113        // just below bottom edge
114        rect2.offset(0, rect1.height() - 1);
115        assertBeamsOverlap(View.FOCUS_LEFT, rect1, rect2);
116        assertBeamsOverlap(View.FOCUS_RIGHT, rect1, rect2);
117
118        // at edge
119        rect2.offset(0, 1);
120        assertBeamsOverlap(View.FOCUS_LEFT, rect1, rect2);
121        assertBeamsOverlap(View.FOCUS_RIGHT, rect1, rect2);
122
123        // just beyond
124        rect2.offset(0, 1);
125        assertBeamsDontOverlap(View.FOCUS_LEFT, rect1, rect2);
126        assertBeamsDontOverlap(View.FOCUS_RIGHT, rect1, rect2);
127
128        // just below top edge
129        rect2.set(rect1);
130        rect2.offset(0, -(rect1.height() - 1));
131        assertBeamsOverlap(View.FOCUS_LEFT, rect1, rect2);
132        assertBeamsOverlap(View.FOCUS_RIGHT, rect1, rect2);
133
134        // at top edge
135        rect2.offset(0, -1);
136        assertBeamsOverlap(View.FOCUS_LEFT, rect1, rect2);
137        assertBeamsOverlap(View.FOCUS_RIGHT, rect1, rect2);
138
139        // just beyond top edge
140        rect2.offset(0, -1);
141        assertBeamsDontOverlap(View.FOCUS_LEFT, rect1, rect2);
142        assertBeamsDontOverlap(View.FOCUS_RIGHT, rect1, rect2);
143    }
144
145    @SmallTest
146    public void testOverlapBeamsUpDownUpToEdge() {
147        final Rect rect1 = new Rect(0, 0, 20, 20);
148        final Rect rect2 = new Rect(rect1);
149
150        // just short of right edge
151        rect2.offset(rect1.width() - 1, 0);
152        assertBeamsOverlap(View.FOCUS_UP, rect1, rect2);
153        assertBeamsOverlap(View.FOCUS_DOWN, rect1, rect2);
154
155        // at edge
156        rect2.offset(1, 0);
157        assertBeamsOverlap(View.FOCUS_UP, rect1, rect2);
158        assertBeamsOverlap(View.FOCUS_DOWN, rect1, rect2);
159
160        // just beyond
161        rect2.offset(1, 0);
162        assertBeamsDontOverlap(View.FOCUS_UP, rect1, rect2);
163        assertBeamsDontOverlap(View.FOCUS_DOWN, rect1, rect2);
164
165        // just short of left edge
166        rect2.set(rect1);
167        rect2.offset(-(rect1.width() - 1), 0);
168        assertBeamsOverlap(View.FOCUS_UP, rect1, rect2);
169        assertBeamsOverlap(View.FOCUS_DOWN, rect1, rect2);
170
171        // at edge
172        rect2.offset(-1, 0);
173        assertBeamsOverlap(View.FOCUS_UP, rect1, rect2);
174        assertBeamsOverlap(View.FOCUS_DOWN, rect1, rect2);
175
176        // just beyond edge
177        rect2.offset(-1, 0);
178        assertBeamsDontOverlap(View.FOCUS_UP, rect1, rect2);
179        assertBeamsDontOverlap(View.FOCUS_DOWN, rect1, rect2);
180    }
181
182    @SmallTest
183    public void testDirectlyAboveTrumpsAboveLeft() {
184        Rect src = new Rect(0, 50, 20, 70);  // src (left, top, right, bottom)
185
186        Rect directlyAbove = new Rect(src);
187        directlyAbove.offset(0, -(1 + src.height()));
188
189        Rect aboveLeft = new Rect(src);
190        aboveLeft.offset(-(1 + src.width()), -(1 + src.height()));
191
192        assertBetterCandidate(View.FOCUS_UP, src, directlyAbove, aboveLeft);
193    }
194
195    @SmallTest
196    public void testAboveInBeamTrumpsSlightlyCloserOutOfBeam() {
197        Rect src = new Rect(0, 50, 20, 70);  // src (left, top, right, bottom)
198
199        Rect directlyAbove = new Rect(src);
200        directlyAbove.offset(0, -(1 + src.height()));
201
202        Rect aboveLeft = new Rect(src);
203        aboveLeft.offset(-(1 + src.width()), -(1 + src.height()));
204
205        // offset directly above a little further up
206        directlyAbove.offset(0, -5);
207        assertBetterCandidate(View.FOCUS_UP, src, directlyAbove, aboveLeft);
208    }
209
210    @SmallTest
211    public void testOutOfBeamBeatsInBeamUp() {
212
213        Rect src = new Rect(0, 0, 50, 50); // (left, top, right, bottom)
214
215        Rect aboveLeftOfBeam = new Rect(src);
216        aboveLeftOfBeam.offset(-(src.width() + 1), -src.height());
217        assertBeamsDontOverlap(View.FOCUS_UP, src, aboveLeftOfBeam);
218
219        Rect aboveInBeam = new Rect(src);
220        aboveInBeam.offset(0, -src.height());
221        assertBeamsOverlap(View.FOCUS_UP, src, aboveInBeam);
222
223        // in beam wins
224        assertBetterCandidate(View.FOCUS_UP, src, aboveInBeam, aboveLeftOfBeam);
225
226        // still wins while aboveInBeam's bottom edge is < out of beams' top
227        aboveInBeam.offset(0, -(aboveLeftOfBeam.height() - 1));
228        assertTrue("aboveInBeam.bottom > aboveLeftOfBeam.top", aboveInBeam.bottom > aboveLeftOfBeam.top);
229        assertBetterCandidate(View.FOCUS_UP, src, aboveInBeam, aboveLeftOfBeam);
230
231        // cross the threshold: the out of beam prevails
232        aboveInBeam.offset(0, -1);
233        assertEquals(aboveInBeam.bottom, aboveLeftOfBeam.top);
234        assertBetterCandidate(View.FOCUS_UP, src, aboveLeftOfBeam, aboveInBeam);
235    }
236
237    /**
238     * A non-candidate (even a much closer one) is always a worse choice
239     * than a real candidate.
240     */
241    @SmallTest
242    public void testSomeCandidateBetterThanNonCandidate() {
243        Rect src = new Rect(0, 0, 50, 50); // (left, top, right, bottom)
244
245        Rect nonCandidate = new Rect(src);
246        nonCandidate.offset(src.width() + 1, 0);
247
248        assertIsNotCandidate(View.FOCUS_LEFT, src, nonCandidate);
249
250        Rect candidate = new Rect(src);
251        candidate.offset(-(4 * src.width()), 0);
252        assertDirectionIsCandidate(View.FOCUS_LEFT, src, candidate);
253
254        assertBetterCandidate(View.FOCUS_LEFT, src, candidate, nonCandidate);
255    }
256
257    /**
258     * Grabbed from {@link android.widget.focus.VerticalFocusSearchTest#testSearchFromMidLeft()}
259     */
260    @SmallTest
261    public void testVerticalFocusSearchScenario() {
262        assertBetterCandidate(View.FOCUS_DOWN,
263                //       L    T    R    B
264                new Rect(0,   109, 153, 169),   // src
265                new Rect(166, 169, 319, 229),  // expectedbetter
266                new Rect(0,   229, 320, 289)); // expectedworse
267
268        // failing test 4/10/2008, the values were tweaked somehow in functional
269        // test...
270        assertBetterCandidate(View.FOCUS_DOWN,
271                //       L    T    R    B
272                new Rect(0,   91, 153, 133),   // src
273                new Rect(166, 133, 319, 175),  // expectedbetter
274                new Rect(0,   175, 320, 217)); // expectedworse
275
276    }
277
278    /**
279     * Example: going down from a thin button all the way to the left of a
280     * screen where, just below, is a very wide button, and just below that,
281     * is an equally skinny button all the way to the left.  want to make
282     * sure any minor axis factor doesn't override the fact that the one below
283     * in vertical beam should be next focus
284     */
285    @SmallTest
286    public void testBeamsOverlapMajorAxisCloserMinorAxisFurther() {
287        assertBetterCandidate(View.FOCUS_DOWN,
288                //       L   T    R    B
289                new Rect(0,  0,   100,  100),  // src
290                new Rect(0,  100, 480,  200),  // expectedbetter
291                new Rect(0,  200, 100,  300)); // expectedworse
292    }
293
294    /**
295     * Real scenario grabbed from song playback screen.
296     */
297    @SmallTest
298    public void testMusicPlaybackScenario() {
299        assertBetterCandidate(View.FOCUS_LEFT,
300                //       L    T    R    B
301                new Rect(227, 185, 312, 231),   // src
302                new Rect(195, 386, 266, 438),   // expectedbetter
303                new Rect(124, 386, 195, 438));  // expectedworse
304    }
305
306    /**
307     * more generalized version of {@link #testMusicPlaybackScenario()}
308     */
309    @SmallTest
310    public void testOutOfBeamOverlapBeatsOutOfBeamFurtherOnMajorAxis() {
311        assertBetterCandidate(View.FOCUS_DOWN,
312                //       L    T    R    B
313                new Rect(0,   0,   50,  50),   // src
314                new Rect(60,  40,  110, 90),   // expectedbetter
315                new Rect(60,  70,  110, 120));  // expectedworse
316    }
317
318    /**
319     * Make sure that going down prefers views that are actually
320     * down (and not those next to but still a candidate because
321     * they are overlapping on the major axis)
322     */
323    @SmallTest
324    public void testInBeamTrumpsOutOfBeamOverlapping() {
325        assertBetterCandidate(View.FOCUS_DOWN,
326                //       L    T    R    B
327                new Rect(0,   0,   50,  50),   // src
328                new Rect(0,   60,  50,  110),  // expectedbetter
329                new Rect(51,  1,   101, 51)); // expectedworse
330    }
331
332    @SmallTest
333    public void testOverlappingBeatsNonOverlapping() {
334        assertBetterCandidate(View.FOCUS_DOWN,
335                //       L    T    R    B
336                new Rect(0,   0,   50,  50),   // src
337                new Rect(0,   40,  50,  90),   // expectedbetter
338                new Rect(0,   75,  50,  125)); // expectedworse
339    }
340
341    @SmallTest
342    public void testEditContactScenarioLeftFromDiscardChangesGoesToSaveContactInLandscape() {
343        assertBetterCandidate(View.FOCUS_LEFT,
344                //       L    T    R    B
345                new Rect(357, 258, 478, 318),  // src
346                new Rect(2,   258, 100, 318),  // better
347                new Rect(106, 120, 424, 184)); // worse
348    }
349
350    /**
351     * A dial pad with 9 squares arranged in a grid.  no padding, so
352     * the edges are equal.  see {@link android.widget.focus.LinearLayoutGrid}
353     */
354    @SmallTest
355    public void testGridWithTouchingEdges() {
356        assertBetterCandidate(View.FOCUS_DOWN,
357                //       L    T    R    B
358                new Rect(106, 49,  212, 192),  // src
359                new Rect(106, 192, 212, 335),  // better
360                new Rect(0,   192, 106, 335)); // worse
361
362        assertBetterCandidate(View.FOCUS_DOWN,
363                //       L    T    R    B
364                new Rect(106, 49,  212, 192),  // src
365                new Rect(106, 192, 212, 335),  // better
366                new Rect(212, 192, 318, 335)); // worse
367    }
368
369    @SmallTest
370    public void testSearchFromEmptyRect() {
371        assertBetterCandidate(View.FOCUS_DOWN,
372                //       L   T    R    B
373                new Rect(0,  0,   0,   0),    // src
374                new Rect(0,  0,   320, 45),   // better
375                new Rect(0,  45,  320, 545)); // worse
376    }
377
378    /**
379     * Reproduce bug 1124559, drilling down to actual bug
380     * (majorAxisDistance was wrong for direction left)
381     */
382    @SmallTest
383    public void testGmailReplyButtonsScenario() {
384        assertBetterCandidate(View.FOCUS_LEFT,
385                //       L    T    R    B
386                new Rect(223, 380, 312, 417),  // src
387                new Rect(102, 380, 210, 417),  // better
388                new Rect(111, 443, 206, 480)); // worse
389
390        assertBeamBeats(View.FOCUS_LEFT,
391            //       L    T    R    B
392            new Rect(223, 380, 312, 417),  // src
393            new Rect(102, 380, 210, 417),  // better
394            new Rect(111, 443, 206, 480)); // worse
395
396        assertBeamsOverlap(View.FOCUS_LEFT,
397                //       L    T    R    B
398                new Rect(223, 380, 312, 417),
399                new Rect(102, 380, 210, 417));
400
401        assertBeamsDontOverlap(View.FOCUS_LEFT,
402                //       L    T    R    B
403                new Rect(223, 380, 312, 417),
404                new Rect(111, 443, 206, 480));
405
406        assertTrue(
407                "major axis distance less than major axis distance to "
408                        + "far edge",
409                FocusFinderHelper.majorAxisDistance(View.FOCUS_LEFT,
410                        //       L    T    R    B
411                        new Rect(223, 380, 312, 417),
412                        new Rect(102, 380, 210, 417)) <
413                FocusFinderHelper.majorAxisDistanceToFarEdge(View.FOCUS_LEFT,
414                        //       L    T    R    B
415                        new Rect(223, 380, 312, 417),
416                        new Rect(111, 443, 206, 480)));
417    }
418
419    @SmallTest
420    public void testGmailScenarioBug1203288() {
421        assertBetterCandidate(View.FOCUS_DOWN,
422                //       L    T    R    B
423                new Rect(0,   2,   480, 82),   // src
424                new Rect(344, 87,  475, 124),  // better
425                new Rect(0,   130, 480, 203)); // worse
426    }
427
428    @SmallTest
429    public void testHomeShortcutScenarioBug1295354() {
430        assertBetterCandidate(View.FOCUS_RIGHT,
431                //       L    T    R    B
432                new Rect(3, 338, 77, 413),   // src
433                new Rect(163, 338, 237, 413),  // better
434                new Rect(83, 38, 157, 113)); // worse
435    }
436
437    @SmallTest
438    public void testBeamAlwaysBeatsHoriz() {
439        assertBetterCandidate(View.FOCUS_RIGHT,
440                //       L    T    R    B
441                new Rect(0,   0,   50,  50),   // src
442                new Rect(150, 0,   200, 50),   // better, (way further, but in beam)
443                new Rect(60,  51,  110, 101)); // worse, even though it is closer
444
445        assertBetterCandidate(View.FOCUS_LEFT,
446                //       L    T    R    B
447                new Rect(150, 0,   200,  50),   // src
448                new Rect(0,   50,  50,   50),   // better, (way further, but in beam)
449                new Rect(49,  99,  149,  101)); // worse, even though it is closer
450    }
451
452    @SmallTest
453    public void testIsCandidateOverlappingEdgeFromEmptyRect() {
454        assertDirectionIsCandidate(View.FOCUS_DOWN,
455                //       L   T    R    B
456                new Rect(0,  0,   0,   0),   // src
457                new Rect(0,  0,   20,  1));  // candidate
458
459        assertDirectionIsCandidate(View.FOCUS_UP,
460                //       L   T    R    B
461                new Rect(0,  0,   0,   0),   // src
462                new Rect(0,  -1,  20,  0));  // candidate
463
464        assertDirectionIsCandidate(View.FOCUS_LEFT,
465                //       L   T    R    B
466                new Rect(0,  0,   0,   0),    // src
467                new Rect(-1,  0,  0,   20));  // candidate
468
469        assertDirectionIsCandidate(View.FOCUS_RIGHT,
470                //       L   T    R    B
471                new Rect(0,  0,   0,   0),    // src
472                new Rect(0,  0,   1,   20));  // candidate
473    }
474
475    private void assertBeamsOverlap(int direction, Rect rect1, Rect rect2) {
476        String directionStr = validateAndGetStringFor(direction);
477        String assertMsg = String.format("Expected beams to overlap in direction %s "
478                + "for rectangles %s and %s", directionStr, rect1, rect2);
479        assertTrue(assertMsg, mFocusFinder.beamsOverlap(direction, rect1, rect2));
480    }
481
482    private void assertBeamsDontOverlap(int direction, Rect rect1, Rect rect2) {
483        String directionStr = validateAndGetStringFor(direction);
484        String assertMsg = String.format("Expected beams not to overlap in direction %s "
485                + "for rectangles %s and %s", directionStr, rect1, rect2);
486        assertFalse(assertMsg, mFocusFinder.beamsOverlap(direction, rect1, rect2));
487    }
488
489    /**
490     * Assert that particular rect is a better focus search candidate from a
491     * source rect than another.
492     * @param direction The direction of focus search.
493     * @param srcRect The src rectangle.
494     * @param expectedBetter The candidate that should be better.
495     * @param expectedWorse The candidate that should be worse.
496     */
497    private void assertBetterCandidate(int direction, Rect srcRect,
498            Rect expectedBetter, Rect expectedWorse) {
499
500        String directionStr = validateAndGetStringFor(direction);
501        String assertMsg = String.format(
502                "expected %s to be a better focus search candidate than "
503                        + "%s when searching "
504                        + "from %s in direction %s",
505                expectedBetter, expectedWorse, srcRect, directionStr);
506
507        assertTrue(assertMsg,
508                mFocusFinder.isBetterCandidate(direction, srcRect,
509                        expectedBetter, expectedWorse));
510
511        assertMsg = String.format(
512                "expected %s to not be a better focus search candidate than "
513                        + "%s when searching "
514                        + "from %s in direction %s",
515                expectedWorse, expectedBetter, srcRect, directionStr);
516
517        assertFalse(assertMsg,
518                mFocusFinder.isBetterCandidate(direction, srcRect,
519                        expectedWorse, expectedBetter));
520    }
521
522    private void assertIsNotCandidate(int direction, Rect src, Rect dest) {
523        String directionStr = validateAndGetStringFor(direction);
524
525        final String assertMsg = String.format(
526                "expected going from %s to %s in direction %s to be an invalid "
527                        + "focus search candidate",
528                src, dest, directionStr);
529        assertFalse(assertMsg, mFocusFinder.isCandidate(src, dest, direction));
530    }
531
532    private void assertBeamBeats(int direction, Rect srcRect,
533            Rect rect1, Rect rect2) {
534
535        String directionStr = validateAndGetStringFor(direction);
536        String assertMsg = String.format(
537                "expecting %s to beam beat %s w.r.t %s in direction %s",
538                rect1, rect2, srcRect, directionStr);
539        assertTrue(assertMsg, mFocusFinder.beamBeats(direction, srcRect, rect1, rect2));
540    }
541
542
543    private void assertDirectionIsCandidate(int direction, Rect src, Rect dest) {
544        String directionStr = validateAndGetStringFor(direction);
545
546        final String assertMsg = String.format(
547                "expected going from %s to %s in direction %s to be a valid "
548                        + "focus search candidate",
549                src, dest, directionStr);
550        assertTrue(assertMsg, mFocusFinder.isCandidate(src, dest, direction));
551    }
552
553    private String validateAndGetStringFor(int direction) {
554        String directionStr = "??";
555        switch(direction) {
556            case View.FOCUS_UP:
557                directionStr = "FOCUS_UP";
558                break;
559            case View.FOCUS_DOWN:
560                directionStr = "FOCUS_DOWN";
561                break;
562            case View.FOCUS_LEFT:
563                directionStr = "FOCUS_LEFT";
564                break;
565            case View.FOCUS_RIGHT:
566                directionStr = "FOCUS_RIGHT";
567                break;
568            default:
569                fail("passed in unknown direction, ya blewit!");
570        }
571        return directionStr;
572    }
573
574
575}
576