Loading packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt +120 −30 Original line number Diff line number Diff line Loading @@ -23,8 +23,10 @@ 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.flags.DisableSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep Loading Loading @@ -76,7 +78,7 @@ class WallpaperFocalAreaViewModelTest : SysuiTestCase() { ) ) ) .thenReturn(2f) .thenReturn(1f) kosmos.wallpaperFocalAreaInteractor = WallpaperFocalAreaInteractor( context = kosmos.mockedContext, Loading @@ -91,58 +93,146 @@ class WallpaperFocalAreaViewModelTest : SysuiTestCase() { } @Test @DisableSceneContainer fun focalAreaBoundsSent_whenFinishTransitioningToLockscreen() = testScope.runTest { overrideMockedResources( mockedResources, OverrideResources( screenWidth = 1600, screenHeight = 2000, centerAlignFocalArea = false, ), ) val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.wallpaperFocalAreaRepository.setHasFocalArea(true) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf( TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN), TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN), ), listOf(TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN)), testScope, ) setTestFocalAreaBounds() kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN)), testScope, ) assertThat(bounds).isNotNull() } @Test @DisableSceneContainer fun wallpaperHasFocalArea_shouldSendBounds() = testScope.runTest { val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.wallpaperFocalAreaRepository.setHasFocalArea(true) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN)), testScope, ) setTestFocalAreaBounds() kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN)), testScope, ) assertThat(bounds).isEqualTo(RectF(400F, 510F, 1200F, 700F)) assertThat(bounds).isNotNull() } @Test fun focalAreaBoundsNotSent_whenNotFinishTransitioningToLockscreen() = @DisableSceneContainer fun wallpaperDoesNotHaveFocalArea_shouldNotSendBounds() = testScope.runTest { overrideMockedResources( mockedResources, OverrideResources( screenWidth = 1600, screenHeight = 2000, centerAlignFocalArea = false, ), val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.wallpaperFocalAreaRepository.setHasFocalArea(false) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN)), testScope, ) kosmos.wallpaperFocalAreaRepository.setWallpaperFocalAreaBounds( DEFAULT_FOCAL_AREA_BOUNDS ) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN)), testScope, ) assertThat(bounds).isNull() } @Test @DisableSceneContainer fun boundsChangeWhenGoingFromLockscreenToGone_shouldNotSendBounds() = testScope.runTest { val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.wallpaperFocalAreaRepository.setHasFocalArea(true) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN)), testScope, ) setTestFocalAreaBounds() kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN)), testScope, ) assertThat(bounds?.top).isEqualTo(20F) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf( TransitionStep( transitionState = TransitionState.STARTED, from = LOCKSCREEN, to = GONE, ) ), testScope, ) setTestFocalAreaBounds( activeNotifs = 3, shortcutAbsoluteTop = 400F, notificationDefaultTop = 20F, notificationStackAbsoluteBottom = 200F, ) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf( TransitionStep( transitionState = TransitionState.FINISHED, from = LOCKSCREEN, to = GONE, ) ), testScope, ) assertThat(bounds?.top).isEqualTo(20F) } @Test @DisableSceneContainer fun boundsChangeOnLockscreen_shouldSendBounds() = testScope.runTest { val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.wallpaperFocalAreaRepository.setHasFocalArea(true) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN)), testScope, ) setTestFocalAreaBounds() assertThat(bounds).isEqualTo(null) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN)), testScope, ) assertThat(bounds).isNotNull() } private fun setTestFocalAreaBounds( shadeLayoutWide: Boolean = false, activeNotifs: Int = 0, shortcutAbsoluteTop: Float = 400F, notificationDefaultTop: Float = 20F, notificationStackAbsoluteBottom: Float = 20F, ) { kosmos.shadeRepository.setShadeLayoutWide(shadeLayoutWide) kosmos.activeNotificationListRepository.setActiveNotifs(activeNotifs) kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(shortcutAbsoluteTop) kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(notificationDefaultTop) kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom( notificationStackAbsoluteBottom ) } private fun setTestFocalAreaBounds() { kosmos.shadeRepository.setShadeLayoutWide(false) kosmos.activeNotificationListRepository.setActiveNotifs(0) kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(400F) kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(20F) kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(20F) companion object { val DEFAULT_FOCAL_AREA_BOUNDS = RectF(0f, 400f, 1000F, 1900F) } } packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +6 −13 Original line number Diff line number Diff line Loading @@ -362,20 +362,7 @@ object KeyguardRootViewBinder { } } } } } burnInParams.update { current -> current.copy( translationX = { childViews[burnInLayerId]?.translationX }, translationY = { childViews[burnInLayerId]?.translationY }, ) } disposables += view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { if (wallpaperFocalAreaViewModel.hasFocalArea.value) { launch { wallpaperFocalAreaViewModel.wallpaperFocalAreaBounds.collect { wallpaperFocalAreaViewModel.setFocalAreaBounds(it) Loading @@ -383,6 +370,12 @@ object KeyguardRootViewBinder { } } } burnInParams.update { current -> current.copy( translationX = { childViews[burnInLayerId]?.translationX }, translationY = { childViews[burnInLayerId]?.translationY }, ) } disposables += Loading packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt +7 −0 Original line number Diff line number Diff line Loading @@ -180,6 +180,13 @@ constructor( .map { val focalAreaTarget = context.resources.getString(SysUIR.string.focal_area_target) val shouldSendNotificationLayout = it?.component?.className == focalAreaTarget if (DEBUG) { Log.d( TAG, "shouldSendNotificationLayout:$shouldSendNotificationLayout " + "wallpaperInfo:${it?.component?.className}", ) } shouldSendNotificationLayout } .stateIn( Loading packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt +33 −25 Original line number Diff line number Diff line Loading @@ -24,10 +24,11 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractor import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.flatMapLatest class WallpaperFocalAreaViewModel @Inject Loading @@ -37,32 +38,39 @@ constructor( ) { val hasFocalArea = wallpaperFocalAreaInteractor.hasFocalArea @OptIn(ExperimentalCoroutinesApi::class) val wallpaperFocalAreaBounds = hasFocalArea.flatMapLatest { hasFocalArea -> if (hasFocalArea) { combine( wallpaperFocalAreaInteractor.wallpaperFocalAreaBounds, keyguardTransitionInteractor.startedKeyguardTransitionStep, // Emit transition state when FINISHED instead of STARTED to avoid race with // wakingup command, causing layout change command not be received. // Emit bounds when finishing transition to LOCKSCREEN to avoid race // condition with COMMAND_WAKING_UP keyguardTransitionInteractor .transition( edge = Edge.create(to = Scenes.Lockscreen), edgeWithoutSceneContainer = Edge.create(to = KeyguardState.LOCKSCREEN), edgeWithoutSceneContainer = Edge.create(to = KeyguardState.LOCKSCREEN), ) .filter { it.transitionState == TransitionState.FINISHED }, ::Triple, ::Pair, ) .map { (bounds, startedStep, _) -> // Avoid sending wrong bounds when transitioning from LOCKSCREEN to GONE .flatMapLatest { (startedStep, _) -> // Subscribe to bounds within the period of transitioning to the lockscreen, // prior to any transitions away. if ( startedStep.to == KeyguardState.LOCKSCREEN && startedStep.from != KeyguardState.LOCKSCREEN ) { bounds wallpaperFocalAreaInteractor.wallpaperFocalAreaBounds } else { null emptyFlow() } } } else { emptyFlow() } } .filterNotNull() fun setFocalAreaBounds(bounds: RectF) { wallpaperFocalAreaInteractor.setFocalAreaBounds(bounds) Loading packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt +4 −0 Original line number Diff line number Diff line Loading @@ -62,4 +62,8 @@ class FakeWallpaperFocalAreaRepository : WallpaperFocalAreaRepository { override fun setTapPosition(tapPosition: PointF) { _wallpaperFocalAreaTapPosition.value = tapPosition } fun setHasFocalArea(hasFocalArea: Boolean) { _hasFocalArea.value = hasFocalArea } } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt +120 −30 Original line number Diff line number Diff line Loading @@ -23,8 +23,10 @@ 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.flags.DisableSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep Loading Loading @@ -76,7 +78,7 @@ class WallpaperFocalAreaViewModelTest : SysuiTestCase() { ) ) ) .thenReturn(2f) .thenReturn(1f) kosmos.wallpaperFocalAreaInteractor = WallpaperFocalAreaInteractor( context = kosmos.mockedContext, Loading @@ -91,58 +93,146 @@ class WallpaperFocalAreaViewModelTest : SysuiTestCase() { } @Test @DisableSceneContainer fun focalAreaBoundsSent_whenFinishTransitioningToLockscreen() = testScope.runTest { overrideMockedResources( mockedResources, OverrideResources( screenWidth = 1600, screenHeight = 2000, centerAlignFocalArea = false, ), ) val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.wallpaperFocalAreaRepository.setHasFocalArea(true) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf( TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN), TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN), ), listOf(TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN)), testScope, ) setTestFocalAreaBounds() kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN)), testScope, ) assertThat(bounds).isNotNull() } @Test @DisableSceneContainer fun wallpaperHasFocalArea_shouldSendBounds() = testScope.runTest { val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.wallpaperFocalAreaRepository.setHasFocalArea(true) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN)), testScope, ) setTestFocalAreaBounds() kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN)), testScope, ) assertThat(bounds).isEqualTo(RectF(400F, 510F, 1200F, 700F)) assertThat(bounds).isNotNull() } @Test fun focalAreaBoundsNotSent_whenNotFinishTransitioningToLockscreen() = @DisableSceneContainer fun wallpaperDoesNotHaveFocalArea_shouldNotSendBounds() = testScope.runTest { overrideMockedResources( mockedResources, OverrideResources( screenWidth = 1600, screenHeight = 2000, centerAlignFocalArea = false, ), val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.wallpaperFocalAreaRepository.setHasFocalArea(false) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN)), testScope, ) kosmos.wallpaperFocalAreaRepository.setWallpaperFocalAreaBounds( DEFAULT_FOCAL_AREA_BOUNDS ) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN)), testScope, ) assertThat(bounds).isNull() } @Test @DisableSceneContainer fun boundsChangeWhenGoingFromLockscreenToGone_shouldNotSendBounds() = testScope.runTest { val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.wallpaperFocalAreaRepository.setHasFocalArea(true) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN)), testScope, ) setTestFocalAreaBounds() kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN)), testScope, ) assertThat(bounds?.top).isEqualTo(20F) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf( TransitionStep( transitionState = TransitionState.STARTED, from = LOCKSCREEN, to = GONE, ) ), testScope, ) setTestFocalAreaBounds( activeNotifs = 3, shortcutAbsoluteTop = 400F, notificationDefaultTop = 20F, notificationStackAbsoluteBottom = 200F, ) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf( TransitionStep( transitionState = TransitionState.FINISHED, from = LOCKSCREEN, to = GONE, ) ), testScope, ) assertThat(bounds?.top).isEqualTo(20F) } @Test @DisableSceneContainer fun boundsChangeOnLockscreen_shouldSendBounds() = testScope.runTest { val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.wallpaperFocalAreaRepository.setHasFocalArea(true) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN)), testScope, ) setTestFocalAreaBounds() assertThat(bounds).isEqualTo(null) kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( listOf(TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN)), testScope, ) assertThat(bounds).isNotNull() } private fun setTestFocalAreaBounds( shadeLayoutWide: Boolean = false, activeNotifs: Int = 0, shortcutAbsoluteTop: Float = 400F, notificationDefaultTop: Float = 20F, notificationStackAbsoluteBottom: Float = 20F, ) { kosmos.shadeRepository.setShadeLayoutWide(shadeLayoutWide) kosmos.activeNotificationListRepository.setActiveNotifs(activeNotifs) kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(shortcutAbsoluteTop) kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(notificationDefaultTop) kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom( notificationStackAbsoluteBottom ) } private fun setTestFocalAreaBounds() { kosmos.shadeRepository.setShadeLayoutWide(false) kosmos.activeNotificationListRepository.setActiveNotifs(0) kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(400F) kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(20F) kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(20F) companion object { val DEFAULT_FOCAL_AREA_BOUNDS = RectF(0f, 400f, 1000F, 1900F) } }
packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +6 −13 Original line number Diff line number Diff line Loading @@ -362,20 +362,7 @@ object KeyguardRootViewBinder { } } } } } burnInParams.update { current -> current.copy( translationX = { childViews[burnInLayerId]?.translationX }, translationY = { childViews[burnInLayerId]?.translationY }, ) } disposables += view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { if (wallpaperFocalAreaViewModel.hasFocalArea.value) { launch { wallpaperFocalAreaViewModel.wallpaperFocalAreaBounds.collect { wallpaperFocalAreaViewModel.setFocalAreaBounds(it) Loading @@ -383,6 +370,12 @@ object KeyguardRootViewBinder { } } } burnInParams.update { current -> current.copy( translationX = { childViews[burnInLayerId]?.translationX }, translationY = { childViews[burnInLayerId]?.translationY }, ) } disposables += Loading
packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt +7 −0 Original line number Diff line number Diff line Loading @@ -180,6 +180,13 @@ constructor( .map { val focalAreaTarget = context.resources.getString(SysUIR.string.focal_area_target) val shouldSendNotificationLayout = it?.component?.className == focalAreaTarget if (DEBUG) { Log.d( TAG, "shouldSendNotificationLayout:$shouldSendNotificationLayout " + "wallpaperInfo:${it?.component?.className}", ) } shouldSendNotificationLayout } .stateIn( Loading
packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt +33 −25 Original line number Diff line number Diff line Loading @@ -24,10 +24,11 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractor import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.flatMapLatest class WallpaperFocalAreaViewModel @Inject Loading @@ -37,32 +38,39 @@ constructor( ) { val hasFocalArea = wallpaperFocalAreaInteractor.hasFocalArea @OptIn(ExperimentalCoroutinesApi::class) val wallpaperFocalAreaBounds = hasFocalArea.flatMapLatest { hasFocalArea -> if (hasFocalArea) { combine( wallpaperFocalAreaInteractor.wallpaperFocalAreaBounds, keyguardTransitionInteractor.startedKeyguardTransitionStep, // Emit transition state when FINISHED instead of STARTED to avoid race with // wakingup command, causing layout change command not be received. // Emit bounds when finishing transition to LOCKSCREEN to avoid race // condition with COMMAND_WAKING_UP keyguardTransitionInteractor .transition( edge = Edge.create(to = Scenes.Lockscreen), edgeWithoutSceneContainer = Edge.create(to = KeyguardState.LOCKSCREEN), edgeWithoutSceneContainer = Edge.create(to = KeyguardState.LOCKSCREEN), ) .filter { it.transitionState == TransitionState.FINISHED }, ::Triple, ::Pair, ) .map { (bounds, startedStep, _) -> // Avoid sending wrong bounds when transitioning from LOCKSCREEN to GONE .flatMapLatest { (startedStep, _) -> // Subscribe to bounds within the period of transitioning to the lockscreen, // prior to any transitions away. if ( startedStep.to == KeyguardState.LOCKSCREEN && startedStep.from != KeyguardState.LOCKSCREEN ) { bounds wallpaperFocalAreaInteractor.wallpaperFocalAreaBounds } else { null emptyFlow() } } } else { emptyFlow() } } .filterNotNull() fun setFocalAreaBounds(bounds: RectF) { wallpaperFocalAreaInteractor.setFocalAreaBounds(bounds) Loading
packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt +4 −0 Original line number Diff line number Diff line Loading @@ -62,4 +62,8 @@ class FakeWallpaperFocalAreaRepository : WallpaperFocalAreaRepository { override fun setTapPosition(tapPosition: PointF) { _wallpaperFocalAreaTapPosition.value = tapPosition } fun setHasFocalArea(hasFocalArea: Boolean) { _hasFocalArea.value = hasFocalArea } }