summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Matt Pietal <mpietal@google.com> 2024-07-31 16:39:20 +0000
committer Matt Pietal <mpietal@google.com> 2024-08-01 13:08:40 +0000
commit2235a04ee4c1804a88bff67abcaf06e3d8edcce5 (patch)
tree32f885d2a2bd37da0bbe29933b10094787887113
parent6dd8be5ee6847b37e0ca4d79a0e247c9d62b0343 (diff)
Fix race condition with manual transitions
Transitions are triggered on background scope. Calls to startTransition were transfered to the main dispatcher, but, manual transitions that used updateTransition were not. This could lead to a very rare race condition where a RUNNING step could be sent after the corresponding CANCEL. Test: atest KeyguardTransitionRepositoryTest Fixes: 353698157 Flag: com.android.systemui.migrate_clocks_to_blueprint Change-Id: I515358f0a9230d660fba4b0703eaad38477edd46
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InternalKeyguardTransitionInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt68
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt2
8 files changed, 108 insertions, 46 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index a8eccc5add1a..03647b9a3956 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -136,30 +136,6 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
}
@Test
- fun finishedKeyguardTransitionStepTests() =
- testScope.runTest {
- val finishedSteps by collectValues(underTest.finishedKeyguardTransitionStep)
- val steps = mutableListOf<TransitionStep>()
-
- steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
- steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
- steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
- steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
- steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
- steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
- steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
-
- steps.forEach {
- repository.sendTransitionStep(it)
- runCurrent()
- }
-
- // Ignore the default state.
- assertThat(finishedSteps.subList(1, finishedSteps.size))
- .isEqualTo(listOf(steps[2], steps[5]))
- }
-
- @Test
fun startedKeyguardTransitionStepTests() =
testScope.runTest {
val startedSteps by collectValues(underTest.startedKeyguardTransitionStep)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
index 6a20610da3a6..c780aac5aaca 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
@@ -175,7 +175,7 @@ constructor(
}
}
- private fun finishCurrentTransition() {
+ private suspend fun finishCurrentTransition() {
internalTransitionInteractor.updateTransition(
currentTransitionId!!,
1f,
@@ -285,7 +285,7 @@ constructor(
currentTransitionId = internalTransitionInteractor.startTransition(transitionInfo)
}
- private fun updateProgress(progress: Float) {
+ private suspend fun updateProgress(progress: Float) {
if (currentTransitionId == null) return
internalTransitionInteractor.updateTransition(
currentTransitionId!!,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index aaeeb3911797..de60c1117c19 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -105,7 +105,7 @@ interface KeyguardTransitionRepository {
* When the transition is over, TransitionState.FINISHED must be passed into the [state]
* parameter.
*/
- fun updateTransition(
+ suspend fun updateTransition(
transitionId: UUID,
@FloatRange(from = 0.0, to = 1.0) value: Float,
state: TransitionState
@@ -173,9 +173,9 @@ constructor(
Log.d(TAG, "(Internal) Setting current transition info: $info")
// There is no fairness guarantee with 'withContext', which means that transitions could
- // be processed out of order. Use a Mutex to guarantee ordering.
+ // be processed out of order. Use a Mutex to guarantee ordering. [updateTransition]
+ // requires the same lock
_currentTransitionMutex.lock()
-
// Only used in a test environment
if (forceDelayForRaceConditionTest) {
delay(50L)
@@ -184,7 +184,6 @@ constructor(
// Animators must be started on the main thread.
return withContext("$TAG#startTransition", mainDispatcher) {
_currentTransitionMutex.unlock()
-
if (lastStep.from == info.from && lastStep.to == info.to) {
Log.i(TAG, "Duplicate call to start the transition, rejecting: $info")
return@withContext null
@@ -206,7 +205,7 @@ constructor(
// Cancel any existing manual transitions
updateTransitionId?.let { uuid ->
- updateTransition(uuid, lastStep.value, TransitionState.CANCELED)
+ updateTransitionInternal(uuid, lastStep.value, TransitionState.CANCELED)
}
info.animator?.let { animator ->
@@ -264,7 +263,23 @@ constructor(
}
}
- override fun updateTransition(
+ override suspend fun updateTransition(
+ transitionId: UUID,
+ @FloatRange(from = 0.0, to = 1.0) value: Float,
+ state: TransitionState
+ ) {
+ // There is no fairness guarantee with 'withContext', which means that transitions could
+ // be processed out of order. Use a Mutex to guarantee ordering. [startTransition]
+ // requires the same lock
+ _currentTransitionMutex.lock()
+ withContext("$TAG#updateTransition", mainDispatcher) {
+ _currentTransitionMutex.unlock()
+
+ updateTransitionInternal(transitionId, value, state)
+ }
+ }
+
+ private suspend fun updateTransitionInternal(
transitionId: UUID,
@FloatRange(from = 0.0, to = 1.0) value: Float,
state: TransitionState
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InternalKeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InternalKeyguardTransitionInteractor.kt
index a51421c10309..2cc6afa2f407 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InternalKeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/InternalKeyguardTransitionInteractor.kt
@@ -63,7 +63,7 @@ constructor(
suspend fun startTransition(info: TransitionInfo) = repository.startTransition(info)
- fun updateTransition(
+ suspend fun updateTransition(
transitionId: UUID,
@FloatRange(from = 0.0, to = 1.0) value: Float,
state: TransitionState
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 797d4667c56d..afbe3579315d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
@@ -55,6 +56,7 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
/** Encapsulates business-logic related to the keyguard transitions. */
@@ -150,6 +152,12 @@ constructor(
startedStep.to != prevStep.from
) {
getTransitionValueFlow(prevStep.from).emit(0f)
+ } else if (prevStep.transitionState == TransitionState.RUNNING) {
+ Log.e(
+ TAG,
+ "STARTED step ($startedStep) was preceded by a RUNNING step " +
+ "($prevStep), which should never happen. Things could go badly here."
+ )
}
}
}
@@ -252,15 +260,12 @@ constructor(
val startedKeyguardTransitionStep: Flow<TransitionStep> =
repository.transitions.filter { step -> step.transitionState == TransitionState.STARTED }
- /** The last [TransitionStep] with a [TransitionState] of FINISHED */
- val finishedKeyguardTransitionStep: Flow<TransitionStep> =
- repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED }
-
/** The destination state of the last [TransitionState.STARTED] transition. */
@SuppressLint("SharedFlowCreation")
val startedKeyguardState: SharedFlow<KeyguardState> =
startedKeyguardTransitionStep
.map { step -> step.to }
+ .buffer(2, BufferOverflow.DROP_OLDEST)
.shareIn(scope, SharingStarted.Eagerly, replay = 1)
/** The from state of the last [TransitionState.STARTED] transition. */
@@ -269,6 +274,7 @@ constructor(
val startedKeyguardFromState: SharedFlow<KeyguardState> =
startedKeyguardTransitionStep
.map { step -> step.from }
+ .buffer(2, BufferOverflow.DROP_OLDEST)
.shareIn(scope, SharingStarted.Eagerly, replay = 1)
/** Which keyguard state to use when the device goes to sleep. */
@@ -310,8 +316,13 @@ constructor(
*/
@SuppressLint("SharedFlowCreation")
val finishedKeyguardState: SharedFlow<KeyguardState> =
- finishedKeyguardTransitionStep
- .map { step -> step.to }
+ repository.transitions
+ .transform { step ->
+ if (step.transitionState == TransitionState.FINISHED) {
+ emit(step.to)
+ }
+ }
+ .buffer(2, BufferOverflow.DROP_OLDEST)
.shareIn(scope, SharingStarted.Eagerly, replay = 1)
/**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
index 324811443e9d..b8500952d90a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
@@ -124,7 +124,7 @@ constructor(
}
}
- private fun finishCurrentTransition() {
+ private suspend fun finishCurrentTransition() {
internalTransitionInteractor.updateTransition(currentTransitionId!!, 1f, FINISHED)
resetTransitionData()
}
@@ -223,7 +223,7 @@ constructor(
currentTransitionId = internalTransitionInteractor.startTransition(transitionInfo)
}
- private fun updateProgress(progress: Float) {
+ private suspend fun updateProgress(progress: Float) {
if (currentTransitionId == null) return
internalTransitionInteractor.updateTransition(
currentTransitionId!!,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
index 48a5df91d47c..2af4d872f6a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -45,6 +45,7 @@ import java.math.BigDecimal
import java.math.RoundingMode
import java.util.UUID
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -332,10 +333,11 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
}
@Test
- fun attemptTomanuallyUpdateTransitionWithInvalidUUIDthrowsException() {
- underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING)
- assertThat(wtfHandler.failed).isTrue()
- }
+ fun attemptTomanuallyUpdateTransitionWithInvalidUUIDthrowsException() =
+ testScope.runTest {
+ underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING)
+ assertThat(wtfHandler.failed).isTrue()
+ }
@Test
fun attemptToManuallyUpdateTransitionAfterFINISHEDstateThrowsException() =
@@ -414,6 +416,64 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
)
}
+ @Test
+ fun simulateRaceConditionIsProcessedInOrderUsingUpdateTransition() =
+ testScope.runTest {
+ val ktr = KeyguardTransitionRepositoryImpl(kosmos.testDispatcher)
+ val steps by collectValues(ktr.transitions.dropWhile { step -> step.from == OFF })
+
+ // Begin a manual transition
+ val info1 = TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator = null)
+ launch {
+ ktr.forceDelayForRaceConditionTest = false
+ val uuid = ktr.startTransition(info1)
+
+ // Pause here to allow another transition to start
+ delay(20)
+
+ // Attempt to send an update, which should fail
+ ktr.updateTransition(uuid!!, 0.5f, TransitionState.RUNNING)
+ }
+
+ // Now start another transition, which should acquire the preempt the first
+ val info2 = TransitionInfo(OWNER_NAME, LOCKSCREEN, OCCLUDED, animator = null)
+ launch {
+ delay(10)
+ ktr.forceDelayForRaceConditionTest = true
+ ktr.startTransition(info2)
+ }
+
+ runCurrent()
+
+ // Manual transition has started
+ assertThat(steps[0])
+ .isEqualTo(
+ TransitionStep(info1.from, info1.to, 0f, TransitionState.STARTED, OWNER_NAME)
+ )
+
+ // The second transition has requested to start, and grabbed the mutex. But it is
+ // delayed
+ advanceTimeBy(15L)
+
+ // Advancing another 10ms should now trigger the first transition to request an update,
+ // which should not happen as the second transition has the mutex
+ advanceTimeBy(10L)
+
+ // Finally, advance past the delay in the second transition so it can run
+ advanceTimeBy(50L)
+
+ assertThat(steps[1])
+ .isEqualTo(
+ TransitionStep(info1.from, info1.to, 0f, TransitionState.CANCELED, OWNER_NAME)
+ )
+ assertThat(steps[2])
+ .isEqualTo(
+ TransitionStep(info2.from, info2.to, 0f, TransitionState.STARTED, OWNER_NAME)
+ )
+
+ assertThat(steps.size).isEqualTo(3)
+ }
+
private fun listWithStep(
step: BigDecimal,
start: BigDecimal = BigDecimal.ZERO,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index b5ea619cd57f..616f2b688746 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -283,7 +283,7 @@ class FakeKeyguardTransitionRepository(
)
}
- override fun updateTransition(
+ override suspend fun updateTransition(
transitionId: UUID,
@FloatRange(from = 0.0, to = 1.0) value: Float,
state: TransitionState