1 /*
2  * Copyright (C) 2016 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.assertFalse;
21 import static org.junit.Assert.assertTrue;
22 
23 import android.app.Activity;
24 import android.app.Instrumentation;
25 import android.os.SystemClock;
26 import android.support.test.InstrumentationRegistry;
27 import android.support.test.filters.LargeTest;
28 import android.support.test.rule.ActivityTestRule;
29 import android.support.test.runner.AndroidJUnit4;
30 import android.util.Log;
31 import android.view.Gravity;
32 import android.view.InputDevice;
33 import android.view.KeyEvent;
34 import android.view.MotionEvent;
35 import android.view.View;
36 import android.view.ViewConfiguration;
37 import android.view.ViewGroup;
38 import android.widget.PopupWindow;
39 import android.widget.TextView;
40 
41 import com.android.compatibility.common.util.CtsTouchUtils;
42 import com.android.compatibility.common.util.PollingCheck;
43 
44 import org.junit.Before;
45 import org.junit.Rule;
46 import org.junit.Test;
47 import org.junit.runner.RunWith;
48 
49 /**
50  * Test {@link View}.
51  */
52 @LargeTest
53 @RunWith(AndroidJUnit4.class)
54 public class TooltipTest {
55     private static final String LOG_TAG = "TooltipTest";
56 
57     private static final long TIMEOUT_DELTA = 10000;
58     private static final long WAIT_MARGIN = 100;
59 
60     private Instrumentation mInstrumentation;
61     private Activity mActivity;
62     private ViewGroup mTopmostView;
63     private ViewGroup mGroupView;
64     private View mNoTooltipView;
65     private View mTooltipView;
66     private View mNoTooltipView2;
67     private View mEmptyGroup;
68 
69     @Rule
70     public ActivityTestRule<TooltipActivity> mActivityRule =
71             new ActivityTestRule<>(TooltipActivity.class);
72 
73     @Rule
74     public ActivityTestRule<CtsActivity> mCtsActivityRule =
75             new ActivityTestRule<>(CtsActivity.class, false, false);
76 
77     @Before
setup()78     public void setup() {
79         mInstrumentation = InstrumentationRegistry.getInstrumentation();
80         mActivity = mActivityRule.getActivity();
81         mTopmostView = (ViewGroup) mActivity.findViewById(R.id.tooltip_layout);
82         mGroupView = (ViewGroup) mActivity.findViewById(R.id.tooltip_group);
83         mNoTooltipView = mActivity.findViewById(R.id.no_tooltip);
84         mTooltipView = mActivity.findViewById(R.id.has_tooltip);
85         mNoTooltipView2 = mActivity.findViewById(R.id.no_tooltip2);
86         mEmptyGroup = mActivity.findViewById(R.id.empty_group);
87 
88         PollingCheck.waitFor(TIMEOUT_DELTA, mActivity::hasWindowFocus);
89     }
90 
waitOut(long msDelay)91     private void waitOut(long msDelay) {
92         try {
93             Thread.sleep(msDelay + WAIT_MARGIN);
94         } catch (InterruptedException e) {
95             Log.e(LOG_TAG, "Wait interrupted. Test may fail!", e);
96         }
97     }
98 
setTooltipText(View view, CharSequence tooltipText)99     private void setTooltipText(View view, CharSequence tooltipText) throws Throwable {
100         mActivityRule.runOnUiThread(() -> view.setTooltipText(tooltipText));
101     }
102 
hasTooltip(View view)103     private boolean hasTooltip(View view) {
104         final View tooltipView = view.getTooltipView();
105         return tooltipView != null && tooltipView.getParent() != null;
106     }
107 
108 
addView(ViewGroup parent, View view)109     private void addView(ViewGroup parent, View view) throws Throwable {
110         mActivityRule.runOnUiThread(() -> parent.addView(view));
111         mInstrumentation.waitForIdleSync();
112     }
113 
removeView(View view)114     private void removeView(View view) throws Throwable {
115         mActivityRule.runOnUiThread(() -> ((ViewGroup) (view.getParent())).removeView(view));
116         mInstrumentation.waitForIdleSync();
117     }
118 
setVisibility(View view, int visibility)119     private void setVisibility(View view, int visibility) throws Throwable {
120         mActivityRule.runOnUiThread(() -> view.setVisibility(visibility));
121     }
122 
setClickable(View view)123     private void setClickable(View view) throws Throwable {
124         mActivityRule.runOnUiThread(() -> view.setClickable(true));
125     }
126 
setLongClickable(View view)127     private void setLongClickable(View view) throws Throwable {
128         mActivityRule.runOnUiThread(() -> view.setLongClickable(true));
129     }
130 
setContextClickable(View view)131     private void setContextClickable(View view) throws Throwable {
132         mActivityRule.runOnUiThread(() -> view.setContextClickable(true));
133     }
134 
callPerformLongClick(View view)135     private void callPerformLongClick(View view) throws Throwable {
136         mActivityRule.runOnUiThread(() -> view.performLongClick(0, 0));
137     }
138 
requestLowProfileSystemUi()139     private void requestLowProfileSystemUi() throws Throwable {
140         final int flag = View.SYSTEM_UI_FLAG_LOW_PROFILE;
141         mActivityRule.runOnUiThread(() -> mTooltipView.setSystemUiVisibility(flag));
142         PollingCheck.waitFor(TIMEOUT_DELTA,
143                 () -> (mTooltipView.getWindowSystemUiVisibility() & flag) == flag);
144     }
145 
injectKeyPress(View target, int keyCode, int duration)146     private void injectKeyPress(View target, int keyCode, int duration) throws Throwable {
147         if (target != null) {
148             mActivityRule.runOnUiThread(() -> {
149                 target.setFocusableInTouchMode(true);
150                 target.requestFocus();
151             });
152             mInstrumentation.waitForIdleSync();
153             assertTrue(target.isFocused());
154         }
155         mInstrumentation.sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
156         waitOut(duration);
157         mInstrumentation.sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
158     }
159 
injectArbitraryShortKeyPress()160     private void injectArbitraryShortKeyPress() throws Throwable {
161         injectKeyPress(null, KeyEvent.KEYCODE_0, 0);
162     }
163 
injectLongKeyPress(View target, int keyCode)164     private void injectLongKeyPress(View target, int keyCode) throws Throwable {
165         injectKeyPress(target, keyCode, ViewConfiguration.getLongPressTimeout());
166     }
167 
injectLongEnter(View target)168     private void injectLongEnter(View target) throws Throwable {
169         injectLongKeyPress(target, KeyEvent.KEYCODE_ENTER);
170     }
171 
injectShortClick(View target)172     private void injectShortClick(View target) {
173         CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, target);
174     }
175 
injectLongClick(View target)176     private void injectLongClick(View target) {
177         CtsTouchUtils.emulateLongPressOnView(mInstrumentation, target,
178                 target.getWidth() / 2, target.getHeight() / 2);
179     }
180 
injectMotionEvent(MotionEvent event)181     private void injectMotionEvent(MotionEvent event) {
182         mInstrumentation.sendPointerSync(event);
183     }
184 
injectHoverMove(int source, View target, int offsetX, int offsetY)185     private void injectHoverMove(int source, View target, int offsetX, int offsetY) {
186         injectMotionEvent(obtainMotionEvent(
187                     source, target, MotionEvent.ACTION_HOVER_MOVE, offsetX,  offsetY));
188     }
189 
injectHoverMove(View target, int offsetX, int offsetY)190     private void injectHoverMove(View target, int offsetX, int offsetY) {
191         injectHoverMove(InputDevice.SOURCE_MOUSE, target, offsetX,  offsetY);
192     }
193 
injectHoverMove(View target)194     private void injectHoverMove(View target) {
195         injectHoverMove(target, 0, 0);
196     }
197 
injectLongHoverMove(View target)198     private void injectLongHoverMove(View target) {
199         injectHoverMove(target);
200         waitOut(ViewConfiguration.getHoverTooltipShowTimeout());
201     }
202 
obtainMouseEvent(View target, int action, int offsetX, int offsetY)203     private static MotionEvent obtainMouseEvent(View target, int action, int offsetX, int offsetY) {
204         return obtainMotionEvent(InputDevice.SOURCE_MOUSE, target, action, offsetX, offsetY);
205     }
206 
obtainMotionEvent( int source, View target, int action, int offsetX, int offsetY)207     private static MotionEvent obtainMotionEvent(
208                 int source, View target, int action, int offsetX, int offsetY) {
209         final long eventTime = SystemClock.uptimeMillis();
210         final int[] xy = new int[2];
211         target.getLocationOnScreen(xy);
212         MotionEvent event = MotionEvent.obtain(eventTime, eventTime, action,
213                 xy[0] + target.getWidth() / 2 + offsetX, xy[1] + target.getHeight() / 2 + offsetY,
214                 0);
215         event.setSource(source);
216         return event;
217     }
218 
219     @Test
testGetSetTooltip()220     public void testGetSetTooltip() throws Throwable {
221         // No tooltip set in resource
222         assertEquals(null, mNoTooltipView.getTooltipText());
223 
224         // Set the tooltip, read it back
225         final String tooltipText1 = "new tooltip";
226         setTooltipText(mNoTooltipView, tooltipText1);
227         assertEquals(tooltipText1, mNoTooltipView.getTooltipText());
228 
229         // Clear the tooltip.
230         setTooltipText(mNoTooltipView, null);
231         assertEquals(null, mNoTooltipView.getTooltipText());
232 
233         // Check the tooltip set in resource
234         assertEquals("tooltip text", mTooltipView.getTooltipText());
235 
236         // Clear the tooltip set in resource
237         setTooltipText(mTooltipView, null);
238         assertEquals(null, mTooltipView.getTooltipText());
239 
240         // Set the tooltip again, read it back
241         final String tooltipText2 = "new tooltip 2";
242         setTooltipText(mTooltipView, tooltipText2);
243         assertEquals(tooltipText2, mTooltipView.getTooltipText());
244     }
245 
246     @Test
testNoTooltipWhenNotSet()247     public void testNoTooltipWhenNotSet() throws Throwable {
248         callPerformLongClick(mNoTooltipView);
249         assertFalse(hasTooltip(mNoTooltipView));
250 
251         injectLongClick(mNoTooltipView);
252         assertFalse(hasTooltip(mNoTooltipView));
253 
254         injectLongEnter(mNoTooltipView);
255         assertFalse(hasTooltip(mNoTooltipView));
256 
257         injectLongHoverMove(mNoTooltipView);
258         assertFalse(hasTooltip(mNoTooltipView));
259     }
260 
261     @Test
testTooltipOnDisabledView()262     public void testTooltipOnDisabledView() throws Throwable {
263         mActivityRule.runOnUiThread(() -> mTooltipView.setEnabled(false));
264 
265         // Long click has no effect on a disabled view.
266         injectLongClick(mTooltipView);
267         assertFalse(hasTooltip(mTooltipView));
268 
269         // Hover does show the tooltip on a disabled view.
270         injectLongHoverMove(mTooltipView);
271         assertTrue(hasTooltip(mTooltipView));
272     }
273 
274     @Test
testUpdateOpenTooltip()275     public void testUpdateOpenTooltip() throws Throwable {
276         callPerformLongClick(mTooltipView);
277         assertTrue(hasTooltip(mTooltipView));
278 
279         setTooltipText(mTooltipView, "updated tooltip");
280         assertTrue(hasTooltip(mTooltipView));
281 
282         setTooltipText(mTooltipView, null);
283         assertFalse(hasTooltip(mTooltipView));
284     }
285 
286     @Test
testTooltipHidesOnActivityFocusChange()287     public void testTooltipHidesOnActivityFocusChange() throws Throwable {
288         callPerformLongClick(mTooltipView);
289         assertTrue(hasTooltip(mTooltipView));
290 
291         CtsActivity activity = mCtsActivityRule.launchActivity(null);
292         PollingCheck.waitFor(TIMEOUT_DELTA, () -> !mActivity.hasWindowFocus());
293         assertFalse(hasTooltip(mTooltipView));
294         activity.finish();
295     }
296 
297     @Test
testTooltipHidesOnWindowFocusChange()298     public void testTooltipHidesOnWindowFocusChange() throws Throwable {
299         callPerformLongClick(mTooltipView);
300         assertTrue(hasTooltip(mTooltipView));
301 
302         // Show a context menu on another widget.
303         mActivity.registerForContextMenu(mNoTooltipView);
304         mActivityRule.runOnUiThread(() -> mNoTooltipView.showContextMenu(0, 0));
305 
306         PollingCheck.waitFor(TIMEOUT_DELTA, () -> !mTooltipView.hasWindowFocus());
307         mInstrumentation.waitForIdleSync();
308         assertFalse(hasTooltip(mTooltipView));
309     }
310 
311     // Tests for tooltips triggered by long click.
312 
313     @Test
testShortClickDoesNotShowTooltip()314     public void testShortClickDoesNotShowTooltip() throws Throwable {
315         injectShortClick(mTooltipView);
316         assertFalse(hasTooltip(mTooltipView));
317     }
318 
319     @Test
testPerformLongClickShowsTooltipImmediately()320     public void testPerformLongClickShowsTooltipImmediately() throws Throwable {
321         callPerformLongClick(mTooltipView);
322         assertTrue(hasTooltip(mTooltipView));
323     }
324 
325     @Test
testLongClickTooltipBlockedByLongClickListener()326     public void testLongClickTooltipBlockedByLongClickListener() throws Throwable {
327         mTooltipView.setOnLongClickListener(v -> true);
328         injectLongClick(mTooltipView);
329         assertFalse(hasTooltip(mTooltipView));
330     }
331 
332     @Test
testLongClickTooltipBlockedByContextMenu()333     public void testLongClickTooltipBlockedByContextMenu() throws Throwable {
334         mActivity.registerForContextMenu(mTooltipView);
335         injectLongClick(mTooltipView);
336         assertFalse(hasTooltip(mTooltipView));
337     }
338 
339     @Test
testLongClickTooltipOnNonClickableView()340     public void testLongClickTooltipOnNonClickableView() throws Throwable {
341         injectLongClick(mTooltipView);
342         assertTrue(hasTooltip(mTooltipView));
343     }
344 
345     @Test
testLongClickTooltipOnClickableView()346     public void testLongClickTooltipOnClickableView() throws Throwable {
347         setClickable(mTooltipView);
348         injectLongClick(mTooltipView);
349         assertTrue(hasTooltip(mTooltipView));
350     }
351 
352     @Test
testLongClickTooltipOnLongClickableView()353     public void testLongClickTooltipOnLongClickableView() throws Throwable {
354         setLongClickable(mTooltipView);
355         injectLongClick(mTooltipView);
356         assertTrue(hasTooltip(mTooltipView));
357     }
358 
359     @Test
testLongClickTooltipOnContextClickableView()360     public void testLongClickTooltipOnContextClickableView() throws Throwable {
361         setContextClickable(mTooltipView);
362         injectLongClick(mTooltipView);
363         assertTrue(hasTooltip(mTooltipView));
364     }
365 
366     @Test
testLongClickTooltipStaysOnMouseMove()367     public void testLongClickTooltipStaysOnMouseMove() throws Throwable {
368         injectLongClick(mTooltipView);
369         assertTrue(hasTooltip(mTooltipView));
370 
371         // Tooltip stays while the mouse moves over the widget.
372         injectHoverMove(mTooltipView);
373         assertTrue(hasTooltip(mTooltipView));
374 
375         // Long-click-triggered tooltip stays while the mouse to another widget.
376         injectHoverMove(mNoTooltipView);
377         assertTrue(hasTooltip(mTooltipView));
378     }
379 
380     @Test
testLongClickTooltipHidesAfterUp()381     public void testLongClickTooltipHidesAfterUp() throws Throwable {
382         injectLongClick(mTooltipView);
383         assertTrue(hasTooltip(mTooltipView));
384 
385         // Long-click-triggered tooltip hides after ACTION_UP (with a delay).
386         waitOut(ViewConfiguration.getLongPressTooltipHideTimeout());
387         assertFalse(hasTooltip(mTooltipView));
388     }
389 
390     @Test
testLongClickTooltipHidesOnClick()391     public void testLongClickTooltipHidesOnClick() throws Throwable {
392         injectLongClick(mTooltipView);
393         assertTrue(hasTooltip(mTooltipView));
394 
395         injectShortClick(mTooltipView);
396         assertFalse(hasTooltip(mTooltipView));
397     }
398 
399     @Test
testLongClickTooltipHidesOnClickElsewhere()400     public void testLongClickTooltipHidesOnClickElsewhere() throws Throwable {
401         injectLongClick(mTooltipView);
402         assertTrue(hasTooltip(mTooltipView));
403 
404         injectShortClick(mNoTooltipView);
405         assertFalse(hasTooltip(mTooltipView));
406     }
407 
408     @Test
testLongClickTooltipHidesOnKey()409     public void testLongClickTooltipHidesOnKey() throws Throwable {
410         injectLongClick(mTooltipView);
411         assertTrue(hasTooltip(mTooltipView));
412 
413         injectArbitraryShortKeyPress();
414         assertFalse(hasTooltip(mTooltipView));
415     }
416 
417     // Tests for tooltips triggered by long key press.
418 
419     @Test
testShortKeyPressDoesNotShowTooltip()420     public void testShortKeyPressDoesNotShowTooltip() throws Throwable {
421         injectKeyPress(null, KeyEvent.KEYCODE_ENTER, 0);
422         assertFalse(hasTooltip(mTooltipView));
423 
424         injectKeyPress(mTooltipView, KeyEvent.KEYCODE_ENTER, 0);
425         assertFalse(hasTooltip(mTooltipView));
426     }
427 
428     @Test
testLongArbitraryKeyPressDoesNotShowTooltip()429     public void testLongArbitraryKeyPressDoesNotShowTooltip() throws Throwable {
430         injectLongKeyPress(mTooltipView, KeyEvent.KEYCODE_0);
431         assertFalse(hasTooltip(mTooltipView));
432     }
433 
434     @Test
testLongKeyPressWithoutFocusDoesNotShowTooltip()435     public void testLongKeyPressWithoutFocusDoesNotShowTooltip() throws Throwable {
436         injectLongEnter(null);
437         assertFalse(hasTooltip(mTooltipView));
438     }
439 
440     @Test
testLongKeyPressOnAnotherViewDoesNotShowTooltip()441     public void testLongKeyPressOnAnotherViewDoesNotShowTooltip() throws Throwable {
442         injectLongEnter(mNoTooltipView);
443         assertFalse(hasTooltip(mTooltipView));
444     }
445 
446     @Test
testLongKeyPressTooltipOnNonClickableView()447     public void testLongKeyPressTooltipOnNonClickableView() throws Throwable {
448         injectLongEnter(mTooltipView);
449         assertTrue(hasTooltip(mTooltipView));
450     }
451 
452     @Test
testLongKeyPressTooltipOnClickableView()453     public void testLongKeyPressTooltipOnClickableView() throws Throwable {
454         setClickable(mTooltipView);
455         injectLongEnter(mTooltipView);
456         assertTrue(hasTooltip(mTooltipView));
457     }
458 
459     @Test
testLongKeyPressTooltipOnLongClickableView()460     public void testLongKeyPressTooltipOnLongClickableView() throws Throwable {
461         setLongClickable(mTooltipView);
462         injectLongEnter(mTooltipView);
463         assertTrue(hasTooltip(mTooltipView));
464     }
465 
466     @Test
testLongKeyPressTooltipOnContextClickableView()467     public void testLongKeyPressTooltipOnContextClickableView() throws Throwable {
468         setContextClickable(mTooltipView);
469         injectLongEnter(mTooltipView);
470         assertTrue(hasTooltip(mTooltipView));
471     }
472 
473     @Test
testLongKeyPressTooltipStaysOnMouseMove()474     public void testLongKeyPressTooltipStaysOnMouseMove() throws Throwable {
475         injectLongEnter(mTooltipView);
476         assertTrue(hasTooltip(mTooltipView));
477 
478         // Tooltip stays while the mouse moves over the widget.
479         injectHoverMove(mTooltipView);
480         assertTrue(hasTooltip(mTooltipView));
481 
482         // Long-keypress-triggered tooltip stays while the mouse to another widget.
483         injectHoverMove(mNoTooltipView);
484         assertTrue(hasTooltip(mTooltipView));
485     }
486 
487     @Test
testLongKeyPressTooltipHidesAfterUp()488     public void testLongKeyPressTooltipHidesAfterUp() throws Throwable {
489         injectLongEnter(mTooltipView);
490         assertTrue(hasTooltip(mTooltipView));
491 
492         // Long-keypress-triggered tooltip hides after ACTION_UP (with a delay).
493         waitOut(ViewConfiguration.getLongPressTooltipHideTimeout());
494         assertFalse(hasTooltip(mTooltipView));
495     }
496 
497     @Test
testLongKeyPressTooltipHidesOnClick()498     public void testLongKeyPressTooltipHidesOnClick() throws Throwable {
499         injectLongEnter(mTooltipView);
500         assertTrue(hasTooltip(mTooltipView));
501 
502         injectShortClick(mTooltipView);
503         assertFalse(hasTooltip(mTooltipView));
504     }
505 
506     @Test
testLongKeyPressTooltipHidesOnClickElsewhere()507     public void testLongKeyPressTooltipHidesOnClickElsewhere() throws Throwable {
508         injectLongEnter(mTooltipView);
509         assertTrue(hasTooltip(mTooltipView));
510 
511         injectShortClick(mNoTooltipView);
512         assertFalse(hasTooltip(mTooltipView));
513     }
514 
515     @Test
testLongKeyPressTooltipHidesOnKey()516     public void testLongKeyPressTooltipHidesOnKey() throws Throwable {
517         injectLongEnter(mTooltipView);
518         assertTrue(hasTooltip(mTooltipView));
519 
520         injectArbitraryShortKeyPress();
521         assertFalse(hasTooltip(mTooltipView));
522     }
523 
524     // Tests for tooltips triggered by mouse hover.
525 
526     @Test
testMouseClickDoesNotShowTooltip()527     public void testMouseClickDoesNotShowTooltip() throws Throwable {
528         injectMotionEvent(obtainMouseEvent(mTooltipView, MotionEvent.ACTION_DOWN, 0, 0));
529         injectMotionEvent(obtainMouseEvent(mTooltipView, MotionEvent.ACTION_BUTTON_PRESS, 0, 0));
530         injectMotionEvent(obtainMouseEvent(mTooltipView, MotionEvent.ACTION_BUTTON_RELEASE, 0, 0));
531         injectMotionEvent(obtainMouseEvent(mTooltipView, MotionEvent.ACTION_UP, 0, 0));
532         assertFalse(hasTooltip(mTooltipView));
533     }
534 
535     @Test
testMouseHoverDoesNotShowTooltipImmediately()536     public void testMouseHoverDoesNotShowTooltipImmediately() throws Throwable {
537         injectHoverMove(mTooltipView, 0, 0);
538         assertFalse(hasTooltip(mTooltipView));
539 
540         injectHoverMove(mTooltipView, 1, 1);
541         assertFalse(hasTooltip(mTooltipView));
542 
543         injectHoverMove(mTooltipView, 2, 2);
544         assertFalse(hasTooltip(mTooltipView));
545     }
546 
547     @Test
testMouseHoverExitCancelsPendingTooltip()548     public void testMouseHoverExitCancelsPendingTooltip() throws Throwable {
549         injectHoverMove(mTooltipView);
550         assertFalse(hasTooltip(mTooltipView));
551 
552         injectLongHoverMove(mNoTooltipView);
553         assertFalse(hasTooltip(mTooltipView));
554     }
555 
556     @Test
testMouseHoverTooltipOnClickableView()557     public void testMouseHoverTooltipOnClickableView() throws Throwable {
558         setClickable(mTooltipView);
559         injectLongHoverMove(mTooltipView);
560         assertTrue(hasTooltip(mTooltipView));
561     }
562 
563     @Test
testMouseHoverTooltipOnLongClickableView()564     public void testMouseHoverTooltipOnLongClickableView() throws Throwable {
565         setLongClickable(mTooltipView);
566         injectLongHoverMove(mTooltipView);
567         assertTrue(hasTooltip(mTooltipView));
568     }
569 
570     @Test
testMouseHoverTooltipOnContextClickableView()571     public void testMouseHoverTooltipOnContextClickableView() throws Throwable {
572         setContextClickable(mTooltipView);
573         injectLongHoverMove(mTooltipView);
574         assertTrue(hasTooltip(mTooltipView));
575     }
576 
577     @Test
testMouseHoverTooltipStaysOnMouseMove()578     public void testMouseHoverTooltipStaysOnMouseMove() throws Throwable {
579         injectLongHoverMove(mTooltipView);
580         assertTrue(hasTooltip(mTooltipView));
581 
582         // Tooltip stays while the mouse moves over the widget.
583         injectHoverMove(mTooltipView, 1, 1);
584         assertTrue(hasTooltip(mTooltipView));
585 
586         injectHoverMove(mTooltipView, 2, 2);
587         assertTrue(hasTooltip(mTooltipView));
588     }
589 
590     @Test
testMouseHoverTooltipHidesOnExit()591     public void testMouseHoverTooltipHidesOnExit() throws Throwable {
592         injectLongHoverMove(mTooltipView);
593         assertTrue(hasTooltip(mTooltipView));
594 
595         // Tooltip hides once the mouse moves out of the widget.
596         injectHoverMove(mNoTooltipView);
597         assertFalse(hasTooltip(mTooltipView));
598     }
599 
600     @Test
testMouseHoverTooltipHidesOnClick()601     public void testMouseHoverTooltipHidesOnClick() throws Throwable {
602         injectLongHoverMove(mTooltipView);
603         assertTrue(hasTooltip(mTooltipView));
604 
605         injectShortClick(mTooltipView);
606         assertFalse(hasTooltip(mTooltipView));
607     }
608 
609     @Test
testMouseHoverTooltipHidesOnClickOnElsewhere()610     public void testMouseHoverTooltipHidesOnClickOnElsewhere() throws Throwable {
611         injectLongHoverMove(mTooltipView);
612         assertTrue(hasTooltip(mTooltipView));
613 
614         injectShortClick(mNoTooltipView);
615         assertFalse(hasTooltip(mTooltipView));
616     }
617 
618     @Test
testMouseHoverTooltipHidesOnKey()619     public void testMouseHoverTooltipHidesOnKey() throws Throwable {
620         injectLongHoverMove(mTooltipView);
621         assertTrue(hasTooltip(mTooltipView));
622 
623         injectArbitraryShortKeyPress();
624         assertFalse(hasTooltip(mTooltipView));
625     }
626 
627     @Test
testMouseHoverTooltipHidesOnTimeout()628     public void testMouseHoverTooltipHidesOnTimeout() throws Throwable {
629         injectLongHoverMove(mTooltipView);
630         assertTrue(hasTooltip(mTooltipView));
631 
632         waitOut(ViewConfiguration.getHoverTooltipHideTimeout());
633         assertFalse(hasTooltip(mTooltipView));
634     }
635 
636     @Test
testMouseHoverTooltipHidesOnShortTimeout()637     public void testMouseHoverTooltipHidesOnShortTimeout() throws Throwable {
638         requestLowProfileSystemUi();
639 
640         injectLongHoverMove(mTooltipView);
641         assertTrue(hasTooltip(mTooltipView));
642 
643         waitOut(ViewConfiguration.getHoverTooltipHideShortTimeout());
644         assertFalse(hasTooltip(mTooltipView));
645     }
646 
647     @Test
testMouseHoverTooltipWithHoverListener()648     public void testMouseHoverTooltipWithHoverListener() throws Throwable {
649         mTooltipView.setOnHoverListener((v, event) -> true);
650         injectLongHoverMove(mTooltipView);
651         assertTrue(hasTooltip(mTooltipView));
652     }
653 
654     @Test
testMouseHoverTooltipUnsetWhileHovering()655     public void testMouseHoverTooltipUnsetWhileHovering() throws Throwable {
656         injectHoverMove(mTooltipView);
657         setTooltipText(mTooltipView, null);
658         waitOut(ViewConfiguration.getHoverTooltipShowTimeout());
659         assertFalse(hasTooltip(mTooltipView));
660     }
661 
662     @Test
testMouseHoverTooltipDisableWhileHovering()663     public void testMouseHoverTooltipDisableWhileHovering() throws Throwable {
664         injectHoverMove(mTooltipView);
665         mActivityRule.runOnUiThread(() -> mTooltipView.setEnabled(false));
666         waitOut(ViewConfiguration.getHoverTooltipShowTimeout());
667         // Disabled view still displays a hover tooltip.
668         assertTrue(hasTooltip(mTooltipView));
669     }
670 
671     @Test
testMouseHoverTooltipFromParent()672     public void testMouseHoverTooltipFromParent() throws Throwable {
673         // Hover listeners should not interfere with tooltip dispatch.
674         mNoTooltipView.setOnHoverListener((v, event) -> true);
675         mTooltipView.setOnHoverListener((v, event) -> true);
676 
677         setTooltipText(mTopmostView, "tooltip");
678 
679         // Hover over a child with a tooltip works normally.
680         injectLongHoverMove(mTooltipView);
681         assertFalse(hasTooltip(mTopmostView));
682         assertTrue(hasTooltip(mTooltipView));
683         injectShortClick(mTopmostView);
684         assertFalse(hasTooltip(mTooltipView));
685 
686         // Hover over a child with no tooltip triggers a tooltip on its parent.
687         injectLongHoverMove(mNoTooltipView2);
688         assertFalse(hasTooltip(mNoTooltipView2));
689         assertTrue(hasTooltip(mTopmostView));
690         injectShortClick(mTopmostView);
691         assertFalse(hasTooltip(mTopmostView));
692 
693         // Same but the child is and empty view group.
694         injectLongHoverMove(mEmptyGroup);
695         assertFalse(hasTooltip(mEmptyGroup));
696         assertTrue(hasTooltip(mTopmostView));
697         injectShortClick(mTopmostView);
698         assertFalse(hasTooltip(mTopmostView));
699 
700         // Hover over a grandchild with no tooltip triggers a tooltip on its grandparent.
701         injectLongHoverMove(mNoTooltipView);
702         assertFalse(hasTooltip(mNoTooltipView));
703         assertTrue(hasTooltip(mTopmostView));
704         // Move to another child one level up, the tooltip stays.
705         injectHoverMove(mNoTooltipView2);
706         assertTrue(hasTooltip(mTopmostView));
707         injectShortClick(mTopmostView);
708         assertFalse(hasTooltip(mTopmostView));
709 
710         // Set a tooltip on the intermediate parent, now it is showing tooltips.
711         setTooltipText(mGroupView, "tooltip");
712         injectLongHoverMove(mNoTooltipView);
713         assertFalse(hasTooltip(mNoTooltipView));
714         assertFalse(hasTooltip(mTopmostView));
715         assertTrue(hasTooltip(mGroupView));
716 
717         // Move out of this group, the tooltip is now back on the grandparent.
718         injectLongHoverMove(mNoTooltipView2);
719         assertFalse(hasTooltip(mGroupView));
720         assertTrue(hasTooltip(mTopmostView));
721         injectShortClick(mTopmostView);
722         assertFalse(hasTooltip(mTopmostView));
723     }
724 
725     @Test
testMouseHoverTooltipRemoveWhileWaiting()726     public void testMouseHoverTooltipRemoveWhileWaiting() throws Throwable {
727         // Remove the view while hovering.
728         injectHoverMove(mTooltipView);
729         removeView(mTooltipView);
730         waitOut(ViewConfiguration.getHoverTooltipShowTimeout());
731         assertFalse(hasTooltip(mTooltipView));
732         addView(mGroupView, mTooltipView);
733 
734         // Remove and re-add the view while hovering.
735         injectHoverMove(mTooltipView);
736         removeView(mTooltipView);
737         addView(mGroupView, mTooltipView);
738         waitOut(ViewConfiguration.getHoverTooltipShowTimeout());
739         assertFalse(hasTooltip(mTooltipView));
740 
741         // Remove the view's parent while hovering.
742         injectHoverMove(mTooltipView);
743         removeView(mGroupView);
744         waitOut(ViewConfiguration.getHoverTooltipShowTimeout());
745         assertFalse(hasTooltip(mTooltipView));
746         addView(mTopmostView, mGroupView);
747 
748         // Remove and re-add view's parent while hovering.
749         injectHoverMove(mTooltipView);
750         removeView(mGroupView);
751         addView(mTopmostView, mGroupView);
752         waitOut(ViewConfiguration.getHoverTooltipShowTimeout());
753         assertFalse(hasTooltip(mTooltipView));
754     }
755 
756     @Test
testMouseHoverTooltipRemoveWhileShowing()757     public void testMouseHoverTooltipRemoveWhileShowing() throws Throwable {
758         // Remove the view while showing the tooltip.
759         injectLongHoverMove(mTooltipView);
760         assertTrue(hasTooltip(mTooltipView));
761         removeView(mTooltipView);
762         assertFalse(hasTooltip(mTooltipView));
763         addView(mGroupView, mTooltipView);
764         assertFalse(hasTooltip(mTooltipView));
765 
766         // Remove the view's parent while showing the tooltip.
767         injectLongHoverMove(mTooltipView);
768         assertTrue(hasTooltip(mTooltipView));
769         removeView(mGroupView);
770         assertFalse(hasTooltip(mTooltipView));
771         addView(mTopmostView, mGroupView);
772         assertFalse(hasTooltip(mTooltipView));
773     }
774 
775     @Test
testMouseHoverOverlap()776     public void testMouseHoverOverlap() throws Throwable {
777         final View parent = mActivity.findViewById(R.id.overlap_group);
778         final View child1 = mActivity.findViewById(R.id.overlap1);
779         final View child2 = mActivity.findViewById(R.id.overlap2);
780         final View child3 = mActivity.findViewById(R.id.overlap3);
781 
782         injectLongHoverMove(parent);
783         assertTrue(hasTooltip(child3));
784 
785         setVisibility(child3, View.GONE);
786         injectLongHoverMove(parent);
787         assertTrue(hasTooltip(child2));
788 
789         setTooltipText(child2, null);
790         injectLongHoverMove(parent);
791         assertTrue(hasTooltip(child1));
792 
793         setVisibility(child1, View.INVISIBLE);
794         injectLongHoverMove(parent);
795         assertTrue(hasTooltip(parent));
796     }
797 
798     @Test
testMouseHoverWithJitter()799     public void testMouseHoverWithJitter() throws Throwable {
800         testHoverWithJitter(InputDevice.SOURCE_MOUSE);
801     }
802 
803     @Test
testStylusHoverWithJitter()804     public void testStylusHoverWithJitter() throws Throwable {
805         testHoverWithJitter(InputDevice.SOURCE_STYLUS);
806     }
807 
808     @Test
testTouchscreenHoverWithJitter()809     public void testTouchscreenHoverWithJitter() throws Throwable {
810         testHoverWithJitter(InputDevice.SOURCE_TOUCHSCREEN);
811     }
812 
testHoverWithJitter(int source)813     private void testHoverWithJitter(int source) {
814         final int hoverSlop = ViewConfiguration.get(mTooltipView.getContext()).getScaledHoverSlop();
815         if (hoverSlop == 0) {
816             // Zero hoverSlop makes this test redundant.
817             return;
818         }
819 
820         final int tooltipTimeout = ViewConfiguration.getHoverTooltipShowTimeout();
821         final long halfTimeout = tooltipTimeout / 2;
822         assertTrue(halfTimeout + WAIT_MARGIN < tooltipTimeout);
823 
824         // Imitate strong jitter (above hoverSlop threshold). No tooltip should be shown.
825         int jitterHigh = hoverSlop + 1;
826         assertTrue(jitterHigh <= mTooltipView.getWidth());
827         assertTrue(jitterHigh <= mTooltipView.getHeight());
828 
829         injectHoverMove(source, mTooltipView, 0, 0);
830         waitOut(halfTimeout);
831         assertFalse(hasTooltip(mTooltipView));
832 
833         injectHoverMove(source, mTooltipView, jitterHigh, 0);
834         waitOut(halfTimeout);
835         assertFalse(hasTooltip(mTooltipView));
836 
837         injectHoverMove(source, mTooltipView, 0, 0);
838         waitOut(halfTimeout);
839         assertFalse(hasTooltip(mTooltipView));
840 
841         injectHoverMove(source, mTooltipView, 0, jitterHigh);
842         waitOut(halfTimeout);
843         assertFalse(hasTooltip(mTooltipView));
844 
845         // Jitter below threshold should be ignored and the tooltip should be shown.
846         injectHoverMove(source, mTooltipView, 0, 0);
847         waitOut(halfTimeout);
848         assertFalse(hasTooltip(mTooltipView));
849 
850         int jitterLow = hoverSlop - 1;
851         injectHoverMove(source, mTooltipView, jitterLow, 0);
852         waitOut(halfTimeout);
853         assertTrue(hasTooltip(mTooltipView));
854 
855         // Dismiss the tooltip
856         injectShortClick(mTooltipView);
857         assertFalse(hasTooltip(mTooltipView));
858 
859         injectHoverMove(source, mTooltipView, 0, 0);
860         waitOut(halfTimeout);
861         assertFalse(hasTooltip(mTooltipView));
862 
863         injectHoverMove(source, mTooltipView, 0, jitterLow);
864         waitOut(halfTimeout);
865         assertTrue(hasTooltip(mTooltipView));
866     }
867 
868     @Test
testTooltipInPopup()869     public void testTooltipInPopup() throws Throwable {
870         TextView popupContent = new TextView(mActivity);
871 
872         mActivityRule.runOnUiThread(() -> {
873             popupContent.setText("Popup view");
874             popupContent.setTooltipText("Tooltip");
875 
876             PopupWindow popup = new PopupWindow(popupContent,
877                     ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
878             popup.showAtLocation(mGroupView, Gravity.CENTER, 0, 0);
879         });
880         mInstrumentation.waitForIdleSync();
881 
882         injectLongClick(popupContent);
883         assertTrue(hasTooltip(popupContent));
884     }
885 }
886