xref: /aosp_15_r20/cts/tests/tests/systemui/src/android/systemui/cts/LightBarTests.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
2  * Copyright (C) 2015 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.systemui.cts;
18 
19 import static android.Manifest.permission.POST_NOTIFICATIONS;
20 import static android.Manifest.permission.REVOKE_POST_NOTIFICATIONS_WITHOUT_KILL;
21 import static android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS;
22 import static android.server.wm.BarTestUtils.assumeHasColoredNavigationBar;
23 import static android.server.wm.BarTestUtils.assumeHasColoredStatusBar;
24 
25 import static androidx.test.InstrumentationRegistry.getInstrumentation;
26 
27 import static org.junit.Assert.assertTrue;
28 
29 import android.app.Notification;
30 import android.app.NotificationChannel;
31 import android.app.NotificationManager;
32 import android.app.UiAutomation;
33 import android.content.Context;
34 import android.graphics.Bitmap;
35 import android.graphics.Color;
36 import android.graphics.Insets;
37 import android.graphics.drawable.ColorDrawable;
38 import android.os.Process;
39 import android.os.SystemClock;
40 import android.permission.PermissionManager;
41 import android.permission.cts.PermissionUtils;
42 import android.platform.test.annotations.AppModeFull;
43 import android.platform.test.annotations.PlatinumTest;
44 import android.server.wm.IgnoreOrientationRequestSession;
45 import android.view.Gravity;
46 import android.view.InputDevice;
47 import android.view.MotionEvent;
48 import android.view.WindowInsets.Type;
49 import android.view.WindowManager;
50 import android.view.WindowMetrics;
51 
52 import androidx.test.rule.ActivityTestRule;
53 import androidx.test.runner.AndroidJUnit4;
54 
55 import com.android.compatibility.common.util.SystemUtil;
56 import com.android.compatibility.common.util.ThrowingRunnable;
57 import com.android.settingslib.flags.Flags;
58 
59 import org.junit.After;
60 import org.junit.Before;
61 import org.junit.Rule;
62 import org.junit.Test;
63 import org.junit.rules.TestName;
64 import org.junit.runner.RunWith;
65 
66 /**
67  * Test for light status bar.
68  *
69  * atest CtsSystemUiTestCases:LightBarTests
70  */
71 @RunWith(AndroidJUnit4.class)
72 public class LightBarTests extends LightBarTestBase {
73 
74     public static final String TAG = "LightStatusBarTests";
75 
76     /**
77      * Color may be slightly off-spec when resources are resized for lower densities. Use this error
78      * margin to accommodate for that when comparing colors.
79      */
80     private static final int COLOR_COMPONENT_ERROR_MARGIN = 20;
81 
82     /**
83      * It's possible for the device to have color sampling enabled in the nav bar -- in that
84      * case we need to pick a background color that would result in the same dark icon tint
85      * that matches the default visibility flags used when color sampling is not enabled.
86      */
87     private static final int LIGHT_BG_COLOR = Color.rgb(255, 128, 128);
88 
89     /**
90      * Flags.newStatusBarIcons() changes the default light mode tint (i.e., dark icons) to 100%
91      * black. If the flag is on we need to change the foreground color we're looking for.
92      */
93     private static final int DARK_ICON_TINT_LEGACY = 0x99000000;
94     private static final int DARK_ICON_TINT = 0xff000000;
95 
96     private final String NOTIFICATION_TAG = "TEST_TAG";
97     private final String NOTIFICATION_CHANNEL_ID = "test_channel";
98     private final String NOTIFICATION_GROUP_KEY = "test_group";
99     private NotificationManager mNm;
100     private IgnoreOrientationRequestSession mOrientationRequestSession;
101 
102     @Rule
103     public ActivityTestRule<LightBarActivity> mActivityRule = new ActivityTestRule<>(
104             LightBarActivity.class);
105     @Rule
106     public TestName mTestName = new TestName();
107 
108 
109 
110     @Before
setUp()111     public void setUp() {
112         // We need to prevent letterboxing because when an activity is letterboxed, then the status
113         // bar icons are outside the activity space so our verification will fail. See b/246515090.
114         //
115         // When ignore_orientation_request is set to true and the device is in landscape but the
116         // activity is in portrait, then the device remains in landscape but letterboxes the
117         // activity (so the activity is *not* full screen). Setting ignore_orientation_request to
118         // false will cause the device to instead rotate to portrait to match the activity, thus
119         // preventing letterboxing.
120         mOrientationRequestSession = new IgnoreOrientationRequestSession(false /* enable */);
121     }
122 
123     @After
tearDown()124     public void tearDown() {
125         if (mOrientationRequestSession != null) {
126             mOrientationRequestSession.close();
127         }
128     }
129 
130     @Test
131     @AppModeFull // Instant apps cannot create notifications
132     @PlatinumTest(focusArea = "sysui")
testLightStatusBarIcons()133     public void testLightStatusBarIcons() throws Throwable {
134         assumeHasColoredStatusBar(mActivityRule);
135 
136         runInNotificationSession(() -> {
137             requestLightBars(LIGHT_BG_COLOR);
138             Thread.sleep(WAIT_TIME);
139 
140             Bitmap bitmap = takeStatusBarScreenshot(mActivityRule.getActivity());
141             Stats s = evaluateLightBarBitmap(bitmap, LIGHT_BG_COLOR, 0);
142             assertStats(bitmap, s, true /* light */);
143         });
144     }
145 
146     @Test
147     @AppModeFull // Instant apps cannot create notifications
148     @PlatinumTest(focusArea = "sysui")
testAppearanceCanOverwriteLegacyFlags()149     public void testAppearanceCanOverwriteLegacyFlags() throws Throwable {
150         assumeHasColoredStatusBar(mActivityRule);
151 
152         runInNotificationSession(() -> {
153             final LightBarActivity activity = mActivityRule.getActivity();
154             activity.runOnUiThread(() -> {
155                 activity.getWindow().setBackgroundDrawable(new ColorDrawable(LIGHT_BG_COLOR));
156 
157                 activity.setLightStatusBarLegacy(true);
158                 activity.setLightNavigationBarLegacy(true);
159 
160                 // The new appearance APIs can overwrite the appearance specified by the legacy
161                 // flags.
162                 activity.setLightStatusBarAppearance(false);
163                 activity.setLightNavigationBarAppearance(false);
164             });
165             Thread.sleep(WAIT_TIME);
166 
167             Bitmap bitmap = takeStatusBarScreenshot(mActivityRule.getActivity());
168             Stats s = evaluateDarkBarBitmap(bitmap, LIGHT_BG_COLOR, 0);
169             assertStats(bitmap, s, false /* light */);
170         });
171     }
172 
173     @Test
174     @AppModeFull // Instant apps cannot create notifications
175     @PlatinumTest(focusArea = "sysui")
testLegacyFlagsCannotOverwriteAppearance()176     public void testLegacyFlagsCannotOverwriteAppearance() throws Throwable {
177         assumeHasColoredStatusBar(mActivityRule);
178 
179         runInNotificationSession(() -> {
180             final LightBarActivity activity = mActivityRule.getActivity();
181             activity.runOnUiThread(() -> {
182                 activity.getWindow().setBackgroundDrawable(new ColorDrawable(LIGHT_BG_COLOR));
183 
184                 activity.setLightStatusBarAppearance(false);
185                 activity.setLightNavigationBarAppearance(false);
186 
187                 // Once the client starts using the new appearance APIs, the legacy flags won't
188                 // change the appearance anymore.
189                 activity.setLightStatusBarLegacy(true);
190                 activity.setLightNavigationBarLegacy(true);
191             });
192             Thread.sleep(WAIT_TIME);
193 
194             Bitmap bitmap = takeStatusBarScreenshot(mActivityRule.getActivity());
195             Stats s = evaluateDarkBarBitmap(bitmap, LIGHT_BG_COLOR, 0);
196             assertStats(bitmap, s, false /* light */);
197         });
198     }
199 
200     @Test
testLightNavigationBar()201     public void testLightNavigationBar() throws Throwable {
202         assumeHasColoredNavigationBar(mActivityRule);
203 
204         requestLightBars(LIGHT_BG_COLOR);
205         Thread.sleep(WAIT_TIME);
206 
207         // Inject a cancelled interaction with the nav bar to ensure it is at full opacity.
208         int x = mActivityRule.getActivity().getWidth() / 2;
209         int y = mActivityRule.getActivity().getBottom() + 10;
210         injectCanceledTap(x, y);
211         Thread.sleep(WAIT_TIME);
212 
213         LightBarActivity activity = mActivityRule.getActivity();
214         Bitmap bitmap = takeNavigationBarScreenshot(activity);
215         Stats s = evaluateLightBarBitmap(bitmap, LIGHT_BG_COLOR, activity.getBottom());
216         assertStats(bitmap, s, true /* light */);
217     }
218 
219     @Test
220     @AppModeFull // Instant apps cannot create notifications
testLightBarIsNotAllowed_fitStatusBar()221     public void testLightBarIsNotAllowed_fitStatusBar() throws Throwable {
222         assumeHasColoredStatusBar(mActivityRule);
223 
224         runInNotificationSession(() -> {
225             final LightBarActivity activity = mActivityRule.getActivity();
226             activity.runOnUiThread(() -> {
227                 final WindowMetrics metrics = activity.getWindowManager().getCurrentWindowMetrics();
228                 final Insets insets = metrics.getWindowInsets().getInsets(Type.statusBars());
229                 final WindowManager.LayoutParams attrs = activity.getWindow().getAttributes();
230                 attrs.gravity = Gravity.LEFT | Gravity.TOP;
231                 attrs.x = insets.left;
232                 attrs.y = insets.top;
233                 attrs.width = metrics.getBounds().width() - insets.left - insets.right;
234                 attrs.height = metrics.getBounds().height() - insets.top - insets.bottom;
235                 activity.getWindow().setAttributes(attrs);
236                 activity.getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK));
237                 activity.setLightStatusBarAppearance(true);
238                 activity.setLightNavigationBarAppearance(true);
239             });
240             Thread.sleep(WAIT_TIME);
241 
242             Bitmap bitmap = takeStatusBarScreenshot(activity);
243             Stats s = evaluateDarkBarBitmap(bitmap, Color.TRANSPARENT, 0);
244             assertStats(bitmap, s, false /* light */);
245         });
246     }
247 
runInNotificationSession(ThrowingRunnable task)248     private void runInNotificationSession(ThrowingRunnable task) throws Exception {
249         Context context = getInstrumentation().getContext();
250         String packageName = getInstrumentation().getTargetContext().getPackageName();
251         try {
252             PermissionUtils.grantPermission(packageName, POST_NOTIFICATIONS);
253             mNm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
254             NotificationChannel channel1 = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
255                     NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
256             mNm.createNotificationChannel(channel1);
257 
258             // post 10 notifications to ensure enough icons in the status bar
259             for (int i = 0; i < 10; i++) {
260                 Notification.Builder noti1 =
261                         new Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
262                                 .setSmallIcon(R.drawable.ic_save)
263                                 .setChannelId(NOTIFICATION_CHANNEL_ID)
264                                 .setPriority(Notification.PRIORITY_LOW)
265                                 .setGroup(NOTIFICATION_GROUP_KEY);
266                 mNm.notify(NOTIFICATION_TAG, i, noti1.build());
267             }
268 
269             task.run();
270         } finally {
271             mNm.cancelAll();
272             mNm.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
273 
274             // Use test API to prevent PermissionManager from killing the test process when revoking
275             // permission.
276             SystemUtil.runWithShellPermissionIdentity(
277                     () -> context.getSystemService(PermissionManager.class)
278                             .revokePostNotificationPermissionWithoutKillForTest(
279                                     packageName,
280                                     Process.myUserHandle().getIdentifier()),
281                     REVOKE_POST_NOTIFICATIONS_WITHOUT_KILL,
282                     REVOKE_RUNTIME_PERMISSIONS);
283         }
284     }
285 
injectCanceledTap(int x, int y)286     private void injectCanceledTap(int x, int y) {
287         long downTime = SystemClock.uptimeMillis();
288         injectEvent(MotionEvent.ACTION_DOWN, x, y, downTime);
289         injectEvent(MotionEvent.ACTION_CANCEL, x, y, downTime);
290     }
291 
injectEvent(int action, int x, int y, long downTime)292     private void injectEvent(int action, int x, int y, long downTime) {
293         final UiAutomation automation = getInstrumentation().getUiAutomation();
294         final long eventTime = SystemClock.uptimeMillis();
295         MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0);
296         event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
297         assertTrue(automation.injectInputEvent(event, true));
298         event.recycle();
299     }
300 
assertStats(Bitmap bitmap, Stats s, boolean light)301     private void assertStats(Bitmap bitmap, Stats s, boolean light) {
302         boolean success = false;
303         try {
304             assumeNavigationBarChangesColor(s.backgroundPixels, s.totalPixels());
305 
306             final String spec = light ? "60% black and 24% black" : "100% white and 30% white";
307             assertMoreThan("Not enough pixels colored as in the spec", 0.3f,
308                     (float) s.iconPixels / (float) s.foregroundPixels(),
309                     "Are the bar icons colored according to the spec (" + spec + ")?");
310 
311             final String unexpected = light ? "lighter" : "darker";
312             final String expected = light ? "dark" : "light";
313             final int sameHuePixels = light ? s.sameHueLightPixels : s.sameHueDarkPixels;
314             assertLessThan("Too many pixels " + unexpected + " than the background", 0.05f,
315                     (float) sameHuePixels / (float) s.foregroundPixels(),
316                     "Are the bar icons " + expected + "?");
317 
318             // New status bar icons introduce color into the battery icon more regularly. This
319             // value can't be asserted in this way anymore
320             if (!Flags.newStatusBarIcons()) {
321                 assertLessThan("Too many pixels with a changed hue", 0.05f,
322                         (float) s.unexpectedHuePixels / (float) s.foregroundPixels(),
323                         "Are the bar icons color-free?");
324             }
325 
326             success = true;
327         } finally {
328             if (!success) {
329                 dumpBitmap(bitmap, mTestName.getMethodName());
330             }
331         }
332     }
333 
requestLightBars(final int background)334     private void requestLightBars(final int background) {
335         final LightBarActivity activity = mActivityRule.getActivity();
336         activity.runOnUiThread(() -> {
337             activity.getWindow().setBackgroundDrawable(new ColorDrawable(background));
338             activity.setLightStatusBarLegacy(true);
339             activity.setLightNavigationBarLegacy(true);
340         });
341     }
342 
343     private static class Stats {
344         int backgroundPixels;
345         int iconPixels;
346         int sameHueDarkPixels;
347         int sameHueLightPixels;
348         int unexpectedHuePixels;
349 
totalPixels()350         int totalPixels() {
351             return backgroundPixels + iconPixels + sameHueDarkPixels
352                     + sameHueLightPixels + unexpectedHuePixels;
353         }
354 
foregroundPixels()355         int foregroundPixels() {
356             return iconPixels + sameHueDarkPixels
357                     + sameHueLightPixels + unexpectedHuePixels;
358         }
359 
360         @Override
toString()361         public String toString() {
362             return String.format("{bg=%d, ic=%d, dark=%d, light=%d, bad=%d}",
363                     backgroundPixels, iconPixels, sameHueDarkPixels, sameHueLightPixels,
364                     unexpectedHuePixels);
365         }
366     }
367 
evaluateLightBarBitmap(Bitmap bitmap, int background, int shiftY)368     private Stats evaluateLightBarBitmap(Bitmap bitmap, int background, int shiftY) {
369         if (Flags.newStatusBarIcons()) {
370             return evaluateBarBitmap(
371                 bitmap,
372                 background,
373                 shiftY,
374                 DARK_ICON_TINT,
375                 0x3d000000
376             );
377         } else {
378             return evaluateBarBitmap(
379                 bitmap,
380                 background,
381                 shiftY,
382                 DARK_ICON_TINT_LEGACY,
383                 0x3d000000
384             );
385         }
386     }
387 
evaluateDarkBarBitmap(Bitmap bitmap, int background, int shiftY)388     private Stats evaluateDarkBarBitmap(Bitmap bitmap, int background, int shiftY) {
389         return evaluateBarBitmap(bitmap, background, shiftY, 0xffffffff, 0x4dffffff);
390     }
391 
evaluateBarBitmap(Bitmap bitmap, int background, int shiftY, int iconColor, int iconPartialColor)392     private Stats evaluateBarBitmap(Bitmap bitmap, int background, int shiftY, int iconColor,
393             int iconPartialColor) {
394 
395         int mixedIconColor = mixSrcOver(background, iconColor);
396         int mixedIconPartialColor = mixSrcOver(background, iconPartialColor);
397         float [] hsvMixedIconColor = new float[3];
398         float [] hsvMixedPartialColor = new float[3];
399         Color.RGBToHSV(Color.red(mixedIconColor), Color.green(mixedIconColor),
400                 Color.blue(mixedIconColor), hsvMixedIconColor);
401         Color.RGBToHSV(Color.red(mixedIconPartialColor), Color.green(mixedIconPartialColor),
402                 Color.blue(mixedIconPartialColor), hsvMixedPartialColor);
403 
404         float maxHsvValue = Math.max(hsvMixedIconColor[2], hsvMixedPartialColor[2]);
405         float minHsvValue = Math.min(hsvMixedIconColor[2], hsvMixedPartialColor[2]);
406 
407         int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()];
408         bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
409 
410         Stats s = new Stats();
411         float eps = 0.005f;
412 
413         loadCutout(mActivityRule.getActivity());
414         float [] hsvPixel = new float[3];
415         int i = 0;
416         for (int c : pixels) {
417             int x = i % bitmap.getWidth();
418             int y = i / bitmap.getWidth();
419             i++;
420             if (isInsideCutout(x, shiftY + y)) {
421                 continue;
422             }
423 
424             if (isColorSame(c, background)) {
425                 s.backgroundPixels++;
426                 continue;
427             }
428 
429             // What we expect the icons to be colored according to the spec.
430             Color.RGBToHSV(Color.red(c), Color.green(c), Color.blue(c), hsvPixel);
431             if (isColorSame(c, mixedIconColor) || isColorSame(c, mixedIconPartialColor)
432                     || (hsvPixel[2] >= minHsvValue && hsvPixel[2] <= maxHsvValue)) {
433                 s.iconPixels++;
434                 continue;
435             }
436 
437             // Due to anti-aliasing, there will be deviations from the ideal icon color, but it
438             // should still be mostly the same hue.
439             float hueDiff = Math.abs(ColorUtils.hue(background) - ColorUtils.hue(c));
440             if (hueDiff < eps || hueDiff > 1 - eps) {
441                 // .. it shouldn't be lighter than the original background though.
442                 if (ColorUtils.brightness(c) > ColorUtils.brightness(background)) {
443                     s.sameHueLightPixels++;
444                 } else {
445                     s.sameHueDarkPixels++;
446                 }
447                 continue;
448             }
449 
450             s.unexpectedHuePixels++;
451         }
452 
453         return s;
454     }
455 
mixSrcOver(int background, int foreground)456     private int mixSrcOver(int background, int foreground) {
457         int bgAlpha = Color.alpha(background);
458         int bgRed = Color.red(background);
459         int bgGreen = Color.green(background);
460         int bgBlue = Color.blue(background);
461 
462         int fgAlpha = Color.alpha(foreground);
463         int fgRed = Color.red(foreground);
464         int fgGreen = Color.green(foreground);
465         int fgBlue = Color.blue(foreground);
466 
467         return Color.argb(fgAlpha + (255 - fgAlpha) * bgAlpha / 255,
468                     fgRed + (255 - fgAlpha) * bgRed / 255,
469                     fgGreen + (255 - fgAlpha) * bgGreen / 255,
470                     fgBlue + (255 - fgAlpha) * bgBlue / 255);
471     }
472 
473     /**
474      * Check if two colors' diff is in the error margin as defined in
475      * {@link #COLOR_COMPONENT_ERROR_MARGIN}.
476      */
isColorSame(int c1, int c2)477     private boolean isColorSame(int c1, int c2){
478         return Math.abs(Color.alpha(c1) - Color.alpha(c2)) < COLOR_COMPONENT_ERROR_MARGIN
479                 && Math.abs(Color.red(c1) - Color.red(c2)) < COLOR_COMPONENT_ERROR_MARGIN
480                 && Math.abs(Color.green(c1) - Color.green(c2)) < COLOR_COMPONENT_ERROR_MARGIN
481                 && Math.abs(Color.blue(c1) - Color.blue(c2)) < COLOR_COMPONENT_ERROR_MARGIN;
482     }
483 }
484