diff options
16 files changed, 431 insertions, 53 deletions
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 63a2474e70c1..ed1d434257df 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -14,3 +14,10 @@ flag { description: "Enables desktop windowing" bug: "304778354" } + +flag { + name: "enable_desktop_windowing_modals_policy" + namespace: "lse_desktop_experience" + description: "Enables policy for modals activities" + bug: "319492844" +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 0d6b71066557..6a510bd13f7c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -85,6 +85,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.pointer.motionEventSpy import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.boundsInWindow @@ -177,33 +179,43 @@ fun CommunalHub( } .thenIf(!viewModel.isEditMode) { Modifier.pointerInput( - gridState, - contentOffset, - communalContent, - gridCoordinates - ) { - detectLongPressGesture { offset -> - // Deduct both grid offset relative to its container and content offset. - val adjustedOffset = - gridCoordinates?.let { - offset - it.positionInWindow() - contentOffset + gridState, + contentOffset, + communalContent, + gridCoordinates + ) { + detectLongPressGesture { offset -> + // Deduct both grid offset relative to its container and content + // offset. + val adjustedOffset = + gridCoordinates?.let { + offset - it.positionInWindow() - contentOffset + } + val index = + adjustedOffset?.let { firstIndexAtOffset(gridState, it) } + // Display the button only when the gesture initiates from widgets, + // the CTA tile, or an empty area on the screen. UMO/smartspace have + // their own long-press handlers. To prevent user confusion, we + // should + // not display this button. + if ( + index == null || + communalContent[index].isWidgetContent() || + communalContent[index] is + CommunalContentModel.CtaTileInViewMode + ) { + isButtonToEditWidgetsShowing = true } - val index = adjustedOffset?.let { firstIndexAtOffset(gridState, it) } - // Display the button only when the gesture initiates from widgets, - // the CTA tile, or an empty area on the screen. UMO/smartspace have - // their own long-press handlers. To prevent user confusion, we should - // not display this button. - if ( - index == null || - communalContent[index].isWidgetContent() || - communalContent[index] is CommunalContentModel.CtaTileInViewMode - ) { - isButtonToEditWidgetsShowing = true + val key = + index?.let { keyAtIndexIfEditable(communalContent, index) } + viewModel.setSelectedKey(key) } - val key = index?.let { keyAtIndexIfEditable(communalContent, index) } - viewModel.setSelectedKey(key) } - } + .onPreviewKeyEvent { + onKeyEvent(viewModel) + false + } + .motionEventSpy { onMotionEvent(viewModel) } }, ) { CommunalHubLazyGrid( @@ -311,6 +323,14 @@ fun CommunalHub( } } +private fun onKeyEvent(viewModel: BaseCommunalViewModel) { + viewModel.signalUserInteraction() +} + +private fun onMotionEvent(viewModel: BaseCommunalViewModel) { + viewModel.signalUserInteraction() +} + @Composable private fun ScrollOnNewSmartspaceEffect( viewModel: BaseCommunalViewModel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt index ce6445b75fb9..d624bf7165f3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal +import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -25,13 +26,17 @@ import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dock.dockManager import com.android.systemui.dock.fakeDockManager +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope @@ -54,11 +59,15 @@ class CommunalSceneStartableTest : SysuiTestCase() { @Before fun setUp() { with(kosmos) { + fakeSettings.putInt(Settings.System.SCREEN_OFF_TIMEOUT, SCREEN_TIMEOUT) + underTest = CommunalSceneStartable( dockManager = dockManager, communalInteractor = communalInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, + keyguardInteractor = keyguardInteractor, + systemSettings = fakeSettings, applicationScope = applicationCoroutineScope, bgScope = applicationCoroutineScope, ) @@ -246,6 +255,95 @@ class CommunalSceneStartableTest : SysuiTestCase() { } } + @Test + fun hubTimeout_whenDreaming() = + with(kosmos) { + testScope.runTest { + // Device is dreaming and on communal. + fakeKeyguardRepository.setDreaming(true) + communalInteractor.onSceneChanged(CommunalScenes.Communal) + + val scene by collectLastValue(communalInteractor.desiredScene) + assertThat(scene).isEqualTo(CommunalScenes.Communal) + + // Scene times out back to blank after the screen timeout. + advanceTimeBy(SCREEN_TIMEOUT.milliseconds) + assertThat(scene).isEqualTo(CommunalScenes.Blank) + } + } + + @Test + fun hubTimeout_dreamStopped() = + with(kosmos) { + testScope.runTest { + // Device is dreaming and on communal. + fakeKeyguardRepository.setDreaming(true) + communalInteractor.onSceneChanged(CommunalScenes.Communal) + + val scene by collectLastValue(communalInteractor.desiredScene) + assertThat(scene).isEqualTo(CommunalScenes.Communal) + + // Wait a bit, but not long enough to timeout. + advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds) + assertThat(scene).isEqualTo(CommunalScenes.Communal) + + // Dream stops, timeout is cancelled and device stays on hub, because the regular + // screen timeout will take effect at this point. + fakeKeyguardRepository.setDreaming(false) + advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds) + assertThat(scene).isEqualTo(CommunalScenes.Communal) + } + } + + @Test + fun hubTimeout_userActivityTriggered_resetsTimeout() = + with(kosmos) { + testScope.runTest { + // Device is dreaming and on communal. + fakeKeyguardRepository.setDreaming(true) + communalInteractor.onSceneChanged(CommunalScenes.Communal) + + val scene by collectLastValue(communalInteractor.desiredScene) + assertThat(scene).isEqualTo(CommunalScenes.Communal) + + // Wait a bit, but not long enough to timeout. + advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds) + + // Send user interaction to reset timeout. + communalInteractor.signalUserInteraction() + + // If user activity didn't reset timeout, we would have gone back to Blank by now. + advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds) + assertThat(scene).isEqualTo(CommunalScenes.Communal) + + // Timeout happens one interval after the user interaction. + advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds) + assertThat(scene).isEqualTo(CommunalScenes.Blank) + } + } + + @Test + fun hubTimeout_screenTimeoutChanged() = + with(kosmos) { + testScope.runTest { + fakeSettings.putInt(Settings.System.SCREEN_OFF_TIMEOUT, SCREEN_TIMEOUT * 2) + + // Device is dreaming and on communal. + fakeKeyguardRepository.setDreaming(true) + communalInteractor.onSceneChanged(CommunalScenes.Communal) + + val scene by collectLastValue(communalInteractor.desiredScene) + assertThat(scene).isEqualTo(CommunalScenes.Communal) + + // Scene times out back to blank after the screen timeout. + advanceTimeBy(SCREEN_TIMEOUT.milliseconds) + assertThat(scene).isEqualTo(CommunalScenes.Communal) + + advanceTimeBy(SCREEN_TIMEOUT.milliseconds) + assertThat(scene).isEqualTo(CommunalScenes.Blank) + } + } + private fun TestScope.updateDocked(docked: Boolean) = with(kosmos) { runCurrent() @@ -260,4 +358,8 @@ class CommunalSceneStartableTest : SysuiTestCase() { setCommunalAvailable(true) runCurrent() } + + companion object { + private const val SCREEN_TIMEOUT = 1000 + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt index 71866b3957b6..82ce6d785e90 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt @@ -37,7 +37,7 @@ class DefaultComponentsLayoutManagerTest : SysuiTestCase() { DefaultComponentsLayoutManager( BOTTOM_BAR, headerComponents = listOf(COMPONENT_1), - footerComponents = listOf(COMPONENT_2), + footerComponents = listOf(COMPONENT_5, COMPONENT_2), ) @Test @@ -48,10 +48,18 @@ class DefaultComponentsLayoutManagerTest : SysuiTestCase() { val component2 = ComponentState(COMPONENT_2, kosmos.mockVolumePanelUiComponent, false) val component3 = ComponentState(COMPONENT_3, kosmos.mockVolumePanelUiComponent, false) val component4 = ComponentState(COMPONENT_4, kosmos.mockVolumePanelUiComponent, false) + val component5 = ComponentState(COMPONENT_5, kosmos.mockVolumePanelUiComponent, false) val layout = underTest.layout( VolumePanelState(0, false, false), - setOf(bottomBarComponentState, component1, component2, component3, component4) + setOf( + bottomBarComponentState, + component1, + component2, + component3, + component4, + component5, + ) ) Truth.assertThat(layout.bottomBarComponent).isEqualTo(bottomBarComponentState) @@ -59,7 +67,7 @@ class DefaultComponentsLayoutManagerTest : SysuiTestCase() { .containsExactlyElementsIn(listOf(component1)) .inOrder() Truth.assertThat(layout.footerComponents) - .containsExactlyElementsIn(listOf(component2)) + .containsExactlyElementsIn(listOf(component5, component2)) .inOrder() Truth.assertThat(layout.contentComponents) .containsExactlyElementsIn(listOf(component3, component4)) @@ -85,5 +93,6 @@ class DefaultComponentsLayoutManagerTest : SysuiTestCase() { const val COMPONENT_2: VolumePanelComponentKey = "test_component:2" const val COMPONENT_3: VolumePanelComponentKey = "test_component:3" const val COMPONENT_4: VolumePanelComponentKey = "test_component:4" + const val COMPONENT_5: VolumePanelComponentKey = "test_component:5" } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt index c3c7411c401d..98c8205abbb3 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal +import android.provider.Settings import com.android.compose.animation.scene.SceneKey import com.android.systemui.CoreStartable import com.android.systemui.communal.domain.interactor.CommunalInteractor @@ -24,25 +25,32 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dock.DockManager +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.util.kotlin.emitOnStart +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import com.android.systemui.util.settings.SystemSettings import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch /** * A [CoreStartable] responsible for automatically navigating between communal scenes when certain * conditions are met. */ -@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class CommunalSceneStartable @Inject @@ -50,9 +58,13 @@ constructor( private val dockManager: DockManager, private val communalInteractor: CommunalInteractor, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val keyguardInteractor: KeyguardInteractor, + private val systemSettings: SystemSettings, @Application private val applicationScope: CoroutineScope, @Background private val bgScope: CoroutineScope, ) : CoreStartable { + private var screenTimeout: Int = DEFAULT_SCREEN_TIMEOUT + override fun start() { // Handle automatically switching based on keyguard state. keyguardTransitionInteractor.startedKeyguardTransitionStep @@ -78,6 +90,43 @@ constructor( // } // } // .launchIn(bgScope) + + systemSettings + .observerFlow(Settings.System.SCREEN_OFF_TIMEOUT) + // Read the setting value on start. + .emitOnStart() + .onEach { + screenTimeout = + systemSettings.getInt( + Settings.System.SCREEN_OFF_TIMEOUT, + DEFAULT_SCREEN_TIMEOUT + ) + } + .launchIn(bgScope) + + // Handle timing out back to the dream. + bgScope.launch { + combine( + communalInteractor.desiredScene, + keyguardInteractor.isDreaming, + // Emit a value on start so the combine starts. + communalInteractor.userActivity.emitOnStart() + ) { scene, isDreaming, _ -> + // Time out should run whenever we're dreaming and the hub is open, even if not + // docked. + scene == CommunalScenes.Communal && isDreaming + } + // collectLatest cancels the previous action block when new values arrive, so any + // already running timeout gets cancelled when conditions change or user interaction + // is detected. + .collectLatest { shouldTimeout -> + if (!shouldTimeout) { + return@collectLatest + } + delay(screenTimeout.milliseconds) + communalInteractor.onSceneChanged(CommunalScenes.Blank) + } + } } private suspend fun determineSceneAfterTransition( @@ -105,5 +154,6 @@ constructor( companion object { val AWAKE_DEBOUNCE_DELAY = 5.seconds val DOCK_DEBOUNCE_DELAY = 1.seconds + val DEFAULT_SCREEN_TIMEOUT = 15000 } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 940b48cc94c9..52025b177b6e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -63,10 +63,13 @@ import com.android.systemui.util.kotlin.emitOnStart import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -84,7 +87,7 @@ import kotlinx.coroutines.flow.shareIn class CommunalInteractor @Inject constructor( - @Application applicationScope: CoroutineScope, + @Application val applicationScope: CoroutineScope, broadcastDispatcher: BroadcastDispatcher, private val communalRepository: CommunalRepository, private val widgetRepository: CommunalWidgetRepository, @@ -152,6 +155,14 @@ constructor( /** Transition state of the hub mode. */ val transitionState: StateFlow<ObservableTransitionState> = communalRepository.transitionState + val _userActivity: MutableSharedFlow<Unit> = + MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + val userActivity: Flow<Unit> = _userActivity.asSharedFlow() + + fun signalUserInteraction() { + _userActivity.tryEmit(Unit) + } + /** * Updates the transition state of the hub [SceneTransitionLayout]. * diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index 85f3c202f10f..c913300f5ace 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -45,6 +45,10 @@ abstract class BaseCommunalViewModel( val selectedKey: StateFlow<String?> get() = _selectedKey + fun signalUserInteraction() { + communalInteractor.signalUserInteraction() + } + fun onSceneChanged(scene: SceneKey) { communalInteractor.onSceneChanged(scene) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/DefaultComponentsLayoutManager.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/DefaultComponentsLayoutManager.kt index 7fd9c8a2063a..635191a1793a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/DefaultComponentsLayoutManager.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/DefaultComponentsLayoutManager.kt @@ -46,12 +46,18 @@ constructor( !footerComponents.contains(it.key) && it.key != bottomBar } - val headerComponents = components.filter { headerComponents.contains(it.key) } - val footerComponents = components.filter { footerComponents.contains(it.key) } + val headerComponents = + components + .filter { it.key in headerComponents } + .sortedBy { headerComponents.indexOf(it.key) } + val footerComponents = + components + .filter { it.key in footerComponents } + .sortedBy { footerComponents.indexOf(it.key) } return ComponentsLayout( - headerComponents = headerComponents.sortedBy { it.key }, + headerComponents = headerComponents, contentComponents = contentComponents.sortedBy { it.key }, - footerComponents = footerComponents.sortedBy { it.key }, + footerComponents = footerComponents, bottomBarComponent = components.find { it.key == bottomBar } ?: error( "VolumePanelComponents.BOTTOM_BAR must be present in the default " + diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java index 40b2f5ab852f..10030b3c9176 100644 --- a/services/core/java/com/android/server/display/BrightnessRangeController.java +++ b/services/core/java/com/android/server/display/BrightnessRangeController.java @@ -21,6 +21,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.display.brightness.clamper.HdrClamper; import com.android.server.display.feature.DisplayManagerFlags; @@ -30,8 +31,7 @@ import java.util.function.BooleanSupplier; class BrightnessRangeController { private final HighBrightnessModeController mHbmController; - private final NormalBrightnessModeController mNormalBrightnessModeController = - new NormalBrightnessModeController(); + private final NormalBrightnessModeController mNormalBrightnessModeController; private final HdrClamper mHdrClamper; @@ -45,17 +45,21 @@ class BrightnessRangeController { Runnable modeChangeCallback, DisplayDeviceConfig displayDeviceConfig, Handler handler, DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) { this(hbmController, modeChangeCallback, displayDeviceConfig, + new NormalBrightnessModeController(), new HdrClamper(modeChangeCallback::run, new Handler(handler.getLooper())), flags, displayToken, info); } + @VisibleForTesting BrightnessRangeController(HighBrightnessModeController hbmController, Runnable modeChangeCallback, DisplayDeviceConfig displayDeviceConfig, + NormalBrightnessModeController normalBrightnessModeController, HdrClamper hdrClamper, DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) { mHbmController = hbmController; mModeChangeCallback = modeChangeCallback; mHdrClamper = hdrClamper; + mNormalBrightnessModeController = normalBrightnessModeController; mUseHdrClamper = flags.isHdrClamperEnabled(); mUseNbmController = flags.isNbmControllerEnabled(); if (mUseNbmController) { @@ -126,8 +130,11 @@ class BrightnessRangeController { float getCurrentBrightnessMax() { - if (mUseNbmController && mHbmController.getHighBrightnessMode() - == BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF) { + // nbmController might adjust maxBrightness only if device does not support HBM or + // hbm is currently not allowed + if (mUseNbmController + && (!mHbmController.deviceSupportsHbm() + || !mHbmController.isHbmCurrentlyAllowed())) { return Math.min(mHbmController.getCurrentBrightnessMax(), mNormalBrightnessModeController.getCurrentBrightnessMax()); } diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java index ab7c503bcb83..a12d2481330b 100644 --- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java +++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java @@ -42,6 +42,9 @@ import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.notifications.DisplayNotificationManager; import com.android.server.display.utils.DebugUtils; +import java.util.HashSet; +import java.util.Set; + /** * Listens for Skin thermal sensor events, disables external displays if thermal status becomes * equal or above {@link android.os.Temperature#THROTTLING_CRITICAL}, enables external displays if @@ -106,6 +109,10 @@ class ExternalDisplayPolicy { private final ExternalDisplayStatsService mExternalDisplayStatsService; @ThrottlingStatus private volatile int mStatus = THROTTLING_NONE; + //@GuardedBy("mSyncRoot") + private boolean mIsBootCompleted; + //@GuardedBy("mSyncRoot") + private final Set<Integer> mDisplayIdsWaitingForBootCompletion = new HashSet<>(); ExternalDisplayPolicy(@NonNull final Injector injector) { mInjector = injector; @@ -121,6 +128,17 @@ class ExternalDisplayPolicy { * Starts listening for temperature changes. */ void onBootCompleted() { + synchronized (mSyncRoot) { + mIsBootCompleted = true; + for (var displayId : mDisplayIdsWaitingForBootCompletion) { + var logicalDisplay = mLogicalDisplayMapper.getDisplayLocked(displayId); + if (logicalDisplay != null) { + handleExternalDisplayConnectedLocked(logicalDisplay); + } + } + mDisplayIdsWaitingForBootCompletion.clear(); + } + if (!mFlags.isConnectedDisplayManagementEnabled()) { if (DEBUG) { Slog.d(TAG, "External display management is not enabled on your device:" @@ -189,6 +207,11 @@ class ExternalDisplayPolicy { return; } + if (!mIsBootCompleted) { + mDisplayIdsWaitingForBootCompletion.add(logicalDisplay.getDisplayIdLocked()); + return; + } + mExternalDisplayStatsService.onDisplayConnected(logicalDisplay); if ((Build.IS_ENG || Build.IS_USERDEBUG) @@ -227,7 +250,12 @@ class ExternalDisplayPolicy { return; } - mExternalDisplayStatsService.onDisplayDisconnected(logicalDisplay.getDisplayIdLocked()); + var displayId = logicalDisplay.getDisplayIdLocked(); + if (mDisplayIdsWaitingForBootCompletion.remove(displayId)) { + return; + } + + mExternalDisplayStatsService.onDisplayDisconnected(displayId); } /** diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java index a9f78fd5bb2a..47176fe331bf 100644 --- a/services/core/java/com/android/server/display/HighBrightnessModeController.java +++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java @@ -168,7 +168,7 @@ class HighBrightnessModeController { } float getCurrentBrightnessMax() { - if (!deviceSupportsHbm() || isCurrentlyAllowed()) { + if (!deviceSupportsHbm() || isHbmCurrentlyAllowed()) { // Either the device doesn't support HBM, or HBM range is currently allowed (device // it in a high-lux environment). In either case, return the highest brightness // level supported by the device. @@ -356,7 +356,7 @@ class HighBrightnessModeController { return event.getStartTimeMillis(); } - private boolean isCurrentlyAllowed() { + boolean isHbmCurrentlyAllowed() { // Returns true if HBM is allowed (above the ambient lux threshold) and there's still // time within the current window for additional HBM usage. We return false if there is an // HDR layer because we don't want the brightness MAX to change for HDR, which has its @@ -369,7 +369,7 @@ class HighBrightnessModeController { && !mIsBlockedByLowPowerMode); } - private boolean deviceSupportsHbm() { + boolean deviceSupportsHbm() { return mHbmData != null && mHighBrightnessModeMetadata != null; } @@ -462,7 +462,7 @@ class HighBrightnessModeController { + ", isOnlyAllowedToStayOn: " + isOnlyAllowedToStayOn + ", remainingAllowedTime: " + remainingTime + ", isLuxHigh: " + mIsInAllowedAmbientRange - + ", isHBMCurrentlyAllowed: " + isCurrentlyAllowed() + + ", isHBMCurrentlyAllowed: " + isHbmCurrentlyAllowed() + ", isHdrLayerPresent: " + mIsHdrLayerPresent + ", mMaxDesiredHdrSdrRatio: " + mMaxDesiredHdrSdrRatio + ", isAutoBrightnessEnabled: " + mIsAutoBrightnessEnabled @@ -575,7 +575,7 @@ class HighBrightnessModeController { return BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF; } else if (mIsHdrLayerPresent) { return BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR; - } else if (isCurrentlyAllowed()) { + } else if (isHbmCurrentlyAllowed()) { return BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT; } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java index 0165d65283dc..65ab12930c59 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java @@ -314,6 +314,11 @@ public class WallpaperDataParser { wallpaper.wallpaperId = makeWallpaperIdLocked(); } + Rect legacyCropHint = new Rect( + getAttributeInt(parser, "cropLeft", 0), + getAttributeInt(parser, "cropTop", 0), + getAttributeInt(parser, "cropRight", 0), + getAttributeInt(parser, "cropBottom", 0)); Rect totalCropHint = new Rect( getAttributeInt(parser, "totalCropLeft", 0), getAttributeInt(parser, "totalCropTop", 0), @@ -332,18 +337,19 @@ public class WallpaperDataParser { parser.getAttributeInt(null, "cropBottom" + pair.second, 0)); if (!cropHint.isEmpty()) wallpaper.mCropHints.put(pair.first, cropHint); } - if (wallpaper.mCropHints.size() == 0) { + if (wallpaper.mCropHints.size() == 0 && totalCropHint.isEmpty()) { // migration case: the crops per screen orientation are not specified. - // use the old attributes to find the crop for one screen orientation. - Integer orientation = totalCropHint.width() < totalCropHint.height() + int orientation = legacyCropHint.width() < legacyCropHint.height() ? WallpaperManager.PORTRAIT : WallpaperManager.LANDSCAPE; - if (!totalCropHint.isEmpty()) wallpaper.mCropHints.put(orientation, totalCropHint); + if (!legacyCropHint.isEmpty()) { + wallpaper.mCropHints.put(orientation, legacyCropHint); + } } else { wallpaper.cropHint.set(totalCropHint); } wallpaper.mSampleSize = parser.getAttributeFloat(null, "sampleSize", 1f); } else { - wallpaper.cropHint.set(totalCropHint); + wallpaper.cropHint.set(legacyCropHint); } final DisplayData wpData = mWallpaperDisplayHelper .getDisplayDataOrCreate(DEFAULT_DISPLAY); diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessRangeControllerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/BrightnessRangeControllerTest.kt new file mode 100644 index 000000000000..1f3184d056f2 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessRangeControllerTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display + +import android.os.IBinder +import androidx.test.filters.SmallTest +import com.android.server.display.brightness.clamper.HdrClamper +import com.android.server.display.feature.DisplayManagerFlags +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +private const val MAX_BRIGHTNESS = 1.0f +private const val TRANSITION_POINT = 0.7f +private const val NORMAL_BRIGHTNESS_HIGH = 0.8f +private const val NORMAL_BRIGHTNESS_LOW = 0.6f + +@SmallTest +class BrightnessRangeControllerTest { + + private val mockHbmController = mock<HighBrightnessModeController>() + private val mockCallback = mock<Runnable>() + private val mockConfig = mock<DisplayDeviceConfig>() + private val mockNormalBrightnessController = mock<NormalBrightnessModeController>() + private val mockHdrClamper = mock<HdrClamper>() + private val mockFlags = mock<DisplayManagerFlags>() + private val mockToken = mock<IBinder>() + + @Test + fun `returns HBC max brightness if HBM supported and ON`() { + val controller = createController() + assertThat(controller.currentBrightnessMax).isEqualTo(MAX_BRIGHTNESS) + } + + @Test + fun `returns NBC max brightness if device does not support HBM`() { + val controller = createController(hbmSupported = false) + assertThat(controller.currentBrightnessMax).isEqualTo(NORMAL_BRIGHTNESS_LOW) + } + + @Test + fun `returns NBC max brightness if HBM not allowed`() { + val controller = createController(hbmAllowed = false) + assertThat(controller.currentBrightnessMax).isEqualTo(NORMAL_BRIGHTNESS_LOW) + } + + @Test + fun `returns HBC max brightness if NBM is disabled`() { + val controller = createController(nbmEnabled = false, hbmAllowed = false) + assertThat(controller.currentBrightnessMax).isEqualTo(MAX_BRIGHTNESS) + } + + @Test + fun `returns HBC max brightness if lower than NBC max brightness`() { + val controller = createController( + hbmAllowed = false, + hbmMaxBrightness = TRANSITION_POINT, + nbmMaxBrightness = NORMAL_BRIGHTNESS_HIGH + ) + assertThat(controller.currentBrightnessMax).isEqualTo(TRANSITION_POINT) + } + + private fun createController( + nbmEnabled: Boolean = true, + hbmSupported: Boolean = true, + hbmAllowed: Boolean = true, + hbmMaxBrightness: Float = MAX_BRIGHTNESS, + nbmMaxBrightness: Float = NORMAL_BRIGHTNESS_LOW + ): BrightnessRangeController { + whenever(mockFlags.isNbmControllerEnabled).thenReturn(nbmEnabled) + whenever(mockHbmController.deviceSupportsHbm()).thenReturn(hbmSupported) + whenever(mockHbmController.isHbmCurrentlyAllowed).thenReturn(hbmAllowed) + whenever(mockHbmController.currentBrightnessMax).thenReturn(hbmMaxBrightness) + whenever(mockNormalBrightnessController.currentBrightnessMax).thenReturn(nbmMaxBrightness) + + return BrightnessRangeController(mockHbmController, mockCallback, mockConfig, + mockNormalBrightnessController, mockHdrClamper, mockFlags, mockToken, + DisplayDeviceInfo()) + } +}
\ No newline at end of file diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index b142334db9e9..18f03113c01c 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -2408,6 +2408,7 @@ public class DisplayManagerServiceTest { when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); DisplayManagerInternal localService = displayManager.new LocalService(); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -2440,6 +2441,7 @@ public class DisplayManagerServiceTest { .when(() -> SystemProperties.getBoolean(ENABLE_ON_CONNECT, false)); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); DisplayManagerInternal localService = displayManager.new LocalService(); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -2487,6 +2489,7 @@ public class DisplayManagerServiceTest { when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); @@ -2652,6 +2655,7 @@ public class DisplayManagerServiceTest { when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); DisplayManagerService.BinderService bs = displayManager.new BinderService(); DisplayManagerInternal localService = displayManager.new LocalService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -2699,6 +2703,7 @@ public class DisplayManagerServiceTest { when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index 76b77808b880..fb2321316e00 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -1927,6 +1927,8 @@ public final class DisplayPowerControllerTest { mock(ScreenOffBrightnessSensorController.class); final HighBrightnessModeController hbmController = mock(HighBrightnessModeController.class); final HdrClamper hdrClamper = mock(HdrClamper.class); + final NormalBrightnessModeController normalBrightnessModeController = mock( + NormalBrightnessModeController.class); BrightnessClamperController clamperController = mock(BrightnessClamperController.class); when(hbmController.getCurrentBrightnessMax()).thenReturn(PowerManager.BRIGHTNESS_MAX); @@ -1939,7 +1941,8 @@ public final class DisplayPowerControllerTest { TestInjector injector = spy(new TestInjector(displayPowerState, animator, automaticBrightnessController, wakelockController, brightnessMappingStrategy, - hysteresisLevels, screenOffBrightnessSensorController, hbmController, hdrClamper, + hysteresisLevels, screenOffBrightnessSensorController, + hbmController, normalBrightnessModeController, hdrClamper, clamperController, mDisplayManagerFlagsMock)); final LogicalDisplay display = mock(LogicalDisplay.class); @@ -2027,6 +2030,8 @@ public final class DisplayPowerControllerTest { private final ScreenOffBrightnessSensorController mScreenOffBrightnessSensorController; private final HighBrightnessModeController mHighBrightnessModeController; + private final NormalBrightnessModeController mNormalBrightnessModeController; + private final HdrClamper mHdrClamper; private final BrightnessClamperController mClamperController; @@ -2040,6 +2045,7 @@ public final class DisplayPowerControllerTest { HysteresisLevels hysteresisLevels, ScreenOffBrightnessSensorController screenOffBrightnessSensorController, HighBrightnessModeController highBrightnessModeController, + NormalBrightnessModeController normalBrightnessModeController, HdrClamper hdrClamper, BrightnessClamperController clamperController, DisplayManagerFlags flags) { @@ -2051,6 +2057,7 @@ public final class DisplayPowerControllerTest { mHysteresisLevels = hysteresisLevels; mScreenOffBrightnessSensorController = screenOffBrightnessSensorController; mHighBrightnessModeController = highBrightnessModeController; + mNormalBrightnessModeController = normalBrightnessModeController; mHdrClamper = hdrClamper; mClamperController = clamperController; mFlags = flags; @@ -2163,7 +2170,8 @@ public final class DisplayPowerControllerTest { DisplayDeviceConfig displayDeviceConfig, Handler handler, DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) { return new BrightnessRangeController(hbmController, modeChangeCallback, - displayDeviceConfig, mHdrClamper, mFlags, displayToken, info); + displayDeviceConfig, mNormalBrightnessModeController, mHdrClamper, + mFlags, displayToken, info); } @Override diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java index 1529a087c284..1a71e77a3b1b 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java @@ -228,13 +228,27 @@ public class ExternalDisplayPolicyTest { @Test public void testOnExternalDisplayAvailable() { - when(mMockedLogicalDisplay.isEnabledLocked()).thenReturn(false); + mExternalDisplayPolicy.handleExternalDisplayConnectedLocked(mMockedLogicalDisplay); + assertNotAskedToEnableDisplay(); + verify(mMockedExternalDisplayStatsService, never()).onDisplayConnected(any()); + + mExternalDisplayPolicy.onBootCompleted(); assertAskedToEnableDisplay(); verify(mMockedExternalDisplayStatsService).onDisplayConnected(eq(mMockedLogicalDisplay)); } @Test + public void testOnExternalDisplayUnpluggedBeforeBootCompletes() { + mExternalDisplayPolicy.handleExternalDisplayConnectedLocked(mMockedLogicalDisplay); + mExternalDisplayPolicy.handleLogicalDisplayDisconnectedLocked(mMockedLogicalDisplay); + mExternalDisplayPolicy.onBootCompleted(); + assertNotAskedToEnableDisplay(); + verify(mMockedExternalDisplayStatsService, never()).onDisplayConnected(any()); + verify(mMockedExternalDisplayStatsService, never()).onDisplayDisconnected(anyInt()); + } + + @Test public void testOnExternalDisplayAvailable_criticalThermalCondition() throws RemoteException { // Disallow external displays due to thermals. @@ -303,8 +317,14 @@ public class ExternalDisplayPolicyTest { mDisplayEventCaptor.capture()); assertThat(mLogicalDisplayCaptor.getValue()).isEqualTo(mMockedLogicalDisplay); assertThat(mDisplayEventCaptor.getValue()).isEqualTo(EVENT_DISPLAY_CONNECTED); + verify(mMockedLogicalDisplay).setEnabledLocked(false); clearInvocations(mMockedLogicalDisplayMapper); - when(mMockedLogicalDisplay.isEnabledLocked()).thenReturn(true); + clearInvocations(mMockedLogicalDisplay); + } + + private void assertNotAskedToEnableDisplay() { + verify(mMockedInjector, never()).sendExternalDisplayEventLocked(any(), anyInt()); + verify(mMockedLogicalDisplay, never()).setEnabledLocked(anyBoolean()); } private void assertIsExternalDisplayAllowed(final boolean enabled) { |