blob: ffa58bafc9753f27e60b6b8a151f40294d89c6f1 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package android.systemui.cts;
import static android.server.wm.BarTestUtils.assumeHasColoredNavigationBar;
import static android.server.wm.BarTestUtils.assumeHasColoredStatusBar;
import static android.server.wm.BarTestUtils.assumeStatusBarContainsCutout;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static org.junit.Assert.assertTrue;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.UiAutomation;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Insets;
import android.os.SystemClock;
import android.platform.test.annotations.AppModeFull;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.WindowInsets.Type;
import android.view.WindowManager;
import android.view.WindowMetrics;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.ThrowingRunnable;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
/**
* Test for light status bar.
*
* atest CtsSystemUiTestCases:LightBarTests
*/
@RunWith(AndroidJUnit4.class)
public class LightBarTests extends LightBarTestBase {
public static final String TAG = "LightStatusBarTests";
/**
* Color may be slightly off-spec when resources are resized for lower densities. Use this error
* margin to accommodate for that when comparing colors.
*/
private static final int COLOR_COMPONENT_ERROR_MARGIN = 20;
/**
* It's possible for the device to have color sampling enabled in the nav bar -- in that
* case we need to pick a background color that would result in the same dark icon tint
* that matches the default visibility flags used when color sampling is not enabled.
*/
private static final int LIGHT_BG_COLOR = Color.rgb(255, 128, 128);
private final String NOTIFICATION_TAG = "TEST_TAG";
private final String NOTIFICATION_CHANNEL_ID = "test_channel";
private final String NOTIFICATION_GROUP_KEY = "test_group";
private NotificationManager mNm;
@Rule
public ActivityTestRule<LightBarActivity> mActivityRule = new ActivityTestRule<>(
LightBarActivity.class);
@Rule
public TestName mTestName = new TestName();
@Test
@AppModeFull // Instant apps cannot create notifications
public void testLightStatusBarIcons() throws Throwable {
assumeHasColoredStatusBar(mActivityRule);
runInNotificationSession(() -> {
requestLightBars(LIGHT_BG_COLOR);
Thread.sleep(WAIT_TIME);
Bitmap bitmap = takeStatusBarScreenshot(mActivityRule.getActivity());
Stats s = evaluateLightBarBitmap(bitmap, LIGHT_BG_COLOR, 0);
assertStats(bitmap, s, true /* light */);
});
}
@Test
@AppModeFull // Instant apps cannot create notifications
public void testAppearanceCanOverwriteLegacyFlags() throws Throwable {
assumeHasColoredStatusBar(mActivityRule);
runInNotificationSession(() -> {
final LightBarActivity activity = mActivityRule.getActivity();
activity.runOnUiThread(() -> {
activity.getWindow().setStatusBarColor(LIGHT_BG_COLOR);
activity.getWindow().setNavigationBarColor(LIGHT_BG_COLOR);
activity.setLightStatusBarLegacy(true);
activity.setLightNavigationBarLegacy(true);
// The new appearance APIs can overwrite the appearance specified by the legacy
// flags.
activity.setLightStatusBarAppearance(false);
activity.setLightNavigationBarAppearance(false);
});
Thread.sleep(WAIT_TIME);
Bitmap bitmap = takeStatusBarScreenshot(mActivityRule.getActivity());
Stats s = evaluateDarkBarBitmap(bitmap, LIGHT_BG_COLOR, 0);
assertStats(bitmap, s, false /* light */);
});
}
@Test
@AppModeFull // Instant apps cannot create notifications
public void testLegacyFlagsCannotOverwriteAppearance() throws Throwable {
assumeHasColoredStatusBar(mActivityRule);
runInNotificationSession(() -> {
final LightBarActivity activity = mActivityRule.getActivity();
activity.runOnUiThread(() -> {
activity.getWindow().setStatusBarColor(LIGHT_BG_COLOR);
activity.getWindow().setNavigationBarColor(LIGHT_BG_COLOR);
activity.setLightStatusBarAppearance(false);
activity.setLightNavigationBarAppearance(false);
// Once the client starts using the new appearance APIs, the legacy flags won't
// change the appearance anymore.
activity.setLightStatusBarLegacy(true);
activity.setLightNavigationBarLegacy(true);
});
Thread.sleep(WAIT_TIME);
Bitmap bitmap = takeStatusBarScreenshot(mActivityRule.getActivity());
Stats s = evaluateDarkBarBitmap(bitmap, LIGHT_BG_COLOR, 0);
assertStats(bitmap, s, false /* light */);
});
}
@Test
public void testLightNavigationBar() throws Throwable {
assumeHasColoredNavigationBar(mActivityRule);
requestLightBars(LIGHT_BG_COLOR);
Thread.sleep(WAIT_TIME);
// Inject a cancelled interaction with the nav bar to ensure it is at full opacity.
int x = mActivityRule.getActivity().getWidth() / 2;
int y = mActivityRule.getActivity().getBottom() + 10;
injectCanceledTap(x, y);
Thread.sleep(WAIT_TIME);
LightBarActivity activity = mActivityRule.getActivity();
Bitmap bitmap = takeNavigationBarScreenshot(activity);
Stats s = evaluateLightBarBitmap(bitmap, LIGHT_BG_COLOR, activity.getBottom());
assertStats(bitmap, s, true /* light */);
}
@Test
public void testNavigationBarDivider() throws Throwable {
assumeHasColoredNavigationBar(mActivityRule);
mActivityRule.runOnUiThread(() -> {
mActivityRule.getActivity().getWindow().setNavigationBarColor(Color.RED);
mActivityRule.getActivity().getWindow().setNavigationBarDividerColor(Color.WHITE);
});
Thread.sleep(WAIT_TIME);
checkNavigationBarDivider(mActivityRule.getActivity(), Color.WHITE, Color.RED,
mTestName.getMethodName());
}
@Test
@AppModeFull // Instant apps cannot create notifications
public void testLightBarIsNotAllowed_fitStatusBar() throws Throwable {
assumeHasColoredStatusBar(mActivityRule);
runInNotificationSession(() -> {
final LightBarActivity activity = mActivityRule.getActivity();
activity.runOnUiThread(() -> {
final WindowMetrics metrics = activity.getWindowManager().getCurrentWindowMetrics();
final Insets insets = metrics.getWindowInsets().getInsets(Type.statusBars());
final WindowManager.LayoutParams attrs = activity.getWindow().getAttributes();
attrs.gravity = Gravity.LEFT | Gravity.TOP;
attrs.x = insets.left;
attrs.y = insets.top;
attrs.width = metrics.getBounds().width() - insets.left - insets.right;
attrs.height = metrics.getBounds().height() - insets.top - insets.bottom;
activity.getWindow().setAttributes(attrs);
activity.getWindow().setStatusBarColor(Color.BLACK);
activity.getWindow().setNavigationBarColor(Color.BLACK);
activity.setLightStatusBarAppearance(true);
activity.setLightNavigationBarAppearance(true);
});
Thread.sleep(WAIT_TIME);
Bitmap bitmap = takeStatusBarScreenshot(activity);
Stats s = evaluateDarkBarBitmap(bitmap, Color.BLACK, 0);
assertStats(bitmap, s, false /* light */);
});
}
@Test
@AppModeFull // Instant apps cannot create notifications
public void testLightBarIsNotAllowed_fitDisplayCutout() throws Throwable {
assumeHasColoredStatusBar(mActivityRule);
assumeStatusBarContainsCutout(mActivityRule);
runInNotificationSession(() -> {
final LightBarActivity activity = mActivityRule.getActivity();
activity.runOnUiThread(() -> {
final WindowManager.LayoutParams attrs = activity.getWindow().getAttributes();
attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
activity.getWindow().setAttributes(attrs);
activity.getWindow().setStatusBarColor(Color.BLACK);
activity.getWindow().setNavigationBarColor(Color.BLACK);
activity.setLightStatusBarAppearance(true);
activity.setLightNavigationBarAppearance(true);
});
Thread.sleep(WAIT_TIME);
Bitmap bitmap = takeStatusBarScreenshot(activity);
Stats s = evaluateDarkBarBitmap(bitmap, Color.BLACK, 0);
assertStats(bitmap, s, false /* light */);
});
}
private void runInNotificationSession(ThrowingRunnable task) throws Exception {
try {
mNm = (NotificationManager) getInstrumentation().getContext()
.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel1 = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
mNm.createNotificationChannel(channel1);
// post 10 notifications to ensure enough icons in the status bar
for (int i = 0; i < 10; i++) {
Notification.Builder noti1 = new Notification.Builder(
getInstrumentation().getContext(),
NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_save)
.setChannelId(NOTIFICATION_CHANNEL_ID)
.setPriority(Notification.PRIORITY_LOW)
.setGroup(NOTIFICATION_GROUP_KEY);
mNm.notify(NOTIFICATION_TAG, i, noti1.build());
}
task.run();
} finally {
mNm.cancelAll();
mNm.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
}
}
private void injectCanceledTap(int x, int y) {
long downTime = SystemClock.uptimeMillis();
injectEvent(MotionEvent.ACTION_DOWN, x, y, downTime);
injectEvent(MotionEvent.ACTION_CANCEL, x, y, downTime);
}
private void injectEvent(int action, int x, int y, long downTime) {
final UiAutomation automation = getInstrumentation().getUiAutomation();
final long eventTime = SystemClock.uptimeMillis();
MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
assertTrue(automation.injectInputEvent(event, true));
event.recycle();
}
private void assertStats(Bitmap bitmap, Stats s, boolean light) {
boolean success = false;
try {
assumeNavigationBarChangesColor(s.backgroundPixels, s.totalPixels());
final String spec = light ? "60% black and 24% black" : "100% white and 30% white";
assertMoreThan("Not enough pixels colored as in the spec", 0.3f,
(float) s.iconPixels / (float) s.foregroundPixels(),
"Are the bar icons colored according to the spec (" + spec + ")?");
final String unexpected = light ? "lighter" : "darker";
final String expected = light ? "dark" : "light";
final int sameHuePixels = light ? s.sameHueLightPixels : s.sameHueDarkPixels;
assertLessThan("Too many pixels " + unexpected + " than the background", 0.05f,
(float) sameHuePixels / (float) s.foregroundPixels(),
"Are the bar icons " + expected + "?");
assertLessThan("Too many pixels with a changed hue", 0.05f,
(float) s.unexpectedHuePixels / (float) s.foregroundPixels(),
"Are the bar icons color-free?");
success = true;
} finally {
if (!success) {
dumpBitmap(bitmap, mTestName.getMethodName());
}
}
}
private void requestLightBars(final int background) {
final LightBarActivity activity = mActivityRule.getActivity();
activity.runOnUiThread(() -> {
activity.getWindow().setStatusBarColor(background);
activity.getWindow().setNavigationBarColor(background);
activity.setLightStatusBarLegacy(true);
activity.setLightNavigationBarLegacy(true);
});
}
private static class Stats {
int backgroundPixels;
int iconPixels;
int sameHueDarkPixels;
int sameHueLightPixels;
int unexpectedHuePixels;
int totalPixels() {
return backgroundPixels + iconPixels + sameHueDarkPixels
+ sameHueLightPixels + unexpectedHuePixels;
}
int foregroundPixels() {
return iconPixels + sameHueDarkPixels
+ sameHueLightPixels + unexpectedHuePixels;
}
@Override
public String toString() {
return String.format("{bg=%d, ic=%d, dark=%d, light=%d, bad=%d}",
backgroundPixels, iconPixels, sameHueDarkPixels, sameHueLightPixels,
unexpectedHuePixels);
}
}
private Stats evaluateLightBarBitmap(Bitmap bitmap, int background, int shiftY) {
return evaluateBarBitmap(bitmap, background, shiftY, 0x99000000, 0x3d000000);
}
private Stats evaluateDarkBarBitmap(Bitmap bitmap, int background, int shiftY) {
return evaluateBarBitmap(bitmap, background, shiftY, 0xffffffff, 0x4dffffff);
}
private Stats evaluateBarBitmap(Bitmap bitmap, int background, int shiftY, int iconColor,
int iconPartialColor) {
int mixedIconColor = mixSrcOver(background, iconColor);
int mixedIconPartialColor = mixSrcOver(background, iconPartialColor);
float [] hsvMixedIconColor = new float[3];
float [] hsvMixedPartialColor = new float[3];
Color.RGBToHSV(Color.red(mixedIconColor), Color.green(mixedIconColor),
Color.blue(mixedIconColor), hsvMixedIconColor);
Color.RGBToHSV(Color.red(mixedIconPartialColor), Color.green(mixedIconPartialColor),
Color.blue(mixedIconPartialColor), hsvMixedPartialColor);
float maxHsvValue = Math.max(hsvMixedIconColor[2], hsvMixedPartialColor[2]);
float minHsvValue = Math.min(hsvMixedIconColor[2], hsvMixedPartialColor[2]);
int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()];
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
Stats s = new Stats();
float eps = 0.005f;
loadCutout(mActivityRule.getActivity());
float [] hsvPixel = new float[3];
int i = 0;
for (int c : pixels) {
int x = i % bitmap.getWidth();
int y = i / bitmap.getWidth();
i++;
if (isInsideCutout(x, shiftY + y)) {
continue;
}
if (isColorSame(c, background)) {
s.backgroundPixels++;
continue;
}
// What we expect the icons to be colored according to the spec.
Color.RGBToHSV(Color.red(c), Color.green(c), Color.blue(c), hsvPixel);
if (isColorSame(c, mixedIconColor) || isColorSame(c, mixedIconPartialColor)
|| (hsvPixel[2] >= minHsvValue && hsvPixel[2] <= maxHsvValue)) {
s.iconPixels++;
continue;
}
// Due to anti-aliasing, there will be deviations from the ideal icon color, but it
// should still be mostly the same hue.
float hueDiff = Math.abs(ColorUtils.hue(background) - ColorUtils.hue(c));
if (hueDiff < eps || hueDiff > 1 - eps) {
// .. it shouldn't be lighter than the original background though.
if (ColorUtils.brightness(c) > ColorUtils.brightness(background)) {
s.sameHueLightPixels++;
} else {
s.sameHueDarkPixels++;
}
continue;
}
s.unexpectedHuePixels++;
}
return s;
}
private int mixSrcOver(int background, int foreground) {
int bgAlpha = Color.alpha(background);
int bgRed = Color.red(background);
int bgGreen = Color.green(background);
int bgBlue = Color.blue(background);
int fgAlpha = Color.alpha(foreground);
int fgRed = Color.red(foreground);
int fgGreen = Color.green(foreground);
int fgBlue = Color.blue(foreground);
return Color.argb(fgAlpha + (255 - fgAlpha) * bgAlpha / 255,
fgRed + (255 - fgAlpha) * bgRed / 255,
fgGreen + (255 - fgAlpha) * bgGreen / 255,
fgBlue + (255 - fgAlpha) * bgBlue / 255);
}
/**
* Check if two colors' diff is in the error margin as defined in
* {@link #COLOR_COMPONENT_ERROR_MARGIN}.
*/
private boolean isColorSame(int c1, int c2){
return Math.abs(Color.alpha(c1) - Color.alpha(c2)) < COLOR_COMPONENT_ERROR_MARGIN
&& Math.abs(Color.red(c1) - Color.red(c2)) < COLOR_COMPONENT_ERROR_MARGIN
&& Math.abs(Color.green(c1) - Color.green(c2)) < COLOR_COMPONENT_ERROR_MARGIN
&& Math.abs(Color.blue(c1) - Color.blue(c2)) < COLOR_COMPONENT_ERROR_MARGIN;
}
}