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