Create FoldStateLoggingProvider

The aim of this class it to output fold state changes with the time between them.
Those will be logged in a follow up change to uptimize fold/unfold experience.

+ Changed files have been reformatted according to ktfmt. Those are minimal changes.
+ Making some return type explicit in dagger modules

Bug: 198305865
Test: atest FoldStateLoggingProviderTest
Change-Id: I3dcfa742f2cf3c712796b5c6f5481f319b680a12
This commit is contained in:
Nicolo' Mazzucato
2022-01-06 14:49:44 +01:00
parent 5223943b0e
commit 2e4d663ee0
6 changed files with 452 additions and 49 deletions

View File

@@ -21,24 +21,24 @@ import android.content.Context
import android.hardware.SensorManager
import android.hardware.devicestate.DeviceStateManager
import android.os.Handler
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.progress.FixedTimingTransitionProgressProvider
import com.android.systemui.unfold.progress.PhysicsBasedUnfoldTransitionProgressProvider
import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider
import com.android.systemui.unfold.updates.DeviceFoldStateProvider
import com.android.systemui.unfold.updates.FoldStateProvider
import com.android.systemui.unfold.updates.hinge.EmptyHingeAngleProvider
import com.android.systemui.unfold.updates.hinge.HingeSensorAngleProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.util.ATraceLoggerTransitionProgressListener
import java.lang.IllegalStateException
import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider
import java.util.concurrent.Executor
/**
* Factory for [UnfoldTransitionProgressProvider].
*
* This is needed as Launcher has to create the object manually.
* Sysui create it using dagger (see [UnfoldTransitionModule]).
* This is needed as Launcher has to create the object manually. Sysui create it using dagger (see
* [UnfoldTransitionModule]).
*/
fun createUnfoldTransitionProgressProvider(
context: Context,
@@ -52,10 +52,45 @@ fun createUnfoldTransitionProgressProvider(
): UnfoldTransitionProgressProvider {
if (!config.isEnabled) {
throw IllegalStateException("Trying to create " +
"UnfoldTransitionProgressProvider when the transition is disabled")
throw IllegalStateException(
"Trying to create " +
"UnfoldTransitionProgressProvider when the transition is disabled")
}
val foldStateProvider =
createFoldStateProvider(
context,
config,
screenStatusProvider,
deviceStateManager,
sensorManager,
mainHandler,
mainExecutor)
val unfoldTransitionProgressProvider =
if (config.isHingeAngleEnabled) {
PhysicsBasedUnfoldTransitionProgressProvider(foldStateProvider)
} else {
FixedTimingTransitionProgressProvider(foldStateProvider)
}
return ScaleAwareTransitionProgressProvider(
unfoldTransitionProgressProvider, context.contentResolver)
.apply {
// Always present callback that logs animation beginning and end.
addCallback(ATraceLoggerTransitionProgressListener(tracingTagPrefix))
}
}
fun createFoldStateProvider(
context: Context,
config: UnfoldTransitionConfig,
screenStatusProvider: ScreenStatusProvider,
deviceStateManager: DeviceStateManager,
sensorManager: SensorManager,
mainHandler: Handler,
mainExecutor: Executor
): FoldStateProvider {
val hingeAngleProvider =
if (config.isHingeAngleEnabled) {
HingeSensorAngleProvider(sensorManager)
@@ -63,28 +98,13 @@ fun createUnfoldTransitionProgressProvider(
EmptyHingeAngleProvider()
}
val foldStateProvider = DeviceFoldStateProvider(
return DeviceFoldStateProvider(
context,
hingeAngleProvider,
screenStatusProvider,
deviceStateManager,
mainExecutor,
mainHandler
)
val unfoldTransitionProgressProvider = if (config.isHingeAngleEnabled) {
PhysicsBasedUnfoldTransitionProgressProvider(foldStateProvider)
} else {
FixedTimingTransitionProgressProvider(foldStateProvider)
}
return ScaleAwareTransitionProgressProvider(
unfoldTransitionProgressProvider,
context.contentResolver
).apply {
// Always present callback that logs animation beginning and end.
addCallback(ATraceLoggerTransitionProgressListener(tracingTagPrefix))
}
mainHandler)
}
fun createConfig(context: Context): UnfoldTransitionConfig =
ResourceUnfoldTransitionConfig(context)
fun createConfig(context: Context): UnfoldTransitionConfig = ResourceUnfoldTransitionConfig(context)

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2022 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.unfold
import android.annotation.IntDef
import com.android.systemui.statusbar.policy.CallbackController
import com.android.systemui.unfold.FoldStateLoggingProvider.FoldStateLoggingListener
import com.android.systemui.unfold.FoldStateLoggingProvider.LoggedFoldedStates
/** Reports device fold states for logging purposes. */
// TODO(b/198305865): Log state changes.
interface FoldStateLoggingProvider : CallbackController<FoldStateLoggingListener> {
fun init()
fun uninit()
interface FoldStateLoggingListener {
fun onFoldUpdate(foldStateUpdate: FoldStateChange)
}
@IntDef(prefix = ["LOGGED_FOLD_STATE_"], value = [FULLY_OPENED, FULLY_CLOSED, HALF_OPENED])
@Retention(AnnotationRetention.SOURCE)
annotation class LoggedFoldedStates
}
data class FoldStateChange(
@LoggedFoldedStates val previous: Int,
@LoggedFoldedStates val current: Int,
val dtMillis: Long
)
const val FULLY_OPENED = 1
const val FULLY_CLOSED = 2
const val HALF_OPENED = 3

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2022 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.unfold
import android.util.Log
import com.android.systemui.unfold.FoldStateLoggingProvider.FoldStateLoggingListener
import com.android.systemui.unfold.FoldStateLoggingProvider.LoggedFoldedStates
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN
import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING
import com.android.systemui.unfold.updates.FOLD_UPDATE_START_OPENING
import com.android.systemui.unfold.updates.FoldStateProvider
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.systemui.util.time.SystemClock
/**
* Reports device fold states for logging purposes.
*
* Wraps the state provided by [FoldStateProvider] to output only [FULLY_OPENED], [FULLY_CLOSED] and
* [HALF_OPENED] for logging purposes.
*
* Note that [HALF_OPENED] state is only emitted after the device angle is stable for some timeout.
* Check [FoldStateProvider] impl for it.
*
* This doesn't log the following transitions:
* - [HALF_OPENED] -> [FULLY_OPENED]: not interesting, as there is no transition going on
* - [HALF_OPENED] -> [HALF_OPENED]: not meaningful.
*/
class FoldStateLoggingProviderImpl(
private val foldStateProvider: FoldStateProvider,
private val clock: SystemClock
) : FoldStateLoggingProvider, FoldStateProvider.FoldUpdatesListener {
private val outputListeners: MutableList<FoldStateLoggingListener> = mutableListOf()
@LoggedFoldedStates private var lastState: Int? = null
private var actionStartMillis: Long? = null
override fun init() {
foldStateProvider.addCallback(this)
foldStateProvider.start()
}
override fun uninit() {
foldStateProvider.removeCallback(this)
foldStateProvider.stop()
}
override fun onHingeAngleUpdate(angle: Float) {}
override fun onFoldUpdate(@FoldUpdate update: Int) {
val now = clock.elapsedRealtime()
when (update) {
FOLD_UPDATE_START_OPENING -> {
lastState = FULLY_CLOSED
actionStartMillis = now
}
FOLD_UPDATE_START_CLOSING -> actionStartMillis = now
FOLD_UPDATE_FINISH_HALF_OPEN -> dispatchState(HALF_OPENED)
FOLD_UPDATE_FINISH_FULL_OPEN -> dispatchState(FULLY_OPENED)
FOLD_UPDATE_FINISH_CLOSED -> dispatchState(FULLY_CLOSED)
}
}
private fun dispatchState(@LoggedFoldedStates current: Int) {
val now = clock.elapsedRealtime()
val previous = lastState
val lastActionStart = actionStartMillis
if (previous != null && previous != current && lastActionStart != null) {
val time = now - lastActionStart
val foldStateChange = FoldStateChange(previous, current, time)
outputListeners.forEach { it.onFoldUpdate(foldStateChange) }
if (DEBUG) {
Log.d(TAG, "From $previous to $current in $time")
}
}
actionStartMillis = null
lastState = current
}
override fun addCallback(listener: FoldStateLoggingListener) {
outputListeners.add(listener)
}
override fun removeCallback(listener: FoldStateLoggingListener) {
outputListeners.remove(listener)
}
}
private const val DEBUG = false
private const val TAG = "FoldStateLoggingProviderImpl"

View File

@@ -17,10 +17,11 @@
package com.android.systemui.unfold
import com.android.keyguard.KeyguardUnfoldTransition
import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.phone.StatusBarMoveFromCenterAnimationController
import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
import com.android.systemui.util.kotlin.getOrNull
import dagger.BindsInstance
import dagger.Module
import dagger.Provides
@@ -36,15 +37,17 @@ annotation class SysUIUnfoldScope
/**
* Creates an injectable [SysUIUnfoldComponent] that provides objects that have been scoped with
* [@SysUIUnfoldScope]. Since [SysUIUnfoldComponent] depends upon:
* [@SysUIUnfoldScope].
*
* Since [SysUIUnfoldComponent] depends upon:
* * [Optional<UnfoldTransitionProgressProvider>]
* * [Optional<ScopedUnfoldTransitionProgressProvider>]
* * [Optional<NaturalRotationProgressProvider>]
*
* no objects will get constructed if these parameters are empty.
*/
@Module(subcomponents = [SysUIUnfoldComponent::class])
class SysUIUnfoldModule {
constructor() {}
@Provides
@SysUISingleton
@@ -53,12 +56,16 @@ class SysUIUnfoldModule {
rotationProvider: Optional<NaturalRotationUnfoldProgressProvider>,
@Named(UNFOLD_STATUS_BAR) scopedProvider: Optional<ScopedUnfoldTransitionProgressProvider>,
factory: SysUIUnfoldComponent.Factory
) =
provider.flatMap { p1 ->
rotationProvider.flatMap { p2 ->
scopedProvider.map { p3 -> factory.create(p1, p2, p3) }
}
): Optional<SysUIUnfoldComponent> {
val p1 = provider.getOrNull()
val p2 = rotationProvider.getOrNull()
val p3 = scopedProvider.getOrNull()
return if (p1 == null || p2 == null || p3 == null) {
Optional.empty()
} else {
Optional.of(factory.create(p1, p2, p3))
}
}
}
@SysUIUnfoldScope

View File

@@ -24,8 +24,10 @@ import android.view.IWindowManager
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.LifecycleScreenStatusProvider
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.updates.FoldStateProvider
import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
import com.android.systemui.util.time.SystemClockImpl
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider
import dagger.Lazy
import dagger.Module
@@ -48,7 +50,7 @@ class UnfoldTransitionModule {
sensorManager: SensorManager,
@Main executor: Executor,
@Main handler: Handler
) =
): Optional<UnfoldTransitionProgressProvider> =
if (config.isEnabled) {
Optional.of(
createUnfoldTransitionProgressProvider(
@@ -59,13 +61,45 @@ class UnfoldTransitionModule {
sensorManager,
handler,
executor,
tracingTagPrefix = "systemui"
)
)
tracingTagPrefix = "systemui"))
} else {
Optional.empty()
}
@Provides
@Singleton
fun provideFoldStateProvider(
context: Context,
config: UnfoldTransitionConfig,
screenStatusProvider: Lazy<LifecycleScreenStatusProvider>,
deviceStateManager: DeviceStateManager,
sensorManager: SensorManager,
@Main executor: Executor,
@Main handler: Handler
): Optional<FoldStateProvider> =
if (!config.isHingeAngleEnabled) {
Optional.empty()
} else {
Optional.of(
createFoldStateProvider(
context,
config,
screenStatusProvider.get(),
deviceStateManager,
sensorManager,
handler,
executor))
}
@Provides
@Singleton
fun providesFoldStateLoggingProvider(
optionalFoldStateProvider: Optional<FoldStateProvider>
): Optional<FoldStateLoggingProvider> =
optionalFoldStateProvider.map { foldStateProvider ->
FoldStateLoggingProviderImpl(foldStateProvider, SystemClockImpl())
}
@Provides
@Singleton
fun provideUnfoldTransitionConfig(context: Context): UnfoldTransitionConfig =
@@ -77,13 +111,9 @@ class UnfoldTransitionModule {
context: Context,
windowManager: IWindowManager,
unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider>
) =
unfoldTransitionProgressProvider.map {
provider -> NaturalRotationUnfoldProgressProvider(
context,
windowManager,
provider
)
): Optional<NaturalRotationUnfoldProgressProvider> =
unfoldTransitionProgressProvider.map { provider ->
NaturalRotationUnfoldProgressProvider(context, windowManager, provider)
}
@Provides
@@ -91,10 +121,8 @@ class UnfoldTransitionModule {
@Singleton
fun provideStatusBarScopedTransitionProvider(
source: Optional<NaturalRotationUnfoldProgressProvider>
) =
source.map {
provider -> ScopedUnfoldTransitionProgressProvider(provider)
}
): Optional<ScopedUnfoldTransitionProgressProvider> =
source.map { provider -> ScopedUnfoldTransitionProgressProvider(provider) }
@Provides
@Singleton

View File

@@ -0,0 +1,192 @@
/*
* Copyright (C) 2022 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.unfold
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.unfold.FoldStateLoggingProvider.FoldStateLoggingListener
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN
import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING
import com.android.systemui.unfold.updates.FOLD_UPDATE_START_OPENING
import com.android.systemui.unfold.updates.FoldStateProvider
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@SmallTest
class FoldStateLoggingProviderTest : SysuiTestCase() {
@Captor
private lateinit var foldUpdatesListener: ArgumentCaptor<FoldStateProvider.FoldUpdatesListener>
@Mock private lateinit var foldStateProvider: FoldStateProvider
private val fakeClock = FakeSystemClock()
private lateinit var foldStateLoggingProvider: FoldStateLoggingProvider
private val foldLoggingUpdates: MutableList<FoldStateChange> = arrayListOf()
private val foldStateLoggingListener =
object : FoldStateLoggingListener {
override fun onFoldUpdate(foldStateUpdate: FoldStateChange) {
foldLoggingUpdates.add(foldStateUpdate)
}
}
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
foldStateLoggingProvider =
FoldStateLoggingProviderImpl(foldStateProvider, fakeClock).apply {
addCallback(foldStateLoggingListener)
init()
}
verify(foldStateProvider).addCallback(foldUpdatesListener.capture())
}
@Test
fun onFoldUpdate_noPreviousOne_finishHalfOpen_nothingReported() {
sendFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
assertThat(foldLoggingUpdates).isEmpty()
}
@Test
fun onFoldUpdate_noPreviousOne_finishFullOpen_nothingReported() {
sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN)
assertThat(foldLoggingUpdates).isEmpty()
}
@Test
fun onFoldUpdate_noPreviousOne_finishClosed_nothingReported() {
sendFoldUpdate(FOLD_UPDATE_FINISH_CLOSED)
assertThat(foldLoggingUpdates).isEmpty()
}
@Test
fun onFoldUpdate_startOpening_fullOpen_changeReported() {
val dtTime = 10L
sendFoldUpdate(FOLD_UPDATE_START_OPENING)
fakeClock.advanceTime(dtTime)
sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN)
assertThat(foldLoggingUpdates)
.containsExactly(FoldStateChange(FULLY_CLOSED, FULLY_OPENED, dtTime))
}
@Test
fun onFoldUpdate_startClosingThenFinishClosed_noInitialState_nothingReported() {
val dtTime = 10L
sendFoldUpdate(FOLD_UPDATE_START_CLOSING)
fakeClock.advanceTime(dtTime)
sendFoldUpdate(FOLD_UPDATE_FINISH_CLOSED)
assertThat(foldLoggingUpdates).isEmpty()
}
@Test
fun onFoldUpdate_startClosingThenFinishClosed_initiallyOpened_changeReported() {
val dtTime = 10L
sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN)
sendFoldUpdate(FOLD_UPDATE_START_CLOSING)
fakeClock.advanceTime(dtTime)
sendFoldUpdate(FOLD_UPDATE_FINISH_CLOSED)
assertThat(foldLoggingUpdates)
.containsExactly(FoldStateChange(FULLY_OPENED, FULLY_CLOSED, dtTime))
}
@Test
fun onFoldUpdate_startOpeningThenHalf_initiallyClosed_changeReported() {
val dtTime = 10L
sendFoldUpdate(FOLD_UPDATE_FINISH_CLOSED)
sendFoldUpdate(FOLD_UPDATE_START_OPENING)
fakeClock.advanceTime(dtTime)
sendFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
assertThat(foldLoggingUpdates)
.containsExactly(FoldStateChange(FULLY_CLOSED, HALF_OPENED, dtTime))
}
@Test
fun onFoldUpdate_startClosingThenHalf_initiallyOpened_changeReported() {
val dtTime = 10L
sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN)
sendFoldUpdate(FOLD_UPDATE_START_CLOSING)
fakeClock.advanceTime(dtTime)
sendFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN)
assertThat(foldLoggingUpdates)
.containsExactly(FoldStateChange(FULLY_OPENED, HALF_OPENED, dtTime))
}
@Test
fun onFoldUpdate_foldThenUnfold_multipleReported() {
val foldTime = 24L
val unfoldTime = 42L
val waitingTime = 424L
sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN)
// Fold
sendFoldUpdate(FOLD_UPDATE_START_CLOSING)
fakeClock.advanceTime(foldTime)
sendFoldUpdate(FOLD_UPDATE_FINISH_CLOSED)
fakeClock.advanceTime(waitingTime)
// unfold
sendFoldUpdate(FOLD_UPDATE_START_OPENING)
fakeClock.advanceTime(unfoldTime)
sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN)
assertThat(foldLoggingUpdates)
.containsExactly(
FoldStateChange(FULLY_OPENED, FULLY_CLOSED, foldTime),
FoldStateChange(FULLY_CLOSED, FULLY_OPENED, unfoldTime))
}
@Test
fun uninit_removesCallback() {
foldStateLoggingProvider.uninit()
verify(foldStateProvider).removeCallback(foldUpdatesListener.value)
}
private fun sendFoldUpdate(@FoldUpdate foldUpdate: Int) {
foldUpdatesListener.value.onFoldUpdate(foldUpdate)
}
}