summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt43
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractorTest.kt60
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt59
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt42
-rw-r--r--packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt91
-rw-r--r--packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt88
-rw-r--r--packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt106
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt9
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt21
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractorKosmos.kt29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelKosmos.kt30
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) }