diff options
15 files changed, 654 insertions, 6 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index 3d3d666825ad..ee9cb141a700 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -19,6 +19,8 @@ import android.content.res.Configuration import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper.RunWithLooper +import android.view.Choreographer +import android.view.MotionEvent import android.widget.FrameLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -44,6 +46,7 @@ import com.android.systemui.settings.brightness.data.repository.BrightnessMirror import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor +import com.android.systemui.statusbar.BlurUtils import com.android.systemui.statusbar.DragDownHelper import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.NotificationInsetsController @@ -66,6 +69,7 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.window.ui.viewmodel.WindowRootViewModel import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -91,6 +95,9 @@ import org.mockito.kotlin.eq @SmallTest class NotificationShadeWindowViewTest : SysuiTestCase() { + @Mock private lateinit var choreographer: Choreographer + @Mock private lateinit var blurUtils: BlurUtils + @Mock private lateinit var windowRootViewModelFactory: WindowRootViewModel.Factory @Mock private lateinit var dragDownHelper: DragDownHelper @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController @Mock private lateinit var shadeController: ShadeController @@ -168,6 +175,9 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { testScope = TestScope() controller = NotificationShadeWindowViewController( + blurUtils, + windowRootViewModelFactory, + choreographer, lockscreenShadeTransitionController, FalsingCollectorFake(), statusBarStateController, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt index 9f94cff4ead4..f48b3e1eaeff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -17,15 +17,19 @@ package com.android.systemui.statusbar import android.os.IBinder +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper.RunWithLooper import android.view.Choreographer import android.view.View import android.view.ViewRootImpl import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.dump.DumpManager +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R import com.android.systemui.shade.ShadeExpansionChangeEvent @@ -35,8 +39,10 @@ import com.android.systemui.statusbar.phone.ScrimController import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController +import com.android.systemui.testKosmos import com.android.systemui.util.WallpaperController import com.android.systemui.util.mockito.eq +import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor import com.google.common.truth.Truth.assertThat import java.util.function.Consumer import org.junit.Before @@ -44,6 +50,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.floatThat import org.mockito.Captor @@ -63,7 +70,10 @@ import org.mockito.junit.MockitoJUnit @RunWithLooper @SmallTest class NotificationShadeDepthControllerTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val applicationScope = kosmos.testScope.backgroundScope + @Mock private lateinit var windowRootViewBlurInteractor: WindowRootViewBlurInteractor @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var blurUtils: BlurUtils @Mock private lateinit var biometricUnlockController: BiometricUnlockController @@ -115,9 +125,12 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { notificationShadeWindowController, dozeParameters, context, - ResourcesSplitShadeStateController(), + ResourcesSplitShadeStateController(), + windowRootViewBlurInteractor, + applicationScope, dumpManager, - configurationController,) + configurationController, + ) notificationShadeDepthController.shadeAnimation = shadeAnimation notificationShadeDepthController.brightnessMirrorSpring = brightnessSpring notificationShadeDepthController.root = root @@ -356,6 +369,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_BOUNCER_UI_REVAMP) fun ignoreShadeBlurUntilHidden_schedulesFrame() { notificationShadeDepthController.blursDisabledForAppLaunch = true verify(blurUtils).prepareBlur(any(), anyInt()) @@ -364,6 +378,13 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_BOUNCER_UI_REVAMP) + fun ignoreShadeBlurUntilHidden_requestsBlur_windowBlurFlag() { + notificationShadeDepthController.blursDisabledForAppLaunch = true + verify(windowRootViewBlurInteractor).requestBlurForShade(anyInt(), anyBoolean()) + } + + @Test fun ignoreBlurForUnlock_ignores() { notificationShadeDepthController.onPanelExpansionChanged( ShadeExpansionChangeEvent( @@ -410,6 +431,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_BOUNCER_UI_REVAMP) fun brightnessMirror_hidesShadeBlur() { // Brightness mirror is fully visible `when`(brightnessSpring.ratio).thenReturn(1f) @@ -427,6 +449,23 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_BOUNCER_UI_REVAMP) + fun brightnessMirror_hidesShadeBlur_withWindowBlurFlag() { + // Brightness mirror is fully visible + `when`(brightnessSpring.ratio).thenReturn(1f) + // And shade is blurred + notificationShadeDepthController.onPanelExpansionChanged( + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) + `when`(shadeAnimation.radius).thenReturn(maxBlur.toFloat()) + + notificationShadeDepthController.updateBlurCallback.doFrame(0) + verify(notificationShadeWindowController).setBackgroundBlurRadius(eq(0)) + verify(wallpaperController).setNotificationShadeZoom(eq(1f)) + verify(windowRootViewBlurInteractor).requestBlurForShade(0, false) + } + + @Test fun ignoreShadeBlurUntilHidden_whennNull_ignoresIfShadeHasNoBlur() { `when`(shadeAnimation.radius).thenReturn(0f) notificationShadeDepthController.blursDisabledForAppLaunch = true diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractorTest.kt new file mode 100644 index 000000000000..bc16bec5d5cd --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractorTest.kt @@ -0,0 +1,60 @@ +/* + * 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.systemui.window.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class WindowRootViewBlurInteractorTest : SysuiTestCase() { + val kosmos = testKosmos() + val testScope = kosmos.testScope + + val underTest by lazy { kosmos.windowRootViewBlurInteractor } + + @Test + fun bouncerBlurIsAppliedImmediately() = + testScope.runTest { + val blurRadius by collectLastValue(underTest.blurRadius) + val isBlurOpaque by collectLastValue(underTest.isBlurOpaque) + + underTest.requestBlurForBouncer(10) + + assertThat(blurRadius).isEqualTo(10) + assertThat(isBlurOpaque).isFalse() + } + + @Test + fun shadeBlurIsNotAppliedWhenBouncerBlurIsActive() = + testScope.runTest { + kosmos.fakeKeyguardBouncerRepository.setPrimaryShow(true) + + assertThat(underTest.requestBlurForShade(30, true)).isFalse() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt new file mode 100644 index 000000000000..b97fe5725285 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt @@ -0,0 +1,59 @@ +/* + * 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.systemui.window.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class WindowRootViewModelTest : SysuiTestCase() { + val kosmos = testKosmos() + val testScope = kosmos.testScope + + val underTest by lazy { kosmos.windowRootViewModel } + + @Before + fun setup() { + underTest.activateIn(testScope) + } + + @Test + fun bouncerTransitionChangesWindowBlurRadius() = + testScope.runTest { + val blurState by collectLastValue(underTest.blurState) + runCurrent() + + kosmos.fakeBouncerTransitions.first().windowBlurRadius.value = 30.0f + runCurrent() + + assertThat(blurState).isEqualTo(BlurState(radius = 30, isOpaque = false)) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 839d4596bb7c..e5dcd2338b9d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -23,6 +23,7 @@ import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.app.StatusBarManager; import android.util.Log; +import android.view.Choreographer; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.MotionEvent; @@ -60,6 +61,7 @@ import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirr import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor; import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround; import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener; +import com.android.systemui.statusbar.BlurUtils; import com.android.systemui.statusbar.DragDownHelper; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationInsetsController; @@ -79,6 +81,8 @@ import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.unfold.SysUIUnfoldComponent; import com.android.systemui.unfold.UnfoldTransitionProgressProvider; import com.android.systemui.util.time.SystemClock; +import com.android.systemui.window.ui.WindowRootViewBinder; +import com.android.systemui.window.ui.viewmodel.WindowRootViewModel; import kotlinx.coroutines.ExperimentalCoroutinesApi; @@ -160,6 +164,9 @@ public class NotificationShadeWindowViewController implements Dumpable { @ExperimentalCoroutinesApi @Inject public NotificationShadeWindowViewController( + BlurUtils blurUtils, + WindowRootViewModel.Factory windowRootViewModelFactory, + Choreographer choreographer, LockscreenShadeTransitionController transitionController, FalsingCollector falsingCollector, SysuiStatusBarStateController statusBarStateController, @@ -259,9 +266,18 @@ public class NotificationShadeWindowViewController implements Dumpable { if (ShadeWindowGoesAround.isEnabled()) { mView.setConfigurationForwarder(configurationForwarder.get()); } + bindWindowRootView(blurUtils, windowRootViewModelFactory, choreographer); dumpManager.registerDumpable(this); } + private void bindWindowRootView(BlurUtils blurUtils, + WindowRootViewModel.Factory windowRootViewModelFactory, Choreographer choreographer) { + if (SceneContainerFlag.isEnabled()) return; + + WindowRootViewBinder.INSTANCE.bind(mView, windowRootViewModelFactory, blurUtils, + choreographer); + } + private void bindBouncer(BouncerViewBinder bouncerViewBinder) { mBouncerParentView = mView.findViewById(R.id.keyguard_bouncer_container); bouncerViewBinder.bind(mBouncerParentView); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index 3408f4ffd082..443b5415b3a0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -34,9 +34,9 @@ import androidx.dynamicanimation.animation.SpringAnimation import androidx.dynamicanimation.animation.SpringForce import com.android.app.animation.Interpolators import com.android.systemui.Dumpable -import com.android.systemui.Flags.notificationShadeBlur import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionChangeEvent @@ -50,10 +50,14 @@ import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.WallpaperController +import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor +import com.android.systemui.window.flag.WindowBlurFlag import java.io.PrintWriter import javax.inject.Inject import kotlin.math.max import kotlin.math.sign +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * Responsible for blurring the notification shade window, and applying a zoom effect to the @@ -73,6 +77,8 @@ constructor( private val dozeParameters: DozeParameters, private val context: Context, private val splitShadeStateController: SplitShadeStateController, + private val windowRootViewBlurInteractor: WindowRootViewBlurInteractor, + @Application private val applicationScope: CoroutineScope, dumpManager: DumpManager, configurationController: ConfigurationController, ) : ShadeExpansionListener, Dumpable { @@ -162,6 +168,8 @@ constructor( shadeAnimation.finishIfRunning() } + private var zoomOutCalculatedFromShadeRadius: Float = 0.0f + /** We're unlocking, and should not blur as the panel expansion changes. */ var blursDisabledForUnlock: Boolean = false set(value) { @@ -216,7 +224,7 @@ constructor( val zoomOut = blurRadiusToZoomOut(blurRadius = shadeRadius) // Make blur be 0 if it is necessary to stop blur effect. if (scrimsVisible) { - if (!notificationShadeBlur()) { + if (!WindowBlurFlag.isEnabled) { blur = 0 } } @@ -244,7 +252,7 @@ constructor( } private val shouldBlurBeOpaque: Boolean - get() = if (notificationShadeBlur()) false else scrimsVisible && !blursDisabledForAppLaunch + get() = if (WindowBlurFlag.isEnabled) false else scrimsVisible && !blursDisabledForAppLaunch /** Callback that updates the window blur value and is called only once per frame. */ @VisibleForTesting @@ -363,6 +371,26 @@ constructor( } } ) + initBlurListeners() + } + + private fun initBlurListeners() { + if (!WindowBlurFlag.isEnabled) return + + applicationScope.launch { + Log.d(TAG, "Starting coroutines for window root view blur") + windowRootViewBlurInteractor.onBlurAppliedEvent.collect { appliedBlurRadius -> + if (updateScheduled) { + // Process the blur applied event only if we scheduled the update + Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", appliedBlurRadius) + updateScheduled = false + onBlurApplied(appliedBlurRadius, zoomOutCalculatedFromShadeRadius) + } else { + // Try scheduling an update now, maybe our blur request will be scheduled now. + scheduleUpdate() + } + } + } } private fun updateResources() { @@ -480,11 +508,17 @@ constructor( } private fun scheduleUpdate() { + val (blur, zoomOutFromShadeRadius) = computeBlurAndZoomOut() + zoomOutCalculatedFromShadeRadius = zoomOutFromShadeRadius + if (WindowBlurFlag.isEnabled) { + updateScheduled = + windowRootViewBlurInteractor.requestBlurForShade(blur, shouldBlurBeOpaque) + return + } if (updateScheduled) { return } updateScheduled = true - val (blur, _) = computeBlurAndZoomOut() blurUtils.prepareBlur(root.viewRootImpl, blur) choreographer.postFrameCallback(updateBlurCallback) } diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt new file mode 100644 index 000000000000..6b7de982e00a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt @@ -0,0 +1,33 @@ +/* + * 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.systemui.window.data.repository + +import android.annotation.SuppressLint +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow + +/** Repository that maintains state for the window blur effect. */ +@SysUISingleton +class WindowRootViewBlurRepository @Inject constructor() { + val blurRadius = MutableStateFlow(0) + + val isBlurOpaque = MutableStateFlow(false) + + @SuppressLint("SharedFlowCreation") val onBlurApplied = MutableSharedFlow<Int>() +} diff --git a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt new file mode 100644 index 000000000000..fee32b5e7e78 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt @@ -0,0 +1,91 @@ +/* + * 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.systemui.window.domain.interactor + +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.window.data.repository.WindowRootViewBlurRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Interactor that provides the blur state for the window root view + * [com.android.systemui.scene.ui.view.WindowRootView] + */ +@SysUISingleton +class WindowRootViewBlurInteractor +@Inject +constructor( + private val keyguardInteractor: KeyguardInteractor, + private val repository: WindowRootViewBlurRepository, +) { + + /** + * Invoked by the view after blur of [appliedBlurRadius] was successfully applied on the window + * root view. + */ + suspend fun onBlurApplied(appliedBlurRadius: Int) { + repository.onBlurApplied.emit(appliedBlurRadius) + } + + /** Radius of blur to be applied on the window root view. */ + val blurRadius: StateFlow<Int> = repository.blurRadius.asStateFlow() + + /** Whether the blur applied is opaque or transparent. */ + val isBlurOpaque: StateFlow<Boolean> = repository.isBlurOpaque.asStateFlow() + + /** + * Emits the applied blur radius whenever blur is successfully applied to the window root view. + */ + val onBlurAppliedEvent: Flow<Int> = repository.onBlurApplied + + /** + * Request to apply blur while on bouncer, this takes precedence over other blurs (from + * shade). + */ + fun requestBlurForBouncer(blurRadius: Int) { + repository.isBlurOpaque.value = false + repository.blurRadius.value = blurRadius + } + + /** + * Method that requests blur to be applied on window root view. It is applied only when other + * blurs are not applied. + * + * This method is present to temporarily support the blur for notification shade, ideally shade + * should expose state that is used by this interactor to determine the blur that has to be + * applied. + * + * @return whether the request for blur was processed or not. + */ + fun requestBlurForShade(blurRadius: Int, opaque: Boolean): Boolean { + if (keyguardInteractor.primaryBouncerShowing.value) { + return false + } + Log.d(TAG, "requestingBlurForShade for $blurRadius $opaque") + repository.blurRadius.value = blurRadius + repository.isBlurOpaque.value = opaque + return true + } + + companion object { + const val TAG = "WindowRootViewBlurInteractor" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt new file mode 100644 index 000000000000..2491ca7565c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt @@ -0,0 +1,88 @@ +/* + * 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.systemui.window.ui + +import android.util.Log +import android.view.Choreographer +import android.view.Choreographer.FrameCallback +import com.android.systemui.lifecycle.WindowLifecycleState +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.lifecycle.viewModel +import com.android.systemui.scene.ui.view.WindowRootView +import com.android.systemui.statusbar.BlurUtils +import com.android.systemui.window.flag.WindowBlurFlag +import com.android.systemui.window.ui.viewmodel.WindowRootViewModel +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch + +/** + * View binder that wires up window level UI transformations like blur to the [WindowRootView] + * instance. + */ +object WindowRootViewBinder { + private const val TAG = "WindowRootViewBinder" + + fun bind( + view: WindowRootView, + viewModelFactory: WindowRootViewModel.Factory, + blurUtils: BlurUtils?, + choreographer: Choreographer?, + ) { + if (!WindowBlurFlag.isEnabled) return + if (blurUtils == null || choreographer == null) return + + view.repeatWhenAttached { + Log.d(TAG, "Binding root view") + var frameCallbackPendingExecution: FrameCallback? = null + val viewRootImpl = view.rootView.viewRootImpl + view.viewModel( + minWindowLifecycleState = WindowLifecycleState.ATTACHED, + factory = { viewModelFactory.create() }, + traceName = "WindowRootViewBinder#bind", + ) { viewModel -> + try { + Log.d(TAG, "Launching coroutines that update window root view state") + launch { + viewModel.blurState + .filter { it.radius >= 0 } + .collect { blurState -> + val newFrameCallback = FrameCallback { + frameCallbackPendingExecution = null + blurUtils.applyBlur( + viewRootImpl, + blurState.radius, + blurState.isOpaque, + ) + viewModel.onBlurApplied(blurState.radius) + } + blurUtils.prepareBlur(viewRootImpl, blurState.radius) + if (frameCallbackPendingExecution != null) { + choreographer.removeFrameCallback(frameCallbackPendingExecution) + } + frameCallbackPendingExecution = newFrameCallback + choreographer.postFrameCallback(newFrameCallback) + } + } + awaitCancellation() + } finally { + Log.d(TAG, "Wrapped up coroutines that update window root view state") + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt new file mode 100644 index 000000000000..199d02d267ed --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt @@ -0,0 +1,106 @@ +/* + * 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.systemui.window.ui.viewmodel + +import android.os.Build +import android.util.Log +import com.android.app.tracing.coroutines.launchTraced +import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach + +typealias BlurAppliedUiEvent = Int + +/** View model for window root view. */ +class WindowRootViewModel +@AssistedInject +constructor( + private val primaryBouncerTransitions: Set<@JvmSuppressWildcards PrimaryBouncerTransition>, + private val blurInteractor: WindowRootViewBlurInteractor, +) : ExclusiveActivatable() { + + private val blurEvents = Channel<BlurAppliedUiEvent>(Channel.BUFFERED) + private val _blurState = MutableStateFlow(BlurState(0, false)) + val blurState = _blurState.asStateFlow() + + override suspend fun onActivated(): Nothing { + coroutineScope { + launchTraced("WindowRootViewModel#blurEvents") { + for (event in blurEvents) { + if (isLoggable) { + Log.d(TAG, "blur applied for $event") + } + blurInteractor.onBlurApplied(event) + } + } + + launchTraced("WindowRootViewModel#blurState") { + combine(blurInteractor.blurRadius, blurInteractor.isBlurOpaque, ::BlurState) + .collect { _blurState.value = it } + } + + launchTraced("WindowRootViewModel#bouncerTransitions") { + primaryBouncerTransitions + .map { transition -> + transition.windowBlurRadius.onEach { blurRadius -> + if (isLoggable) { + Log.d( + TAG, + "${transition.javaClass.simpleName} windowBlurRadius $blurRadius", + ) + } + } + } + .merge() + .collect { blurRadius -> + blurInteractor.requestBlurForBouncer(blurRadius.toInt()) + } + } + } + awaitCancellation() + } + + fun onBlurApplied(blurRadius: Int) { + blurEvents.trySend(blurRadius) + } + + @AssistedFactory + interface Factory { + fun create(): WindowRootViewModel + } + + private companion object { + const val TAG = "WindowRootViewModel" + val isLoggable = Log.isLoggable(TAG, Log.DEBUG) || Build.isDebuggable() + } +} + +/** + * @property radius Radius of blur to be applied on the window root view. + * @property isOpaque Whether the blur applied is opaque or transparent. + */ +data class BlurState(val radius: Int, val isOpaque: Boolean) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 4b11e2c24722..e68153ad2606 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -22,6 +22,7 @@ import android.platform.test.annotations.RequiresFlagsDisabled import android.platform.test.flag.junit.FlagsParameterization import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper +import android.view.Choreographer import android.view.KeyEvent import android.view.MotionEvent import android.view.View @@ -57,6 +58,7 @@ import com.android.systemui.settings.brightness.data.repository.BrightnessMirror import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor +import com.android.systemui.statusbar.BlurUtils import com.android.systemui.statusbar.DragDownHelper import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.NotificationInsetsController @@ -80,6 +82,7 @@ import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.window.ui.viewmodel.WindowRootViewModel import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.Dispatchers @@ -155,6 +158,9 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : @Mock lateinit var sysUIKeyEventHandler: SysUIKeyEventHandler @Mock lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor @Mock lateinit var alternateBouncerInteractor: AlternateBouncerInteractor + @Mock private lateinit var blurUtils: BlurUtils + @Mock private lateinit var choreographer: Choreographer + @Mock private lateinit var windowViewModelFactory: WindowRootViewModel.Factory private val notificationLaunchAnimationRepository = NotificationLaunchAnimationRepository() private val notificationLaunchAnimationInteractor = NotificationLaunchAnimationInteractor(notificationLaunchAnimationRepository) @@ -203,6 +209,9 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : fakeClock = FakeSystemClock() underTest = NotificationShadeWindowViewController( + blurUtils, + windowViewModelFactory, + choreographer, lockscreenShadeTransitionController, falsingCollector, sysuiStatusBarStateController, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt new file mode 100644 index 000000000000..15d00d9f6994 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt @@ -0,0 +1,23 @@ +/* + * 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.systemui.keyguard.ui.transitions + +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeBouncerTransition : PrimaryBouncerTransition { + override val windowBlurRadius: MutableStateFlow<Float> = MutableStateFlow(0.0f) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt new file mode 100644 index 000000000000..7281e03a5ea4 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt @@ -0,0 +1,21 @@ +/* + * 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.systemui.window.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.windowRootViewBlurRepository by Kosmos.Fixture { WindowRootViewBlurRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractorKosmos.kt new file mode 100644 index 000000000000..ad30ea26c5b8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.systemui.window.domain.interactor + +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.window.data.repository.windowRootViewBlurRepository + +val Kosmos.windowRootViewBlurInteractor by + Kosmos.Fixture { + WindowRootViewBlurInteractor( + repository = windowRootViewBlurRepository, + keyguardInteractor = keyguardInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelKosmos.kt new file mode 100644 index 000000000000..864048d51873 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelKosmos.kt @@ -0,0 +1,30 @@ +/* + * 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.systemui.window.ui.viewmodel + +import com.android.systemui.keyguard.ui.transitions.FakeBouncerTransition +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.window.domain.interactor.windowRootViewBlurInteractor +import org.mockito.internal.util.collections.Sets + +val Kosmos.fakeBouncerTransitions by + Kosmos.Fixture<Set<FakeBouncerTransition>> { + Sets.newSet(FakeBouncerTransition(), FakeBouncerTransition()) + } + +val Kosmos.windowRootViewModel by + Kosmos.Fixture { WindowRootViewModel(fakeBouncerTransitions, windowRootViewBlurInteractor) } |