blob: cc3591c895605591aeaeca12815eba11271faaaa [file] [log] [blame]
/*
* Copyright (C) 2021 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 com.android.server.display;
import static android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR;
import static android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF;
import static android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.Binder;
import android.os.Handler;
import android.os.IThermalEventListener;
import android.os.IThermalService;
import android.os.Message;
import android.os.PowerManager;
import android.os.Temperature;
import android.os.Temperature.ThrottlingStatus;
import android.os.test.TestLooper;
import android.platform.test.annotations.Presubmit;
import android.test.mock.MockContentResolver;
import android.util.MathUtils;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.internal.util.test.FakeSettingsProviderRule;
import com.android.server.display.DisplayDeviceConfig.HighBrightnessModeData;
import com.android.server.display.HighBrightnessModeController.Injector;
import com.android.server.testutils.OffsettableClock;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@Presubmit
@RunWith(AndroidJUnit4.class)
public class HighBrightnessModeControllerTest {
private static final int MINIMUM_LUX = 100;
private static final float TRANSITION_POINT = 0.763f;
private static final long TIME_WINDOW_MILLIS = 55 * 1000;
private static final long TIME_ALLOWED_IN_WINDOW_MILLIS = 12 * 1000;
private static final long TIME_MINIMUM_AVAILABLE_TO_ENABLE_MILLIS = 5 * 1000;
private static final int THERMAL_STATUS_LIMIT = PowerManager.THERMAL_STATUS_SEVERE;
private static final boolean ALLOW_IN_LOW_POWER_MODE = false;
private static final float DEFAULT_MIN = 0.01f;
private static final float DEFAULT_MAX = 0.80f;
private static final int DISPLAY_WIDTH = 900;
private static final int DISPLAY_HEIGHT = 1600;
private static final float EPSILON = 0.000001f;
private OffsettableClock mClock;
private TestLooper mTestLooper;
private Handler mHandler;
private Binder mDisplayToken;
private Context mContextSpy;
@Rule
public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
@Mock IThermalService mThermalServiceMock;
@Mock Injector mInjectorMock;
@Captor ArgumentCaptor<IThermalEventListener> mThermalEventListenerCaptor;
private static final HighBrightnessModeData DEFAULT_HBM_DATA =
new HighBrightnessModeData(MINIMUM_LUX, TRANSITION_POINT, TIME_WINDOW_MILLIS,
TIME_ALLOWED_IN_WINDOW_MILLIS, TIME_MINIMUM_AVAILABLE_TO_ENABLE_MILLIS,
THERMAL_STATUS_LIMIT, ALLOW_IN_LOW_POWER_MODE);
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mClock = new OffsettableClock.Stopped();
mTestLooper = new TestLooper(mClock::now);
mDisplayToken = null;
mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
final MockContentResolver resolver = mSettingsProviderRule.mockContentResolver(mContextSpy);
when(mContextSpy.getContentResolver()).thenReturn(resolver);
when(mInjectorMock.getThermalService()).thenReturn(mThermalServiceMock);
}
/////////////////
// Test Methods
/////////////////
@Test
public void testNoHbmData() {
initHandler(null);
final HighBrightnessModeController hbmc = new HighBrightnessModeController(
mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken, DEFAULT_MIN,
DEFAULT_MAX, null, () -> {}, mContextSpy);
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_OFF);
}
@Test
public void testNoHbmData_Enabled() {
initHandler(null);
final HighBrightnessModeController hbmc = new HighBrightnessModeController(
mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken, DEFAULT_MIN,
DEFAULT_MAX, null, () -> {}, mContextSpy);
hbmc.setAutoBrightnessEnabled(true);
hbmc.onAmbientLuxChange(MINIMUM_LUX - 1); // below allowed range
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_OFF);
}
@Test
public void testOffByDefault() {
final HighBrightnessModeController hbmc = createDefaultHbm();
assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
}
@Test
public void testAutoBrightnessEnabled_NoLux() {
final HighBrightnessModeController hbmc = createDefaultHbm();
hbmc.setAutoBrightnessEnabled(true);
assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
}
@Test
public void testAutoBrightnessEnabled_LowLux() {
final HighBrightnessModeController hbmc = createDefaultHbm();
hbmc.setAutoBrightnessEnabled(true);
hbmc.onAmbientLuxChange(MINIMUM_LUX - 1); // below allowed range
assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
}
@Test
public void testAutoBrightnessEnabled_HighLux() {
final HighBrightnessModeController hbmc = createDefaultHbm();
hbmc.setAutoBrightnessEnabled(true);
hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
}
@Test
public void testAutoBrightnessEnabled_HighLux_ThenDisable() {
final HighBrightnessModeController hbmc = createDefaultHbm();
hbmc.setAutoBrightnessEnabled(true);
hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
hbmc.setAutoBrightnessEnabled(false);
assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
}
@Test
public void testWithinHighRange_thenOverTime_thenEarnBackTime() {
final HighBrightnessModeController hbmc = createDefaultHbm();
hbmc.setAutoBrightnessEnabled(true);
hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
hbmc.onBrightnessChanged(TRANSITION_POINT + 0.01f);
// Verify we are in HBM
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
// Use up all the time in the window.
advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS);
// Verify we are not out of HBM
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
// Shift time so that the HBM event is at the beginning of the current window
advanceTime(TIME_WINDOW_MILLIS - TIME_ALLOWED_IN_WINDOW_MILLIS);
// Shift time again so that we are just below the minimum allowable
advanceTime(TIME_MINIMUM_AVAILABLE_TO_ENABLE_MILLIS - 1);
// Verify we are not out of HBM
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
// Advance the necessary millisecond
advanceTime(1);
// Verify we are allowed HBM again.
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
}
@Test
public void testInHBM_ThenLowLux() {
final HighBrightnessModeController hbmc = createDefaultHbm();
hbmc.setAutoBrightnessEnabled(true);
hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
hbmc.onBrightnessChanged(TRANSITION_POINT + 0.01f);
// Verify we are in HBM
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2);
// Verify we are in HBM
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
hbmc.onAmbientLuxChange(1);
advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2 + 1);
// Verify we are out of HBM
assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
}
@Test
public void testInHBM_TestMultipleEvents_DueToAutoBrightness() {
final HighBrightnessModeController hbmc = createDefaultHbm();
hbmc.setAutoBrightnessEnabled(true);
hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
hbmc.onBrightnessChanged(TRANSITION_POINT + 0.01f);
advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2);
// Verify we are in HBM
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
hbmc.onBrightnessChanged(TRANSITION_POINT - 0.01f);
advanceTime(1);
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
hbmc.onBrightnessChanged(TRANSITION_POINT + 0.01f);
advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2);
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
advanceTime(2);
// Now we should be out again.
assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
}
@Test
public void testInHBM_TestMultipleEvents_DueToLux() {
final HighBrightnessModeController hbmc = createDefaultHbm();
hbmc.setAutoBrightnessEnabled(true);
hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
// Go into HBM for half the allowed window
hbmc.onBrightnessChanged(TRANSITION_POINT + 0.01f);
advanceTime(TIME_ALLOWED_IN_WINDOW_MILLIS / 2);
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
// Move lux below threshold (ending first event);
hbmc.onAmbientLuxChange(MINIMUM_LUX - 1);
hbmc.onBrightnessChanged(TRANSITION_POINT);
assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
// Move up some amount of time so that there's still time in the window even after a
// second event.
advanceTime((TIME_WINDOW_MILLIS - TIME_ALLOWED_IN_WINDOW_MILLIS) / 2);
assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
// Go into HBM for just under the second half of allowed window
hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
hbmc.onBrightnessChanged(TRANSITION_POINT + 1);
advanceTime((TIME_ALLOWED_IN_WINDOW_MILLIS / 2) - 1);
assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_SUNLIGHT);
// Now exhaust the time
advanceTime(2);
assertState(hbmc, DEFAULT_MIN, TRANSITION_POINT, HIGH_BRIGHTNESS_MODE_OFF);
}
@Test
public void testNoHbmInHighThermalState() throws Exception {
final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
verify(mThermalServiceMock).registerThermalEventListenerWithType(
mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
// Set the thermal status too high.
listener.notifyThrottling(getSkinTemp(Temperature.THROTTLING_CRITICAL));
// Try to go into HBM mode but fail
hbmc.setAutoBrightnessEnabled(true);
hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
advanceTime(10);
assertEquals(HIGH_BRIGHTNESS_MODE_OFF, hbmc.getHighBrightnessMode());
}
@Test
public void testHbmTurnsOffInHighThermalState() throws Exception {
final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
verify(mThermalServiceMock).registerThermalEventListenerWithType(
mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
// Set the thermal status tolerable
listener.notifyThrottling(getSkinTemp(Temperature.THROTTLING_LIGHT));
// Try to go into HBM mode
hbmc.setAutoBrightnessEnabled(true);
hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
advanceTime(1);
assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
// Set the thermal status too high and verify we're off.
listener.notifyThrottling(getSkinTemp(Temperature.THROTTLING_CRITICAL));
advanceTime(10);
assertEquals(HIGH_BRIGHTNESS_MODE_OFF, hbmc.getHighBrightnessMode());
// Set the thermal status low again and verify we're back on.
listener.notifyThrottling(getSkinTemp(Temperature.THROTTLING_SEVERE));
advanceTime(1);
assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
}
@Test
public void testHdrRequires50PercentOfScreen() {
final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
final int layerWidth = DISPLAY_WIDTH;
final int smallLayerHeight = DISPLAY_HEIGHT / 2 - 1; // height to use for <50%
final int largeLayerHeight = DISPLAY_HEIGHT / 2 + 1; // height to use for >50%
// ensure hdr doesn't turn on if layer is too small
hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
layerWidth, smallLayerHeight, 0 /*flags*/);
advanceTime(0);
assertEquals(HIGH_BRIGHTNESS_MODE_OFF, hbmc.getHighBrightnessMode());
// Now check with layer larger than 50%
hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
layerWidth, largeLayerHeight, 0 /*flags*/);
advanceTime(0);
assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
}
@Test
public void testHdrTrumpsSunlight() {
final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
// Turn on sunlight
hbmc.setAutoBrightnessEnabled(true);
hbmc.onAmbientLuxChange(MINIMUM_LUX + 1);
advanceTime(0);
assertEquals(HIGH_BRIGHTNESS_MODE_SUNLIGHT, hbmc.getHighBrightnessMode());
assertEquals(DEFAULT_MAX, hbmc.getCurrentBrightnessMax(), EPSILON);
// turn on hdr
hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/);
advanceTime(0);
assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
assertEquals(TRANSITION_POINT, hbmc.getCurrentBrightnessMax(), EPSILON);
}
@Test
public void testHdrBrightnessLimitSameAsNormalLimit() {
final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
// Check limit when HBM is off
hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/);
advanceTime(0);
assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
assertEquals(TRANSITION_POINT, hbmc.getCurrentBrightnessMax(), EPSILON);
// Check limit with HBM is set to HDR
hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 0 /*numberOfHdrLayers*/,
DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/);
advanceTime(0);
assertEquals(HIGH_BRIGHTNESS_MODE_OFF, hbmc.getHighBrightnessMode());
assertEquals(TRANSITION_POINT, hbmc.getCurrentBrightnessMax(), EPSILON);
}
@Test
public void testHdrBrightnessScaledNormalBrightness() {
final HighBrightnessModeController hbmc = createDefaultHbm(new OffsettableClock());
hbmc.getHdrListener().onHdrInfoChanged(null /*displayToken*/, 1 /*numberOfHdrLayers*/,
DISPLAY_WIDTH, DISPLAY_HEIGHT, 0 /*flags*/);
advanceTime(0);
assertEquals(HIGH_BRIGHTNESS_MODE_HDR, hbmc.getHighBrightnessMode());
// verify things are scaled for 0.5f
float brightness = 0.5f;
float expectedHdrBrightness = MathUtils.map(DEFAULT_MIN, TRANSITION_POINT,
DEFAULT_MIN, DEFAULT_MAX, brightness); // map value from normal range to hdr range
hbmc.onBrightnessChanged(brightness);
advanceTime(0);
assertEquals(expectedHdrBrightness, hbmc.getHdrBrightnessValue(), EPSILON);
// Try another value
brightness = 0.33f;
expectedHdrBrightness = MathUtils.map(DEFAULT_MIN, TRANSITION_POINT,
DEFAULT_MIN, DEFAULT_MAX, brightness); // map value from normal range to hdr range
hbmc.onBrightnessChanged(brightness);
advanceTime(0);
assertEquals(expectedHdrBrightness, hbmc.getHdrBrightnessValue(), EPSILON);
// Try the min value
brightness = DEFAULT_MIN;
expectedHdrBrightness = DEFAULT_MIN;
hbmc.onBrightnessChanged(brightness);
advanceTime(0);
assertEquals(expectedHdrBrightness, hbmc.getHdrBrightnessValue(), EPSILON);
// Try the max value
brightness = TRANSITION_POINT;
expectedHdrBrightness = DEFAULT_MAX;
hbmc.onBrightnessChanged(brightness);
advanceTime(0);
assertEquals(expectedHdrBrightness, hbmc.getHdrBrightnessValue(), EPSILON);
}
private void assertState(HighBrightnessModeController hbmc,
float brightnessMin, float brightnessMax, int hbmMode) {
assertEquals(brightnessMin, hbmc.getCurrentBrightnessMin(), EPSILON);
assertEquals(brightnessMax, hbmc.getCurrentBrightnessMax(), EPSILON);
assertEquals(hbmMode, hbmc.getHighBrightnessMode());
}
private HighBrightnessModeController createDefaultHbm() {
return createDefaultHbm(null);
}
// Creates instance with standard initialization values.
private HighBrightnessModeController createDefaultHbm(OffsettableClock clock) {
initHandler(clock);
return new HighBrightnessModeController(mInjectorMock, mHandler, DISPLAY_WIDTH,
DISPLAY_HEIGHT, mDisplayToken, DEFAULT_MIN, DEFAULT_MAX, DEFAULT_HBM_DATA, () -> {},
mContextSpy);
}
private void initHandler(OffsettableClock clock) {
mClock = clock != null ? clock : new OffsettableClock.Stopped();
when(mInjectorMock.getClock()).thenReturn(mClock::now);
mTestLooper = new TestLooper(mClock::now);
mHandler = new Handler(mTestLooper.getLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
return true;
}
});
}
private void advanceTime(long timeMs) {
mClock.fastForward(timeMs);
mTestLooper.dispatchAll();
}
private Temperature getSkinTemp(@ThrottlingStatus int status) {
return new Temperature(30.0f, Temperature.TYPE_SKIN, "test_skin_temp", status);
}
}