• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget.cts;
18 
19 import static android.Manifest.permission.POST_NOTIFICATIONS;
20 import static android.Manifest.permission.UNLIMITED_TOASTS;
21 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
22 
23 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
24 
25 import static com.google.common.truth.Truth.assertThat;
26 
27 import static org.junit.Assert.assertEquals;
28 import static org.junit.Assert.assertFalse;
29 import static org.junit.Assert.assertNotNull;
30 import static org.junit.Assert.assertNull;
31 import static org.junit.Assert.assertSame;
32 import static org.junit.Assert.assertTrue;
33 import static org.junit.Assert.fail;
34 import static org.junit.Assume.assumeFalse;
35 
36 import static java.util.stream.Collectors.toList;
37 
38 import android.app.ActivityOptions;
39 import android.app.NotificationManager;
40 import android.app.UiAutomation;
41 import android.app.UiAutomation.AccessibilityEventFilter;
42 import android.content.BroadcastReceiver;
43 import android.content.ComponentName;
44 import android.content.Context;
45 import android.content.Intent;
46 import android.content.IntentFilter;
47 import android.content.pm.PackageManager;
48 import android.graphics.drawable.Drawable;
49 import android.os.ConditionVariable;
50 import android.os.SystemClock;
51 import android.provider.Settings;
52 import android.util.Log;
53 import android.view.Gravity;
54 import android.view.View;
55 import android.view.ViewTreeObserver;
56 import android.view.WindowManager;
57 import android.view.accessibility.AccessibilityEvent;
58 import android.view.accessibility.AccessibilityManager;
59 import android.widget.ImageView;
60 import android.widget.TextView;
61 import android.widget.Toast;
62 
63 import androidx.annotation.NonNull;
64 import androidx.annotation.Nullable;
65 import androidx.test.annotation.UiThreadTest;
66 import androidx.test.filters.LargeTest;
67 import androidx.test.rule.ActivityTestRule;
68 import androidx.test.runner.AndroidJUnit4;
69 
70 import com.android.compatibility.common.util.ApiTest;
71 import com.android.compatibility.common.util.PollingCheck;
72 import com.android.compatibility.common.util.SystemUtil;
73 import com.android.compatibility.common.util.TestUtils;
74 import com.android.compatibility.common.util.UserHelper;
75 
76 import junit.framework.Assert;
77 
78 import org.junit.After;
79 import org.junit.Before;
80 import org.junit.Rule;
81 import org.junit.Test;
82 import org.junit.runner.RunWith;
83 
84 import java.util.ArrayList;
85 import java.util.List;
86 import java.util.concurrent.CompletableFuture;
87 import java.util.concurrent.CountDownLatch;
88 import java.util.concurrent.TimeUnit;
89 import java.util.stream.Stream;
90 
91 @LargeTest
92 @RunWith(AndroidJUnit4.class)
93 public class ToastTest {
94     private static final String TAG = ToastTest.class.getSimpleName();
95     private static final String TEST_TOAST_TEXT = "test toast";
96     private static final String TEST_CUSTOM_TOAST_TEXT = "test custom toast";
97     private static final String SETTINGS_ACCESSIBILITY_UI_TIMEOUT =
98             "accessibility_non_interactive_ui_timeout_ms";
99     private static final int ACCESSIBILITY_STATE_WAIT_TIMEOUT_MS = 3000;
100     private static final long TIME_FOR_UI_OPERATION  = 1000L;
101     private static final long TIME_OUT = 5000L;
102     private static final int MAX_PACKAGE_TOASTS_LIMIT = 5;
103     private static final String ACTION_TRANSLUCENT_ACTIVITY_RESUMED =
104             "android.widget.cts.app.TRANSLUCENT_ACTIVITY_RESUMED";
105     private static final ComponentName COMPONENT_CTS_ACTIVITY =
106             ComponentName.unflattenFromString("android.widget.cts/.CtsActivity");
107     private static final ComponentName COMPONENT_TRANSLUCENT_ACTIVITY =
108             ComponentName.unflattenFromString("android.widget.cts.app/.TranslucentActivity");
109     private static final double TOAST_DURATION_ERROR_TOLERANCE_FRACTION = 0.25;
110 
111     // The following two fields work together to define rate limits for toasts, where each limit is
112     // defined as TOAST_RATE_LIMITS[i] toasts are allowed in the window of length
113     // TOAST_WINDOW_SIZES_MS[i].
114     private static final int[] TOAST_RATE_LIMITS = {3, 5, 6};
115     private static final long[] TOAST_WINDOW_SIZES_MS = {20_000, 42_000, 68_000};
116 
117     private Toast mToast;
118     private Context mContext;
119     private boolean mLayoutDone;
120     private ViewTreeObserver.OnGlobalLayoutListener mLayoutListener;
121     private ConditionVariable mToastShown;
122     private ConditionVariable mToastHidden;
123     private NotificationManager mNotificationManager;
124     private UserHelper mUserHelper;
125 
126     @Rule
127     public ActivityTestRule<CtsActivity> mActivityRule =
128             new ActivityTestRule<>(CtsActivity.class);
129     private UiAutomation mUiAutomation;
130 
131     @Before
setup()132     public void setup() {
133         mContext = getInstrumentation().getContext();
134         mUserHelper = new UserHelper(mContext);
135         mUiAutomation = getInstrumentation().getUiAutomation();
136         mLayoutListener = () -> mLayoutDone = true;
137         mNotificationManager =
138                 mContext.getSystemService(NotificationManager.class);
139         // disable rate limiting for tests
140         SystemUtil.runWithShellPermissionIdentity(() -> mNotificationManager
141                 .setToastRateLimitingEnabled(false));
142 
143         // Wait until the activity is resumed. Otherwise, custom toasts from non-resumed activity
144         // may be blocked depending on the timing.
145         PollingCheck.waitFor(TIME_OUT, () -> null != mActivityRule.getActivity());
146         try {
147             assertTrue("Timeout while waiting for onResume()",
148                     mActivityRule.getActivity().waitUntilResumed(TIME_OUT));
149         } catch (InterruptedException e) {
150             fail("Got InterruptedException while waiting for onResume()");
151         }
152     }
153 
154     @After
teardown()155     public void teardown() {
156         waitForToastToExpire();
157         // re-enable rate limiting
158         SystemUtil.runWithShellPermissionIdentity(() -> mNotificationManager
159                 .setToastRateLimitingEnabled(true));
160     }
161 
162     @UiThreadTest
163     @Test
testConstructor()164     public void testConstructor() {
165         new Toast(mContext);
166     }
167 
168     @UiThreadTest
169     @Test(expected=NullPointerException.class)
testConstructorNullContext()170     public void testConstructorNullContext() {
171         new Toast(null);
172     }
173 
assertCustomToastShown(final View view)174     private static void assertCustomToastShown(final View view) {
175         PollingCheck.waitFor(TIME_OUT, () -> null != view.getParent());
176     }
177 
assertCustomToastShown(CustomToastInfo customToastInfo)178     private static void assertCustomToastShown(CustomToastInfo customToastInfo) {
179         PollingCheck.waitFor(TIME_OUT, customToastInfo::isShowing);
180     }
181 
assertCustomToastHidden(CustomToastInfo customToastInfo)182     private static void assertCustomToastHidden(CustomToastInfo customToastInfo) {
183         PollingCheck.waitFor(TIME_OUT, () -> !customToastInfo.isShowing());
184     }
185 
assertCustomToastShownAndHidden(final View view)186     private static void assertCustomToastShownAndHidden(final View view) {
187         assertCustomToastShown(view);
188         PollingCheck.waitFor(TIME_OUT, () -> null == view.getParent());
189     }
190 
assertCustomToastShownAndHidden(CustomToastInfo customToastInfo)191     private static void assertCustomToastShownAndHidden(CustomToastInfo customToastInfo) {
192         assertCustomToastShown(customToastInfo);
193         assertCustomToastHidden(customToastInfo);
194     }
195 
assertTextToastShownAndHidden()196     private void assertTextToastShownAndHidden() {
197         assertTrue(mToastShown.block(TIME_OUT));
198         assertTrue(mToastHidden.block(TIME_OUT));
199     }
200 
assertTextToastShownAndHidden(TextToastInfo textToastInfo)201     private void assertTextToastShownAndHidden(TextToastInfo textToastInfo) {
202         assertTrue(textToastInfo.blockOnToastShown(TIME_OUT));
203         assertTrue(textToastInfo.blockOnToastHidden(TIME_OUT));
204     }
205 
assertCustomToastNotShown(final View view)206     private static void assertCustomToastNotShown(final View view) {
207         // sleep a while and then make sure do not show toast
208         SystemClock.sleep(TIME_FOR_UI_OPERATION);
209         assertNull(view.getParent());
210     }
211 
assertCustomToastNotShown(CustomToastInfo customToastInfo)212     private static void assertCustomToastNotShown(CustomToastInfo customToastInfo) {
213         assertThat(customToastInfo.isShowing()).isFalse();
214 
215         // sleep a while and then make sure it's still not shown
216         SystemClock.sleep(TIME_FOR_UI_OPERATION);
217         assertThat(customToastInfo.isShowing()).isFalse();
218     }
219 
assertTextToastNotShown(TextToastInfo textToastInfo)220     private void assertTextToastNotShown(TextToastInfo textToastInfo) {
221         assertFalse(textToastInfo.blockOnToastShown(TIME_FOR_UI_OPERATION));
222     }
223 
registerLayoutListener(final View view)224     private void registerLayoutListener(final View view) {
225         mLayoutDone = false;
226         view.getViewTreeObserver().addOnGlobalLayoutListener(mLayoutListener);
227     }
228 
assertLayoutDone(final View view)229     private void assertLayoutDone(final View view) {
230         PollingCheck.waitFor(TIME_OUT, () -> mLayoutDone);
231         view.getViewTreeObserver().removeOnGlobalLayoutListener(mLayoutListener);
232     }
233 
makeTextToast()234     private void makeTextToast() throws Throwable {
235         makeTextToast(mContext);
236     }
237 
makeTextToast(Context context)238     private void makeTextToast(Context context) throws Throwable {
239         Log.d(TAG, "creating Toast on context " + context + " (user=" + context.getUserId()
240                 + ", display=" + context.getDisplayId() + ")");
241         mToastShown = new ConditionVariable(false);
242         mToastHidden = new ConditionVariable(false);
243         mActivityRule.runOnUiThread(
244                 () -> {
245                     mToast = Toast.makeText(context, TEST_TOAST_TEXT, Toast.LENGTH_LONG);
246                     mToast.addCallback(new ConditionCallback(mToastShown, mToastHidden));
247                 });
248     }
249 
makeCustomToast()250     private void makeCustomToast() throws Throwable {
251         mActivityRule.runOnUiThread(
252                 () -> {
253                     mToast = new Toast(mContext);
254                     mToast.setDuration(Toast.LENGTH_LONG);
255                     TextView view = new TextView(mContext);
256                     view.setText(TEST_CUSTOM_TOAST_TEXT);
257                     mToast.setView(view);
258                 }
259         );
260     }
261 
waitForToastToExpire()262     private void waitForToastToExpire() {
263         if (mToast == null) {
264             return;
265         }
266         // text toast case
267         if (mToastShown != null && mToastHidden != null) {
268             boolean toastShown = mToastShown.block(/* return immediately */ 1);
269             boolean toastHidden = mToastHidden.block(/* return immediately */ 1);
270 
271             if (toastShown && !toastHidden) {
272                 assertTrue(mToastHidden.block(TIME_OUT));
273             }
274             return;
275         }
276 
277         // custom toast case
278         View view = mToast.getView();
279         if (view != null && view.getParent() != null) {
280             PollingCheck.waitFor(TIME_OUT, () -> view.getParent() == null);
281         }
282     }
283 
284     @Test
testShow_whenCustomToast()285     public void testShow_whenCustomToast() throws Throwable {
286         makeCustomToast();
287 
288         final View view = mToast.getView();
289 
290         // view has not been attached to screen yet
291         assertNull(view.getParent());
292         assertEquals(View.VISIBLE, view.getVisibility());
293 
294         runOnMainAndDrawSync(view, () -> showToastWithNotificationPermission(mToast));
295 
296         // view will be attached to screen when show it
297         assertEquals(View.VISIBLE, view.getVisibility());
298         assertCustomToastShownAndHidden(view);
299     }
300 
301     @Test
302     @ApiTest(apis = {"android.widget.Toast#show"})
testShow_whenTextToast()303     public void testShow_whenTextToast() throws Throwable {
304         makeTextToast();
305 
306         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
307 
308         assertTextToastShownAndHidden();
309     }
310 
311     @Test
312     @ApiTest(apis = {"android.widget.Toast#show"})
testShow_activityContext_whenTextToast()313     public void testShow_activityContext_whenTextToast() throws Throwable {
314         makeTextToast(mActivityRule.getActivity());
315 
316         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
317 
318         assertTextToastShownAndHidden();
319     }
320 
321     @Test
322     @ApiTest(apis = {"android.widget.Toast#show"})
testShow_appContext_whenTextToast()323     public void testShow_appContext_whenTextToast() throws Throwable {
324         makeTextToast(mContext.getApplicationContext());
325 
326         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
327 
328         assertTextToastShownAndHidden();
329     }
330 
331     @Test
testHideTextToastAfterExpirationOfFirstShowCall_despiteRepeatedShowCalls()332     public void testHideTextToastAfterExpirationOfFirstShowCall_despiteRepeatedShowCalls()
333             throws Throwable {
334         // Measure the length of a long toast.
335         makeTextToast();
336         long start1 = SystemClock.uptimeMillis();
337         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
338         assertEquals(Toast.LENGTH_LONG, mToast.getDuration());
339         assertTextToastShownAndHidden();
340         long longDurationMs = SystemClock.uptimeMillis() - start1;
341 
342         // Call show in the middle of the toast duration.
343         makeTextToast();
344         long start2 = SystemClock.uptimeMillis();
345         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
346         assertEquals(Toast.LENGTH_LONG, mToast.getDuration());
347         SystemClock.sleep(longDurationMs / 2);
348         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
349         assertTextToastShownAndHidden();
350         long repeatCallDurationMs = SystemClock.uptimeMillis() - start2;
351 
352         // Assert duration was roughly the same despite a repeat call.
353         assertThat((double) repeatCallDurationMs)
354                 .isWithin(longDurationMs * TOAST_DURATION_ERROR_TOLERANCE_FRACTION)
355                 .of(longDurationMs);
356     }
357 
358     @Test
testHideCustomToastAfterExpirationOfFirstShowCall_despiteRepeatedShowCalls()359     public void testHideCustomToastAfterExpirationOfFirstShowCall_despiteRepeatedShowCalls()
360             throws Throwable {
361         // Measure the length of a long toast.
362         makeCustomToast();
363         long start1 = SystemClock.uptimeMillis();
364         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
365         assertEquals(Toast.LENGTH_LONG, mToast.getDuration());
366         assertCustomToastShownAndHidden(mToast.getView());
367         long longDurationMs = SystemClock.uptimeMillis() - start1;
368 
369         // Call show in the middle of the toast duration.
370         makeCustomToast();
371         long start2 = SystemClock.uptimeMillis();
372         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
373         assertEquals(Toast.LENGTH_LONG, mToast.getDuration());
374         SystemClock.sleep(longDurationMs / 2);
375         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
376         assertCustomToastShownAndHidden(mToast.getView());
377         long repeatCallDurationMs = SystemClock.uptimeMillis() - start2;
378 
379         // Assert duration was roughly the same despite a repeat call.
380         assertThat((double) repeatCallDurationMs)
381                 .isWithin(longDurationMs * TOAST_DURATION_ERROR_TOLERANCE_FRACTION)
382                 .of(longDurationMs);
383     }
384 
385     @UiThreadTest
386     @Test(expected=RuntimeException.class)
testShowFailure()387     public void testShowFailure() {
388         Toast toast = new Toast(mContext);
389         // do not have any views or text
390         assertNull(toast.getView());
391         showToastWithNotificationPermission(mToast);
392     }
393 
394     @Test
testCancel_whenCustomToast()395     public void testCancel_whenCustomToast() throws Throwable {
396         makeCustomToast();
397 
398         final View view = mToast.getView();
399 
400         // view has not been attached to screen yet
401         assertNull(view.getParent());
402         mActivityRule.runOnUiThread(() -> {
403             showToastWithNotificationPermission(mToast);
404             mToast.cancel();
405         });
406 
407         assertCustomToastNotShown(view);
408     }
409 
410     @Test
testAccessView_whenCustomToast()411     public void testAccessView_whenCustomToast() throws Throwable {
412         makeTextToast();
413         assertFalse(mToast.getView() instanceof ImageView);
414 
415         final ImageView imageView = new ImageView(mContext);
416         Drawable drawable = mContext.getResources().getDrawable(R.drawable.pass);
417         imageView.setImageDrawable(drawable);
418 
419         runOnMainAndDrawSync(imageView, () -> {
420             mToast.setView(imageView);
421             showToastWithNotificationPermission(mToast);
422         });
423         assertSame(imageView, mToast.getView());
424         assertCustomToastShownAndHidden(imageView);
425     }
426 
427     @Test
testAccessDuration_whenCustomToast()428     public void testAccessDuration_whenCustomToast() throws Throwable {
429         long start = SystemClock.uptimeMillis();
430         makeCustomToast();
431         runOnMainAndDrawSync(mToast.getView(),
432                 () -> showToastWithNotificationPermission(mToast));
433         assertEquals(Toast.LENGTH_LONG, mToast.getDuration());
434 
435         View view = mToast.getView();
436         assertCustomToastShownAndHidden(view);
437         long longDuration = SystemClock.uptimeMillis() - start;
438 
439         start = SystemClock.uptimeMillis();
440         runOnMainAndDrawSync(mToast.getView(), () -> {
441             mToast.setDuration(Toast.LENGTH_SHORT);
442             showToastWithNotificationPermission(mToast);
443         });
444         assertEquals(Toast.LENGTH_SHORT, mToast.getDuration());
445 
446         view = mToast.getView();
447         assertCustomToastShownAndHidden(view);
448         long shortDuration = SystemClock.uptimeMillis() - start;
449 
450         assertTrue(longDuration > shortDuration);
451     }
452 
453     @Test
testAccessDuration_whenTextToast()454     public void testAccessDuration_whenTextToast() throws Throwable {
455         long start = SystemClock.uptimeMillis();
456         makeTextToast();
457         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
458         assertEquals(Toast.LENGTH_LONG, mToast.getDuration());
459 
460         assertTextToastShownAndHidden();
461         long longDuration = SystemClock.uptimeMillis() - start;
462 
463         start = SystemClock.uptimeMillis();
464         makeTextToast();
465         mActivityRule.runOnUiThread(() -> {
466             mToast.setDuration(Toast.LENGTH_SHORT);
467             showToastWithNotificationPermission(mToast);
468         });
469         assertEquals(Toast.LENGTH_SHORT, mToast.getDuration());
470 
471         assertTextToastShownAndHidden();
472         long shortDuration = SystemClock.uptimeMillis() - start;
473 
474         assertTrue(longDuration > shortDuration);
475     }
476 
477     // TODO(b/255426725): remove method, callers, and mUserHelper when A11Y supports it
requireRunNotOnVisibleBackgroundNonProfileUser()478     private void requireRunNotOnVisibleBackgroundNonProfileUser() {
479         assumeFalse("Not supported on visible background user",
480                 mUserHelper.isVisibleBackgroundUser());
481     }
482 
483     @Test
testAccessDuration_whenCustomToastAndWithA11yTimeoutEnabled()484     public void testAccessDuration_whenCustomToastAndWithA11yTimeoutEnabled() throws Throwable {
485         requireRunNotOnVisibleBackgroundNonProfileUser();
486         makeCustomToast();
487         final Runnable showToast = () -> {
488             mToast.setDuration(Toast.LENGTH_SHORT);
489             showToastWithNotificationPermission(mToast);
490         };
491         long start = SystemClock.uptimeMillis();
492         runOnMainAndDrawSync(mToast.getView(), showToast);
493         assertCustomToastShownAndHidden(mToast.getView());
494         final long shortDuration = SystemClock.uptimeMillis() - start;
495 
496         final String originalSetting = Settings.Secure.getString(mContext.getContentResolver(),
497                 SETTINGS_ACCESSIBILITY_UI_TIMEOUT);
498         try {
499             final int a11ySettingDuration = (int) shortDuration + 1000;
500             putSecureSetting(SETTINGS_ACCESSIBILITY_UI_TIMEOUT,
501                     Integer.toString(a11ySettingDuration));
502             waitForA11yRecommendedTimeoutChanged(mContext,
503                     ACCESSIBILITY_STATE_WAIT_TIMEOUT_MS, a11ySettingDuration);
504             start = SystemClock.uptimeMillis();
505             runOnMainAndDrawSync(mToast.getView(), showToast);
506             assertCustomToastShownAndHidden(mToast.getView());
507             final long a11yDuration = SystemClock.uptimeMillis() - start;
508             assertTrue("Toast duration " + a11yDuration + "ms < A11y setting " + a11ySettingDuration
509                     + "ms", a11yDuration >= a11ySettingDuration);
510         } finally {
511             putSecureSetting(SETTINGS_ACCESSIBILITY_UI_TIMEOUT, originalSetting);
512         }
513     }
514 
515     @Test
testAccessDuration_whenTextToastAndWithA11yTimeoutEnabled()516     public void testAccessDuration_whenTextToastAndWithA11yTimeoutEnabled() throws Throwable {
517         requireRunNotOnVisibleBackgroundNonProfileUser();
518         makeTextToast();
519         final Runnable showToast = () -> {
520             mToast.setDuration(Toast.LENGTH_SHORT);
521             showToastWithNotificationPermission(mToast);
522         };
523         long start = SystemClock.uptimeMillis();
524         mActivityRule.runOnUiThread(showToast);
525         assertTextToastShownAndHidden();
526         final long shortDuration = SystemClock.uptimeMillis() - start;
527 
528         final String originalSetting = Settings.Secure.getString(mContext.getContentResolver(),
529                 SETTINGS_ACCESSIBILITY_UI_TIMEOUT);
530         try {
531             final int a11ySettingDuration = (int) shortDuration + 1000;
532             putSecureSetting(SETTINGS_ACCESSIBILITY_UI_TIMEOUT,
533                     Integer.toString(a11ySettingDuration));
534             waitForA11yRecommendedTimeoutChanged(mContext,
535                     ACCESSIBILITY_STATE_WAIT_TIMEOUT_MS, a11ySettingDuration);
536             makeTextToast();
537             start = SystemClock.uptimeMillis();
538             mActivityRule.runOnUiThread(showToast);
539             assertTextToastShownAndHidden();
540             final long a11yDuration = SystemClock.uptimeMillis() - start;
541             assertTrue("Toast duration " + a11yDuration + "ms < A11y setting " + a11ySettingDuration
542                     + "ms", a11yDuration >= a11ySettingDuration);
543         } finally {
544             putSecureSetting(SETTINGS_ACCESSIBILITY_UI_TIMEOUT, originalSetting);
545         }
546     }
547 
548     /**
549      * Wait for accessibility recommended timeout changed and equals to expected timeout.
550      *
551      * @param expectedTimeoutMs expected recommended timeout
552      */
waitForA11yRecommendedTimeoutChanged(Context context, long waitTimeoutMs, int expectedTimeoutMs)553     private void waitForA11yRecommendedTimeoutChanged(Context context,
554             long waitTimeoutMs, int expectedTimeoutMs) {
555         final AccessibilityManager manager =
556                 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
557         final Object lock = new Object();
558         AccessibilityManager.AccessibilityServicesStateChangeListener listener = (m) -> {
559             synchronized (lock) {
560                 lock.notifyAll();
561             }
562         };
563         manager.addAccessibilityServicesStateChangeListener(listener);
564         try {
565             TestUtils.waitOn(lock,
566                     () -> manager.getRecommendedTimeoutMillis(0,
567                             AccessibilityManager.FLAG_CONTENT_TEXT) == expectedTimeoutMs,
568                     waitTimeoutMs,
569                     "Wait for accessibility recommended timeout changed");
570         } finally {
571             manager.removeAccessibilityServicesStateChangeListener(listener);
572         }
573     }
574 
putSecureSetting(String name, String value)575     private void putSecureSetting(String name, String value) {
576         final StringBuilder cmd = new StringBuilder("settings put secure ")
577                 .append(name).append(" ")
578                 .append(value);
579         SystemUtil.runShellCommand(cmd.toString());
580     }
581 
582     @Test
testAccessMargin_whenCustomToast()583     public void testAccessMargin_whenCustomToast() throws Throwable {
584         assumeFalse("Skipping test: Auto does not support toast with margin", isCar());
585 
586         makeCustomToast();
587         View view = mToast.getView();
588         assertFalse(view.getLayoutParams() instanceof WindowManager.LayoutParams);
589 
590         final float horizontal1 = 1.0f;
591         final float vertical1 = 1.0f;
592         runOnMainAndDrawSync(view, () -> {
593             mToast.setMargin(horizontal1, vertical1);
594             showToastWithNotificationPermission(mToast);
595             registerLayoutListener(mToast.getView());
596         });
597         assertCustomToastShown(view);
598 
599         assertEquals(horizontal1, mToast.getHorizontalMargin(), 0.0f);
600         assertEquals(vertical1, mToast.getVerticalMargin(), 0.0f);
601         WindowManager.LayoutParams params1 = (WindowManager.LayoutParams) view.getLayoutParams();
602         assertEquals(horizontal1, params1.horizontalMargin, 0.0f);
603         assertEquals(vertical1, params1.verticalMargin, 0.0f);
604         assertLayoutDone(view);
605 
606         int[] xy1 = new int[2];
607         view.getLocationOnScreen(xy1);
608         assertCustomToastShownAndHidden(view);
609 
610         final float horizontal2 = 0.1f;
611         final float vertical2 = 0.1f;
612         runOnMainAndDrawSync(view, () -> {
613             mToast.setMargin(horizontal2, vertical2);
614             showToastWithNotificationPermission(mToast);
615             registerLayoutListener(mToast.getView());
616         });
617         assertCustomToastShown(view);
618 
619         assertEquals(horizontal2, mToast.getHorizontalMargin(), 0.0f);
620         assertEquals(vertical2, mToast.getVerticalMargin(), 0.0f);
621         WindowManager.LayoutParams params2 = (WindowManager.LayoutParams) view.getLayoutParams();
622         assertEquals(horizontal2, params2.horizontalMargin, 0.0f);
623         assertEquals(vertical2, params2.verticalMargin, 0.0f);
624 
625         assertLayoutDone(view);
626         int[] xy2 = new int[2];
627         view.getLocationOnScreen(xy2);
628         assertCustomToastShownAndHidden(view);
629 
630         /** Check if the test is being run on a watch.
631          *
632          * Change I8180e5080e0a6860b40dbb2faa791f0ede926ca7 updated how toast are displayed on the
633          * watch. Unlike the phone, which displays toast centered horizontally at the bottom of the
634          * screen, the watch now displays toast in the center of the screen.
635          */
636         if (Gravity.CENTER == mToast.getGravity()) {
637             assertTrue(xy1[0] > xy2[0]);
638             assertTrue(xy1[1] > xy2[1]);
639         } else {
640             assertTrue(xy1[0] > xy2[0]);
641             assertTrue(xy1[1] < xy2[1]);
642         }
643     }
644 
645     @Test
646     public void testAccessGravity_whenCustomToast() throws Throwable {
647         assumeFalse("Skipping test: Auto does not support toast with gravity", isCar());
648 
649         makeCustomToast();
650         runOnMainAndDrawSync(mToast.getView(), () -> {
651             mToast.setGravity(Gravity.CENTER, 0, 0);
652             showToastWithNotificationPermission(mToast);
653             registerLayoutListener(mToast.getView());
654         });
655         View view = mToast.getView();
656         assertCustomToastShown(view);
657         assertEquals(Gravity.CENTER, mToast.getGravity());
658         assertEquals(0, mToast.getXOffset());
659         assertEquals(0, mToast.getYOffset());
660         assertLayoutDone(view);
661         int[] centerXY = new int[2];
662         view.getLocationOnScreen(centerXY);
663         assertCustomToastShownAndHidden(view);
664 
665         runOnMainAndDrawSync(mToast.getView(), () -> {
666             mToast.setGravity(Gravity.BOTTOM, 0, 0);
667             showToastWithNotificationPermission(mToast);
668             registerLayoutListener(mToast.getView());
669         });
670         view = mToast.getView();
671         assertCustomToastShown(view);
672         assertEquals(Gravity.BOTTOM, mToast.getGravity());
673         assertEquals(0, mToast.getXOffset());
674         assertEquals(0, mToast.getYOffset());
675         assertLayoutDone(view);
676         int[] bottomXY = new int[2];
677         view.getLocationOnScreen(bottomXY);
678         assertCustomToastShownAndHidden(view);
679 
680         // x coordinate is the same
681         assertEquals(centerXY[0], bottomXY[0]);
682         // bottom view is below of center view
683         assertTrue(centerXY[1] < bottomXY[1]);
684 
685         final int xOffset = 20;
686         final int yOffset = 10;
687         runOnMainAndDrawSync(mToast.getView(), () -> {
688             mToast.setGravity(Gravity.BOTTOM, xOffset, yOffset);
689             mToast.show();
690             registerLayoutListener(mToast.getView());
691         });
692         view = mToast.getView();
693         assertCustomToastShown(view);
694         assertEquals(Gravity.BOTTOM, mToast.getGravity());
695         assertEquals(xOffset, mToast.getXOffset());
696         assertEquals(yOffset, mToast.getYOffset());
697         assertLayoutDone(view);
698         int[] bottomOffsetXY = new int[2];
699         view.getLocationOnScreen(bottomOffsetXY);
700         assertCustomToastShownAndHidden(view);
701 
702         assertEquals(bottomXY[0] + xOffset, bottomOffsetXY[0]);
703         assertEquals(bottomXY[1] - yOffset, bottomOffsetXY[1]);
704     }
705 
706     @UiThreadTest
707     @Test
testMakeTextFromString()708     public void testMakeTextFromString() {
709         assumeFalse("Skipping test: Watch does not support new Toast behavior yet", isWatch());
710         Toast toast = Toast.makeText(mContext, "android", Toast.LENGTH_SHORT);
711         assertNotNull(toast);
712         assertEquals(Toast.LENGTH_SHORT, toast.getDuration());
713         View view = toast.getView();
714         assertNull(view);
715 
716         toast = Toast.makeText(mContext, "cts", Toast.LENGTH_LONG);
717         assertNotNull(toast);
718         assertEquals(Toast.LENGTH_LONG, toast.getDuration());
719         view = toast.getView();
720         assertNull(view);
721 
722         toast = Toast.makeText(mContext, null, Toast.LENGTH_LONG);
723         assertNotNull(toast);
724         assertEquals(Toast.LENGTH_LONG, toast.getDuration());
725         view = toast.getView();
726         assertNull(view);
727     }
728 
729     @UiThreadTest
730     @Test(expected=NullPointerException.class)
testMakeTextFromStringNullContext()731     public void testMakeTextFromStringNullContext() {
732         Toast.makeText(null, "test", Toast.LENGTH_LONG);
733     }
734 
735     @UiThreadTest
736     @Test
testMakeTextFromResource()737     public void testMakeTextFromResource() {
738         assumeFalse("Skipping test: Watch does not support new Toast behavior yet", isWatch());
739         Toast toast = Toast.makeText(mContext, R.string.hello_world, Toast.LENGTH_LONG);
740 
741         assertNotNull(toast);
742         assertEquals(Toast.LENGTH_LONG, toast.getDuration());
743         View view = toast.getView();
744         assertNull(view);
745 
746         toast = Toast.makeText(mContext, R.string.hello_android, Toast.LENGTH_SHORT);
747         assertNotNull(toast);
748         assertEquals(Toast.LENGTH_SHORT, toast.getDuration());
749         view = toast.getView();
750         assertNull(view);
751     }
752 
753     @UiThreadTest
754     @Test(expected=NullPointerException.class)
testMakeTextFromResourceNullContext()755     public void testMakeTextFromResourceNullContext() {
756         Toast.makeText(null, R.string.hello_android, Toast.LENGTH_SHORT);
757     }
758 
759     @UiThreadTest
760     @Test
testSetTextFromResource()761     public void testSetTextFromResource() {
762         Toast toast = Toast.makeText(mContext, R.string.text, Toast.LENGTH_LONG);
763 
764         toast.setText(R.string.hello_world);
765         // TODO: how to getText to assert?
766 
767         toast.setText(R.string.hello_android);
768         // TODO: how to getText to assert?
769     }
770 
771     @UiThreadTest
772     @Test(expected=RuntimeException.class)
testSetTextFromInvalidResource()773     public void testSetTextFromInvalidResource() {
774         Toast toast = Toast.makeText(mContext, R.string.text, Toast.LENGTH_LONG);
775         toast.setText(-1);
776     }
777 
778     @UiThreadTest
779     @Test
testSetTextFromString()780     public void testSetTextFromString() {
781         Toast toast = Toast.makeText(mContext, R.string.text, Toast.LENGTH_LONG);
782 
783         toast.setText("cts");
784         // TODO: how to getText to assert?
785 
786         toast.setText("android");
787         // TODO: how to getText to assert?
788     }
789 
790     @UiThreadTest
791     @Test(expected = IllegalStateException.class)
testSetTextFromStringNonNullView()792     public void testSetTextFromStringNonNullView() {
793         assumeFalse("Skipping test: Watch does not support new Toast behavior yet", isWatch());
794         Toast toast = Toast.makeText(mContext, R.string.text, Toast.LENGTH_LONG);
795         toast.setView(new TextView(mContext));
796         toast.setText(null);
797     }
798 
799     @Test
testRemovedCallbackIsNotCalled()800     public void testRemovedCallbackIsNotCalled() throws Throwable {
801         CompletableFuture<Void> toastShown = new CompletableFuture<>();
802         CompletableFuture<Void> toastHidden = new CompletableFuture<>();
803         Toast.Callback testCallback = new Toast.Callback() {
804             @Override
805             public void onToastShown() {
806                 toastShown.complete(null);
807             }
808             @Override
809             public void onToastHidden() {
810                 toastHidden.complete(null);
811             }
812         };
813         mToastShown = new ConditionVariable(false);
814         mToastHidden = new ConditionVariable(false);
815         mActivityRule.runOnUiThread(
816                 () -> {
817                     mToast = Toast.makeText(mContext, TEST_TOAST_TEXT, Toast.LENGTH_LONG);
818                     mToast.addCallback(testCallback);
819                     mToast.addCallback(new ConditionCallback(mToastShown, mToastHidden));
820                     mToast.removeCallback(testCallback);
821                 });
822 
823         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
824 
825         assertTextToastShownAndHidden();
826         assertFalse(toastShown.isDone());
827         assertFalse(toastHidden.isDone());
828     }
829 
830     @Test(expected = NullPointerException.class)
testAddCallback_whenNull_throws()831     public void testAddCallback_whenNull_throws() throws Throwable {
832         makeTextToast();
833         mToast.addCallback(null);
834     }
835 
836     @Test
testCallback_whenTextToast_isCalled()837     public void testCallback_whenTextToast_isCalled() throws Throwable {
838         ConditionVariable toastShown = new ConditionVariable(false);
839         ConditionVariable toastHidden = new ConditionVariable(false);
840         mActivityRule.runOnUiThread(
841                 () -> {
842                     mToast = Toast.makeText(mContext, TEST_TOAST_TEXT, Toast.LENGTH_LONG);
843                     mToast.addCallback(new ConditionCallback(toastShown, toastHidden));
844                 });
845 
846         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
847 
848         assertTrue(toastShown.block(TIME_OUT));
849         assertTrue(toastHidden.block(TIME_OUT));
850     }
851 
852     @Test
testCallback_whenCustomToast_isCalled()853     public void testCallback_whenCustomToast_isCalled() throws Throwable {
854         makeCustomToast();
855         ConditionVariable toastShown = new ConditionVariable(false);
856         ConditionVariable toastHidden = new ConditionVariable(false);
857         mActivityRule.runOnUiThread(
858                 () -> mToast.addCallback(new ConditionCallback(toastShown, toastHidden)));
859 
860         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
861 
862         assertTrue(toastShown.block(TIME_OUT));
863         assertTrue(toastHidden.block(TIME_OUT));
864     }
865 
866     @Test
testTextToastAllowed_whenInTheForeground()867     public void testTextToastAllowed_whenInTheForeground()
868             throws Throwable {
869         makeTextToast();
870 
871         mActivityRule.runOnUiThread(
872                 () -> showToastWithoutNotificationPermission(mToast)
873         );
874 
875         assertTextToastShownAndHidden();
876     }
877 
878     @Test
testCustomToastAllowed_whenInTheForeground()879     public void testCustomToastAllowed_whenInTheForeground() throws Throwable {
880         makeCustomToast();
881         View view = mToast.getView();
882         // View has not been attached to screen yet
883         assertNull(view.getParent());
884 
885         mActivityRule.runOnUiThread(
886                 () -> showToastWithoutNotificationPermission(mToast)
887         );
888 
889         assertCustomToastShownAndHidden(view);
890     }
891 
892     @Test
testTextToastAllowed_whenInTheBackground_withNotificationPermission()893     public void testTextToastAllowed_whenInTheBackground_withNotificationPermission()
894             throws Throwable {
895         assumeFalse("Skipping test: Watch does not support new Toast behavior yet", isWatch());
896         // Make it background
897         mActivityRule.finishActivity();
898         makeTextToast();
899 
900         mActivityRule.runOnUiThread(
901                 () -> showToastWithNotificationPermission(mToast)
902         );
903         assertTextToastShownAndHidden();
904     }
905 
906     @Test
testTextToastNotAllowed_whenInTheBackground_withoutNotificationPermission()907     public void testTextToastNotAllowed_whenInTheBackground_withoutNotificationPermission()
908             throws Throwable {
909         assumeFalse("Skipping test: Watch does not support new Toast behavior yet", isWatch());
910 
911         // Make it background
912         mActivityRule.finishActivity();
913         // may take time for the app process importance to get downgraded from foreground:
914         SystemClock.sleep(TIME_FOR_UI_OPERATION);
915 
916         List<TextToastInfo> toastInfoList = createTextToasts(1, "Text", Toast.LENGTH_SHORT);
917 
918         mActivityRule.runOnUiThread(
919                 () -> showToastWithoutNotificationPermission(toastInfoList.get(0).getToast())
920         );
921 
922         assertTextToastNotShown(toastInfoList.get(0));
923     }
924 
925     @Test
testCustomToastBlocked_whenInTheBackground()926     public void testCustomToastBlocked_whenInTheBackground() throws Throwable {
927         // Make it background
928         mActivityRule.finishActivity();
929         makeCustomToast();
930         View view = mToast.getView();
931         // View has not been attached to screen yet
932         assertNull(view.getParent());
933 
934         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
935 
936         assertCustomToastNotShown(view);
937     }
938 
939     @Test
testCustomToastBlocked_whenBehindTranslucentActivity()940     public void testCustomToastBlocked_whenBehindTranslucentActivity() throws Throwable {
941         ConditionVariable activityStarted = registerBlockingReceiver(
942                 ACTION_TRANSLUCENT_ACTIVITY_RESUMED);
943         Intent intent = new Intent();
944         intent.setComponent(COMPONENT_TRANSLUCENT_ACTIVITY);
945         // Launch the translucent activity in fullscreen to ensure the test activity won't resume
946         // even on the freeform-first multi-window device.
947         final ActivityOptions options = ActivityOptions.makeBasic();
948         options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN);
949         mActivityRule.getActivity().startActivity(intent, options.toBundle());
950         activityStarted.block();
951         makeCustomToast();
952         View view = mToast.getView();
953 
954         mActivityRule.runOnUiThread(() -> showToastWithNotificationPermission(mToast));
955 
956         assertCustomToastNotShown(view);
957 
958         // Start CtsActivity with CLEAR_TOP flag to finish the TranslucentActivity on top.
959         intent = new Intent();
960         intent.setComponent(COMPONENT_CTS_ACTIVITY);
961         intent.setAction(Intent.ACTION_MAIN);
962         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP
963                 | Intent.FLAG_ACTIVITY_SINGLE_TOP);
964         mActivityRule.getActivity().startActivity(intent);
965     }
966 
967     @UiThreadTest
968     @Test
testGetWindowParams_whenTextToast_returnsNull()969     public void testGetWindowParams_whenTextToast_returnsNull() {
970           assumeFalse("Skipping test: Watch does not support new Toast behavior yet", isWatch());
971         Toast toast = Toast.makeText(mContext, "Text", Toast.LENGTH_LONG);
972         assertNull(toast.getWindowParams());
973     }
974 
975     @UiThreadTest
976     @Test
testGetWindowParams_whenCustomToast_doesNotReturnNull()977     public void testGetWindowParams_whenCustomToast_doesNotReturnNull() {
978         Toast toast = new Toast(mContext);
979         toast.setView(new TextView(mContext));
980         assertNotNull(toast.getWindowParams());
981     }
982 
983     @Test
testShow_whenTextToast_sendsAccessibilityEvent()984     public void testShow_whenTextToast_sendsAccessibilityEvent() throws Throwable {
985         makeTextToast();
986         AccessibilityEventFilter filter =
987                 event -> event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED;
988 
989         AccessibilityEvent event = mUiAutomation.executeAndWaitForEvent(
990                 () -> uncheck(() -> mActivityRule.runOnUiThread(
991                         () -> showToastWithNotificationPermission(mToast))),
992                 filter,
993                 TIME_OUT);
994         assertThat(event.getEventType()).isEqualTo(
995                 AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
996         assertThat(event.getClassName()).isEqualTo(Toast.class.getCanonicalName());
997         assertThat(event.getPackageName()).isEqualTo(mContext.getPackageName());
998         assertThat(event.getText()).contains(TEST_TOAST_TEXT);
999     }
1000 
1001     @Test
testShow_whenCustomToast_sendsAccessibilityEvent()1002     public void testShow_whenCustomToast_sendsAccessibilityEvent() throws Throwable {
1003         makeCustomToast();
1004         AccessibilityEventFilter filter =
1005                 event -> event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED;
1006 
1007         AccessibilityEvent event = mUiAutomation.executeAndWaitForEvent(
1008                 () -> uncheck(() -> mActivityRule.runOnUiThread(
1009                         () -> showToastWithNotificationPermission(mToast))),
1010                 filter,
1011                 TIME_OUT);
1012 
1013         assertThat(event.getEventType()).isEqualTo(
1014                 AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
1015         assertThat(event.getClassName()).isEqualTo(Toast.class.getCanonicalName());
1016         assertThat(event.getPackageName()).isEqualTo(mContext.getPackageName());
1017         assertThat(event.getText()).contains(TEST_CUSTOM_TOAST_TEXT);
1018     }
1019 
1020     @Test
testPackageCantPostMoreThanMaxToastsQuickly()1021     public void testPackageCantPostMoreThanMaxToastsQuickly() throws Throwable {
1022         List<TextToastInfo> toasts =
1023                 createTextToasts(MAX_PACKAGE_TOASTS_LIMIT + 1, "Text", Toast.LENGTH_SHORT);
1024         showToasts(toasts);
1025 
1026         assertTextToastsShownAndHidden(toasts.subList(0, MAX_PACKAGE_TOASTS_LIMIT));
1027         assertTextToastNotShown(toasts.get(MAX_PACKAGE_TOASTS_LIMIT));
1028     }
1029 
1030     /**
1031      * Long running test (~50 seconds) - we need to test toast rate limiting limits, which requires
1032      * us to wait a given amount of time to test that the limit has been enforced/lifted.
1033      */
1034     @Test
1035     @ApiTest(apis = {"android.widget.Toast#show"})
testRateLimitingToastsWhenInBackground()1036     public void testRateLimitingToastsWhenInBackground() throws Throwable {
1037         assumeFalse("Skipping test: Watch does not support new Toast behavior yet", isWatch());
1038         // enable rate limiting to test it
1039         SystemUtil.runWithShellPermissionIdentity(() -> mNotificationManager
1040                 .setToastRateLimitingEnabled(true));
1041         // move to background
1042         mActivityRule.finishActivity();
1043 
1044         long totalTimeSpentMs = 0;
1045         int shownToastsNum = 0;
1046         // We add additional 3 seconds just to be sure we get into the next window.
1047         long additionalWaitTime = 3_000L;
1048 
1049         for (int i = 0; i < TOAST_RATE_LIMITS.length; i++) {
1050             int currentToastNum = TOAST_RATE_LIMITS[i] - shownToastsNum;
1051             List<TextToastInfo> toasts =
1052                     createTextToasts(currentToastNum + 1, "Text", Toast.LENGTH_SHORT);
1053             long startTime = SystemClock.elapsedRealtime();
1054             showToasts(toasts);
1055 
1056             assertTextToastsShownAndHidden(toasts.subList(0, currentToastNum));
1057             assertTextToastNotShown(toasts.get(currentToastNum));
1058             long endTime = SystemClock.elapsedRealtime();
1059 
1060             // We won't check after the last limit, no need to sleep then.
1061             if (i != TOAST_RATE_LIMITS.length - 1) {
1062                 totalTimeSpentMs += endTime - startTime;
1063                 shownToastsNum += currentToastNum;
1064                 long sleepTime = Math.max(
1065                         TOAST_WINDOW_SIZES_MS[i] - totalTimeSpentMs + additionalWaitTime, 0);
1066                 SystemClock.sleep(sleepTime);
1067                 totalTimeSpentMs += sleepTime;
1068             }
1069         }
1070     }
1071 
1072     @Test
testDontRateLimitToastsWhenInForeground()1073     public void testDontRateLimitToastsWhenInForeground() throws Throwable {
1074         // enable rate limiting to test it
1075         SystemUtil.runWithShellPermissionIdentity(() -> mNotificationManager
1076                 .setToastRateLimitingEnabled(true));
1077 
1078         List<TextToastInfo> toasts =
1079                 createTextToasts(TOAST_RATE_LIMITS[0] + 1, "Text", Toast.LENGTH_SHORT);
1080         showToasts(toasts);
1081         assertTextToastsShownAndHidden(toasts);
1082     }
1083 
1084     @Test
testCustomToastPostedWhileInForeground_notShownWhenAppGoesToBackground()1085     public void testCustomToastPostedWhileInForeground_notShownWhenAppGoesToBackground()
1086             throws Throwable {
1087         List<CustomToastInfo> toasts = createCustomToasts(2, "Custom", Toast.LENGTH_SHORT);
1088         showToasts(toasts);
1089         assertCustomToastShown(toasts.get(0));
1090 
1091         // move to background
1092         mActivityRule.finishActivity();
1093 
1094         assertCustomToastHidden(toasts.get(0));
1095         assertCustomToastNotShown(toasts.get(1));
1096     }
1097 
1098     @Test
1099     @ApiTest(apis = {"android.widget.Toast#show"})
testAppWithUnlimitedToastsPermissionCanPostUnlimitedToasts()1100     public void testAppWithUnlimitedToastsPermissionCanPostUnlimitedToasts() throws Throwable {
1101         assumeFalse("Skipping test: Watch does not support new Toast behavior yet", isWatch());
1102         // enable rate limiting to test it
1103         SystemUtil.runWithShellPermissionIdentity(() -> mNotificationManager
1104                 .setToastRateLimitingEnabled(true));
1105         // move to background
1106         mActivityRule.finishActivity();
1107 
1108         int highestToastRateLimit = TOAST_RATE_LIMITS[TOAST_RATE_LIMITS.length - 1];
1109         List<TextToastInfo> toasts = createTextToasts(highestToastRateLimit + 1, "Text",
1110                 Toast.LENGTH_SHORT);
1111 
1112         // We have to show one by one to avoid max number of toasts enqueued by a single package at
1113         // a time.
1114         for (TextToastInfo t : toasts) {
1115             // Run with both android.permission.UNLIMITED_TOASTS and POST_NOTIFICATIONS permissions.
1116             showToastWithUnlimitedToastAndPostNotificationsPermissions(t);
1117             assertTextToastShownAndHidden(t);
1118         }
1119     }
1120 
1121     /** Create given number of text toasts with the same given text and length. */
createTextToasts(int num, String text, int length)1122     private List<TextToastInfo> createTextToasts(int num, String text, int length)
1123             throws Throwable {
1124         List<TextToastInfo> toasts = new ArrayList<>();
1125         mActivityRule.runOnUiThread(() -> {
1126             toasts.addAll(Stream
1127                     .generate(() -> TextToastInfo.create(mContext, text, length))
1128                     .limit(num)
1129                     .collect(toList()));
1130         });
1131         return toasts;
1132     }
1133 
1134     /** Create given number of custom toasts with the same given text and length. */
createCustomToasts(int num, String text, int length)1135     private List<CustomToastInfo> createCustomToasts(int num, String text, int length)
1136             throws Throwable {
1137         List<CustomToastInfo> toasts = new ArrayList<>();
1138         mActivityRule.runOnUiThread(() -> {
1139             toasts.addAll(Stream
1140                     .generate(() -> CustomToastInfo.create(mContext, text, length))
1141                     .limit(num)
1142                     .collect(toList()));
1143         });
1144         return toasts;
1145     }
1146 
showToasts(List<? extends ToastInfo> toasts)1147     private void showToasts(List<? extends ToastInfo> toasts) throws Throwable {
1148         mActivityRule.runOnUiThread(() -> {
1149             for (ToastInfo t : toasts) {
1150                 showToastWithNotificationPermission(t.getToast());
1151             }
1152         });
1153     }
1154 
showToastWithUnlimitedToastAndPostNotificationsPermissions(ToastInfo toast)1155     private void showToastWithUnlimitedToastAndPostNotificationsPermissions(ToastInfo toast)
1156             throws Throwable {
1157         mActivityRule.runOnUiThread(() -> {
1158             showToastWithPermissions(toast.getToast(), POST_NOTIFICATIONS, UNLIMITED_TOASTS);
1159         });
1160     }
1161 
assertTextToastsShownAndHidden(List<TextToastInfo> toasts)1162     private void assertTextToastsShownAndHidden(List<TextToastInfo> toasts) {
1163         for (int i = 0; i < toasts.size(); i++) {
1164             assertTextToastShownAndHidden(toasts.get(i));
1165         }
1166     }
1167 
registerBlockingReceiver(String action)1168     private ConditionVariable registerBlockingReceiver(String action) {
1169         ConditionVariable broadcastReceived = new ConditionVariable(false);
1170         IntentFilter filter = new IntentFilter(action);
1171         mContext.registerReceiver(new BroadcastReceiver() {
1172             @Override
1173             public void onReceive(Context context, Intent intent) {
1174                 broadcastReceived.open();
1175             }
1176         }, filter, Context.RECEIVER_EXPORTED_UNAUDITED);
1177         return broadcastReceived;
1178     }
1179 
runOnMainAndDrawSync(@onNull final View toastView, @Nullable final Runnable runner)1180     private void runOnMainAndDrawSync(@NonNull final View toastView,
1181             @Nullable final Runnable runner) {
1182         final CountDownLatch latch = new CountDownLatch(1);
1183 
1184         try {
1185             mActivityRule.runOnUiThread(() -> {
1186                 final ViewTreeObserver.OnDrawListener listener =
1187                         new ViewTreeObserver.OnDrawListener() {
1188                             @Override
1189                             public void onDraw() {
1190                                 // posting so that the sync happens after the draw that's about
1191                                 // to happen
1192                                 toastView.post(() -> {
1193                                     toastView.getViewTreeObserver().removeOnDrawListener(this);
1194                                     latch.countDown();
1195                                 });
1196                             }
1197                         };
1198 
1199                 toastView.getViewTreeObserver().addOnDrawListener(listener);
1200 
1201                 if (runner != null) {
1202                     runner.run();
1203                 }
1204                 toastView.invalidate();
1205             });
1206 
1207             Assert.assertTrue("Expected toast draw pass occurred within 5 seconds",
1208                     latch.await(5, TimeUnit.SECONDS));
1209         } catch (Throwable t) {
1210             throw new RuntimeException(t);
1211         }
1212     }
1213 
isCar()1214     private boolean isCar() {
1215         PackageManager pm = mContext.getPackageManager();
1216         return pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
1217     }
1218 
isWatch()1219     private boolean isWatch() {
1220         PackageManager pm = mContext.getPackageManager();
1221         return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
1222     }
1223 
uncheck(ThrowingRunnable runnable)1224     private static void uncheck(ThrowingRunnable runnable) {
1225         try {
1226             runnable.run();
1227         } catch (Throwable e) {
1228             throw new RuntimeException(e);
1229         }
1230     }
1231 
showToastWithNotificationPermission(Toast toast)1232     private static void showToastWithNotificationPermission(Toast toast) {
1233         showToast(toast, true);
1234     }
1235 
showToastWithoutNotificationPermission(Toast toast)1236     private static void showToastWithoutNotificationPermission(Toast toast) {
1237         showToast(toast, false);
1238     }
1239 
showToast(Toast toast, boolean runWithPostNotificationPermission)1240     private static void showToast(Toast toast, boolean runWithPostNotificationPermission) {
1241         if (runWithPostNotificationPermission) {
1242             SystemUtil.runWithShellPermissionIdentity(() -> toast.show(), POST_NOTIFICATIONS);
1243         } else {
1244             toast.show();
1245         }
1246     }
1247 
showToastWithPermissions(Toast toast, String... permissions)1248     private static void showToastWithPermissions(Toast toast,  String... permissions) {
1249         if (permissions.length > 0) {
1250             SystemUtil.runWithShellPermissionIdentity(() -> toast.show(), permissions);
1251         } else {
1252             toast.show();
1253         }
1254     }
1255 
1256     private interface ThrowingRunnable {
1257         void run() throws Throwable;
1258     }
1259 
1260     private static class ConditionCallback extends Toast.Callback {
1261         private final ConditionVariable mToastShown;
1262         private final ConditionVariable mToastHidden;
1263 
ConditionCallback(ConditionVariable toastShown, ConditionVariable toastHidden)1264         ConditionCallback(ConditionVariable toastShown, ConditionVariable toastHidden) {
1265             mToastShown = toastShown;
1266             mToastHidden = toastHidden;
1267         }
1268 
1269         @Override
onToastShown()1270         public void onToastShown() {
1271             mToastShown.open();
1272         }
1273 
1274         @Override
onToastHidden()1275         public void onToastHidden() {
1276             mToastHidden.open();
1277         }
1278     }
1279 
1280     private static class TextToastInfo implements ToastInfo {
1281         private final Toast mToast;
1282         private final ConditionVariable mToastShown;
1283         private final ConditionVariable mToastHidden;
1284 
TextToastInfo( Toast toast, ConditionVariable toastShown, ConditionVariable toastHidden)1285         TextToastInfo(
1286                 Toast toast,
1287                 ConditionVariable toastShown,
1288                 ConditionVariable toastHidden) {
1289             mToast = toast;
1290             mToastShown = toastShown;
1291             mToastHidden = toastHidden;
1292         }
1293 
create(Context context, String text, int toastLength)1294         static TextToastInfo create(Context context, String text, int toastLength) {
1295             Toast t = Toast.makeText(context, text, toastLength);
1296             ConditionVariable toastShown = new ConditionVariable(false);
1297             ConditionVariable toastHidden = new ConditionVariable(false);
1298             t.addCallback(new ConditionCallback(toastShown, toastHidden));
1299             return new TextToastInfo(t, toastShown, toastHidden);
1300         }
1301 
1302         @Override
getToast()1303         public Toast getToast() {
1304             return mToast;
1305         }
1306 
blockOnToastShown(long timeout)1307         boolean blockOnToastShown(long timeout) {
1308             return mToastShown.block(timeout);
1309         }
1310 
blockOnToastHidden(long timeout)1311         boolean blockOnToastHidden(long timeout) {
1312             return mToastHidden.block(timeout);
1313         }
1314     }
1315 
1316     private static class CustomToastInfo implements ToastInfo {
1317         private final Toast mToast;
1318 
CustomToastInfo(Toast toast)1319         CustomToastInfo(Toast toast) {
1320             mToast = toast;
1321         }
1322 
create(Context context, String text, int toastLength)1323         static CustomToastInfo create(Context context, String text, int toastLength) {
1324             Toast t = new Toast(context);
1325             t.setDuration(toastLength);
1326             TextView view = new TextView(context);
1327             view.setText(text);
1328             t.setView(view);
1329             return new CustomToastInfo(t);
1330         }
1331 
1332         @Override
getToast()1333         public Toast getToast() {
1334             return mToast;
1335         }
1336 
isShowing()1337         boolean isShowing() {
1338             return mToast.getView().getParent() != null;
1339         }
1340     }
1341 
1342     interface ToastInfo {
1343         Toast getToast();
1344     }
1345 }
1346