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