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