Multi-shade foundation - domain layer (2/5).
Early foundation for the multi-shade framework. Domain layer for the multi-shade foundation. Includes an interactor with methods, each of which representing a use-case / business logic that the system can do. Bug: 272130181 Test: includes unit tests for code in this layer. Tested end-to-end in the last CL in this chain. Change-Id: Iba722a17ac20f9d355c726481ca7bd79123b12ee
This commit is contained in:
committed by
Ale Nijamkin
parent
00b49c070a
commit
664a12de42
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* 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.domain.interactor
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
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.data.repository.MultiShadeRepository
|
||||
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.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.yield
|
||||
|
||||
/** Encapsulates business logic related to interactions with the multi-shade system. */
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@SysUISingleton
|
||||
class MultiShadeInteractor
|
||||
@Inject
|
||||
constructor(
|
||||
@Application private val applicationScope: CoroutineScope,
|
||||
private val repository: MultiShadeRepository,
|
||||
private val inputProxy: MultiShadeInputProxy,
|
||||
) {
|
||||
/** The current configuration of the shade system. */
|
||||
val shadeConfig: StateFlow<ShadeConfig> = repository.shadeConfig
|
||||
|
||||
/** The expansion of the shade that's most expanded. */
|
||||
val maxShadeExpansion: Flow<Float> =
|
||||
repository.shadeConfig.flatMapLatest { shadeConfig ->
|
||||
combine(allShades(shadeConfig)) { shadeModels ->
|
||||
shadeModels.maxOfOrNull { it.expansion } ?: 0f
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A _processed_ version of the proxied input flow.
|
||||
*
|
||||
* All internal dependencies on the proxied input flow *must* use this one for two reasons:
|
||||
* 1. It's a [SharedFlow] so we only do the upstream work once, no matter how many usages we
|
||||
* actually have.
|
||||
* 2. It actually does some preprocessing as the proxied input events stream through, handling
|
||||
* common things like recording the current state of the system based on incoming input
|
||||
* events.
|
||||
*/
|
||||
private val processedProxiedInput: SharedFlow<ProxiedInputModel> =
|
||||
combine(
|
||||
repository.shadeConfig,
|
||||
repository.proxiedInput.distinctUntilChanged(),
|
||||
::Pair,
|
||||
)
|
||||
.map { (shadeConfig, proxiedInput) ->
|
||||
if (proxiedInput !is ProxiedInputModel.OnTap) {
|
||||
// If the user is interacting with any other gesture type (for instance,
|
||||
// dragging),
|
||||
// we no longer want to force collapse all shades.
|
||||
repository.setForceCollapseAll(false)
|
||||
}
|
||||
|
||||
when (proxiedInput) {
|
||||
is ProxiedInputModel.OnDrag -> {
|
||||
val affectedShadeId = affectedShadeId(shadeConfig, proxiedInput.xFraction)
|
||||
// This might be the start of a new drag gesture, let's update our
|
||||
// application
|
||||
// state to record that fact.
|
||||
onUserInteractionStarted(
|
||||
shadeId = affectedShadeId,
|
||||
isProxied = true,
|
||||
)
|
||||
}
|
||||
is ProxiedInputModel.OnTap -> {
|
||||
// Tapping outside any shade collapses all shades. This code path is not hit
|
||||
// for
|
||||
// taps that happen _inside_ a shade as that input event is directly applied
|
||||
// through the UI and is, hence, not a proxied input.
|
||||
collapseAll()
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
proxiedInput
|
||||
}
|
||||
.shareIn(
|
||||
scope = applicationScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
replay = 1,
|
||||
)
|
||||
|
||||
/** Whether the shade with the given ID should be visible. */
|
||||
fun isVisible(shadeId: ShadeId): Flow<Boolean> {
|
||||
return repository.shadeConfig.map { shadeConfig -> shadeConfig.shadeIds.contains(shadeId) }
|
||||
}
|
||||
|
||||
/** Whether direct user input is allowed on the shade with the given ID. */
|
||||
fun isNonProxiedInputAllowed(shadeId: ShadeId): Flow<Boolean> {
|
||||
return combine(
|
||||
isForceCollapsed(shadeId),
|
||||
repository.shadeInteraction,
|
||||
::Pair,
|
||||
)
|
||||
.map { (isForceCollapsed, shadeInteraction) ->
|
||||
!isForceCollapsed && shadeInteraction?.isProxied != true
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the shade with the given ID is forced to collapse. */
|
||||
fun isForceCollapsed(shadeId: ShadeId): Flow<Boolean> {
|
||||
return combine(
|
||||
repository.forceCollapseAll,
|
||||
repository.shadeInteraction.map { it?.shadeId },
|
||||
::Pair,
|
||||
)
|
||||
.map { (collapseAll, userInteractedShadeIdOrNull) ->
|
||||
val counterpartShadeIdOrNull =
|
||||
when (shadeId) {
|
||||
ShadeId.SINGLE -> null
|
||||
ShadeId.LEFT -> ShadeId.RIGHT
|
||||
ShadeId.RIGHT -> ShadeId.LEFT
|
||||
}
|
||||
|
||||
when {
|
||||
// If all shades have been told to collapse (by a tap outside, for example),
|
||||
// then this shade is collapsed.
|
||||
collapseAll -> true
|
||||
// A shade that doesn't have a counterpart shade cannot be force-collapsed by
|
||||
// interactions on the counterpart shade.
|
||||
counterpartShadeIdOrNull == null -> false
|
||||
// If the current user interaction is on the counterpart shade, then this shade
|
||||
// should be force-collapsed.
|
||||
else -> userInteractedShadeIdOrNull == counterpartShadeIdOrNull
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxied input affecting the shade with the given ID. This is input coming from sources
|
||||
* outside of system UI (for example, swiping down on the Launcher or from the status bar) or
|
||||
* outside the UI of any shade (for example, the scrim that's shown behind the shades).
|
||||
*/
|
||||
fun proxiedInput(shadeId: ShadeId): Flow<ProxiedInputModel?> {
|
||||
return combine(
|
||||
processedProxiedInput,
|
||||
isForceCollapsed(shadeId).distinctUntilChanged(),
|
||||
repository.shadeInteraction,
|
||||
::Triple,
|
||||
)
|
||||
.map { (proxiedInput, isForceCollapsed, shadeInteraction) ->
|
||||
when {
|
||||
// If the shade is force-collapsed, we ignored proxied input on it.
|
||||
isForceCollapsed -> null
|
||||
// If the proxied input does not belong to this shade, ignore it.
|
||||
shadeInteraction?.shadeId != shadeId -> null
|
||||
// If there is ongoing non-proxied user input on any shade, ignore the
|
||||
// proxied input.
|
||||
!shadeInteraction.isProxied -> null
|
||||
// Otherwise, send the proxied input downstream.
|
||||
else -> proxiedInput
|
||||
}
|
||||
}
|
||||
.onEach { proxiedInput ->
|
||||
// We use yield() to make sure that the following block of code happens _after_
|
||||
// downstream collectors had a chance to process the proxied input. Otherwise, we
|
||||
// might change our state to clear the current UserInteraction _before_ those
|
||||
// downstream collectors get a chance to process the proxied input, which will make
|
||||
// them ignore it (since they ignore proxied input when the current user interaction
|
||||
// doesn't match their shade).
|
||||
yield()
|
||||
|
||||
if (
|
||||
proxiedInput is ProxiedInputModel.OnDragEnd ||
|
||||
proxiedInput is ProxiedInputModel.OnDragCancel
|
||||
) {
|
||||
onUserInteractionEnded(shadeId = shadeId, isProxied = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the expansion amount for the shade with the given ID. */
|
||||
fun setExpansion(
|
||||
shadeId: ShadeId,
|
||||
@FloatRange(from = 0.0, to = 1.0) expansion: Float,
|
||||
) {
|
||||
repository.setExpansion(shadeId, expansion)
|
||||
}
|
||||
|
||||
/** Collapses all shades. */
|
||||
fun collapseAll() {
|
||||
repository.setForceCollapseAll(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that a new non-proxied interaction may have started. Safe to call multiple times for
|
||||
* the same interaction as it won't overwrite an existing interaction.
|
||||
*
|
||||
* Existing interactions can be cleared by calling [onUserInteractionEnded].
|
||||
*/
|
||||
fun onUserInteractionStarted(shadeId: ShadeId) {
|
||||
onUserInteractionStarted(
|
||||
shadeId = shadeId,
|
||||
isProxied = false,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that the current non-proxied interaction has ended.
|
||||
*
|
||||
* Safe to call multiple times, even if there's no current interaction or even if the current
|
||||
* interaction doesn't belong to the given shade or is proxied as the code is a no-op unless
|
||||
* there's a match between the parameters and the current interaction.
|
||||
*/
|
||||
fun onUserInteractionEnded(
|
||||
shadeId: ShadeId,
|
||||
) {
|
||||
onUserInteractionEnded(
|
||||
shadeId = shadeId,
|
||||
isProxied = false,
|
||||
)
|
||||
}
|
||||
|
||||
fun sendProxiedInput(proxiedInput: ProxiedInputModel) {
|
||||
inputProxy.onProxiedInput(proxiedInput)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that a new interaction may have started. Safe to call multiple times for the same
|
||||
* interaction as it won't overwrite an existing interaction.
|
||||
*
|
||||
* Existing interactions can be cleared by calling [onUserInteractionEnded].
|
||||
*/
|
||||
private fun onUserInteractionStarted(
|
||||
shadeId: ShadeId,
|
||||
isProxied: Boolean,
|
||||
) {
|
||||
if (repository.shadeInteraction.value != null) {
|
||||
return
|
||||
}
|
||||
|
||||
repository.setShadeInteraction(
|
||||
MultiShadeInteractionModel(
|
||||
shadeId = shadeId,
|
||||
isProxied = isProxied,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that the current interaction has ended.
|
||||
*
|
||||
* Safe to call multiple times, even if there's no current interaction or even if the current
|
||||
* interaction doesn't belong to the given shade or [isProxied] value as the code is a no-op
|
||||
* unless there's a match between the parameters and the current interaction.
|
||||
*/
|
||||
private fun onUserInteractionEnded(
|
||||
shadeId: ShadeId,
|
||||
isProxied: Boolean,
|
||||
) {
|
||||
repository.shadeInteraction.value?.let { (interactionShadeId, isInteractionProxied) ->
|
||||
if (shadeId == interactionShadeId && isProxied == isInteractionProxied) {
|
||||
repository.setShadeInteraction(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the shade that's affected by user input at a given coordinate.
|
||||
*
|
||||
* @param config The shade configuration being used.
|
||||
* @param xFraction The horizontal position of the user input as a fraction along the width of
|
||||
* its container where `0` is all the way to the left and `1` is all the way to the right.
|
||||
*/
|
||||
private fun affectedShadeId(
|
||||
config: ShadeConfig,
|
||||
@FloatRange(from = 0.0, to = 1.0) xFraction: Float,
|
||||
): ShadeId {
|
||||
return if (config is ShadeConfig.DualShadeConfig) {
|
||||
if (xFraction <= config.splitFraction) {
|
||||
ShadeId.LEFT
|
||||
} else {
|
||||
ShadeId.RIGHT
|
||||
}
|
||||
} else {
|
||||
ShadeId.SINGLE
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the list of flows of all the shades in the given configuration. */
|
||||
private fun allShades(
|
||||
config: ShadeConfig,
|
||||
): List<Flow<ShadeModel>> {
|
||||
return config.shadeIds.map { shadeId -> repository.getShade(shadeId) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* 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.domain.interactor
|
||||
|
||||
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.remoteproxy.MultiShadeInputProxy
|
||||
import com.android.systemui.multishade.data.repository.MultiShadeRepositoryTest
|
||||
import com.android.systemui.multishade.shared.model.ProxiedInputModel
|
||||
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.test.TestScope
|
||||
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 MultiShadeInteractorTest : SysuiTestCase() {
|
||||
|
||||
private lateinit var testScope: TestScope
|
||||
private lateinit var inputProxy: MultiShadeInputProxy
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
testScope = TestScope()
|
||||
inputProxy = MultiShadeInputProxy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun maxShadeExpansion() =
|
||||
testScope.runTest {
|
||||
val underTest = create()
|
||||
val maxShadeExpansion: Float? by collectLastValue(underTest.maxShadeExpansion)
|
||||
assertWithMessage("maxShadeExpansion must start with 0.0!")
|
||||
.that(maxShadeExpansion)
|
||||
.isEqualTo(0f)
|
||||
|
||||
underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0.441f)
|
||||
assertThat(maxShadeExpansion).isEqualTo(0.441f)
|
||||
|
||||
underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0.442f)
|
||||
assertThat(maxShadeExpansion).isEqualTo(0.442f)
|
||||
|
||||
underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0f)
|
||||
assertThat(maxShadeExpansion).isEqualTo(0.441f)
|
||||
|
||||
underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0f)
|
||||
assertThat(maxShadeExpansion).isEqualTo(0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isVisible_dualShadeConfig() =
|
||||
testScope.runTest {
|
||||
overrideResource(R.bool.dual_shade_enabled, true)
|
||||
val underTest = create()
|
||||
val isLeftShadeVisible: Boolean? by collectLastValue(underTest.isVisible(ShadeId.LEFT))
|
||||
val isRightShadeVisible: Boolean? by
|
||||
collectLastValue(underTest.isVisible(ShadeId.RIGHT))
|
||||
val isSingleShadeVisible: Boolean? by
|
||||
collectLastValue(underTest.isVisible(ShadeId.SINGLE))
|
||||
|
||||
assertThat(isLeftShadeVisible).isTrue()
|
||||
assertThat(isRightShadeVisible).isTrue()
|
||||
assertThat(isSingleShadeVisible).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isVisible_singleShadeConfig() =
|
||||
testScope.runTest {
|
||||
overrideResource(R.bool.dual_shade_enabled, false)
|
||||
val underTest = create()
|
||||
val isLeftShadeVisible: Boolean? by collectLastValue(underTest.isVisible(ShadeId.LEFT))
|
||||
val isRightShadeVisible: Boolean? by
|
||||
collectLastValue(underTest.isVisible(ShadeId.RIGHT))
|
||||
val isSingleShadeVisible: Boolean? by
|
||||
collectLastValue(underTest.isVisible(ShadeId.SINGLE))
|
||||
|
||||
assertThat(isLeftShadeVisible).isFalse()
|
||||
assertThat(isRightShadeVisible).isFalse()
|
||||
assertThat(isSingleShadeVisible).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isNonProxiedInputAllowed() =
|
||||
testScope.runTest {
|
||||
val underTest = create()
|
||||
val isLeftShadeNonProxiedInputAllowed: Boolean? by
|
||||
collectLastValue(underTest.isNonProxiedInputAllowed(ShadeId.LEFT))
|
||||
assertWithMessage("isNonProxiedInputAllowed should start as true!")
|
||||
.that(isLeftShadeNonProxiedInputAllowed)
|
||||
.isTrue()
|
||||
|
||||
// Need to collect proxied input so the flows become hot as the gesture cancelation code
|
||||
// logic sits in side the proxiedInput flow for each shade.
|
||||
collectLastValue(underTest.proxiedInput(ShadeId.LEFT))
|
||||
collectLastValue(underTest.proxiedInput(ShadeId.RIGHT))
|
||||
|
||||
// Starting a proxied interaction on the LEFT shade disallows non-proxied interaction on
|
||||
// the
|
||||
// same shade.
|
||||
inputProxy.onProxiedInput(
|
||||
ProxiedInputModel.OnDrag(xFraction = 0f, yDragAmountPx = 123f)
|
||||
)
|
||||
assertThat(isLeftShadeNonProxiedInputAllowed).isFalse()
|
||||
|
||||
// Registering the end of the proxied interaction re-allows it.
|
||||
inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
|
||||
assertThat(isLeftShadeNonProxiedInputAllowed).isTrue()
|
||||
|
||||
// Starting a proxied interaction on the RIGHT shade force-collapses the LEFT shade,
|
||||
// disallowing non-proxied input on the LEFT shade.
|
||||
inputProxy.onProxiedInput(
|
||||
ProxiedInputModel.OnDrag(xFraction = 1f, yDragAmountPx = 123f)
|
||||
)
|
||||
assertThat(isLeftShadeNonProxiedInputAllowed).isFalse()
|
||||
|
||||
// Registering the end of the interaction on the RIGHT shade re-allows it.
|
||||
inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
|
||||
assertThat(isLeftShadeNonProxiedInputAllowed).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isForceCollapsed_whenOtherShadeInteractionUnderway() =
|
||||
testScope.runTest {
|
||||
val underTest = create()
|
||||
val isLeftShadeForceCollapsed: Boolean? by
|
||||
collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
|
||||
val isRightShadeForceCollapsed: Boolean? by
|
||||
collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
|
||||
val isSingleShadeForceCollapsed: Boolean? by
|
||||
collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
|
||||
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isLeftShadeForceCollapsed)
|
||||
.isFalse()
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isRightShadeForceCollapsed)
|
||||
.isFalse()
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isSingleShadeForceCollapsed)
|
||||
.isFalse()
|
||||
|
||||
// Registering the start of an interaction on the RIGHT shade force-collapses the LEFT
|
||||
// shade.
|
||||
underTest.onUserInteractionStarted(ShadeId.RIGHT)
|
||||
assertThat(isLeftShadeForceCollapsed).isTrue()
|
||||
assertThat(isRightShadeForceCollapsed).isFalse()
|
||||
assertThat(isSingleShadeForceCollapsed).isFalse()
|
||||
|
||||
// Registering the end of the interaction on the RIGHT shade re-allows it.
|
||||
underTest.onUserInteractionEnded(ShadeId.RIGHT)
|
||||
assertThat(isLeftShadeForceCollapsed).isFalse()
|
||||
assertThat(isRightShadeForceCollapsed).isFalse()
|
||||
assertThat(isSingleShadeForceCollapsed).isFalse()
|
||||
|
||||
// Registering the start of an interaction on the LEFT shade force-collapses the RIGHT
|
||||
// shade.
|
||||
underTest.onUserInteractionStarted(ShadeId.LEFT)
|
||||
assertThat(isLeftShadeForceCollapsed).isFalse()
|
||||
assertThat(isRightShadeForceCollapsed).isTrue()
|
||||
assertThat(isSingleShadeForceCollapsed).isFalse()
|
||||
|
||||
// Registering the end of the interaction on the LEFT shade re-allows it.
|
||||
underTest.onUserInteractionEnded(ShadeId.LEFT)
|
||||
assertThat(isLeftShadeForceCollapsed).isFalse()
|
||||
assertThat(isRightShadeForceCollapsed).isFalse()
|
||||
assertThat(isSingleShadeForceCollapsed).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun collapseAll() =
|
||||
testScope.runTest {
|
||||
val underTest = create()
|
||||
val isLeftShadeForceCollapsed: Boolean? by
|
||||
collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
|
||||
val isRightShadeForceCollapsed: Boolean? by
|
||||
collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
|
||||
val isSingleShadeForceCollapsed: Boolean? by
|
||||
collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
|
||||
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isLeftShadeForceCollapsed)
|
||||
.isFalse()
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isRightShadeForceCollapsed)
|
||||
.isFalse()
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isSingleShadeForceCollapsed)
|
||||
.isFalse()
|
||||
|
||||
underTest.collapseAll()
|
||||
assertThat(isLeftShadeForceCollapsed).isTrue()
|
||||
assertThat(isRightShadeForceCollapsed).isTrue()
|
||||
assertThat(isSingleShadeForceCollapsed).isTrue()
|
||||
|
||||
// Receiving proxied input on that's not a tap gesture, on the left-hand side resets the
|
||||
// "collapse all". Note that now the RIGHT shade is force-collapsed because we're
|
||||
// interacting with the LEFT shade.
|
||||
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 0f))
|
||||
assertThat(isLeftShadeForceCollapsed).isFalse()
|
||||
assertThat(isRightShadeForceCollapsed).isTrue()
|
||||
assertThat(isSingleShadeForceCollapsed).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTapOutside_collapsesAll() =
|
||||
testScope.runTest {
|
||||
val underTest = create()
|
||||
val isLeftShadeForceCollapsed: Boolean? by
|
||||
collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
|
||||
val isRightShadeForceCollapsed: Boolean? by
|
||||
collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
|
||||
val isSingleShadeForceCollapsed: Boolean? by
|
||||
collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
|
||||
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isLeftShadeForceCollapsed)
|
||||
.isFalse()
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isRightShadeForceCollapsed)
|
||||
.isFalse()
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isSingleShadeForceCollapsed)
|
||||
.isFalse()
|
||||
|
||||
inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
|
||||
assertThat(isLeftShadeForceCollapsed).isTrue()
|
||||
assertThat(isRightShadeForceCollapsed).isTrue()
|
||||
assertThat(isSingleShadeForceCollapsed).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun proxiedInput_ignoredWhileNonProxiedGestureUnderway() =
|
||||
testScope.runTest {
|
||||
val underTest = create()
|
||||
val proxiedInput: ProxiedInputModel? by
|
||||
collectLastValue(underTest.proxiedInput(ShadeId.RIGHT))
|
||||
underTest.onUserInteractionStarted(shadeId = ShadeId.RIGHT)
|
||||
|
||||
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
|
||||
assertThat(proxiedInput).isNull()
|
||||
|
||||
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.8f, 110f))
|
||||
assertThat(proxiedInput).isNull()
|
||||
|
||||
underTest.onUserInteractionEnded(shadeId = ShadeId.RIGHT)
|
||||
|
||||
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
|
||||
assertThat(proxiedInput).isNotNull()
|
||||
}
|
||||
|
||||
private fun create(): MultiShadeInteractor {
|
||||
return create(
|
||||
testScope = testScope,
|
||||
context = context,
|
||||
inputProxy = inputProxy,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(
|
||||
testScope: TestScope,
|
||||
context: Context,
|
||||
inputProxy: MultiShadeInputProxy,
|
||||
): MultiShadeInteractor {
|
||||
return MultiShadeInteractor(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
repository =
|
||||
MultiShadeRepositoryTest.create(
|
||||
context = context,
|
||||
inputProxy = inputProxy,
|
||||
),
|
||||
inputProxy = inputProxy,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user