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