| /* |
| * Copyright (C) 2012 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.display.cts; |
| |
| import static android.content.pm.PackageManager.FEATURE_LEANBACK; |
| import static android.view.Display.DEFAULT_DISPLAY; |
| |
| import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; |
| |
| import static org.junit.Assert.*; |
| import static org.junit.Assume.*; |
| |
| import android.Manifest; |
| import android.app.Activity; |
| import android.app.Instrumentation; |
| import android.app.Presentation; |
| import android.app.UiModeManager; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.res.Configuration; |
| import android.graphics.Color; |
| import android.graphics.ColorSpace; |
| import android.graphics.PixelFormat; |
| import android.graphics.Point; |
| import android.hardware.display.DeviceProductInfo; |
| import android.hardware.display.DisplayManager; |
| import android.hardware.display.DisplayManager.DisplayListener; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.ParcelFileDescriptor; |
| import android.os.SystemProperties; |
| import android.platform.test.annotations.Presubmit; |
| import android.provider.Settings; |
| import android.text.TextUtils; |
| import android.util.DisplayMetrics; |
| import android.util.Log; |
| import android.view.Display; |
| import android.view.Display.HdrCapabilities; |
| import android.view.SurfaceControl; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.WindowManager; |
| |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.rule.ActivityTestRule; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import com.android.compatibility.common.util.AdoptShellPermissionsRule; |
| import com.android.compatibility.common.util.DisplayUtil; |
| import com.android.compatibility.common.util.PropertyUtil; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.io.FileInputStream; |
| import java.io.InputStream; |
| import java.time.Duration; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.Random; |
| import java.util.Scanner; |
| import java.util.concurrent.TimeoutException; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.locks.Condition; |
| import java.util.concurrent.locks.Lock; |
| import java.util.concurrent.locks.ReentrantLock; |
| import java.util.function.Predicate; |
| |
| @RunWith(AndroidJUnit4.class) |
| public class DisplayTest { |
| private static final String TAG = "DisplayTest"; |
| |
| // The CTS package brings up an overlay display on the target device (see AndroidTest.xml). |
| // The overlay display parameters must match the ones defined there which are |
| // 181x161/214 (wxh/dpi). It only matters that these values are different from any real |
| // display. |
| |
| private static final int SECONDARY_DISPLAY_WIDTH = 181; |
| private static final int SECONDARY_DISPLAY_HEIGHT = 161; |
| private static final int SECONDARY_DISPLAY_DPI = 214; |
| private static final float SCALE_DENSITY_LOWER_BOUND = |
| (float)(SECONDARY_DISPLAY_DPI - 1) / DisplayMetrics.DENSITY_DEFAULT; |
| private static final float SCALE_DENSITY_UPPER_BOUND = |
| (float)(SECONDARY_DISPLAY_DPI + 1) / DisplayMetrics.DENSITY_DEFAULT; |
| // Matches com.android.internal.R.string.display_manager_overlay_display_name. |
| private static final String OVERLAY_DISPLAY_NAME_PREFIX = "Overlay #"; |
| |
| private static final int BRIGHTNESS_MAX = 255; |
| |
| private DisplayManager mDisplayManager; |
| private WindowManager mWindowManager; |
| private UiModeManager mUiModeManager; |
| private Context mContext; |
| private ColorSpace[] mSupportedWideGamuts; |
| private Display mDefaultDisplay; |
| private HdrSettings mOriginalHdrSettings; |
| |
| // To test display mode switches. |
| private TestPresentation mPresentation; |
| |
| private Activity mScreenOnActivity; |
| |
| private static class DisplayModeState { |
| public final int mHeight; |
| public final int mWidth; |
| public final float mRefreshRate; |
| |
| DisplayModeState(Display display) { |
| mHeight = display.getMode().getPhysicalHeight(); |
| mWidth = display.getMode().getPhysicalWidth(); |
| |
| // Starting Android S the, the platform might throttle down |
| // applications frame rate to a divisor of the refresh rate instead if changing the |
| // physical display refresh rate. Applications should use |
| // {@link android.view.Display#getRefreshRate} to know their frame rate as opposed to |
| // {@link android.view.Display.Mode#getRefreshRate} that returns the physical display |
| // refresh rate. See |
| // {@link com.android.server.display.DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE} |
| // for more details. |
| mRefreshRate = display.getRefreshRate(); |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (!(obj instanceof DisplayModeState)) { |
| return false; |
| } |
| |
| DisplayModeState other = (DisplayModeState) obj; |
| return mHeight == other.mHeight |
| && mWidth == other.mWidth |
| && mRefreshRate == other.mRefreshRate; |
| } |
| |
| @Override |
| public String toString() { |
| return new StringBuilder("{") |
| .append("width=").append(mWidth) |
| .append(", height=").append(mHeight) |
| .append(", fps=").append(mRefreshRate) |
| .append("}") |
| .toString(); |
| } |
| } |
| |
| @Rule |
| public ActivityTestRule<DisplayTestActivity> mDisplayTestActivity = |
| new ActivityTestRule<>( |
| DisplayTestActivity.class, |
| false /* initialTouchMode */, |
| false /* launchActivity */); |
| |
| @Rule |
| public ActivityTestRule<RetainedDisplayTestActivity> mRetainedDisplayTestActivity = |
| new ActivityTestRule<>( |
| RetainedDisplayTestActivity.class, |
| false /* initialTouchMode */, |
| false /* launchActivity */); |
| |
| /** |
| * This rule adopts the Shell process permissions, needed because OVERRIDE_DISPLAY_MODE_REQUESTS |
| * and ACCESS_SURFACE_FLINGER are privileged permission. |
| */ |
| @Rule |
| public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule( |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS, |
| Manifest.permission.ACCESS_SURFACE_FLINGER, |
| Manifest.permission.WRITE_SECURE_SETTINGS, |
| Manifest.permission.HDMI_CEC); |
| |
| @Before |
| public void setUp() throws Exception { |
| mScreenOnActivity = launchScreenOnActivity(); |
| mContext = getInstrumentation().getTargetContext(); |
| assertTrue("Physical display is expected.", DisplayUtil.isDisplayConnected(mContext)); |
| |
| mDisplayManager = mContext.getSystemService(DisplayManager.class); |
| mWindowManager = mContext.getSystemService(WindowManager.class); |
| mUiModeManager = mContext.getSystemService(UiModeManager.class); |
| mDefaultDisplay = mDisplayManager.getDisplay(DEFAULT_DISPLAY); |
| mSupportedWideGamuts = mDefaultDisplay.getSupportedWideColorGamut(); |
| mOriginalHdrSettings = new HdrSettings(); |
| cacheAndClearOriginalHdrSettings(); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| restoreOriginalHdrSettings(); |
| if (mScreenOnActivity != null) { |
| mScreenOnActivity.finish(); |
| } |
| } |
| |
| private void enableAppOps() { |
| StringBuilder cmd = new StringBuilder(); |
| cmd.append("appops set "); |
| cmd.append(InstrumentationRegistry.getInstrumentation().getContext().getPackageName()); |
| cmd.append(" android:system_alert_window allow"); |
| InstrumentationRegistry.getInstrumentation().getUiAutomation() |
| .executeShellCommand(cmd.toString()); |
| |
| StringBuilder query = new StringBuilder(); |
| query.append("appops get "); |
| query.append(InstrumentationRegistry.getInstrumentation().getContext().getPackageName()); |
| query.append(" android:system_alert_window"); |
| String queryStr = query.toString(); |
| |
| String result = "No operations."; |
| while (result.contains("No operations")) { |
| ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation() |
| .getUiAutomation().executeShellCommand(queryStr); |
| InputStream inputStream = new FileInputStream(pfd.getFileDescriptor()); |
| result = convertStreamToString(inputStream); |
| } |
| } |
| |
| private String convertStreamToString(InputStream is) { |
| try (java.util.Scanner s = new Scanner(is).useDelimiter("\\A")) { |
| return s.hasNext() ? s.next() : ""; |
| } |
| } |
| |
| /** Check if the display is an overlay display, created by this test. */ |
| private boolean isSecondaryDisplay(Display display) { |
| return display.getType() == Display.TYPE_OVERLAY; |
| } |
| |
| /** Get the overlay display, created by this test. */ |
| private Display getSecondaryDisplay(Display[] displays) { |
| for (Display display : displays) { |
| if (isSecondaryDisplay(display)) { |
| return display; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Verify that the getDisplays method returns both a default and an overlay display. |
| */ |
| @Test |
| public void testGetDisplays() { |
| Display[] displays = mDisplayManager.getDisplays(); |
| assertNotNull(displays); |
| assertTrue(2 <= displays.length); |
| boolean hasDefaultDisplay = false; |
| boolean hasSecondaryDisplay = false; |
| for (Display display : displays) { |
| if (display.getDisplayId() == DEFAULT_DISPLAY) { |
| hasDefaultDisplay = true; |
| } |
| if (isSecondaryDisplay(display)) { |
| hasSecondaryDisplay = true; |
| } |
| } |
| assertTrue(hasDefaultDisplay); |
| assertTrue(hasSecondaryDisplay); |
| } |
| |
| /** |
| * Verify that the WindowManager returns the default display. |
| */ |
| @Presubmit |
| @Test |
| public void testDefaultDisplay() { |
| assertEquals(DEFAULT_DISPLAY, mWindowManager.getDefaultDisplay().getDisplayId()); |
| } |
| |
| /** |
| * Verify default display's HDR capability. |
| */ |
| @Test |
| public void testDefaultDisplayHdrCapability() { |
| HdrCapabilities cap = mDefaultDisplay.getHdrCapabilities(); |
| int[] hdrTypes = cap.getSupportedHdrTypes(); |
| for (int type : hdrTypes) { |
| assertTrue(type >= 1 && type <= 4); |
| } |
| assertFalse(cap.getDesiredMaxLuminance() < -1.0f); |
| assertFalse(cap.getDesiredMinLuminance() < -1.0f); |
| assertFalse(cap.getDesiredMaxAverageLuminance() < -1.0f); |
| assertTrue(cap.getDesiredMinLuminance() <= cap.getDesiredMaxAverageLuminance()); |
| assertTrue(cap.getDesiredMaxAverageLuminance() <= cap.getDesiredMaxLuminance()); |
| if (hdrTypes.length > 0) { |
| assertTrue(mDefaultDisplay.isHdr()); |
| } else { |
| assertFalse(mDefaultDisplay.isHdr()); |
| } |
| } |
| |
| /** |
| * Verifies that getHdrCapabilities filters out specified HDR types after |
| * setUserDisabledHdrTypes is called and setAreUserDisabledHdrTypes is false. |
| */ |
| @Test |
| public void |
| testGetHdrCapabilitiesWhenUserDisabledFormatsAreNotAllowedReturnsFilteredHdrTypes() |
| throws Exception { |
| waitUntil( |
| mDefaultDisplay, |
| mDefaultDisplay -> |
| mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes().length == 4, |
| Duration.ofSeconds(5)); |
| |
| mDisplayManager.setAreUserDisabledHdrTypesAllowed(false); |
| int[] emptyUserDisabledFormats = new int[] {}; |
| mDisplayManager.setUserDisabledHdrTypes(emptyUserDisabledFormats); |
| int[] expectedHdrTypes = new int[]{ |
| HdrCapabilities.HDR_TYPE_DOLBY_VISION, HdrCapabilities.HDR_TYPE_HDR10, |
| HdrCapabilities.HDR_TYPE_HLG, HdrCapabilities.HDR_TYPE_HDR10_PLUS}; |
| assertArrayEquals(expectedHdrTypes, |
| mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes()); |
| |
| int[] userDisabledHdrTypes = |
| {HdrCapabilities.HDR_TYPE_DOLBY_VISION, HdrCapabilities.HDR_TYPE_HLG}; |
| mDisplayManager.setUserDisabledHdrTypes(userDisabledHdrTypes); |
| expectedHdrTypes = new int[]{ |
| HdrCapabilities.HDR_TYPE_HDR10, |
| HdrCapabilities.HDR_TYPE_HDR10_PLUS}; |
| assertArrayEquals(expectedHdrTypes, |
| mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes()); |
| |
| mDisplayManager.setUserDisabledHdrTypes(emptyUserDisabledFormats); |
| expectedHdrTypes = new int[]{ |
| HdrCapabilities.HDR_TYPE_DOLBY_VISION, HdrCapabilities.HDR_TYPE_HDR10, |
| HdrCapabilities.HDR_TYPE_HLG, HdrCapabilities.HDR_TYPE_HDR10_PLUS}; |
| assertArrayEquals(expectedHdrTypes, |
| mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes()); |
| } |
| |
| /** |
| * Verifies that getHdrCapabilities doesn't filter out HDR types after |
| * setUserDisabledHdrTypes is called and setAreUserDisabledHdrTypes is true. |
| */ |
| @Test |
| public void |
| testGetHdrCapabilitiesWhenUserDisabledFormatsAreAllowedReturnsNonFilteredHdrTypes() |
| throws Exception { |
| waitUntil( |
| mDefaultDisplay, |
| mDefaultDisplay -> |
| mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes().length == 4, |
| Duration.ofSeconds(5)); |
| |
| mDisplayManager.setAreUserDisabledHdrTypesAllowed(true); |
| int[] userDisabledHdrTypes = |
| {HdrCapabilities.HDR_TYPE_DOLBY_VISION, HdrCapabilities.HDR_TYPE_HLG}; |
| mDisplayManager.setUserDisabledHdrTypes(userDisabledHdrTypes); |
| int[] expectedHdrTypes = new int[]{ |
| HdrCapabilities.HDR_TYPE_DOLBY_VISION, HdrCapabilities.HDR_TYPE_HDR10, |
| HdrCapabilities.HDR_TYPE_HLG, HdrCapabilities.HDR_TYPE_HDR10_PLUS}; |
| assertArrayEquals(expectedHdrTypes, |
| mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes()); |
| |
| int[] emptyUserDisabledFormats = {}; |
| mDisplayManager.setUserDisabledHdrTypes(emptyUserDisabledFormats); |
| assertArrayEquals(expectedHdrTypes, |
| mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes()); |
| } |
| |
| /** |
| * Verifies that if userDisabledFormats are not allowed, and are modified by |
| * setUserDisabledHdrTypes, the setting is persisted in Settings.Global. |
| */ |
| @Test |
| public void testSetUserDisabledHdrTypesStoresDisabledFormatsInSettings() throws Exception { |
| waitUntil( |
| mDefaultDisplay, |
| mDefaultDisplay -> |
| mDefaultDisplay.getHdrCapabilities().getSupportedHdrTypes().length == 4, |
| Duration.ofSeconds(5)); |
| |
| mDisplayManager.setAreUserDisabledHdrTypesAllowed(false); |
| int[] emptyUserDisabledFormats = {}; |
| mDisplayManager.setUserDisabledHdrTypes(emptyUserDisabledFormats); |
| |
| int[] userDisabledHdrTypes = |
| {HdrCapabilities.HDR_TYPE_DOLBY_VISION, HdrCapabilities.HDR_TYPE_HLG}; |
| mDisplayManager.setUserDisabledHdrTypes(userDisabledHdrTypes); |
| String userDisabledFormatsString = |
| Settings.Global.getString(mContext.getContentResolver(), |
| Settings.Global.USER_DISABLED_HDR_FORMATS); |
| int[] userDisabledFormats = Arrays.stream( |
| TextUtils.split(userDisabledFormatsString, ",")) |
| .mapToInt(Integer::parseInt).toArray(); |
| |
| assertEquals(HdrCapabilities.HDR_TYPE_DOLBY_VISION, userDisabledFormats[0]); |
| assertEquals(HdrCapabilities.HDR_TYPE_HLG, userDisabledFormats[1]); |
| } |
| |
| private static final class HdrSettings { |
| public boolean areUserDisabledHdrTypesAllowed; |
| public int[] userDisabledHdrTypes; |
| } |
| |
| private void cacheAndClearOriginalHdrSettings() { |
| mOriginalHdrSettings.areUserDisabledHdrTypesAllowed = |
| mDisplayManager.areUserDisabledHdrTypesAllowed(); |
| mOriginalHdrSettings.userDisabledHdrTypes = |
| mDisplayManager.getUserDisabledHdrTypes(); |
| final IBinder displayToken = SurfaceControl.getInternalDisplayToken(); |
| SurfaceControl.overrideHdrTypes(displayToken, new int[]{ |
| HdrCapabilities.HDR_TYPE_DOLBY_VISION, HdrCapabilities.HDR_TYPE_HDR10, |
| HdrCapabilities.HDR_TYPE_HLG, HdrCapabilities.HDR_TYPE_HDR10_PLUS}); |
| mDisplayManager.setAreUserDisabledHdrTypesAllowed(true); |
| } |
| |
| private void restoreOriginalHdrSettings() { |
| final IBinder displayToken = SurfaceControl.getInternalDisplayToken(); |
| SurfaceControl.overrideHdrTypes(displayToken, new int[]{}); |
| if (mDisplayManager != null) { |
| mDisplayManager.setUserDisabledHdrTypes( |
| mOriginalHdrSettings.userDisabledHdrTypes); |
| mDisplayManager.setAreUserDisabledHdrTypesAllowed( |
| mOriginalHdrSettings.areUserDisabledHdrTypesAllowed); |
| } |
| } |
| |
| private void waitUntil(Display display, Predicate<Display> pred, Duration maxWait) |
| throws Exception { |
| final int id = display.getDisplayId(); |
| final Lock lock = new ReentrantLock(); |
| final Condition displayChanged = lock.newCondition(); |
| DisplayListener listener = new DisplayListener() { |
| @Override |
| public void onDisplayChanged(int displayId) { |
| if (displayId != id) { |
| return; |
| } |
| lock.lock(); |
| try { |
| displayChanged.signal(); |
| } finally { |
| lock.unlock(); |
| } |
| } |
| @Override |
| public void onDisplayAdded(int displayId) {} |
| @Override |
| public void onDisplayRemoved(int displayId) {} |
| }; |
| Handler handler = new Handler(Looper.getMainLooper()); |
| mDisplayManager.registerDisplayListener(listener, handler); |
| long remainingNanos = maxWait.toNanos(); |
| lock.lock(); |
| try { |
| while (!pred.test(display)) { |
| if (remainingNanos <= 0L) { |
| throw new TimeoutException(); |
| } |
| remainingNanos = displayChanged.awaitNanos(remainingNanos); |
| } |
| } finally { |
| lock.unlock(); |
| } |
| } |
| |
| /** |
| * Verify that there is a secondary display. |
| */ |
| @Test |
| public void testSecondaryDisplay() { |
| Display display = getSecondaryDisplay(mDisplayManager.getDisplays()); |
| assertNotNull(display); |
| assertTrue(DEFAULT_DISPLAY != display.getDisplayId()); |
| } |
| |
| /** |
| * Test the properties of the secondary Display. |
| */ |
| @Test |
| public void testGetDisplayAttrs() { |
| Display display = getSecondaryDisplay(mDisplayManager.getDisplays()); |
| |
| assertEquals(SECONDARY_DISPLAY_WIDTH, display.getWidth()); |
| assertEquals(SECONDARY_DISPLAY_HEIGHT, display.getHeight()); |
| |
| Point outSize = new Point(); |
| display.getSize(outSize); |
| assertEquals(SECONDARY_DISPLAY_WIDTH, outSize.x); |
| assertEquals(SECONDARY_DISPLAY_HEIGHT, outSize.y); |
| |
| assertEquals(0, display.getOrientation()); |
| |
| assertEquals(PixelFormat.RGBA_8888, display.getPixelFormat()); |
| |
| assertTrue(0 < display.getRefreshRate()); |
| |
| assertTrue(display.getName().contains(OVERLAY_DISPLAY_NAME_PREFIX)); |
| |
| assertFalse(display.isWideColorGamut()); |
| } |
| |
| /** |
| * Test that the getMetrics method fills in correct values. |
| */ |
| @Test |
| public void testGetMetrics() { |
| testGetMetrics(mDisplayManager); |
| } |
| |
| /** |
| * Tests getting metrics from the Activity context. |
| */ |
| @Test |
| public void testActivityContextGetMetrics() { |
| final Activity activity = launchActivity(mDisplayTestActivity); |
| final DisplayManager dm = |
| (DisplayManager) activity.getSystemService(Context.DISPLAY_SERVICE); |
| testGetMetrics(dm); |
| } |
| |
| public void testGetMetrics(DisplayManager manager) { |
| Display display = getSecondaryDisplay(manager.getDisplays()); |
| |
| Point outSize = new Point(); |
| display.getSize(outSize); |
| |
| DisplayMetrics outMetrics = new DisplayMetrics(); |
| outMetrics.setToDefaults(); |
| display.getMetrics(outMetrics); |
| |
| assertEquals(SECONDARY_DISPLAY_WIDTH, outMetrics.widthPixels); |
| assertEquals(SECONDARY_DISPLAY_HEIGHT, outMetrics.heightPixels); |
| |
| // The scale is in [0.1, 3], and density is the scale factor. |
| assertTrue(SCALE_DENSITY_LOWER_BOUND <= outMetrics.density |
| && outMetrics.density <= SCALE_DENSITY_UPPER_BOUND); |
| assertTrue(SCALE_DENSITY_LOWER_BOUND <= outMetrics.scaledDensity |
| && outMetrics.scaledDensity <= SCALE_DENSITY_UPPER_BOUND); |
| |
| assertEquals(SECONDARY_DISPLAY_DPI, outMetrics.densityDpi); |
| assertEquals((float)SECONDARY_DISPLAY_DPI, outMetrics.xdpi, 0.0001f); |
| assertEquals((float)SECONDARY_DISPLAY_DPI, outMetrics.ydpi, 0.0001f); |
| } |
| |
| /** Test that the getFlags method returns expected flag bits set for the overlay display. */ |
| @Test |
| public void testFlags() { |
| Display display = getSecondaryDisplay(mDisplayManager.getDisplays()); |
| |
| assertEquals(Display.FLAG_PRESENTATION | Display.FLAG_TRUSTED, display.getFlags()); |
| } |
| |
| /** |
| * Test that a mode switch to every reported display mode is successful. |
| */ |
| @Test |
| public void testModeSwitchOnPrimaryDisplay() throws Exception { |
| Display.Mode[] modes = mDefaultDisplay.getSupportedModes(); |
| assumeTrue("Need two or more display modes to exercise switching.", modes.length > 1); |
| |
| // Create a deterministically shuffled list of display modes, which ends with the |
| // current active mode. We'll switch to the modes in this order. The active mode is last |
| // so we don't need an extra mode switch in case the test completes successfully. |
| Display.Mode activeMode = mDefaultDisplay.getMode(); |
| List<Display.Mode> modesList = new ArrayList<>(modes.length); |
| for (Display.Mode mode : modes) { |
| if (mode.getModeId() != activeMode.getModeId()) { |
| modesList.add(mode); |
| } |
| } |
| Random random = new Random(42); |
| Collections.shuffle(modesList, random); |
| modesList.add(activeMode); |
| |
| try { |
| mDisplayManager.setShouldAlwaysRespectAppRequestedMode(true); |
| assertTrue(mDisplayManager.shouldAlwaysRespectAppRequestedMode()); |
| final DisplayTestActivity activity = launchActivity(mRetainedDisplayTestActivity); |
| for (Display.Mode mode : modesList) { |
| testSwitchToModeId(activity, mode); |
| } |
| } finally { |
| mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false); |
| } |
| } |
| |
| /** |
| * Test that a mode switch to another display mode works when the requesting Activity |
| * is destroyed and re-created as part of the configuration change from the display mode. |
| */ |
| @Test |
| public void testModeSwitchOnPrimaryDisplayWithRestart() throws Exception { |
| final Display.Mode oldMode = mDefaultDisplay.getMode(); |
| final Optional<Display.Mode> newMode = Arrays.stream(mDefaultDisplay.getSupportedModes()) |
| .filter(x -> !getPhysicalSize(x).equals(getPhysicalSize(oldMode))) |
| .findFirst(); |
| assumeTrue("Modes with different sizes are not available", newMode.isPresent()); |
| |
| try { |
| mDisplayManager.setShouldAlwaysRespectAppRequestedMode(true); |
| assertTrue(mDisplayManager.shouldAlwaysRespectAppRequestedMode()); |
| final DisplayTestActivity activity = launchActivity(mDisplayTestActivity); |
| testSwitchToModeId(launchActivity(mDisplayTestActivity), newMode.get()); |
| } finally { |
| mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false); |
| } |
| } |
| |
| private static Point getPhysicalSize(Display.Mode mode) { |
| return new Point(mode.getPhysicalWidth(), mode.getPhysicalHeight()); |
| } |
| |
| private void testSwitchToModeId(DisplayTestActivity activity, Display.Mode mode) |
| throws Exception { |
| Log.i(TAG, "Switching to mode " + mode); |
| |
| final CountDownLatch changeSignal = new CountDownLatch(1); |
| final AtomicInteger changeCounter = new AtomicInteger(0); |
| final DisplayModeState activeMode = new DisplayModeState(mDefaultDisplay); |
| |
| DisplayListener listener = new DisplayListener() { |
| private DisplayModeState mLastMode = activeMode; |
| @Override |
| public void onDisplayAdded(int displayId) {} |
| |
| @Override |
| public void onDisplayChanged(int displayId) { |
| if (displayId != mDefaultDisplay.getDisplayId()) { |
| return; |
| } |
| DisplayModeState newMode = new DisplayModeState(mDefaultDisplay); |
| if (mLastMode.equals(newMode)) { |
| // We assume this display change is caused by an external factor so it's |
| // unrelated. |
| return; |
| } |
| |
| Log.i(TAG, "Switched mode from=" + mLastMode + " to=" + newMode); |
| changeCounter.incrementAndGet(); |
| changeSignal.countDown(); |
| |
| mLastMode = newMode; |
| } |
| |
| @Override |
| public void onDisplayRemoved(int displayId) {} |
| }; |
| |
| Handler handler = new Handler(Looper.getMainLooper()); |
| mDisplayManager.registerDisplayListener(listener, handler); |
| |
| final CountDownLatch presentationSignal = new CountDownLatch(1); |
| handler.post(() -> { |
| activity.setPreferredDisplayMode(mode); |
| presentationSignal.countDown(); |
| }); |
| |
| assertTrue(presentationSignal.await(5, TimeUnit.SECONDS)); |
| |
| // Wait until the display change is effective. |
| assertTrue(changeSignal.await(5, TimeUnit.SECONDS)); |
| DisplayModeState currentMode = new DisplayModeState(mDefaultDisplay); |
| assertEquals(mode.getPhysicalHeight(), currentMode.mHeight); |
| assertEquals(mode.getPhysicalWidth(), currentMode.mWidth); |
| assertEquals(mode.getRefreshRate(), currentMode.mRefreshRate, 0.001f); |
| |
| // Make sure no more display mode changes are registered. |
| Thread.sleep(Duration.ofSeconds(3).toMillis()); |
| assertEquals(1, changeCounter.get()); |
| |
| // Many TV apps use the vendor.display-size sysprop to detect the display size (although |
| // it's not an official API). In Android S the bugs which required this workaround were |
| // fixed and the sysprop should be either unset or should have the same value as the |
| // official API. The assertions are done after the delay above because on some |
| // devices the sysprop is not updated immediately after onDisplayChanged is called. |
| if (PropertyUtil.getVendorApiLevel() >= Build.VERSION_CODES.S) { |
| Point vendorDisplaySize = getVendorDisplaySize(); |
| if (vendorDisplaySize != null) { |
| assertEquals(mode.getPhysicalWidth(), vendorDisplaySize.x); |
| assertEquals(mode.getPhysicalHeight(), vendorDisplaySize.y); |
| } |
| } |
| |
| mDisplayManager.unregisterDisplayListener(listener); |
| } |
| |
| /** |
| * Tests that the mode-related attributes and methods work as expected. |
| */ |
| @Test |
| public void testMode() { |
| Display display = getSecondaryDisplay(mDisplayManager.getDisplays()); |
| assertEquals(2, display.getSupportedModes().length); |
| Display.Mode mode = display.getMode(); |
| assertEquals(display.getSupportedModes()[0], mode); |
| assertEquals(SECONDARY_DISPLAY_WIDTH, mode.getPhysicalWidth()); |
| assertEquals(SECONDARY_DISPLAY_HEIGHT, mode.getPhysicalHeight()); |
| assertEquals(display.getRefreshRate(), mode.getRefreshRate(), 0.0001f); |
| } |
| |
| /** |
| * Tests that getSupportedModes works as expected. |
| */ |
| @Test |
| public void testGetSupportedModesOnDefaultDisplay() { |
| Display.Mode[] supportedModes = mDefaultDisplay.getSupportedModes(); |
| // We need to check that the graph defined by getAlternativeRefreshRates() is symmetric and |
| // transitive. |
| // For that reason we run a primitive Union-Find algorithm. In the end of the algorithm |
| // groups[i] == groups[j] iff supportedModes[i] and supportedModes[j] are in the same |
| // connected component. The complexity is O(N^2*M) where N is the number of modes and M is |
| // the max number of alternative refresh rates). This is okay as we expect a relatively |
| // small number of supported modes. |
| int[] groups = new int[supportedModes.length]; |
| for (int i = 0; i < groups.length; i++) { |
| groups[i] = i; |
| } |
| |
| for (int i = 0; i < supportedModes.length; i++) { |
| Display.Mode supportedMode = supportedModes[i]; |
| for (float alternativeRate : supportedMode.getAlternativeRefreshRates()) { |
| assertTrue(alternativeRate != supportedMode.getRefreshRate()); |
| |
| // The alternative exists. |
| int matchingModeIdx = -1; |
| for (int j = 0; j < supportedModes.length; j++) { |
| boolean matches = displayModeMatches(supportedModes[j], |
| supportedMode.getPhysicalWidth(), |
| supportedMode.getPhysicalHeight(), |
| alternativeRate); |
| if (matches) { |
| matchingModeIdx = j; |
| break; |
| } |
| } |
| String message = "Could not find alternative display mode with refresh rate " |
| + alternativeRate + " for " + supportedMode + ". All supported" |
| + " modes are " + Arrays.toString(supportedModes); |
| assertNotEquals(message, -1, matchingModeIdx); |
| |
| // Merge the groups of i and matchingModeIdx |
| for (int k = 0; k < groups.length; k++) { |
| if (groups[k] == groups[matchingModeIdx]) { |
| groups[k] = groups[i]; |
| } |
| } |
| } |
| } |
| |
| for (int i = 0; i < supportedModes.length; i++) { |
| for (int j = 0; j < supportedModes.length; j++) { |
| if (i != j && groups[i] == groups[j]) { |
| float fpsI = supportedModes[i].getRefreshRate(); |
| boolean iIsAlternativeToJ = false; |
| for (float alternatives : supportedModes[j].getAlternativeRefreshRates()) { |
| if (alternatives == fpsI) { |
| iIsAlternativeToJ = true; |
| break; |
| } |
| } |
| String message = "Expected " + supportedModes[i] + " to be listed as " |
| + "alternative refresh rate of " + supportedModes[j] + ". All supported" |
| + " modes are " + Arrays.toString(supportedModes); |
| assertTrue(message, iIsAlternativeToJ); |
| } |
| } |
| } |
| } |
| |
| private boolean displayModeMatches(Display.Mode mode, int width, int height, |
| float refreshRate) { |
| return mode.getPhysicalWidth() == width && |
| mode.getPhysicalHeight() == height && |
| Float.floatToIntBits(mode.getRefreshRate()) == Float.floatToIntBits(refreshRate); |
| } |
| |
| /** |
| * Tests that getMode() returns a mode which is in getSupportedModes(). |
| */ |
| @Test |
| public void testActiveModeIsSupportedModesOnDefaultDisplay() { |
| Display.Mode[] supportedModes = mDefaultDisplay.getSupportedModes(); |
| Display.Mode activeMode = mDefaultDisplay.getMode(); |
| boolean activeModeIsSupported = false; |
| for (Display.Mode mode : supportedModes) { |
| if (mode.equals(activeMode)) { |
| activeModeIsSupported = true; |
| break; |
| } |
| } |
| assertTrue(activeModeIsSupported); |
| } |
| |
| /** |
| * Test that refresh rate switch app requests are correctly executed on a secondary display. |
| */ |
| @Test |
| public void testRefreshRateSwitchOnSecondaryDisplay() throws Exception { |
| // Standalone VR devices globally ignore SYSTEM_ALERT_WINDOW via AppOps. |
| // Skip this test, which depends on a Presentation SYSTEM_ALERT_WINDOW to pass. |
| if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_VR_HEADSET) { |
| return; |
| } |
| |
| enableAppOps(); |
| final Display display = getSecondaryDisplay(mDisplayManager.getDisplays()); |
| Display.Mode[] modes = display.getSupportedModes(); |
| assertEquals(2, modes.length); |
| Display.Mode mode = display.getMode(); |
| assertEquals(modes[0], mode); |
| final Display.Mode newMode = modes[1]; |
| |
| Handler handler = new Handler(Looper.getMainLooper()); |
| |
| // Register for display events. |
| final CountDownLatch changeSignal = new CountDownLatch(1); |
| mDisplayManager.registerDisplayListener(new DisplayListener() { |
| @Override |
| public void onDisplayAdded(int displayId) {} |
| |
| @Override |
| public void onDisplayChanged(int displayId) { |
| if (displayId == display.getDisplayId()) { |
| changeSignal.countDown(); |
| } |
| } |
| |
| @Override |
| public void onDisplayRemoved(int displayId) {} |
| }, handler); |
| |
| // Show the presentation. |
| final CountDownLatch presentationSignal = new CountDownLatch(1); |
| handler.post(() -> { |
| mPresentation = new TestPresentation( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| display, newMode.getModeId()); |
| mPresentation.show(); |
| presentationSignal.countDown(); |
| }); |
| assertTrue(presentationSignal.await(5, TimeUnit.SECONDS)); |
| |
| // Wait until the display change is effective. |
| assertTrue(changeSignal.await(5, TimeUnit.SECONDS)); |
| |
| assertEquals(newMode, display.getMode()); |
| handler.post(() -> mPresentation.dismiss()); |
| } |
| |
| /** |
| * Verify that getColorSpace method returns the expected color space of the display. |
| */ |
| @Test |
| public void testGetPreferredWideGamutColorSpace() { |
| final ColorSpace colorSpace = mDefaultDisplay.getPreferredWideGamutColorSpace(); |
| |
| if (mDefaultDisplay.isWideColorGamut()) { |
| assertFalse(colorSpace.isSrgb()); |
| assertTrue(colorSpace.isWideGamut()); |
| } else { |
| assertNull(colorSpace); |
| } |
| } |
| |
| @Test |
| public void testGetDeviceProductInfo() { |
| DeviceProductInfo deviceProductInfo = mDefaultDisplay.getDeviceProductInfo(); |
| assumeNotNull(deviceProductInfo); |
| |
| assertNotNull(deviceProductInfo.getManufacturerPnpId()); |
| |
| assertNotNull(deviceProductInfo.getProductId()); |
| |
| final boolean isYearPresent = (deviceProductInfo.getModelYear() != -1) || |
| (deviceProductInfo.getManufactureYear() != -1); |
| assertTrue(isYearPresent); |
| int year = deviceProductInfo.getModelYear() != -1 ? |
| deviceProductInfo.getModelYear() : deviceProductInfo.getManufactureYear(); |
| // Verify if the model year or manufacture year is greater than or equal to 1990. |
| // This assumption is based on Section of 3.4.4 - Week and Year of Manufacture or Model Year |
| // of VESA EDID STANDARD Version 1, Revision 4 |
| assertTrue(year >= 1990); |
| |
| int week = deviceProductInfo.getManufactureWeek(); |
| assertTrue(week == -1 || (week >= 1 && week <= 53)); |
| |
| List<Integer> allowedConnectionToSinkValues = List.of( |
| DeviceProductInfo.CONNECTION_TO_SINK_UNKNOWN, |
| DeviceProductInfo.CONNECTION_TO_SINK_BUILT_IN, |
| DeviceProductInfo.CONNECTION_TO_SINK_DIRECT, |
| DeviceProductInfo.CONNECTION_TO_SINK_TRANSITIVE |
| ); |
| assertTrue( |
| allowedConnectionToSinkValues.contains( |
| deviceProductInfo.getConnectionToSinkType())); |
| } |
| |
| @Test |
| public void testDeviceProductInfo() { |
| DeviceProductInfo deviceProductInfo = new DeviceProductInfo( |
| "DeviceName" /* name */, |
| "TTL" /* manufacturePnpId */, |
| "ProductId1" /* productId */, |
| 2000 /* modelYear */, |
| DeviceProductInfo.CONNECTION_TO_SINK_DIRECT); |
| |
| assertEquals("DeviceName", deviceProductInfo.getName()); |
| assertEquals("TTL", deviceProductInfo.getManufacturerPnpId()); |
| assertEquals("ProductId1", deviceProductInfo.getProductId()); |
| assertEquals(2000, deviceProductInfo.getModelYear()); |
| assertEquals(DeviceProductInfo.CONNECTION_TO_SINK_DIRECT, |
| deviceProductInfo.getConnectionToSinkType()); |
| } |
| |
| @Test |
| public void testFailBrightnessChangeWithoutPermission() throws Exception { |
| final DisplayTestActivity activity = launchActivity(mDisplayTestActivity); |
| final int originalValue = Settings.System.getInt(mContext.getContentResolver(), |
| Settings.System.SCREEN_BRIGHTNESS, BRIGHTNESS_MAX); |
| |
| try { |
| final int valueToSet = originalValue > 128 ? 40 : 200; // sufficiently different value |
| boolean wasSet = setBrightness(((float) valueToSet) / BRIGHTNESS_MAX); |
| |
| assertFalse(wasSet); |
| int newValue = Settings.System.getInt(mContext.getContentResolver(), |
| Settings.System.SCREEN_BRIGHTNESS, BRIGHTNESS_MAX); |
| assertEquals(originalValue, newValue); // verify that setting the new value failed. |
| } finally { |
| try { |
| // Clean up just in case the test fails and we did actually manage to change the |
| // brightness. |
| Settings.System.putInt(mContext.getContentResolver(), |
| Settings.System.SCREEN_BRIGHTNESS, originalValue); |
| } catch (Exception e) { |
| } |
| } |
| } |
| |
| @Test |
| public void testGetSupportedWideColorGamut_shouldNotBeNull() { |
| assertNotNull(mSupportedWideGamuts); |
| } |
| |
| @Test |
| public void testGetSupportWideColorGamut_displayIsWideColorGamut() { |
| final ColorSpace displayP3 = ColorSpace.get(ColorSpace.Named.DISPLAY_P3); |
| final ColorSpace dciP3 = ColorSpace.get(ColorSpace.Named.DCI_P3); |
| final List<ColorSpace> list = Arrays.asList(mSupportedWideGamuts); |
| final boolean supportsWideGamut = mDefaultDisplay.isWideColorGamut() |
| && mSupportedWideGamuts.length > 0; |
| final boolean supportsP3 = list.contains(displayP3) || list.contains(dciP3); |
| assertEquals(supportsWideGamut, supportsP3); |
| } |
| |
| @Test |
| public void testRestrictedFramebufferSize() { |
| PackageManager packageManager = mContext.getPackageManager(); |
| if (packageManager.hasSystemFeature(FEATURE_LEANBACK)) { |
| // TV devices are allowed to restrict their framebuffer size. |
| return; |
| } |
| |
| // Non-TV devices are not allowed by Android CDD to restrict their framebuffer size. |
| String width = SystemProperties.get("ro.surface_flinger.max_graphics_width"); |
| assertEquals("", width); |
| String height = SystemProperties.get("ro.surface_flinger.max_graphics_height"); |
| assertEquals("", height); |
| } |
| |
| /** |
| * Used to force mode changes on a display. |
| * <p> |
| * Note that due to limitations of the Presentation class, the modes must have the same size |
| * otherwise the presentation will be automatically dismissed. |
| */ |
| private static final class TestPresentation extends Presentation { |
| |
| private final int mModeId; |
| |
| public TestPresentation(Context context, Display display, int modeId) { |
| super(context, display); |
| mModeId = modeId; |
| } |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| View content = new View(getContext()); |
| content.setLayoutParams(new ViewGroup.LayoutParams( |
| ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); |
| content.setBackgroundColor(Color.RED); |
| setContentView(content); |
| |
| WindowManager.LayoutParams params = getWindow().getAttributes(); |
| params.preferredDisplayModeId = mModeId; |
| params.setTitle("CtsTestPresentation"); |
| getWindow().setAttributes(params); |
| } |
| |
| @Override |
| public void cancel() { |
| // Ignore attempts to force cancel the presentation. This is going to happen when we |
| // change the mode of the display since doing so will change the display metrics, which |
| // Presentations don't yet support. Ignoring it means the Presentation will stay up and |
| // the mode will stay changed until dismiss is called, preventing a race condition |
| // between the test checking the mode of the display and the mode changing back to the |
| // default because the requesting Presentation is no longer showing. |
| } |
| } |
| |
| private Activity launchScreenOnActivity() { |
| Class clazz = ScreenOnActivity.class; |
| String targetPackage = |
| InstrumentationRegistry.getInstrumentation().getContext().getPackageName(); |
| Instrumentation.ActivityResult result = |
| new Instrumentation.ActivityResult(0, new Intent()); |
| Instrumentation.ActivityMonitor monitor = |
| new Instrumentation.ActivityMonitor(clazz.getName(), result, false); |
| InstrumentationRegistry.getInstrumentation().addMonitor(monitor); |
| launchActivity(targetPackage, clazz, null); |
| return monitor.waitForActivity(); |
| } |
| |
| private <T extends Activity> T launchActivity(ActivityTestRule<T> activityRule) { |
| final T activity = activityRule.launchActivity(null); |
| InstrumentationRegistry.getInstrumentation().waitForIdleSync(); |
| return activity; |
| } |
| |
| /** |
| * Utility method for launching an activity. Copied from InstrumentationTestCase since |
| * InstrumentationRegistry does not provide these APIs anymore. |
| * |
| * <p>The {@link Intent} used to launch the Activity is: |
| * action = {@link Intent#ACTION_MAIN} |
| * extras = null, unless a custom bundle is provided here |
| * All other fields are null or empty. |
| * |
| * <p><b>NOTE:</b> The parameter <i>pkg</i> must refer to the package identifier of the |
| * package hosting the activity to be launched, which is specified in the AndroidManifest.xml |
| * file. This is not necessarily the same as the java package name. |
| * |
| * @param pkg The package hosting the activity to be launched. |
| * @param activityCls The activity class to launch. |
| * @param extras Optional extra stuff to pass to the activity. |
| * @return The activity, or null if non launched. |
| */ |
| private final <T extends Activity> T launchActivity( |
| String pkg, |
| Class<T> activityCls, |
| Bundle extras) { |
| Intent intent = new Intent(Intent.ACTION_MAIN); |
| if (extras != null) { |
| intent.putExtras(extras); |
| } |
| return launchActivityWithIntent(pkg, activityCls, intent); |
| } |
| |
| /** |
| * Utility method for launching an activity with a specific Intent. |
| * |
| * <p><b>NOTE:</b> The parameter <i>pkg</i> must refer to the package identifier of the |
| * package hosting the activity to be launched, which is specified in the AndroidManifest.xml |
| * file. This is not necessarily the same as the java package name. |
| * |
| * @param pkg The package hosting the activity to be launched. |
| * @param activityCls The activity class to launch. |
| * @param intent The intent to launch with |
| * @return The activity, or null if non launched. |
| */ |
| @SuppressWarnings("unchecked") |
| private final <T extends Activity> T launchActivityWithIntent( |
| String pkg, |
| Class<T> activityCls, |
| Intent intent) { |
| intent.setClassName(pkg, activityCls.getName()); |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| T activity = (T) InstrumentationRegistry.getInstrumentation().startActivitySync(intent); |
| InstrumentationRegistry.getInstrumentation().waitForIdleSync(); |
| return activity; |
| } |
| |
| /** |
| * Sets the brightness via the shell cmd. |
| */ |
| public boolean setBrightness(float value) throws Exception { |
| Process process = Runtime.getRuntime().exec("cmd display set-brightness " + value); |
| return 0 == process.waitFor(); |
| } |
| |
| private Point getVendorDisplaySize() { |
| String value = PropertyUtil.getProperty("vendor.display-size"); |
| if (TextUtils.isEmpty(value)) { |
| return null; |
| } |
| |
| String[] parts = value.split("x"); |
| assertEquals(2, parts.length); |
| return new Point(Integer.parseInt(parts[0]), Integer.parseInt(parts[1])); |
| } |
| } |