diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 311990c45d43c..e5cd0c55027bd 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -844,4 +844,44 @@
true
+
+
+
+ true
+
+ 0.67
+
+ 436dp
+
+ 436dp
+
+ #D9D9D9
+
+ 0.1
+
+ 0.5
+
+ 0.5
+
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt
new file mode 100644
index 0000000000000..c48028c31cf0b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.data.model
+
+import com.android.systemui.multishade.shared.model.ShadeId
+
+/** Models the current interaction with one of the shades. */
+data class MultiShadeInteractionModel(
+ /** The ID of the shade that the user is currently interacting with. */
+ val shadeId: ShadeId,
+ /** Whether the interaction is proxied (as in: coming from an external app or different UI). */
+ val isProxied: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt
new file mode 100644
index 0000000000000..86f0c0d15b55b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.data.remoteproxy
+
+import com.android.systemui.multishade.shared.model.ProxiedInputModel
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+
+/**
+ * Acts as a hub for routing proxied user input into the multi shade system.
+ *
+ * "Proxied" user input is coming through a proxy; typically from an external app or different UI.
+ * In other words: it's not user input that's occurring directly on the shade UI itself. This class
+ * is that proxy.
+ */
+@Singleton
+class MultiShadeInputProxy @Inject constructor() {
+ private val _proxiedTouch =
+ MutableSharedFlow(
+ replay = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST,
+ )
+ val proxiedInput: Flow = _proxiedTouch.asSharedFlow()
+
+ fun onProxiedInput(proxiedInput: ProxiedInputModel) {
+ _proxiedTouch.tryEmit(proxiedInput)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt
new file mode 100644
index 0000000000000..1172030127575
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.data.repository
+
+import android.content.Context
+import androidx.annotation.FloatRange
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
+import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
+import com.android.systemui.multishade.shared.model.ProxiedInputModel
+import com.android.systemui.multishade.shared.model.ShadeConfig
+import com.android.systemui.multishade.shared.model.ShadeId
+import com.android.systemui.multishade.shared.model.ShadeModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Encapsulates application state for all shades. */
+@SysUISingleton
+class MultiShadeRepository
+@Inject
+constructor(
+ @Application private val applicationContext: Context,
+ inputProxy: MultiShadeInputProxy,
+) {
+ /**
+ * Remote input coming from sources outside of system UI (for example, swiping down on the
+ * Launcher or from the status bar).
+ */
+ val proxiedInput: Flow = inputProxy.proxiedInput
+
+ /** Width of the left-hand side shade, in pixels. */
+ private val leftShadeWidthPx =
+ applicationContext.resources.getDimensionPixelSize(R.dimen.left_shade_width)
+
+ /** Width of the right-hand side shade, in pixels. */
+ private val rightShadeWidthPx =
+ applicationContext.resources.getDimensionPixelSize(R.dimen.right_shade_width)
+
+ /**
+ * The amount that the user must swipe up when the shade is fully expanded to automatically
+ * collapse once the user lets go of the shade. If the user swipes less than this amount, the
+ * shade will automatically revert back to fully expanded once the user stops swiping.
+ *
+ * This is a fraction between `0` and `1`.
+ */
+ private val swipeCollapseThreshold =
+ checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_collapse_threshold))
+
+ /**
+ * The amount that the user must swipe down when the shade is fully collapsed to automatically
+ * expand once the user lets go of the shade. If the user swipes less than this amount, the
+ * shade will automatically revert back to fully collapsed once the user stops swiping.
+ *
+ * This is a fraction between `0` and `1`.
+ */
+ private val swipeExpandThreshold =
+ checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_expand_threshold))
+
+ /**
+ * Maximum opacity when the scrim that shows up behind the dual shades is fully visible.
+ *
+ * This is a fraction between `0` and `1`.
+ */
+ private val dualShadeScrimAlpha =
+ checkInBounds(applicationContext.resources.getFloat(R.dimen.dual_shade_scrim_alpha))
+
+ /** The current configuration of the shade system. */
+ val shadeConfig: StateFlow =
+ MutableStateFlow(
+ if (applicationContext.resources.getBoolean(R.bool.dual_shade_enabled)) {
+ ShadeConfig.DualShadeConfig(
+ leftShadeWidthPx = leftShadeWidthPx,
+ rightShadeWidthPx = rightShadeWidthPx,
+ swipeCollapseThreshold = swipeCollapseThreshold,
+ swipeExpandThreshold = swipeExpandThreshold,
+ splitFraction =
+ applicationContext.resources.getFloat(
+ R.dimen.dual_shade_split_fraction
+ ),
+ scrimAlpha = dualShadeScrimAlpha,
+ )
+ } else {
+ ShadeConfig.SingleShadeConfig(
+ swipeCollapseThreshold = swipeCollapseThreshold,
+ swipeExpandThreshold = swipeExpandThreshold,
+ )
+ }
+ )
+ .asStateFlow()
+
+ private val _forceCollapseAll = MutableStateFlow(false)
+ /** Whether all shades should be collapsed. */
+ val forceCollapseAll: StateFlow = _forceCollapseAll.asStateFlow()
+
+ private val _shadeInteraction = MutableStateFlow(null)
+ /** The current shade interaction or `null` if no shade is interacted with currently. */
+ val shadeInteraction: StateFlow = _shadeInteraction.asStateFlow()
+
+ private val stateByShade = mutableMapOf>()
+
+ /** The model for the shade with the given ID. */
+ fun getShade(
+ shadeId: ShadeId,
+ ): StateFlow {
+ return getMutableShade(shadeId).asStateFlow()
+ }
+
+ /** Sets the expansion amount for the shade with the given ID. */
+ fun setExpansion(
+ shadeId: ShadeId,
+ @FloatRange(from = 0.0, to = 1.0) expansion: Float,
+ ) {
+ getMutableShade(shadeId).let { mutableState ->
+ mutableState.value = mutableState.value.copy(expansion = expansion)
+ }
+ }
+
+ /** Sets whether all shades should be immediately forced to collapse. */
+ fun setForceCollapseAll(isForced: Boolean) {
+ _forceCollapseAll.value = isForced
+ }
+
+ /** Sets the current shade interaction; use `null` if no shade is interacted with currently. */
+ fun setShadeInteraction(shadeInteraction: MultiShadeInteractionModel?) {
+ _shadeInteraction.value = shadeInteraction
+ }
+
+ private fun getMutableShade(id: ShadeId): MutableStateFlow {
+ return stateByShade.getOrPut(id) { MutableStateFlow(ShadeModel(id)) }
+ }
+
+ /** Asserts that the given [Float] is in the range of `0` and `1`, inclusive. */
+ private fun checkInBounds(float: Float): Float {
+ check(float in 0f..1f) { "$float isn't between 0 and 1." }
+ return float
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt
new file mode 100644
index 0000000000000..ee1dd65b867f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.shared.model
+
+import androidx.annotation.FloatRange
+
+/**
+ * Models a part of an ongoing proxied user input gesture.
+ *
+ * "Proxied" user input is coming through a proxy; typically from an external app or different UI.
+ * In other words: it's not user input that's occurring directly on the shade UI itself.
+ */
+sealed class ProxiedInputModel {
+ /** The user is dragging their pointer. */
+ data class OnDrag(
+ /**
+ * The relative position of the pointer as a fraction of its container width where `0` is
+ * all the way to the left and `1` is all the way to the right.
+ */
+ @FloatRange(from = 0.0, to = 1.0) val xFraction: Float,
+ /** The amount that the pointer was dragged, in pixels. */
+ val yDragAmountPx: Float,
+ ) : ProxiedInputModel()
+
+ /** The user finished dragging by lifting up their pointer. */
+ object OnDragEnd : ProxiedInputModel()
+
+ /**
+ * The drag gesture has been canceled. Usually because the pointer exited the draggable area.
+ */
+ object OnDragCancel : ProxiedInputModel()
+
+ /** The user has tapped (clicked). */
+ object OnTap : ProxiedInputModel()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt
new file mode 100644
index 0000000000000..a4cd35c8a11ab
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.shared.model
+
+import androidx.annotation.FloatRange
+
+/** Enumerates the various possible configurations of the shade system. */
+sealed class ShadeConfig(
+
+ /** IDs of the shade(s) in this configuration. */
+ open val shadeIds: List,
+
+ /**
+ * The amount that the user must swipe up when the shade is fully expanded to automatically
+ * collapse once the user lets go of the shade. If the user swipes less than this amount, the
+ * shade will automatically revert back to fully expanded once the user stops swiping.
+ */
+ @FloatRange(from = 0.0, to = 1.0) open val swipeCollapseThreshold: Float,
+
+ /**
+ * The amount that the user must swipe down when the shade is fully collapsed to automatically
+ * expand once the user lets go of the shade. If the user swipes less than this amount, the
+ * shade will automatically revert back to fully collapsed once the user stops swiping.
+ */
+ @FloatRange(from = 0.0, to = 1.0) open val swipeExpandThreshold: Float,
+) {
+
+ /** There is a single shade. */
+ data class SingleShadeConfig(
+ @FloatRange(from = 0.0, to = 1.0) override val swipeCollapseThreshold: Float,
+ @FloatRange(from = 0.0, to = 1.0) override val swipeExpandThreshold: Float,
+ ) :
+ ShadeConfig(
+ shadeIds = listOf(ShadeId.SINGLE),
+ swipeCollapseThreshold = swipeCollapseThreshold,
+ swipeExpandThreshold = swipeExpandThreshold,
+ )
+
+ /** There are two shades arranged side-by-side. */
+ data class DualShadeConfig(
+ /** Width of the left-hand side shade. */
+ val leftShadeWidthPx: Int,
+ /** Width of the right-hand side shade. */
+ val rightShadeWidthPx: Int,
+ @FloatRange(from = 0.0, to = 1.0) override val swipeCollapseThreshold: Float,
+ @FloatRange(from = 0.0, to = 1.0) override val swipeExpandThreshold: Float,
+ /**
+ * The position of the "split" between interaction areas for each of the shades, as a
+ * fraction of the width of the container.
+ *
+ * Interactions that occur on the start-side (left-hand side in left-to-right languages like
+ * English) affect the start-side shade. Interactions that occur on the end-side (right-hand
+ * side in left-to-right languages like English) affect the end-side shade.
+ */
+ @FloatRange(from = 0.0, to = 1.0) val splitFraction: Float,
+ /** Maximum opacity when the scrim that shows up behind the dual shades is fully visible. */
+ @FloatRange(from = 0.0, to = 1.0) val scrimAlpha: Float,
+ ) :
+ ShadeConfig(
+ shadeIds = listOf(ShadeId.LEFT, ShadeId.RIGHT),
+ swipeCollapseThreshold = swipeCollapseThreshold,
+ swipeExpandThreshold = swipeExpandThreshold,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt
new file mode 100644
index 0000000000000..9e026576e8427
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.shared.model
+
+/** Enumerates all known shade IDs. */
+enum class ShadeId {
+ /** ID of the shade on the left in dual shade configurations. */
+ LEFT,
+ /** ID of the shade on the right in dual shade configurations. */
+ RIGHT,
+ /** ID of the single shade in single shade configurations. */
+ SINGLE,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt
new file mode 100644
index 0000000000000..49ac64c58cb83
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.shared.model
+
+import androidx.annotation.FloatRange
+
+/** Models the current state of a shade. */
+data class ShadeModel(
+ val id: ShadeId,
+ @FloatRange(from = 0.0, to = 1.0) val expansion: Float = 0f,
+)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt
new file mode 100644
index 0000000000000..ceacaf9557ca4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.multishade.data.repository
+
+import android.content.Context
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
+import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
+import com.android.systemui.multishade.shared.model.ProxiedInputModel
+import com.android.systemui.multishade.shared.model.ShadeConfig
+import com.android.systemui.multishade.shared.model.ShadeId
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class MultiShadeRepositoryTest : SysuiTestCase() {
+
+ private lateinit var inputProxy: MultiShadeInputProxy
+
+ @Before
+ fun setUp() {
+ inputProxy = MultiShadeInputProxy()
+ }
+
+ @Test
+ fun proxiedInput() = runTest {
+ val underTest = create()
+ val latest: ProxiedInputModel? by collectLastValue(underTest.proxiedInput)
+
+ assertWithMessage("proxiedInput should start with null").that(latest).isNull()
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
+ assertThat(latest).isEqualTo(ProxiedInputModel.OnTap)
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 100f))
+ assertThat(latest).isEqualTo(ProxiedInputModel.OnDrag(0f, 100f))
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 120f))
+ assertThat(latest).isEqualTo(ProxiedInputModel.OnDrag(0f, 120f))
+
+ inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
+ assertThat(latest).isEqualTo(ProxiedInputModel.OnDragEnd)
+ }
+
+ @Test
+ fun shadeConfig_dualShadeEnabled() = runTest {
+ overrideResource(R.bool.dual_shade_enabled, true)
+ val underTest = create()
+ val shadeConfig: ShadeConfig? by collectLastValue(underTest.shadeConfig)
+
+ assertThat(shadeConfig).isInstanceOf(ShadeConfig.DualShadeConfig::class.java)
+ }
+
+ @Test
+ fun shadeConfig_dualShadeNotEnabled() = runTest {
+ overrideResource(R.bool.dual_shade_enabled, false)
+ val underTest = create()
+ val shadeConfig: ShadeConfig? by collectLastValue(underTest.shadeConfig)
+
+ assertThat(shadeConfig).isInstanceOf(ShadeConfig.SingleShadeConfig::class.java)
+ }
+
+ @Test
+ fun forceCollapseAll() = runTest {
+ val underTest = create()
+ val forceCollapseAll: Boolean? by collectLastValue(underTest.forceCollapseAll)
+
+ assertWithMessage("forceCollapseAll should start as false!")
+ .that(forceCollapseAll)
+ .isFalse()
+
+ underTest.setForceCollapseAll(true)
+ assertThat(forceCollapseAll).isTrue()
+
+ underTest.setForceCollapseAll(false)
+ assertThat(forceCollapseAll).isFalse()
+ }
+
+ @Test
+ fun shadeInteraction() = runTest {
+ val underTest = create()
+ val shadeInteraction: MultiShadeInteractionModel? by
+ collectLastValue(underTest.shadeInteraction)
+
+ assertWithMessage("shadeInteraction should start as null!").that(shadeInteraction).isNull()
+
+ underTest.setShadeInteraction(
+ MultiShadeInteractionModel(shadeId = ShadeId.LEFT, isProxied = false)
+ )
+ assertThat(shadeInteraction)
+ .isEqualTo(MultiShadeInteractionModel(shadeId = ShadeId.LEFT, isProxied = false))
+
+ underTest.setShadeInteraction(
+ MultiShadeInteractionModel(shadeId = ShadeId.RIGHT, isProxied = true)
+ )
+ assertThat(shadeInteraction)
+ .isEqualTo(MultiShadeInteractionModel(shadeId = ShadeId.RIGHT, isProxied = true))
+
+ underTest.setShadeInteraction(null)
+ assertThat(shadeInteraction).isNull()
+ }
+
+ @Test
+ fun expansion() = runTest {
+ val underTest = create()
+ val leftExpansion: Float? by
+ collectLastValue(underTest.getShade(ShadeId.LEFT).map { it.expansion })
+ val rightExpansion: Float? by
+ collectLastValue(underTest.getShade(ShadeId.RIGHT).map { it.expansion })
+ val singleExpansion: Float? by
+ collectLastValue(underTest.getShade(ShadeId.SINGLE).map { it.expansion })
+
+ assertWithMessage("expansion should start as 0!").that(leftExpansion).isZero()
+ assertWithMessage("expansion should start as 0!").that(rightExpansion).isZero()
+ assertWithMessage("expansion should start as 0!").that(singleExpansion).isZero()
+
+ underTest.setExpansion(
+ shadeId = ShadeId.LEFT,
+ 0.4f,
+ )
+ assertThat(leftExpansion).isEqualTo(0.4f)
+ assertThat(rightExpansion).isEqualTo(0f)
+ assertThat(singleExpansion).isEqualTo(0f)
+
+ underTest.setExpansion(
+ shadeId = ShadeId.RIGHT,
+ 0.73f,
+ )
+ assertThat(leftExpansion).isEqualTo(0.4f)
+ assertThat(rightExpansion).isEqualTo(0.73f)
+ assertThat(singleExpansion).isEqualTo(0f)
+
+ underTest.setExpansion(
+ shadeId = ShadeId.LEFT,
+ 0.1f,
+ )
+ underTest.setExpansion(
+ shadeId = ShadeId.SINGLE,
+ 0.88f,
+ )
+ assertThat(leftExpansion).isEqualTo(0.1f)
+ assertThat(rightExpansion).isEqualTo(0.73f)
+ assertThat(singleExpansion).isEqualTo(0.88f)
+ }
+
+ private fun create(): MultiShadeRepository {
+ return create(
+ context = context,
+ inputProxy = inputProxy,
+ )
+ }
+
+ companion object {
+ fun create(
+ context: Context,
+ inputProxy: MultiShadeInputProxy,
+ ): MultiShadeRepository {
+ return MultiShadeRepository(
+ applicationContext = context,
+ inputProxy = inputProxy,
+ )
+ }
+ }
+}