diff options
34 files changed, 1458 insertions, 263 deletions
diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index 99b297abe92a..0e45787c1340 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -283,7 +283,8 @@ public abstract class CameraDevice implements AutoCloseable { * @see StreamConfigurationMap#getInputFormats * @see StreamConfigurationMap#getInputSizes * @see StreamConfigurationMap#getValidOutputFormatsForInput - * @see StreamConfigurationMap#getOutputSizes + * @see StreamConfigurationMap#getOutputSizes(int) + * @see StreamConfigurationMap#getOutputSizes(Class) * @see android.media.ImageWriter * @see android.media.ImageReader * @deprecated Please use {@link diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index a098362f16aa..c2fe0800812f 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -130,14 +130,17 @@ public final class CameraManager { /** * Enable physical camera availability callbacks when the logical camera is unavailable * - * <p>Previously once a logical camera becomes unavailable, no {@link - * #onPhysicalCameraAvailable} or {@link #onPhysicalCameraUnavailable} will be called until - * the logical camera becomes available again. The results in the app opening the logical - * camera not able to receive physical camera availability change.</p> - * - * <p>With this change, the {@link #onPhysicalCameraAvailable} and {@link - * #onPhysicalCameraUnavailable} can still be called while the logical camera is unavailable. - * </p> + * <p>Previously once a logical camera becomes unavailable, no + * {@link AvailabilityCallback#onPhysicalCameraAvailable} or + * {@link AvailabilityCallback#onPhysicalCameraUnavailable} will + * be called until the logical camera becomes available again. The + * results in the app opening the logical camera not able to + * receive physical camera availability change.</p> + * + * <p>With this change, the {@link + * AvailabilityCallback#onPhysicalCameraAvailable} and {@link + * AvailabilityCallback#onPhysicalCameraUnavailable} can still be + * called while the logical camera is unavailable. </p> */ @ChangeId @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) diff --git a/core/java/android/hardware/camera2/package.html b/core/java/android/hardware/camera2/package.html index 719c2f620234..3fd5d7c60832 100644 --- a/core/java/android/hardware/camera2/package.html +++ b/core/java/android/hardware/camera2/package.html @@ -62,12 +62,28 @@ RAW buffers for {@link android.hardware.camera2.DngCreator} can be done with {@link android.media.ImageReader} with the {@link android.graphics.ImageFormat#JPEG} and {@link android.graphics.ImageFormat#RAW_SENSOR} formats. Application-driven -processing of camera data in RenderScript, OpenGL ES, or directly in -managed or native code is best done through {@link -android.renderscript.Allocation} with a YUV {@link -android.renderscript.Type}, {@link android.graphics.SurfaceTexture}, -and {@link android.media.ImageReader} with a {@link -android.graphics.ImageFormat#YUV_420_888} format, respectively.</p> +processing of camera data in OpenGL ES, or directly in managed or +native code is best done through {@link +android.graphics.SurfaceTexture}, or {@link android.media.ImageReader} +with a {@link android.graphics.ImageFormat#YUV_420_888} format, +respectively. </p> + +<p>By default, YUV-format buffers provided by the camera are using the +JFIF YUV<->RGB transform matrix (equivalent to Rec.601 full-range +encoding), and after conversion to RGB with this matrix, the resulting +RGB data is in the sRGB colorspace. Captured JPEG images may contain +an ICC profile to specify their color space information; if not, they +should be assumed to be in the sRGB space as well. On some devices, +the output colorspace can be changed via {@link +android.hardware.camera2.params.SessionConfiguration#setColorSpace}. +</p> +<p> +Note that although the YUV->RGB transform is the JFIF matrix (Rec.601 +full-range), due to legacy and compatibility reasons, the output is in +the sRGB colorspace, which uses the Rec.709 color primaries. Image +processing code can safely treat the output RGB as being in the sRGB +colorspace. +</p> <p>The application then needs to construct a {@link android.hardware.camera2.CaptureRequest}, which defines all the diff --git a/core/java/android/hardware/camera2/params/ColorSpaceProfiles.java b/core/java/android/hardware/camera2/params/ColorSpaceProfiles.java index 2e3af80f9cc0..bb154a96cbe9 100644 --- a/core/java/android/hardware/camera2/params/ColorSpaceProfiles.java +++ b/core/java/android/hardware/camera2/params/ColorSpaceProfiles.java @@ -192,7 +192,7 @@ public final class ColorSpaceProfiles { * @see OutputConfiguration#setDynamicRangeProfile * @see SessionConfiguration#setColorSpace * @see ColorSpace.Named - * @see DynamicRangeProfiles.Profile + * @see DynamicRangeProfiles */ public @NonNull Set<Long> getSupportedDynamicRangeProfiles(@NonNull ColorSpace.Named colorSpace, @ImageFormat.Format int imageFormat) { @@ -230,7 +230,7 @@ public final class ColorSpaceProfiles { * @see SessionConfiguration#setColorSpace * @see OutputConfiguration#setDynamicRangeProfile * @see ColorSpace.Named - * @see DynamicRangeProfiles.Profile + * @see DynamicRangeProfiles */ public @NonNull Set<ColorSpace.Named> getSupportedColorSpacesForDynamicRange( @ImageFormat.Format int imageFormat, diff --git a/core/java/android/hardware/camera2/params/RecommendedStreamConfigurationMap.java b/core/java/android/hardware/camera2/params/RecommendedStreamConfigurationMap.java index 80db38fc9d8f..d4ce0ebbc528 100644 --- a/core/java/android/hardware/camera2/params/RecommendedStreamConfigurationMap.java +++ b/core/java/android/hardware/camera2/params/RecommendedStreamConfigurationMap.java @@ -45,7 +45,7 @@ import java.util.Set; * Immutable class to store the recommended stream configurations to set up * {@link android.view.Surface Surfaces} for creating a * {@link android.hardware.camera2.CameraCaptureSession capture session} with - * {@link android.hardware.camera2.CameraDevice#createCaptureSession}. + * {@link android.hardware.camera2.CameraDevice#createCaptureSession(SessionConfiguration)}. * * <p>The recommended list does not replace or deprecate the exhaustive complete list found in * {@link StreamConfigurationMap}. It is a suggestion about available power and performance @@ -70,7 +70,7 @@ import java.util.Set; * }</code></pre> * * @see CameraCharacteristics#getRecommendedStreamConfigurationMap - * @see CameraDevice#createCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) */ public final class RecommendedStreamConfigurationMap { @@ -282,7 +282,7 @@ public final class RecommendedStreamConfigurationMap { /** * Determine whether or not output surfaces with a particular user-defined format can be passed - * {@link CameraDevice#createCaptureSession createCaptureSession}. + * {@link CameraDevice#createCaptureSession(SessionConfiguration) createCaptureSession}. * * <p> * For further information refer to {@link StreamConfigurationMap#isOutputSupportedFor}. @@ -292,7 +292,7 @@ public final class RecommendedStreamConfigurationMap { * @param format an image format from either {@link ImageFormat} or {@link PixelFormat} * @return * {@code true} if using a {@code surface} with this {@code format} will be - * supported with {@link CameraDevice#createCaptureSession} + * supported with {@link CameraDevice#createCaptureSession(SessionConfiguration)} * * @throws IllegalArgumentException * if the image format was not a defined named constant @@ -508,8 +508,10 @@ public final class RecommendedStreamConfigurationMap { } /** - * Determine whether or not the {@code surface} in its current state is suitable to be included - * in a {@link CameraDevice#createCaptureSession capture session} as an output. + * Determine whether or not the {@code surface} in its current + * state is suitable to be included in a {@link + * CameraDevice#createCaptureSession(SessionConfiguration) capture + * session} as an output. * * <p>For more information refer to {@link StreamConfigurationMap#isOutputSupportedFor}. * </p> diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java index 385f10719509..8f611a831204 100644 --- a/core/java/android/hardware/camera2/params/SessionConfiguration.java +++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java @@ -55,7 +55,7 @@ public final class SessionConfiguration implements Parcelable { * at regular non high speed FPS ranges and optionally {@link InputConfiguration} for * reprocessable sessions. * - * @see CameraDevice#createCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) * @see CameraDevice#createReprocessableCaptureSession */ public static final int SESSION_REGULAR = CameraDevice.SESSION_OPERATION_MODE_NORMAL; @@ -110,10 +110,7 @@ public final class SessionConfiguration implements Parcelable { * * @see #SESSION_REGULAR * @see #SESSION_HIGH_SPEED - * @see CameraDevice#createCaptureSession(List, CameraCaptureSession.StateCallback, Handler) - * @see CameraDevice#createCaptureSessionByOutputConfigurations - * @see CameraDevice#createReprocessableCaptureSession - * @see CameraDevice#createConstrainedHighSpeedCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) */ public SessionConfiguration(@SessionMode int sessionType, @NonNull List<OutputConfiguration> outputs, diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java index aabe149d16f5..ef0db7f8a41c 100644 --- a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java +++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java @@ -41,7 +41,7 @@ import java.util.Set; * {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP configurations} to set up * {@link android.view.Surface Surfaces} for creating a * {@link android.hardware.camera2.CameraCaptureSession capture session} with - * {@link android.hardware.camera2.CameraDevice#createCaptureSession}. + * {@link android.hardware.camera2.CameraDevice#createCaptureSession(SessionConfiguration)}. * <!-- TODO: link to input stream configuration --> * * <p>This is the authoritative list for all <!-- input/ -->output formats (and sizes respectively @@ -62,7 +62,7 @@ import java.util.Set; * }</code></pre> * * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP - * @see CameraDevice#createCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) */ public final class StreamConfigurationMap { @@ -456,7 +456,7 @@ public final class StreamConfigurationMap { /** * Determine whether or not output surfaces with a particular user-defined format can be passed - * {@link CameraDevice#createCaptureSession createCaptureSession}. + * {@link CameraDevice#createCaptureSession(SessionConfiguration) createCaptureSession}. * * <p>This method determines that the output {@code format} is supported by the camera device; * each output {@code surface} target may or may not itself support that {@code format}. @@ -468,7 +468,7 @@ public final class StreamConfigurationMap { * @param format an image format from either {@link ImageFormat} or {@link PixelFormat} * @return * {@code true} iff using a {@code surface} with this {@code format} will be - * supported with {@link CameraDevice#createCaptureSession} + * supported with {@link CameraDevice#createCaptureSession(SessionConfiguration)} * * @throws IllegalArgumentException * if the image format was not a defined named constant @@ -476,7 +476,7 @@ public final class StreamConfigurationMap { * * @see ImageFormat * @see PixelFormat - * @see CameraDevice#createCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) */ public boolean isOutputSupportedFor(int format) { checkArgumentFormat(format); @@ -521,7 +521,7 @@ public final class StreamConfigurationMap { * * <p>Generally speaking this means that creating a {@link Surface} from that class <i>may</i> * provide a producer endpoint that is suitable to be used with - * {@link CameraDevice#createCaptureSession}.</p> + * {@link CameraDevice#createCaptureSession(SessionConfiguration)}.</p> * * <p>Since not all of the above classes support output of all format and size combinations, * the particular combination should be queried with {@link #isOutputSupportedFor(Surface)}.</p> @@ -531,7 +531,7 @@ public final class StreamConfigurationMap { * * @throws NullPointerException if {@code klass} was {@code null} * - * @see CameraDevice#createCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) * @see #isOutputSupportedFor(Surface) */ public static <T> boolean isOutputSupportedFor(Class<T> klass) { @@ -555,8 +555,10 @@ public final class StreamConfigurationMap { } /** - * Determine whether or not the {@code surface} in its current state is suitable to be included - * in a {@link CameraDevice#createCaptureSession capture session} as an output. + * Determine whether or not the {@code surface} in its current + * state is suitable to be included in a {@link + * CameraDevice#createCaptureSession(SessionConfiguration) capture + * session} as an output. * * <p>Not all surfaces are usable with the {@link CameraDevice}, and not all configurations * of that {@code surface} are compatible. Some classes that provide the {@code surface} are @@ -588,7 +590,7 @@ public final class StreamConfigurationMap { * @throws NullPointerException if {@code surface} was {@code null} * @throws IllegalArgumentException if the Surface endpoint is no longer valid * - * @see CameraDevice#createCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) * @see #isOutputSupportedFor(Class) */ public boolean isOutputSupportedFor(Surface surface) { @@ -623,14 +625,16 @@ public final class StreamConfigurationMap { } /** - * Determine whether or not the particular stream configuration is suitable to be included - * in a {@link CameraDevice#createCaptureSession capture session} as an output. + * Determine whether or not the particular stream configuration is + * suitable to be included in a {@link + * CameraDevice#createCaptureSession(SessionConfiguration) capture + * session} as an output. * * @param size stream configuration size * @param format stream configuration format * @return {@code true} if this is supported, {@code false} otherwise * - * @see CameraDevice#createCaptureSession + * @see CameraDevice#createCaptureSession(SessionConfiguration) * @see #isOutputSupportedFor(Class) * @hide */ diff --git a/packages/SystemUI/res/layout/zen_mode_condition.xml b/packages/SystemUI/res/layout/zen_mode_condition.xml index ab52465be63c..3baae3376bd0 100644 --- a/packages/SystemUI/res/layout/zen_mode_condition.xml +++ b/packages/SystemUI/res/layout/zen_mode_condition.xml @@ -15,6 +15,7 @@ limitations under the License. --> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:theme="@style/Theme.SystemUI.QuickSettings" android:layout_width="match_parent" android:layout_height="wrap_content" android:clipChildren="false" diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt index 635f0fa44234..50e5466d0325 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt @@ -12,6 +12,10 @@ class KeyguardClockFrame( ) : FrameLayout(context, attrs) { private var drawAlpha: Int = 255 + init { + setLayerType(View.LAYER_TYPE_SOFTWARE, null) + } + protected override fun onSetAlpha(alpha: Int): Boolean { drawAlpha = alpha return true diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt index 444491f53629..70b43713599b 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt @@ -71,10 +71,23 @@ interface AuthenticationRepository { */ val isBypassEnabled: StateFlow<Boolean> - /** Whether the auto confirm feature is enabled for the currently-selected user. */ + /** + * Whether the auto confirm feature is enabled for the currently-selected user. + * + * Note that the length of the PIN is also important to take into consideration, please see + * [hintedPinLength]. + */ val isAutoConfirmEnabled: StateFlow<Boolean> - /** The length of the PIN for which we should show a hint. */ + /** + * The exact length a PIN should be for us to enable PIN length hinting. + * + * A PIN that's shorter or longer than this is not eligible for the UI to render hints showing + * how many digits the current PIN is, even if [isAutoConfirmEnabled] is enabled. + * + * Note that PIN length hinting is only available if the PIN auto confirmation feature is + * available. + */ val hintedPinLength: Int /** Whether the pattern should be visible for the currently-selected user. */ @@ -166,10 +179,10 @@ constructor( .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), - initialValue = true, + initialValue = false, ) - override val hintedPinLength: Int = LockPatternUtils.MIN_AUTO_PIN_REQUIREMENT_LENGTH + override val hintedPinLength: Int = 6 override val isPatternVisible: StateFlow<Boolean> = userRepository.selectedUserInfo diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index 82674bff6433..3283e406ddb0 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -37,7 +37,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -103,10 +102,11 @@ constructor( /** The length of the hinted PIN, or `null` if pin length hint should not be shown. */ val hintedPinLength: StateFlow<Int?> = - flow { emit(repository.getPinLength()) } - .map { currentPinLength -> - // Hinting is enabled for 6-digit codes only - currentPinLength.takeIf { repository.hintedPinLength == it } + repository.isAutoConfirmEnabled + .map { isAutoConfirmEnabled -> + repository.getPinLength().takeIf { + isAutoConfirmEnabled && it == repository.hintedPinLength + } } .stateIn( scope = applicationScope, diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt index 62a484d42dec..8e14237c0586 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt @@ -88,7 +88,7 @@ constructor( /** Whether the auto confirm feature is enabled for the currently-selected user. */ val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled - /** The length of the PIN for which we should show a hint. */ + /** The length of the hinted PIN, or `null`, if pin length hint should not be shown. */ val hintedPinLength: StateFlow<Int?> = authenticationInteractor.hintedPinLength /** Whether the pattern should be visible for the currently-selected user. */ diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt index c109eb4134bb..324ef4baf7d5 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt @@ -58,6 +58,16 @@ class ToastLogger @Inject constructor( }) } + fun logOnSkipToastForInvalidDisplay(packageName: String, token: String, displayId: Int) { + log(DEBUG, { + str1 = packageName + str2 = token + int1 = displayId + }, { + "[$str2] Skip toast for [$str1] scheduled on unavailable display #$int1" + }) + } + private inline fun log( logLevel: LogLevel, initializer: LogMessage.() -> Unit, diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java index ed14c8ad150c..ae8128d20d3d 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java @@ -32,6 +32,7 @@ import android.os.IBinder; import android.os.ServiceManager; import android.os.UserHandle; import android.util.Log; +import android.view.Display; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IAccessibilityManager; import android.widget.ToastPresenter; @@ -115,8 +116,14 @@ public class ToastUI implements CoreStartable, CommandQueue.Callbacks { Context context = mContext.createContextAsUser(userHandle, 0); DisplayManager mDisplayManager = mContext.getSystemService(DisplayManager.class); - Context displayContext = context.createDisplayContext( - mDisplayManager.getDisplay(displayId)); + Display display = mDisplayManager.getDisplay(displayId); + if (display == null) { + // Display for which this toast was scheduled for is no longer available. + mToastLogger.logOnSkipToastForInvalidDisplay(packageName, token.toString(), + displayId); + return; + } + Context displayContext = context.createDisplayContext(display); mToast = mToastFactory.createToast(mContext /* sysuiContext */, text, packageName, userHandle.getIdentifier(), mOrientation); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java index 92c5014dc5f1..09d0eebd510d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -512,10 +512,11 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { assertNotNull(mirrorView); mirrorView.getBoundsOnScreen(mirrorViewBound); - assertEquals(mirrorViewBound.exactCenterX() - windowBounds.exactCenterX(), - Math.round(offsetRatio * mirrorViewBound.width() / 2), 0.1f); - assertEquals(mirrorViewBound.exactCenterY() - windowBounds.exactCenterY(), - Math.round(offsetRatio * mirrorViewBound.height() / 2), 0.1f); + assertEquals((int) (offsetRatio * mirrorViewBound.width() / 2), + (int) (mirrorViewBound.exactCenterX() - windowBounds.exactCenterX())); + assertEquals((int) (offsetRatio * mirrorViewBound.height() / 2), + (int) (mirrorViewBound.exactCenterY() - windowBounds.exactCenterY())); + } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt index c0c690892958..c223c5af6079 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -164,7 +164,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { testScope.runTest { val isThrottled by collectLastValue(underTest.isThrottled) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue() + assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isTrue() assertThat(isThrottled).isFalse() } @@ -172,7 +172,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun authenticate_withIncorrectPin_returnsFalse() = testScope.runTest { utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse() + assertThat(underTest.authenticate(listOf(9, 8, 7, 6, 5, 4))).isFalse() } @Test(expected = IllegalArgumentException::class) @@ -270,7 +270,15 @@ class AuthenticationInteractorTest : SysuiTestCase() { val isThrottled by collectLastValue(underTest.isThrottled) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setAutoConfirmEnabled(true) - assertThat(underTest.authenticate(listOf(1, 2, 3), tryAutoConfirm = true)).isNull() + assertThat( + underTest.authenticate( + FakeAuthenticationRepository.DEFAULT_PIN.toMutableList().apply { + removeLast() + }, + tryAutoConfirm = true + ) + ) + .isNull() assertThat(isThrottled).isFalse() } @@ -280,7 +288,13 @@ class AuthenticationInteractorTest : SysuiTestCase() { val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setAutoConfirmEnabled(true) - assertThat(underTest.authenticate(listOf(1, 2, 4, 4), tryAutoConfirm = true)).isFalse() + assertThat( + underTest.authenticate( + FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }, + tryAutoConfirm = true + ) + ) + .isFalse() assertThat(isUnlocked).isFalse() } @@ -290,7 +304,12 @@ class AuthenticationInteractorTest : SysuiTestCase() { val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setAutoConfirmEnabled(true) - assertThat(underTest.authenticate(listOf(1, 2, 3, 4, 5), tryAutoConfirm = true)) + assertThat( + underTest.authenticate( + FakeAuthenticationRepository.DEFAULT_PIN + listOf(7), + tryAutoConfirm = true + ) + ) .isFalse() assertThat(isUnlocked).isFalse() } @@ -301,7 +320,13 @@ class AuthenticationInteractorTest : SysuiTestCase() { val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setAutoConfirmEnabled(true) - assertThat(underTest.authenticate(listOf(1, 2, 3, 4), tryAutoConfirm = true)).isTrue() + assertThat( + underTest.authenticate( + FakeAuthenticationRepository.DEFAULT_PIN, + tryAutoConfirm = true + ) + ) + .isTrue() assertThat(isUnlocked).isTrue() } @@ -311,7 +336,13 @@ class AuthenticationInteractorTest : SysuiTestCase() { val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setAutoConfirmEnabled(false) - assertThat(underTest.authenticate(listOf(1, 2, 3, 4), tryAutoConfirm = true)).isNull() + assertThat( + underTest.authenticate( + FakeAuthenticationRepository.DEFAULT_PIN, + tryAutoConfirm = true + ) + ) + .isNull() assertThat(isUnlocked).isFalse() } @@ -334,7 +365,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { val throttling by collectLastValue(underTest.throttling) val isThrottled by collectLastValue(underTest.isThrottled) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - underTest.authenticate(listOf(1, 2, 3, 4)) + underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN) assertThat(isUnlocked).isTrue() assertThat(isThrottled).isFalse() assertThat(throttling).isEqualTo(AuthenticationThrottlingModel()) @@ -366,7 +397,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { ) // Correct PIN, but throttled, so doesn't attempt it: - assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isNull() + assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isNull() assertThat(isUnlocked).isFalse() assertThat(isThrottled).isTrue() assertThat(throttling) @@ -412,9 +443,62 @@ class AuthenticationInteractorTest : SysuiTestCase() { ) // Correct PIN and no longer throttled so unlocks successfully: - assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue() + assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isTrue() assertThat(isUnlocked).isTrue() assertThat(isThrottled).isFalse() assertThat(throttling).isEqualTo(AuthenticationThrottlingModel()) } + + @Test + fun hintedPinLength_withoutAutoConfirm_isNull() = + testScope.runTest { + val hintedPinLength by collectLastValue(underTest.hintedPinLength) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(false) + + assertThat(hintedPinLength).isNull() + } + + @Test + fun hintedPinLength_withAutoConfirmPinTooShort_isNull() = + testScope.runTest { + val hintedPinLength by collectLastValue(underTest.hintedPinLength) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.overrideCredential( + buildList { + repeat(utils.authenticationRepository.hintedPinLength - 1) { add(it + 1) } + } + ) + utils.authenticationRepository.setAutoConfirmEnabled(true) + + assertThat(hintedPinLength).isNull() + } + + @Test + fun hintedPinLength_withAutoConfirmPinAtRightLength_isSameLength() = + testScope.runTest { + val hintedPinLength by collectLastValue(underTest.hintedPinLength) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(true) + utils.authenticationRepository.overrideCredential( + buildList { repeat(utils.authenticationRepository.hintedPinLength) { add(it + 1) } } + ) + + assertThat(hintedPinLength).isEqualTo(utils.authenticationRepository.hintedPinLength) + } + + @Test + fun hintedPinLength_withAutoConfirmPinTooLong_isNull() = + testScope.runTest { + val hintedPinLength by collectLastValue(underTest.hintedPinLength) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.overrideCredential( + buildList { + repeat(utils.authenticationRepository.hintedPinLength + 1) { add(it + 1) } + } + ) + utils.authenticationRepository.setAutoConfirmEnabled(true) + + assertThat(hintedPinLength).isNull() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt index 481f36e8ea38..6babf0490ea9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt @@ -94,7 +94,7 @@ class BouncerInteractorTest : SysuiTestCase() { assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) // Correct input. - assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue() + assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isTrue() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) } @@ -118,13 +118,20 @@ class BouncerInteractorTest : SysuiTestCase() { assertThat(message).isEmpty() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) - // Wrong 4-digit pin - assertThat(underTest.authenticate(listOf(1, 2, 3, 5), tryAutoConfirm = true)).isFalse() + // Wrong 6-digit pin + assertThat(underTest.authenticate(listOf(1, 2, 3, 5, 5, 6), tryAutoConfirm = true)) + .isFalse() assertThat(message).isEqualTo(MESSAGE_WRONG_PIN) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) // Correct input. - assertThat(underTest.authenticate(listOf(1, 2, 3, 4), tryAutoConfirm = true)).isTrue() + assertThat( + underTest.authenticate( + FakeAuthenticationRepository.DEFAULT_PIN, + tryAutoConfirm = true + ) + ) + .isTrue() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) } @@ -147,7 +154,13 @@ class BouncerInteractorTest : SysuiTestCase() { assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) // Correct input. - assertThat(underTest.authenticate(listOf(1, 2, 3, 4), tryAutoConfirm = true)).isNull() + assertThat( + underTest.authenticate( + FakeAuthenticationRepository.DEFAULT_PIN, + tryAutoConfirm = true + ) + ) + .isNull() assertThat(message).isEmpty() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } @@ -309,7 +322,7 @@ class BouncerInteractorTest : SysuiTestCase() { ) // Correct PIN, but throttled, so doesn't change away from the bouncer scene: - assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isNull() + assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isNull() assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer) assertTryAgainMessage( message, @@ -339,7 +352,7 @@ class BouncerInteractorTest : SysuiTestCase() { assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer) // Correct PIN and no longer throttled so changes to the Gone scene: - assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue() + assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isTrue() assertThat(currentScene?.key).isEqualTo(SceneKey.Gone) assertThat(isThrottled).isFalse() assertThat(throttling).isEqualTo(AuthenticationThrottlingModel()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt index 0867558545bd..2cc949326fa0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.bouncer.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.scene.SceneTestUtils @@ -60,10 +61,9 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() { assertThat(animateFailure).isFalse() // Wrong PIN: - underTest.onPinButtonClicked(3) - underTest.onPinButtonClicked(4) - underTest.onPinButtonClicked(5) - underTest.onPinButtonClicked(6) + FakeAuthenticationRepository.DEFAULT_PIN.drop(2).forEach { digit -> + underTest.onPinButtonClicked(digit) + } underTest.onAuthenticateButtonClicked() assertThat(animateFailure).isTrue() @@ -71,10 +71,9 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() { assertThat(animateFailure).isFalse() // Correct PIN: - underTest.onPinButtonClicked(1) - underTest.onPinButtonClicked(2) - underTest.onPinButtonClicked(3) - underTest.onPinButtonClicked(4) + FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit -> + underTest.onPinButtonClicked(digit) + } underTest.onAuthenticateButtonClicked() assertThat(animateFailure).isFalse() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 5c6d4c69b50b..45d1af722369 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.bouncer.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.scene.SceneTestUtils @@ -211,10 +212,9 @@ class PinBouncerViewModelTest : SysuiTestCase() { ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() - underTest.onPinButtonClicked(1) - underTest.onPinButtonClicked(2) - underTest.onPinButtonClicked(3) - underTest.onPinButtonClicked(4) + FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit -> + underTest.onPinButtonClicked(digit) + } underTest.onAuthenticateButtonClicked() @@ -275,10 +275,9 @@ class PinBouncerViewModelTest : SysuiTestCase() { assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) // Enter the correct PIN: - underTest.onPinButtonClicked(1) - underTest.onPinButtonClicked(2) - underTest.onPinButtonClicked(3) - underTest.onPinButtonClicked(4) + FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit -> + underTest.onPinButtonClicked(digit) + } assertThat(message?.text).isEmpty() underTest.onAuthenticateButtonClicked() @@ -300,10 +299,9 @@ class PinBouncerViewModelTest : SysuiTestCase() { ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() - underTest.onPinButtonClicked(1) - underTest.onPinButtonClicked(2) - underTest.onPinButtonClicked(3) - underTest.onPinButtonClicked(4) + FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit -> + underTest.onPinButtonClicked(digit) + } assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) } @@ -324,10 +322,12 @@ class PinBouncerViewModelTest : SysuiTestCase() { ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() - underTest.onPinButtonClicked(1) - underTest.onPinButtonClicked(2) - underTest.onPinButtonClicked(3) - underTest.onPinButtonClicked(5) // PIN is now wrong! + FakeAuthenticationRepository.DEFAULT_PIN.dropLast(1).forEach { digit -> + underTest.onPinButtonClicked(digit) + } + underTest.onPinButtonClicked( + FakeAuthenticationRepository.DEFAULT_PIN.last() + 1 + ) // PIN is now wrong! assertThat(entries).hasSize(0) assertThat(message?.text).isEqualTo(WRONG_PIN) @@ -386,47 +386,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { assertThat(confirmButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden) } - @Test - fun hintedPinLength_withoutAutoConfirm_isNull() = - testScope.runTest { - val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.authenticationRepository.setAutoConfirmEnabled(false) - - assertThat(hintedPinLength).isNull() - } - - @Test - fun hintedPinLength_withAutoConfirmPinLessThanSixDigits_isNull() = - testScope.runTest { - val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.authenticationRepository.setAutoConfirmEnabled(true) - - assertThat(hintedPinLength).isNull() - } - - @Test - fun hintedPinLength_withAutoConfirmPinExactlySixDigits_isSix() = - testScope.runTest { - val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.authenticationRepository.setAutoConfirmEnabled(true) - utils.authenticationRepository.overrideCredential(listOf(1, 2, 3, 4, 5, 6)) - - assertThat(hintedPinLength).isEqualTo(6) - } - - @Test - fun hintedPinLength_withAutoConfirmPinMoreThanSixDigits_isNull() = - testScope.runTest { - val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.authenticationRepository.setAutoConfirmEnabled(true) - - assertThat(hintedPinLength).isNull() - } - companion object { private const val ENTER_YOUR_PIN = "Enter your pin" private const val WRONG_PIN = "Wrong pin" diff --git a/packages/SystemUI/tests/src/com/android/systemui/toast/ToastUITest.java b/packages/SystemUI/tests/src/com/android/systemui/toast/ToastUITest.java index 27957ed06d11..f299ad45a7ea 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/toast/ToastUITest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/toast/ToastUITest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.app.Application; @@ -36,6 +37,7 @@ import android.app.ITransientNotificationCallback; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.hardware.display.DisplayManager; import android.os.Binder; import android.os.Build; import android.os.Parcel; @@ -74,6 +76,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; +import java.util.Arrays; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @@ -400,6 +404,18 @@ public class ToastUITest extends SysuiTestCase { verify(mToastLogger, never()).logOnHideToast(PACKAGE_NAME_1, TOKEN_1.toString()); } + @Test + public void testShowToast_invalidDisplayId_logsAndSkipsToast() { + int invalidDisplayId = getInvalidDisplayId(); + + mToastUI.showToast(UID_1, PACKAGE_NAME_1, TOKEN_1, TEXT, WINDOW_TOKEN_1, Toast.LENGTH_LONG, + mCallback, invalidDisplayId); + + verify(mToastLogger).logOnSkipToastForInvalidDisplay(PACKAGE_NAME_1, TOKEN_1.toString(), + invalidDisplayId); + verifyZeroInteractions(mWindowManager); + } + private View verifyWmAddViewAndAttachToParent() { ArgumentCaptor<View> viewCaptor = ArgumentCaptor.forClass(View.class); verify(mWindowManager).addView(viewCaptor.capture(), any()); @@ -416,4 +432,10 @@ public class ToastUITest extends SysuiTestCase { return null; }; } + + private int getInvalidDisplayId() { + return Arrays.stream( + mContext.getSystemService(DisplayManager.class).getDisplays()) + .map(Display::getDisplayId).max(Integer::compare).get() + 1; + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt index bebdd4004c3d..28892baec3c5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt @@ -40,7 +40,7 @@ class FakeAuthenticationRepository( private val _isUnlocked = MutableStateFlow(false) override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow() - override val hintedPinLength: Int = 6 + override val hintedPinLength: Int = HINTING_PIN_LENGTH private val _isPatternVisible = MutableStateFlow(true) override val isPatternVisible: StateFlow<Boolean> = _isPatternVisible.asStateFlow() @@ -82,7 +82,7 @@ class FakeAuthenticationRepository( } override suspend fun getPinLength(): Int { - return (credentialOverride ?: listOf(1, 2, 3, 4)).size + return (credentialOverride ?: DEFAULT_PIN).size } override fun setBypassEnabled(isBypassEnabled: Boolean) { @@ -148,6 +148,15 @@ class FakeAuthenticationRepository( } } + private fun getExpectedCredential(securityMode: SecurityMode): List<Any> { + return when (val credentialType = getCurrentCredentialType(securityMode)) { + LockPatternUtils.CREDENTIAL_TYPE_PIN -> credentialOverride ?: DEFAULT_PIN + LockPatternUtils.CREDENTIAL_TYPE_PASSWORD -> "password".toList() + LockPatternUtils.CREDENTIAL_TYPE_PATTERN -> PATTERN.toCells() + else -> error("Unsupported credential type $credentialType!") + } + } + companion object { val DEFAULT_AUTHENTICATION_METHOD = AuthenticationMethodModel.Pin val PATTERN = @@ -162,6 +171,8 @@ class FakeAuthenticationRepository( ) const val MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING = 5 const val THROTTLE_DURATION_MS = 30000 + const val HINTING_PIN_LENGTH = 6 + val DEFAULT_PIN = buildList { repeat(HINTING_PIN_LENGTH) { add(it + 1) } } private fun AuthenticationMethodModel.toSecurityMode(): SecurityMode { return when (this) { @@ -188,15 +199,6 @@ class FakeAuthenticationRepository( } } - private fun getExpectedCredential(securityMode: SecurityMode): List<Any> { - return when (val credentialType = getCurrentCredentialType(securityMode)) { - LockPatternUtils.CREDENTIAL_TYPE_PIN -> listOf(1, 2, 3, 4) - LockPatternUtils.CREDENTIAL_TYPE_PASSWORD -> "password".toList() - LockPatternUtils.CREDENTIAL_TYPE_PATTERN -> PATTERN.toCells() - else -> error("Unsupported credential type $credentialType!") - } - } - private fun LockscreenCredential.matches(expectedCredential: List<Any>): Boolean { @Suppress("UNCHECKED_CAST") return when { diff --git a/services/core/java/com/android/server/display/BrightnessThrottler.java b/services/core/java/com/android/server/display/BrightnessThrottler.java index c421ec04d6f5..59844e1afd1c 100644 --- a/services/core/java/com/android/server/display/BrightnessThrottler.java +++ b/services/core/java/com/android/server/display/BrightnessThrottler.java @@ -38,17 +38,24 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel; import com.android.server.display.feature.DeviceConfigParameterProvider; +import com.android.server.display.utils.DeviceConfigParsingUtils; import java.io.PrintWriter; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; +import java.util.function.BiFunction; +import java.util.function.Function; /** * This class monitors various conditions, such as skin temperature throttling status, and limits * the allowed brightness range accordingly. + * + * @deprecated will be replaced by + * {@link com.android.server.display.brightness.clamper.BrightnessThermalClamper} */ +@Deprecated class BrightnessThrottler { private static final String TAG = "BrightnessThrottler"; private static final boolean DEBUG = false; @@ -93,8 +100,21 @@ class BrightnessThrottler { // time the underlying display device changes. // This map is indexed by uniqueDisplayId, to provide maps for throttlingId -> throttlingData. // HashMap< uniqueDisplayId, HashMap< throttlingDataId, ThermalBrightnessThrottlingData >> - private final HashMap<String, HashMap<String, ThermalBrightnessThrottlingData>> - mThermalBrightnessThrottlingDataOverride = new HashMap<>(1); + private final Map<String, Map<String, ThermalBrightnessThrottlingData>> + mThermalBrightnessThrottlingDataOverride = new HashMap<>(); + + private final BiFunction<String, String, ThrottlingLevel> mDataPointMapper = (key, value) -> { + try { + int status = DeviceConfigParsingUtils.parseThermalStatus(key); + float brightnessPoint = DeviceConfigParsingUtils.parseBrightness(value); + return new ThrottlingLevel(status, brightnessPoint); + } catch (IllegalArgumentException iae) { + return null; + } + }; + + private final Function<List<ThrottlingLevel>, ThermalBrightnessThrottlingData> + mDataSetMapper = ThermalBrightnessThrottlingData::create; BrightnessThrottler(Handler handler, Runnable throttlingChangeCallback, String uniqueDisplayId, String throttlingDataId, @@ -257,79 +277,15 @@ class BrightnessThrottler { // 456,2,moderate,0.9,critical,0.7,id_2 // displayId, number, <state, val> * number // displayId, <number, <state, val> * number>, throttlingId - private boolean parseAndAddData(@NonNull String strArray, - @NonNull HashMap<String, HashMap<String, ThermalBrightnessThrottlingData>> - displayIdToThrottlingIdToBtd) { - boolean validConfig = true; - String[] items = strArray.split(","); - int i = 0; - - try { - String uniqueDisplayId = items[i++]; - - // number of throttling points - int noOfThrottlingPoints = Integer.parseInt(items[i++]); - List<ThrottlingLevel> throttlingLevels = new ArrayList<>(noOfThrottlingPoints); - - // throttling level and point - for (int j = 0; j < noOfThrottlingPoints; j++) { - String severity = items[i++]; - int status = parseThermalStatus(severity); - float brightnessPoint = parseBrightness(items[i++]); - throttlingLevels.add(new ThrottlingLevel(status, brightnessPoint)); - } - - String throttlingDataId = (i < items.length) ? items[i++] : DEFAULT_ID; - ThermalBrightnessThrottlingData throttlingLevelsData = - DisplayDeviceConfig.ThermalBrightnessThrottlingData.create(throttlingLevels); - - // Add throttlingLevelsData to inner map where necessary. - HashMap<String, ThermalBrightnessThrottlingData> throttlingMapForDisplay = - displayIdToThrottlingIdToBtd.get(uniqueDisplayId); - if (throttlingMapForDisplay == null) { - throttlingMapForDisplay = new HashMap<>(); - throttlingMapForDisplay.put(throttlingDataId, throttlingLevelsData); - displayIdToThrottlingIdToBtd.put(uniqueDisplayId, throttlingMapForDisplay); - } else if (throttlingMapForDisplay.containsKey(throttlingDataId)) { - Slog.e(TAG, "Throttling data for display " + uniqueDisplayId - + "contains duplicate throttling ids: '" + throttlingDataId + "'"); - return false; - } else { - throttlingMapForDisplay.put(throttlingDataId, throttlingLevelsData); - } - } catch (NumberFormatException | IndexOutOfBoundsException - | UnknownThermalStatusException e) { - Slog.e(TAG, "Throttling data is invalid array: '" + strArray + "'", e); - validConfig = false; - } - - if (i != items.length) { - validConfig = false; - } - return validConfig; - } - private void loadThermalBrightnessThrottlingDataFromDeviceConfig() { - HashMap<String, HashMap<String, ThermalBrightnessThrottlingData>> tempThrottlingData = - new HashMap<>(1); mThermalBrightnessThrottlingDataString = mConfigParameterProvider.getBrightnessThrottlingData(); - boolean validConfig = true; mThermalBrightnessThrottlingDataOverride.clear(); if (mThermalBrightnessThrottlingDataString != null) { - String[] throttlingDataSplits = mThermalBrightnessThrottlingDataString.split(";"); - for (String s : throttlingDataSplits) { - if (!parseAndAddData(s, tempThrottlingData)) { - validConfig = false; - break; - } - } - - if (validConfig) { - mThermalBrightnessThrottlingDataOverride.putAll(tempThrottlingData); - tempThrottlingData.clear(); - } - + Map<String, Map<String, ThermalBrightnessThrottlingData>> tempThrottlingData = + DeviceConfigParsingUtils.parseDeviceConfigMap( + mThermalBrightnessThrottlingDataString, mDataPointMapper, mDataSetMapper); + mThermalBrightnessThrottlingDataOverride.putAll(tempThrottlingData); } else { Slog.w(TAG, "DeviceConfig ThermalBrightnessThrottlingData is null"); } @@ -395,42 +351,6 @@ class BrightnessThrottler { } } - private float parseBrightness(String intVal) throws NumberFormatException { - float value = Float.parseFloat(intVal); - if (value < PowerManager.BRIGHTNESS_MIN || value > PowerManager.BRIGHTNESS_MAX) { - throw new NumberFormatException("Brightness constraint value out of bounds."); - } - return value; - } - - @PowerManager.ThermalStatus private int parseThermalStatus(@NonNull String value) - throws UnknownThermalStatusException { - switch (value) { - case "none": - return PowerManager.THERMAL_STATUS_NONE; - case "light": - return PowerManager.THERMAL_STATUS_LIGHT; - case "moderate": - return PowerManager.THERMAL_STATUS_MODERATE; - case "severe": - return PowerManager.THERMAL_STATUS_SEVERE; - case "critical": - return PowerManager.THERMAL_STATUS_CRITICAL; - case "emergency": - return PowerManager.THERMAL_STATUS_EMERGENCY; - case "shutdown": - return PowerManager.THERMAL_STATUS_SHUTDOWN; - default: - throw new UnknownThermalStatusException("Invalid Thermal Status: " + value); - } - } - - private static class UnknownThermalStatusException extends Exception { - UnknownThermalStatusException(String message) { - super(message); - } - } - private final class SkinThermalStatusObserver extends IThermalEventListener.Stub { private final Injector mInjector; private final Handler mHandler; diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index d9cb299db69f..c25b25312c89 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -458,7 +458,7 @@ public class DisplayDeviceConfig { public static final String QUIRK_CAN_SET_BRIGHTNESS_VIA_HWC = "canSetBrightnessViaHwc"; - static final String DEFAULT_ID = "default"; + public static final String DEFAULT_ID = "default"; private static final float BRIGHTNESS_DEFAULT = 0.5f; private static final String ETC_DIR = "etc"; @@ -3127,11 +3127,15 @@ public class DisplayDeviceConfig { public static class ThermalBrightnessThrottlingData { public List<ThrottlingLevel> throttlingLevels; - static class ThrottlingLevel { + /** + * thermal status to brightness cap holder + */ + public static class ThrottlingLevel { public @PowerManager.ThermalStatus int thermalStatus; public float brightness; - ThrottlingLevel(@PowerManager.ThermalStatus int thermalStatus, float brightness) { + public ThrottlingLevel( + @PowerManager.ThermalStatus int thermalStatus, float brightness) { this.thermalStatus = thermalStatus; this.brightness = brightness; } diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java index 37b9d5638dbc..48ff598a24dc 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController2.java +++ b/services/core/java/com/android/server/display/DisplayPowerController2.java @@ -48,6 +48,7 @@ import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; import android.util.FloatProperty; +import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; import android.util.MutableFloat; @@ -64,7 +65,6 @@ import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.FrameworkStatsLog; -import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.RingBuffer; import com.android.server.LocalServices; import com.android.server.am.BatteryStatsService; @@ -73,6 +73,7 @@ import com.android.server.display.brightness.BrightnessEvent; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; import com.android.server.display.brightness.DisplayBrightnessController; +import com.android.server.display.brightness.clamper.BrightnessClamperController; import com.android.server.display.brightness.strategy.AutomaticBrightnessStrategy; import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal; import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener; @@ -380,6 +381,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal private final BrightnessThrottler mBrightnessThrottler; + private final BrightnessClamperController mBrightnessClamperController; + private final Runnable mOnBrightnessChangeRunnable; private final BrightnessEvent mLastBrightnessEvent; @@ -554,6 +557,13 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal mDisplayId, mLogicalDisplay.getDisplayInfoLocked().brightnessDefault, brightnessSetting, () -> postBrightnessChangeRunnable(), new HandlerExecutor(mHandler)); + + mBrightnessClamperController = new BrightnessClamperController(mHandler, + modeChangeCallback::run, new BrightnessClamperController.DisplayDeviceData( + mUniqueDisplayId, + mThermalBrightnessThrottlingDataId, + mDisplayDeviceConfig + )); // Seed the cached brightness saveBrightnessInfo(getScreenBrightnessSetting()); @@ -782,6 +792,10 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal final String thermalBrightnessThrottlingDataId = mLogicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId; + mBrightnessClamperController.onDisplayChanged( + new BrightnessClamperController.DisplayDeviceData(mUniqueDisplayId, + mThermalBrightnessThrottlingDataId, config)); + mHandler.postAtTime(() -> { boolean changed = false; if (mDisplayDevice != device) { @@ -1187,6 +1201,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal mDisplayPowerProximityStateController.cleanup(); mBrightnessRangeController.stop(); mBrightnessThrottler.stop(); + mBrightnessClamperController.stop(); mHandler.removeCallbacksAndMessages(null); // Release any outstanding wakelocks we're still holding because of pending messages. @@ -1519,6 +1534,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // allowed range. float animateValue = clampScreenBrightness(brightnessState); + animateValue = mBrightnessClamperController.clamp(animateValue); + // If there are any HDR layers on the screen, we have a special brightness value that we // use instead. We still preserve the calculated brightness for Standard Dynamic Range // (SDR) layers, but the main brightness value will be the one for HDR. @@ -2412,6 +2429,11 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal if (mDisplayStateController != null) { mDisplayStateController.dumpsys(pw); } + + pw.println(); + if (mBrightnessClamperController != null) { + mBrightnessClamperController.dump(ipw); + } } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java new file mode 100644 index 000000000000..9345a3d97122 --- /dev/null +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2023 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.brightness.clamper; + +import android.annotation.NonNull; +import android.os.PowerManager; + +import java.io.PrintWriter; + +abstract class BrightnessClamper<T> { + + protected float mBrightnessCap = PowerManager.BRIGHTNESS_MAX; + protected boolean mIsActive = false; + + float getBrightnessCap() { + return mBrightnessCap; + } + + boolean isActive() { + return mIsActive; + } + + void dump(PrintWriter writer) { + writer.println("BrightnessClamper:" + getType()); + writer.println(" mBrightnessCap: " + mBrightnessCap); + writer.println(" mIsActive: " + mIsActive); + } + + @NonNull + abstract Type getType(); + + abstract void onDeviceConfigChanged(); + + abstract void onDisplayChanged(T displayData); + + abstract void stop(); + + enum Type { + THERMAL + } +} diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java new file mode 100644 index 000000000000..d0f28c3bea81 --- /dev/null +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2023 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.brightness.clamper; + +import static com.android.server.display.brightness.clamper.BrightnessClamper.Type; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.PowerManager; +import android.provider.DeviceConfig; +import android.provider.DeviceConfigInterface; +import android.util.IndentingPrintWriter; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.DisplayDeviceConfig; +import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData; +import com.android.server.display.feature.DeviceConfigParameterProvider; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Clampers controller, all in DisplayControllerHandler + */ +public class BrightnessClamperController { + + private static final boolean ENABLED = false; + + private final DeviceConfigParameterProvider mDeviceConfigParameterProvider; + private final Handler mHandler; + private final ClamperChangeListener mClamperChangeListenerExternal; + + private final Executor mExecutor; + private final List<BrightnessClamper<? super DisplayDeviceData>> mClampers = new ArrayList<>(); + private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener = + properties -> mClampers.forEach(BrightnessClamper::onDeviceConfigChanged); + private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX; + @Nullable + private Type mClamperType = null; + + public BrightnessClamperController(Handler handler, + ClamperChangeListener clamperChangeListener, DisplayDeviceData data) { + this(new Injector(), handler, clamperChangeListener, data); + } + + @VisibleForTesting + BrightnessClamperController(Injector injector, Handler handler, + ClamperChangeListener clamperChangeListener, DisplayDeviceData data) { + mDeviceConfigParameterProvider = injector.getDeviceConfigParameterProvider(); + mHandler = handler; + mClamperChangeListenerExternal = clamperChangeListener; + mExecutor = new HandlerExecutor(handler); + + Runnable clamperChangeRunnableInternal = this::recalculateBrightnessCap; + + ClamperChangeListener clamperChangeListenerInternal = () -> { + if (!mHandler.hasCallbacks(clamperChangeRunnableInternal)) { + mHandler.post(clamperChangeRunnableInternal); + } + }; + + if (ENABLED) { + mClampers.add( + new BrightnessThermalClamper(handler, clamperChangeListenerInternal, data)); + start(); + } + } + + /** + * Should be called when display changed. Forwards the call to individual clampers + */ + public void onDisplayChanged(DisplayDeviceData data) { + mClampers.forEach(clamper -> clamper.onDisplayChanged(data)); + } + + /** + * Applies clamping + * Called in DisplayControllerHandler + */ + public float clamp(float value) { + return Math.min(value, mBrightnessCap); + } + + /** + * Used to dump ClampersController state. + */ + public void dump(PrintWriter writer) { + writer.println("BrightnessClampersController:"); + writer.println(" mBrightnessCap: " + mBrightnessCap); + writer.println(" mClamperType: " + mClamperType); + IndentingPrintWriter ipw = new IndentingPrintWriter(writer, " "); + mClampers.forEach(clamper -> clamper.dump(ipw)); + } + + /** + * This method should be called when the ClamperController is no longer in use. + * Called in DisplayControllerHandler + */ + public void stop() { + mDeviceConfigParameterProvider.removeOnPropertiesChangedListener( + mOnPropertiesChangedListener); + mClampers.forEach(BrightnessClamper::stop); + } + + + // Called in DisplayControllerHandler + private void recalculateBrightnessCap() { + float brightnessCap = PowerManager.BRIGHTNESS_MAX; + Type clamperType = null; + + BrightnessClamper<?> minClamper = mClampers.stream() + .filter(BrightnessClamper::isActive) + .min((clamper1, clamper2) -> Float.compare(clamper1.getBrightnessCap(), + clamper2.getBrightnessCap())).orElse(null); + + if (minClamper != null) { + brightnessCap = minClamper.getBrightnessCap(); + clamperType = minClamper.getType(); + } + + if (mBrightnessCap != brightnessCap || mClamperType != clamperType) { + mBrightnessCap = brightnessCap; + mClamperType = clamperType; + mClamperChangeListenerExternal.onChanged(); + } + } + + private void start() { + mDeviceConfigParameterProvider.addOnPropertiesChangedListener( + mExecutor, mOnPropertiesChangedListener); + } + + /** + * Clampers change listener + */ + public interface ClamperChangeListener { + /** + * Notifies that clamper state changed + */ + void onChanged(); + } + + @VisibleForTesting + static class Injector { + DeviceConfigParameterProvider getDeviceConfigParameterProvider() { + return new DeviceConfigParameterProvider(DeviceConfigInterface.REAL); + } + } + + /** + * Data for clampers + */ + public static class DisplayDeviceData implements BrightnessThermalClamper.ThermalData { + @NonNull + private final String mUniqueDisplayId; + @NonNull + private final String mThermalThrottlingDataId; + + private final DisplayDeviceConfig mDisplayDeviceConfig; + + public DisplayDeviceData(@NonNull String uniqueDisplayId, + @NonNull String thermalThrottlingDataId, + @NonNull DisplayDeviceConfig displayDeviceConfig) { + mUniqueDisplayId = uniqueDisplayId; + mThermalThrottlingDataId = thermalThrottlingDataId; + mDisplayDeviceConfig = displayDeviceConfig; + } + + + @NonNull + @Override + public String getUniqueDisplayId() { + return mUniqueDisplayId; + } + + @NonNull + @Override + public String getThermalThrottlingDataId() { + return mThermalThrottlingDataId; + } + + @Nullable + @Override + public ThermalBrightnessThrottlingData getThermalBrightnessThrottlingData() { + return mDisplayDeviceConfig.getThermalBrightnessThrottlingDataMapByThrottlingId().get( + mThermalThrottlingDataId); + } + } +} diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessThermalClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessThermalClamper.java new file mode 100644 index 000000000000..8ae962b83c69 --- /dev/null +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessThermalClamper.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2023 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.brightness.clamper; + +import static com.android.server.display.DisplayDeviceConfig.DEFAULT_ID; +import static com.android.server.display.brightness.clamper.BrightnessClamperController.ClamperChangeListener; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.os.Handler; +import android.os.IThermalEventListener; +import android.os.IThermalService; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.Temperature; +import android.provider.DeviceConfigInterface; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData; +import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel; +import com.android.server.display.feature.DeviceConfigParameterProvider; +import com.android.server.display.utils.DeviceConfigParsingUtils; + +import java.io.PrintWriter; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + + +class BrightnessThermalClamper extends + BrightnessClamper<BrightnessThermalClamper.ThermalData> { + + private static final String TAG = "BrightnessThermalClamper"; + + @Nullable + private final IThermalService mThermalService; + @NonNull + private final DeviceConfigParameterProvider mConfigParameterProvider; + @NonNull + private final Handler mHandler; + @NonNull + private final ClamperChangeListener mChangelistener; + // data from DeviceConfig, for all displays, for all dataSets + // mapOf(uniqueDisplayId to mapOf(dataSetId to ThermalBrightnessThrottlingData)) + @NonNull + private Map<String, Map<String, ThermalBrightnessThrottlingData>> + mThermalThrottlingDataOverride = Map.of(); + // data from DisplayDeviceConfig, for particular display+dataSet + @Nullable + private ThermalBrightnessThrottlingData mThermalThrottlingDataFromDeviceConfig = null; + // Active data, if mDataOverride contains data for mUniqueDisplayId, mDataId, then use it, + // otherwise mDataFromDeviceConfig + @Nullable + private ThermalBrightnessThrottlingData mThermalThrottlingDataActive = null; + private boolean mStarted = false; + @Nullable + private String mUniqueDisplayId = null; + @Nullable + private String mDataId = null; + @Temperature.ThrottlingStatus + private int mThrottlingStatus = Temperature.THROTTLING_NONE; + + private final IThermalEventListener mThermalEventListener = new IThermalEventListener.Stub() { + @Override + public void notifyThrottling(Temperature temperature) { + @Temperature.ThrottlingStatus int status = temperature.getStatus(); + mHandler.post(() -> thermalStatusChanged(status)); + } + }; + + private final BiFunction<String, String, ThrottlingLevel> mDataPointMapper = (key, value) -> { + try { + int status = DeviceConfigParsingUtils.parseThermalStatus(key); + float brightnessPoint = DeviceConfigParsingUtils.parseBrightness(value); + return new ThrottlingLevel(status, brightnessPoint); + } catch (IllegalArgumentException iae) { + return null; + } + }; + + private final Function<List<ThrottlingLevel>, ThermalBrightnessThrottlingData> + mDataSetMapper = ThermalBrightnessThrottlingData::create; + + + BrightnessThermalClamper(Handler handler, ClamperChangeListener listener, + ThermalData thermalData) { + this(new Injector(), handler, listener, thermalData); + } + + @VisibleForTesting + BrightnessThermalClamper(Injector injector, Handler handler, + ClamperChangeListener listener, ThermalData thermalData) { + mThermalService = injector.getThermalService(); + mConfigParameterProvider = injector.getDeviceConfigParameterProvider(); + mHandler = handler; + mChangelistener = listener; + mHandler.post(() -> { + setDisplayData(thermalData); + loadOverrideData(); + start(); + }); + + } + + @Override + @NonNull + Type getType() { + return Type.THERMAL; + } + + @Override + void onDeviceConfigChanged() { + mHandler.post(() -> { + loadOverrideData(); + recalculateActiveData(); + }); + } + + @Override + void onDisplayChanged(ThermalData data) { + mHandler.post(() -> { + setDisplayData(data); + recalculateActiveData(); + }); + } + + @Override + void stop() { + if (!mStarted) { + return; + } + try { + mThermalService.unregisterThermalEventListener(mThermalEventListener); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to unregister thermal status listener", e); + } + mStarted = false; + } + + @Override + void dump(PrintWriter writer) { + writer.println("BrightnessThermalClamper:"); + writer.println(" mStarted: " + mStarted); + if (mThermalService != null) { + writer.println(" ThermalService available"); + } else { + writer.println(" ThermalService not available"); + } + writer.println(" mThrottlingStatus: " + mThrottlingStatus); + writer.println(" mUniqueDisplayId: " + mUniqueDisplayId); + writer.println(" mDataId: " + mDataId); + writer.println(" mDataOverride: " + mThermalThrottlingDataOverride); + writer.println(" mDataFromDeviceConfig: " + mThermalThrottlingDataFromDeviceConfig); + writer.println(" mDataActive: " + mThermalThrottlingDataActive); + super.dump(writer); + } + + private void recalculateActiveData() { + if (mUniqueDisplayId == null || mDataId == null) { + return; + } + mThermalThrottlingDataActive = mThermalThrottlingDataOverride + .getOrDefault(mUniqueDisplayId, Map.of()).getOrDefault(mDataId, + mThermalThrottlingDataFromDeviceConfig); + + recalculateBrightnessCap(); + } + + private void loadOverrideData() { + String throttlingDataOverride = mConfigParameterProvider.getBrightnessThrottlingData(); + mThermalThrottlingDataOverride = DeviceConfigParsingUtils.parseDeviceConfigMap( + throttlingDataOverride, mDataPointMapper, mDataSetMapper); + } + + private void setDisplayData(@NonNull ThermalData data) { + mUniqueDisplayId = data.getUniqueDisplayId(); + mDataId = data.getThermalThrottlingDataId(); + mThermalThrottlingDataFromDeviceConfig = data.getThermalBrightnessThrottlingData(); + if (mThermalThrottlingDataFromDeviceConfig == null && !DEFAULT_ID.equals(mDataId)) { + Slog.wtf(TAG, + "Thermal throttling data is missing for thermalThrottlingDataId=" + mDataId); + } + } + + private void recalculateBrightnessCap() { + float brightnessCap = PowerManager.BRIGHTNESS_MAX; + boolean isActive = false; + + if (mThermalThrottlingDataActive != null) { + // Throttling levels are sorted by increasing severity + for (ThrottlingLevel level : mThermalThrottlingDataActive.throttlingLevels) { + if (level.thermalStatus <= mThrottlingStatus) { + brightnessCap = level.brightness; + isActive = true; + } else { + // Throttling levels that are greater than the current status are irrelevant + break; + } + } + } + + if (brightnessCap != mBrightnessCap || mIsActive != isActive) { + mBrightnessCap = brightnessCap; + mIsActive = isActive; + mChangelistener.onChanged(); + } + } + + private void thermalStatusChanged(@Temperature.ThrottlingStatus int status) { + if (mThrottlingStatus != status) { + mThrottlingStatus = status; + recalculateBrightnessCap(); + } + } + + private void start() { + if (mThermalService == null) { + Slog.e(TAG, "Could not observe thermal status. Service not available"); + return; + } + try { + // We get a callback immediately upon registering so there's no need to query + // for the current value. + mThermalService.registerThermalEventListenerWithType(mThermalEventListener, + Temperature.TYPE_SKIN); + mStarted = true; + } catch (RemoteException e) { + Slog.e(TAG, "Failed to register thermal status listener", e); + } + } + + interface ThermalData { + @NonNull + String getUniqueDisplayId(); + + @NonNull + String getThermalThrottlingDataId(); + + @Nullable + ThermalBrightnessThrottlingData getThermalBrightnessThrottlingData(); + } + + @VisibleForTesting + static class Injector { + IThermalService getThermalService() { + return IThermalService.Stub.asInterface( + ServiceManager.getService(Context.THERMAL_SERVICE)); + } + + DeviceConfigParameterProvider getDeviceConfigParameterProvider() { + return new DeviceConfigParameterProvider(DeviceConfigInterface.REAL); + } + } +} diff --git a/services/core/java/com/android/server/display/utils/DeviceConfigParsingUtils.java b/services/core/java/com/android/server/display/utils/DeviceConfigParsingUtils.java new file mode 100644 index 000000000000..a8034c58689d --- /dev/null +++ b/services/core/java/com/android/server/display/utils/DeviceConfigParsingUtils.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2023 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.utils; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.PowerManager; +import android.util.Slog; + +import com.android.server.display.DisplayDeviceConfig; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Provides utility methods for DeviceConfig string parsing + */ +public class DeviceConfigParsingUtils { + private static final String TAG = "DeviceConfigParsingUtils"; + + /** + * Parses map from device config + * Data format: + * (displayId:String,numberOfPoints:Int,(state:T,value:Float){numberOfPoints}, + * dataSetId:String)?;)+ + * result : mapOf(displayId to mapOf(dataSetId to V)) + */ + @NonNull + public static <T, V> Map<String, Map<String, V>> parseDeviceConfigMap( + @Nullable String data, + @NonNull BiFunction<String, String, T> dataPointMapper, + @NonNull Function<List<T>, V> dataSetMapper) { + if (data == null) { + return Map.of(); + } + Map<String, Map<String, V>> result = new HashMap<>(); + String[] dataSets = data.split(";"); // by displayId + dataSetId + for (String dataSet : dataSets) { + String[] items = dataSet.split(","); + int noOfItems = items.length; + // Validate number of items, at least: displayId,1,key1,value1 + if (noOfItems < 4) { + Slog.e(TAG, "Invalid dataSet(not enough items):" + dataSet, new Throwable()); + return Map.of(); + } + int i = 0; + String uniqueDisplayId = items[i++]; + + String numberOfPointsString = items[i++]; + int numberOfPoints; + try { + numberOfPoints = Integer.parseInt(numberOfPointsString); + } catch (NumberFormatException nfe) { + Slog.e(TAG, "Invalid dataSet(invalid number of points):" + dataSet, nfe); + return Map.of(); + } + // Validate number of itmes based on numberOfPoints: + // displayId,numberOfPoints,(key,value) x numberOfPoints,dataSetId(optional) + int expectedMinItems = 2 + numberOfPoints * 2; + if (noOfItems < expectedMinItems || noOfItems > expectedMinItems + 1) { + Slog.e(TAG, "Invalid dataSet(wrong number of points):" + dataSet, new Throwable()); + return Map.of(); + } + // Construct data points + List<T> dataPoints = new ArrayList<>(); + for (int j = 0; j < numberOfPoints; j++) { + String key = items[i++]; + String value = items[i++]; + T dataPoint = dataPointMapper.apply(key, value); + if (dataPoint == null) { + Slog.e(TAG, + "Invalid dataPoint ,key=" + key + ",value=" + value + ",dataSet=" + + dataSet, new Throwable()); + return Map.of(); + } + dataPoints.add(dataPoint); + } + // Construct dataSet + V dataSetMapped = dataSetMapper.apply(dataPoints); + if (dataSetMapped == null) { + Slog.e(TAG, "Invalid dataSetMapped dataPoints=" + dataPoints + ",dataSet=" + + dataSet, new Throwable()); + return Map.of(); + } + // Get dataSetId and dataSets map for displayId + String dataSetId = (i < items.length) ? items[i] : DisplayDeviceConfig.DEFAULT_ID; + Map<String, V> byDisplayId = result.computeIfAbsent(uniqueDisplayId, + k -> new HashMap<>()); + + // Try to store dataSet in datasets for display + if (byDisplayId.put(dataSetId, dataSetMapped) != null) { + Slog.e(TAG, "Duplicate dataSetId=" + dataSetId + ",data=" + data, new Throwable()); + return Map.of(); + } + } + return result; + } + + /** + * Parses thermal string value from device config + */ + @PowerManager.ThermalStatus + public static int parseThermalStatus(@NonNull String value) throws IllegalArgumentException { + switch (value) { + case "none": + return PowerManager.THERMAL_STATUS_NONE; + case "light": + return PowerManager.THERMAL_STATUS_LIGHT; + case "moderate": + return PowerManager.THERMAL_STATUS_MODERATE; + case "severe": + return PowerManager.THERMAL_STATUS_SEVERE; + case "critical": + return PowerManager.THERMAL_STATUS_CRITICAL; + case "emergency": + return PowerManager.THERMAL_STATUS_EMERGENCY; + case "shutdown": + return PowerManager.THERMAL_STATUS_SHUTDOWN; + default: + throw new IllegalArgumentException("Invalid Thermal Status: " + value); + } + } + + /** + * Parses brightness value from device config + */ + public static float parseBrightness(String stringVal) throws IllegalArgumentException { + float value = Float.parseFloat(stringVal); + if (value < PowerManager.BRIGHTNESS_MIN || value > PowerManager.BRIGHTNESS_MAX) { + throw new IllegalArgumentException("Brightness value out of bounds: " + stringVal); + } + return value; + } +} diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 455e7ae8ee45..0a62c881e251 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1594,6 +1594,9 @@ class ActivityStarter { if (forceTransientTransition) { transitionController.collect(mLastStartActivityRecord); transitionController.collect(mPriorAboveTask); + // If keyguard is active and occluded, the transient target won't be moved to front + // to be collected, so set transient again after it is collected. + transitionController.setTransientLaunch(mLastStartActivityRecord, mPriorAboveTask); final DisplayContent dc = mLastStartActivityRecord.getDisplayContent(); // update wallpaper target to TransientHide dc.mWallpaperController.adjustWallpaperWindows(); diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 738797b809a5..50948e1cdec4 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2186,7 +2186,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { * Processes the activities to be stopped or destroyed. This should be called when the resumed * activities are idle or drawn. */ - private void processStoppingAndFinishingActivities(ActivityRecord launchedActivity, + void processStoppingAndFinishingActivities(ActivityRecord launchedActivity, boolean processPausingActivities, String reason) { // Stop any activities that are scheduled to do so but have been waiting for the transition // animation to finish. @@ -2194,7 +2194,10 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { ArrayList<ActivityRecord> readyToStopActivities = null; for (int i = 0; i < mStoppingActivities.size(); i++) { final ActivityRecord s = mStoppingActivities.get(i); - final boolean animating = s.isInTransition(); + // Activity in a force hidden task should not be counted as animating, i.e., we want to + // send onStop before any configuration change when removing pip transition is ongoing. + final boolean animating = s.isInTransition() + && s.getTask() != null && !s.getTask().isForceHidden(); displaySwapping |= s.isDisplaySleepingAndSwapping(); ProtoLog.v(WM_DEBUG_STATES, "Stopping %s: nowVisible=%b animating=%b " + "finishing=%s", s, s.nowVisible, animating, s.finishing); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index f48c5047f2b3..0eb452d29736 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -569,6 +569,13 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } if (forceHiddenForPip) { wc.asTask().setForceHidden(FLAG_FORCE_HIDDEN_FOR_PINNED_TASK, true /* set */); + // When removing pip, make sure that onStop is sent to the app ahead of + // onPictureInPictureModeChanged. + // See also PinnedStackTests#testStopBeforeMultiWindowCallbacksOnDismiss + wc.asTask().ensureActivitiesVisible(null, 0, PRESERVE_WINDOWS); + wc.asTask().mTaskSupervisor.processStoppingAndFinishingActivities( + null /* launchedActivity */, false /* processPausingActivities */, + "force-stop-on-removing-pip"); } int containerEffect = applyWindowContainerChange(wc, entry.getValue(), diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java new file mode 100644 index 000000000000..37d0f6250aaf --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2023 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.brightness.clamper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.hardware.display.DisplayManager; +import android.os.IThermalEventListener; +import android.os.IThermalService; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.Temperature; +import android.provider.DeviceConfig; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.Keep; +import com.android.server.display.DisplayDeviceConfig; +import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData; +import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel; +import com.android.server.display.feature.DeviceConfigParameterProvider; +import com.android.server.testutils.FakeDeviceConfigInterface; +import com.android.server.testutils.TestHandler; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + +@RunWith(JUnitParamsRunner.class) +public class BrightnessThermalClamperTest { + + private static final float FLOAT_TOLERANCE = 0.001f; + + private static final String DISPLAY_ID = "displayId"; + @Mock + private IThermalService mMockThermalService; + @Mock + private BrightnessClamperController.ClamperChangeListener mMockClamperChangeListener; + + private final FakeDeviceConfigInterface mFakeDeviceConfigInterface = + new FakeDeviceConfigInterface(); + private final TestHandler mTestHandler = new TestHandler(null); + private BrightnessThermalClamper mClamper; + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mClamper = new BrightnessThermalClamper(new TestInjector(), mTestHandler, + mMockClamperChangeListener, new TestThermalData()); + mTestHandler.flush(); + } + + @Test + public void testTypeIsThermal() { + assertEquals(BrightnessClamper.Type.THERMAL, mClamper.getType()); + } + + @Test + public void testNoThrottlingData() { + assertFalse(mClamper.isActive()); + assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + } + + @Keep + private static Object[][] testThrottlingData() { + // throttlingLevels, throttlingStatus, expectedActive, expectedBrightness + return new Object[][] { + // no throttling data + {List.of(), Temperature.THROTTLING_LIGHT, false, PowerManager.BRIGHTNESS_MAX}, + // throttlingStatus < min throttling data + {List.of( + new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE, 0.5f), + new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.1f)), + Temperature.THROTTLING_LIGHT, false, PowerManager.BRIGHTNESS_MAX}, + // throttlingStatus = min throttling data + {List.of( + new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE, 0.5f), + new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.1f)), + Temperature.THROTTLING_MODERATE, true, 0.5f}, + // throttlingStatus between min and max throttling data + {List.of( + new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE, 0.5f), + new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.1f)), + Temperature.THROTTLING_SEVERE, true, 0.5f}, + // throttlingStatus = max throttling data + {List.of( + new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE, 0.5f), + new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.1f)), + Temperature.THROTTLING_CRITICAL, true, 0.1f}, + // throttlingStatus > max throttling data + {List.of( + new ThrottlingLevel(PowerManager.THERMAL_STATUS_MODERATE, 0.5f), + new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.1f)), + Temperature.THROTTLING_EMERGENCY, true, 0.1f}, + }; + } + @Test + @Parameters(method = "testThrottlingData") + public void testNotifyThrottlingAfterOnDisplayChange(List<ThrottlingLevel> throttlingLevels, + @Temperature.ThrottlingStatus int throttlingStatus, + boolean expectedActive, float expectedBrightness) throws RemoteException { + IThermalEventListener thermalEventListener = captureThermalEventListener(); + mClamper.onDisplayChanged(new TestThermalData(throttlingLevels)); + mTestHandler.flush(); + assertFalse(mClamper.isActive()); + assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + + thermalEventListener.notifyThrottling(createTemperature(throttlingStatus)); + mTestHandler.flush(); + assertEquals(expectedActive, mClamper.isActive()); + assertEquals(expectedBrightness, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + } + + @Test + @Parameters(method = "testThrottlingData") + public void testOnDisplayChangeAfterNotifyThrottlng(List<ThrottlingLevel> throttlingLevels, + @Temperature.ThrottlingStatus int throttlingStatus, + boolean expectedActive, float expectedBrightness) throws RemoteException { + IThermalEventListener thermalEventListener = captureThermalEventListener(); + thermalEventListener.notifyThrottling(createTemperature(throttlingStatus)); + mTestHandler.flush(); + assertFalse(mClamper.isActive()); + assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + + mClamper.onDisplayChanged(new TestThermalData(throttlingLevels)); + mTestHandler.flush(); + assertEquals(expectedActive, mClamper.isActive()); + assertEquals(expectedBrightness, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + } + + @Test + public void testOverrideData() throws RemoteException { + IThermalEventListener thermalEventListener = captureThermalEventListener(); + thermalEventListener.notifyThrottling(createTemperature(Temperature.THROTTLING_SEVERE)); + mTestHandler.flush(); + assertFalse(mClamper.isActive()); + assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + + mClamper.onDisplayChanged(new TestThermalData( + List.of(new ThrottlingLevel(PowerManager.THERMAL_STATUS_SEVERE, 0.5f)))); + mTestHandler.flush(); + assertTrue(mClamper.isActive()); + assertEquals(0.5f, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + + overrideThrottlingData("displayId,1,emergency,0.4"); + mClamper.onDeviceConfigChanged(); + mTestHandler.flush(); + + assertFalse(mClamper.isActive()); + assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + + overrideThrottlingData("displayId,1,moderate,0.4"); + mClamper.onDeviceConfigChanged(); + mTestHandler.flush(); + + assertTrue(mClamper.isActive()); + assertEquals(0.4f, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + } + + private IThermalEventListener captureThermalEventListener() throws RemoteException { + ArgumentCaptor<IThermalEventListener> captor = ArgumentCaptor.forClass( + IThermalEventListener.class); + verify(mMockThermalService).registerThermalEventListenerWithType(captor.capture(), eq( + Temperature.TYPE_SKIN)); + return captor.getValue(); + } + + private Temperature createTemperature(@Temperature.ThrottlingStatus int status) { + return new Temperature(100, Temperature.TYPE_SKIN, "test_temperature", status); + } + + private void overrideThrottlingData(String data) { + mFakeDeviceConfigInterface.putProperty(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + DisplayManager.DeviceConfig.KEY_BRIGHTNESS_THROTTLING_DATA, data); + } + + private class TestInjector extends BrightnessThermalClamper.Injector { + @Override + IThermalService getThermalService() { + return mMockThermalService; + } + + @Override + DeviceConfigParameterProvider getDeviceConfigParameterProvider() { + return new DeviceConfigParameterProvider(mFakeDeviceConfigInterface); + } + } + + private static class TestThermalData implements BrightnessThermalClamper.ThermalData { + + private final String mUniqueDisplayId; + private final String mDataId; + private final ThermalBrightnessThrottlingData mData; + + private TestThermalData() { + this(DISPLAY_ID, DisplayDeviceConfig.DEFAULT_ID, null); + } + + private TestThermalData(List<ThrottlingLevel> data) { + this(DISPLAY_ID, DisplayDeviceConfig.DEFAULT_ID, data); + } + private TestThermalData(String uniqueDisplayId, String dataId, List<ThrottlingLevel> data) { + mUniqueDisplayId = uniqueDisplayId; + mDataId = dataId; + mData = ThermalBrightnessThrottlingData.create(data); + } + @NonNull + @Override + public String getUniqueDisplayId() { + return mUniqueDisplayId; + } + + @NonNull + @Override + public String getThermalThrottlingDataId() { + return mDataId; + } + + @Nullable + @Override + public ThermalBrightnessThrottlingData getThermalBrightnessThrottlingData() { + return mData; + } + } +} diff --git a/services/tests/displayservicetests/src/com/android/server/display/utils/DeviceConfigParsingUtilsTest.java b/services/tests/displayservicetests/src/com/android/server/display/utils/DeviceConfigParsingUtilsTest.java new file mode 100644 index 000000000000..5e28e63cfa49 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/utils/DeviceConfigParsingUtilsTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2023 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.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import android.os.PowerManager; +import android.util.Pair; + +import androidx.test.filters.SmallTest; + +import com.android.internal.annotations.Keep; +import com.android.server.display.DisplayDeviceConfig; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + +@SmallTest +@RunWith(JUnitParamsRunner.class) +public class DeviceConfigParsingUtilsTest { + private static final String VALID_DATA_STRING = "display1,1,key1,value1"; + private static final float FLOAT_TOLERANCE = 0.001f; + + private final BiFunction<String, String, Pair<String, String>> mDataPointToPair = Pair::create; + private final Function<List<Pair<String, String>>, List<Pair<String, String>>> + mDataSetIdentity = (dataSet) -> dataSet; + + @Keep + private static Object[][] parseDeviceConfigMapData() { + // dataString, expectedMap + return new Object[][]{ + // null + {null, Map.of()}, + // empty string + {"", Map.of()}, + // 1 display, 1 incomplete data point + {"display1,1,key1", Map.of()}, + // 1 display,2 data points required, only 1 present + {"display1,2,key1,value1", Map.of()}, + // 1 display, 1 data point, dataSetId and some extra data + {"display1,1,key1,value1,setId1,extraData", Map.of()}, + // 1 display, random string instead of number of data points + {"display1,one,key1,value1", Map.of()}, + // 1 display, 1 data point no dataSetId + {VALID_DATA_STRING, Map.of("display1", Map.of(DisplayDeviceConfig.DEFAULT_ID, + List.of(Pair.create("key1", "value1"))))}, + // 1 display, 1 data point, dataSetId + {"display1,1,key1,value1,setId1", Map.of("display1", Map.of("setId1", + List.of(Pair.create("key1", "value1"))))}, + // 1 display, 2 data point, dataSetId + {"display1,2,key1,value1,key2,value2,setId1", Map.of("display1", Map.of("setId1", + List.of(Pair.create("key1", "value1"), Pair.create("key2", "value2"))))}, + }; + } + + @Test + @Parameters(method = "parseDeviceConfigMapData") + public void testParseDeviceConfigMap(String dataString, + Map<String, Map<String, List<Pair<String, String>>>> expectedMap) { + Map<String, Map<String, List<Pair<String, String>>>> result = + DeviceConfigParsingUtils.parseDeviceConfigMap(dataString, mDataPointToPair, + mDataSetIdentity); + + assertEquals(expectedMap, result); + } + + @Test + public void testDataPointMapperReturnsNull() { + Map<String, Map<String, List<Pair<String, String>>>> result = + DeviceConfigParsingUtils.parseDeviceConfigMap(VALID_DATA_STRING, (s1, s2) -> null, + mDataSetIdentity); + + assertEquals(Map.of(), result); + } + + @Test + public void testDataSetMapperReturnsNull() { + Map<String, Map<String, List<Pair<String, String>>>> result = + DeviceConfigParsingUtils.parseDeviceConfigMap(VALID_DATA_STRING, mDataPointToPair, + (dataSet) -> null); + + assertEquals(Map.of(), result); + } + + @Keep + private static Object[][] parseThermalStatusData() { + // thermalStatusString, expectedThermalStatus + return new Object[][]{ + {"none", PowerManager.THERMAL_STATUS_NONE}, + {"light", PowerManager.THERMAL_STATUS_LIGHT}, + {"moderate", PowerManager.THERMAL_STATUS_MODERATE}, + {"severe", PowerManager.THERMAL_STATUS_SEVERE}, + {"critical", PowerManager.THERMAL_STATUS_CRITICAL}, + {"emergency", PowerManager.THERMAL_STATUS_EMERGENCY}, + {"shutdown", PowerManager.THERMAL_STATUS_SHUTDOWN}, + }; + } + + @Test + @Parameters(method = "parseThermalStatusData") + public void testParseThermalStatus(String thermalStatusString, + @PowerManager.ThermalStatus int expectedThermalStatus) { + int result = DeviceConfigParsingUtils.parseThermalStatus(thermalStatusString); + + assertEquals(expectedThermalStatus, result); + } + + @Test + public void testParseThermalStatus_illegalStatus() { + Throwable result = assertThrows(IllegalArgumentException.class, + () -> DeviceConfigParsingUtils.parseThermalStatus("invalid_status")); + + assertEquals("Invalid Thermal Status: invalid_status", result.getMessage()); + } + + @Test + public void testParseBrightness() { + float result = DeviceConfigParsingUtils.parseBrightness("0.65"); + + assertEquals(0.65, result, FLOAT_TOLERANCE); + } + + @Test + public void testParseBrightness_lessThanMin() { + Throwable result = assertThrows(IllegalArgumentException.class, + () -> DeviceConfigParsingUtils.parseBrightness("-0.65")); + + assertEquals("Brightness value out of bounds: -0.65", result.getMessage()); + } + + @Test + public void testParseBrightness_moreThanMax() { + Throwable result = assertThrows(IllegalArgumentException.class, + () -> DeviceConfigParsingUtils.parseBrightness("1.65")); + + assertEquals("Brightness value out of bounds: 1.65", result.getMessage()); + } +} diff --git a/services/tests/displayservicetests/src/com/android/server/display/SensorUtilsTest.java b/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java index 4494b0c412dc..4494b0c412dc 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/SensorUtilsTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java |