diff options
16 files changed, 726 insertions, 18 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index ac71664e5590..87a736d926b5 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -54,6 +54,7 @@ import com.android.systemui.statusbar.phone.StatusBarHeadsUpChangeListener import com.android.systemui.stylus.StylusUsiPowerStartable import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.theme.ThemeOverlayController +import com.android.systemui.unfold.DisplaySwitchLatencyTracker import com.android.systemui.usb.StorageNotification import com.android.systemui.util.NotificationChannels import com.android.systemui.util.StartBinderLoggerModule @@ -141,6 +142,12 @@ abstract class SystemUICoreStartableModule { @ClassKey(LatencyTester::class) abstract fun bindLatencyTester(sysui: LatencyTester): CoreStartable + /** Inject into DisplaySwitchLatencyTracker. */ + @Binds + @IntoMap + @ClassKey(DisplaySwitchLatencyTracker::class) + abstract fun bindDisplaySwitchLatencyTracker(sysui: DisplaySwitchLatencyTracker): CoreStartable + /** Inject into NotificationChannels. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 31ef100abbcb..2f937bcd3414 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -110,7 +110,7 @@ interface KeyguardRepository { val isKeyguardGoingAway: Flow<Boolean> /** Is the always-on display available to be used? */ - val isAodAvailable: Flow<Boolean> + val isAodAvailable: StateFlow<Boolean> fun setAodAvailable(value: Boolean) @@ -338,7 +338,7 @@ constructor( .distinctUntilChanged() private val _isAodAvailable = MutableStateFlow(false) - override val isAodAvailable: Flow<Boolean> = _isAodAvailable.asStateFlow() + override val isAodAvailable: StateFlow<Boolean> = _isAodAvailable.asStateFlow() override fun setAodAvailable(value: Boolean) { _isAodAvailable.value = value diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 21651ba2cc2b..6eb3b64d4c09 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -104,7 +104,7 @@ constructor( val dozeTimeTick: Flow<Long> = repository.dozeTimeTick /** Whether Always-on Display mode is available. */ - val isAodAvailable: Flow<Boolean> = repository.isAodAvailable + val isAodAvailable: StateFlow<Boolean> = repository.isAodAvailable /** Doze transition information. */ val dozeTransitionModel: Flow<DozeTransitionModel> = repository.dozeTransitionModel diff --git a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt index faf9fbe3239d..7505566898c0 100644 --- a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt +++ b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt @@ -51,7 +51,10 @@ enum class WakeSleepReason( BIOMETRIC(isTouch = false, PowerManager.WAKE_REASON_BIOMETRIC), /** Something else happened to wake up or sleep the device. */ - OTHER(isTouch = false, PowerManager.WAKE_REASON_UNKNOWN); + OTHER(isTouch = false, PowerManager.WAKE_REASON_UNKNOWN), + + /** Device goes to sleep due to folding of a foldable device. */ + FOLD(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD); companion object { fun fromPowerManagerWakeReason(reason: Int): WakeSleepReason { @@ -72,6 +75,7 @@ enum class WakeSleepReason( fun fromPowerManagerSleepReason(reason: Int): WakeSleepReason { return when (reason) { PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON -> POWER_BUTTON + PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD -> FOLD else -> OTHER } } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt new file mode 100644 index 000000000000..76f7609f81c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt @@ -0,0 +1,55 @@ +/* + * 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.unfold + +import com.android.systemui.shared.system.SysUiStatsLog + +class DisplaySwitchLatencyLogger { + + /** + * Based on data present in [displaySwitchLatencyEvent], logs metrics for atom + * [DisplaySwitchLatencyTracked] + */ + fun log(displaySwitchLatencyEvent: DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent) { + with(displaySwitchLatencyEvent) { + SysUiStatsLog.write( + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED, + latencyMs, + fromFoldableDeviceState, + fromState, + fromFocusedAppUid, + fromPipAppUid, + fromVisibleAppsUid.toIntArray(), + fromDensityDpi, + toState, + toFoldableDeviceState, + toFocusedAppUid, + toPipAppUid, + toVisibleAppsUid.toIntArray(), + toDensityDpi, + notificationCount, + externalDisplayCount, + throttlingLevel, + vskinTemperatureC, + hallSensorToFirstHingeAngleChangeMs, + hallSensorToDeviceStateChangeMs, + onScreenTurningOnToOnDrawnMs, + onDrawnToOnScreenTurnedOnMs, + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt new file mode 100644 index 000000000000..92a64a618ba9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt @@ -0,0 +1,248 @@ +/* + * 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.unfold + +import android.content.Context +import android.util.Log +import com.android.app.tracing.TraceUtils.instantForTrack +import com.android.app.tracing.TraceUtils.traceAsync +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.display.data.repository.DeviceStateRepository +import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.ScreenPowerState +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg +import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor +import com.android.systemui.util.Compile +import com.android.systemui.util.Utils.isDeviceFoldable +import com.android.systemui.util.animation.data.repository.AnimationStatusRepository +import com.android.systemui.util.kotlin.pairwise +import com.android.systemui.util.time.SystemClock +import com.android.systemui.util.time.measureTimeMillis +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch + +/** + * [DisplaySwitchLatencyTracker] tracks latency and related fields for display switch of a foldable + * device. This class populates [DisplaySwitchLatencyEvent] while an ongoing display switch event + */ +@SysUISingleton +class DisplaySwitchLatencyTracker +@Inject +constructor( + private val context: Context, + private val deviceStateRepository: DeviceStateRepository, + private val powerInteractor: PowerInteractor, + private val unfoldTransitionInteractor: UnfoldTransitionInteractor, + private val animationStatusRepository: AnimationStatusRepository, + private val keyguardInteractor: KeyguardInteractor, + @UnfoldSingleThreadBg private val singleThreadBgExecutor: Executor, + @Application private val applicationScope: CoroutineScope, + private val displaySwitchLatencyLogger: DisplaySwitchLatencyLogger, + private val systemClock: SystemClock +) : CoreStartable { + + private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher() + + @OptIn(ExperimentalCoroutinesApi::class) + override fun start() { + if (!isDeviceFoldable(context)) { + return + } + applicationScope.launch(backgroundDispatcher) { + deviceStateRepository.state + .pairwise() + .filter { + // Start tracking only when the foldable device is + //folding(UNFOLDED/HALF_FOLDED -> FOLDED) or + //unfolding(FOLDED -> HALF_FOLD/UNFOLDED) + foldableDeviceState -> + foldableDeviceState.previousValue == DeviceState.FOLDED || + foldableDeviceState.newValue == DeviceState.FOLDED + } + .flatMapLatest { foldableDeviceState -> + flow { + var displaySwitchLatencyEvent = DisplaySwitchLatencyEvent() + val toFoldableDeviceState = foldableDeviceState.newValue.toStatsInt() + displaySwitchLatencyEvent = + displaySwitchLatencyEvent.withBeforeFields( + foldableDeviceState.previousValue.toStatsInt() + ) + + val displaySwitchTimeMs = + measureTimeMillis(systemClock) { + traceAsync(TAG, "displaySwitch") { + waitForDisplaySwitch(toFoldableDeviceState) + } + } + + displaySwitchLatencyEvent = + displaySwitchLatencyEvent.withAfterFields( + toFoldableDeviceState, + displaySwitchTimeMs.toInt(), + getCurrentState() + ) + emit(displaySwitchLatencyEvent) + } + } + .collect { displaySwitchLatencyLogger.log(it) } + } + } + + private fun DeviceState.toStatsInt(): Int = + when (this) { + DeviceState.FOLDED -> FOLDABLE_DEVICE_STATE_CLOSED + DeviceState.HALF_FOLDED -> FOLDABLE_DEVICE_STATE_HALF_OPEN + DeviceState.UNFOLDED -> FOLDABLE_DEVICE_STATE_OPEN + DeviceState.CONCURRENT_DISPLAY -> FOLDABLE_DEVICE_STATE_FLIPPED + else -> FOLDABLE_DEVICE_STATE_UNKNOWN + } + + private suspend fun waitForDisplaySwitch(toFoldableDeviceState: Int) { + val isTransitionEnabled = + unfoldTransitionInteractor.isAvailable && + animationStatusRepository.areAnimationsEnabled().first() + if (shouldWaitForScreenOn(toFoldableDeviceState, isTransitionEnabled)) { + waitForScreenTurnedOn() + } else { + traceAsync(TAG, "waitForTransitionStart()") { + unfoldTransitionInteractor.waitForTransitionStart() + } + } + } + + private fun shouldWaitForScreenOn( + toFoldableDeviceState: Int, + isTransitionEnabled: Boolean + ): Boolean = (toFoldableDeviceState == FOLDABLE_DEVICE_STATE_CLOSED || !isTransitionEnabled) + + private suspend fun waitForScreenTurnedOn() { + traceAsync(TAG, "waitForScreenTurnedOn()") { + powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first() + } + } + + private fun getCurrentState(): Int = + when { + isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD + else -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__UNKNOWN + } + + private fun isStateAod(): Boolean { + val lastWakefulnessEvent = powerInteractor.detailedWakefulness.value + val isAodEnabled = keyguardInteractor.isAodAvailable.value + + return (lastWakefulnessEvent.isAsleep() && + (lastWakefulnessEvent.lastSleepReason == WakeSleepReason.FOLD) && + isAodEnabled) + } + + private inline fun log(msg: () -> String) { + if (DEBUG) Log.d(TAG, msg()) + } + + private fun DisplaySwitchLatencyEvent.withBeforeFields( + fromFoldableDeviceState: Int + ): DisplaySwitchLatencyEvent { + log { "fromFoldableDeviceState=$fromFoldableDeviceState" } + instantForTrack(TAG, "fromFoldableDeviceState=$fromFoldableDeviceState") + + return copy(fromFoldableDeviceState = fromFoldableDeviceState) + } + + private fun DisplaySwitchLatencyEvent.withAfterFields( + toFoldableDeviceState: Int, + displaySwitchTimeMs: Int, + toState: Int + ): DisplaySwitchLatencyEvent { + log { + "toFoldableDeviceState=$toFoldableDeviceState, " + + "toState=$toState, " + + "latencyMs=$displaySwitchTimeMs" + } + instantForTrack(TAG, "toFoldableDeviceState=$toFoldableDeviceState, toState=$toState") + + return copy( + toFoldableDeviceState = toFoldableDeviceState, + latencyMs = displaySwitchTimeMs, + toState = toState + ) + } + + /** + * Stores values corresponding to all respective [DisplaySwitchLatencyTrackedField] in a single + * event of display switch for foldable devices. + * + * Once the data is captured in this data class and appropriate to log, it is logged through + * [DisplaySwitchLatencyLogger] + */ + data class DisplaySwitchLatencyEvent( + val latencyMs: Int = VALUE_UNKNOWN, + val fromFoldableDeviceState: Int = FOLDABLE_DEVICE_STATE_UNKNOWN, + val fromState: Int = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_STATE__UNKNOWN, + val fromFocusedAppUid: Int = VALUE_UNKNOWN, + val fromPipAppUid: Int = VALUE_UNKNOWN, + val fromVisibleAppsUid: Set<Int> = setOf(), + val fromDensityDpi: Int = VALUE_UNKNOWN, + val toFoldableDeviceState: Int = FOLDABLE_DEVICE_STATE_UNKNOWN, + val toState: Int = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_STATE__UNKNOWN, + val toFocusedAppUid: Int = VALUE_UNKNOWN, + val toPipAppUid: Int = VALUE_UNKNOWN, + val toVisibleAppsUid: Set<Int> = setOf(), + val toDensityDpi: Int = VALUE_UNKNOWN, + val notificationCount: Int = VALUE_UNKNOWN, + val externalDisplayCount: Int = VALUE_UNKNOWN, + val throttlingLevel: Int = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__THROTTLING_LEVEL__NONE, + val vskinTemperatureC: Int = VALUE_UNKNOWN, + val hallSensorToFirstHingeAngleChangeMs: Int = VALUE_UNKNOWN, + val hallSensorToDeviceStateChangeMs: Int = VALUE_UNKNOWN, + val onScreenTurningOnToOnDrawnMs: Int = VALUE_UNKNOWN, + val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN + ) + + companion object { + private const val VALUE_UNKNOWN = -1 + private const val TAG = "DisplaySwitchLatency" + private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE) + + private const val FOLDABLE_DEVICE_STATE_UNKNOWN = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_UNKNOWN + const val FOLDABLE_DEVICE_STATE_CLOSED = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_CLOSED + const val FOLDABLE_DEVICE_STATE_HALF_OPEN = + SysUiStatsLog + .DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_HALF_OPENED + private const val FOLDABLE_DEVICE_STATE_OPEN = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_OPENED + private const val FOLDABLE_DEVICE_STATE_FLIPPED = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_FLIPPED + } +} diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt index 94912bf82377..adf50a1e661b 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt @@ -22,8 +22,8 @@ import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.unfold.data.repository.FoldStateRepository import com.android.systemui.unfold.system.DeviceStateRepository -import com.android.systemui.unfold.updates.FoldStateRepository import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt index 50515daedc51..8bef53c8c4fb 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt @@ -27,6 +27,8 @@ import com.android.systemui.keyguard.LifecycleScreenStatusProvider import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.dagger.UnfoldBgProgressFlag import com.android.systemui.unfold.dagger.UnfoldMain +import com.android.systemui.unfold.data.repository.FoldStateRepository +import com.android.systemui.unfold.data.repository.FoldStateRepositoryImpl import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor @@ -168,6 +170,11 @@ class UnfoldTransitionModule { @Provides fun screenStatusProvider(impl: LifecycleScreenStatusProvider): ScreenStatusProvider = impl + @Provides + @Singleton + fun provideDisplaySwitchLatencyLogger(): DisplaySwitchLatencyLogger = + DisplaySwitchLatencyLogger() + @Module interface Bindings { @Binds @@ -178,6 +185,8 @@ class UnfoldTransitionModule { @Binds fun bindRepository(impl: UnfoldTransitionRepositoryImpl): UnfoldTransitionRepository @Binds fun bindInteractor(impl: UnfoldTransitionInteractorImpl): UnfoldTransitionInteractor + + @Binds fun bindFoldStateRepository(impl: FoldStateRepositoryImpl): FoldStateRepository } } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldStateRepository.kt b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/FoldStateRepository.kt index 61b0b40a55bf..04b00ca58c47 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/FoldStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/FoldStateRepository.kt @@ -13,9 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.unfold.updates +package com.android.systemui.unfold.data.repository -import com.android.systemui.unfold.updates.FoldStateRepository.FoldUpdate +import com.android.systemui.unfold.data.repository.FoldStateRepository.FoldUpdate +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 javax.inject.Inject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.awaitClose @@ -50,7 +56,7 @@ interface FoldStateRepository { FOLD_UPDATE_FINISH_HALF_OPEN -> FINISH_HALF_OPEN FOLD_UPDATE_FINISH_FULL_OPEN -> FINISH_FULL_OPEN FOLD_UPDATE_FINISH_CLOSED -> FINISH_CLOSED - else -> error("FoldUpdateNotFound") + else -> error("Fold update with id $oldId is not supported") } } } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt index a2e77afedea6..3e2e564c307c 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt @@ -15,16 +15,26 @@ */ package com.android.systemui.unfold.domain.interactor -import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository +import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished +import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted import javax.inject.Inject import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +/** + * Contains business-logic related to fold-unfold transitions while interacting with + * [UnfoldTransitionRepository] + */ interface UnfoldTransitionInteractor { + /** Returns availability of fold/unfold transitions on the device */ val isAvailable: Boolean + /** Suspends and waits for a fold/unfold transition to finish */ suspend fun waitForTransitionFinish() + + /** Suspends and waits for a fold/unfold transition to start */ + suspend fun waitForTransitionStart() } class UnfoldTransitionInteractorImpl @@ -37,4 +47,8 @@ constructor(private val repository: UnfoldTransitionRepository) : UnfoldTransiti override suspend fun waitForTransitionFinish() { repository.transitionStatus.filter { it is TransitionFinished }.first() } + + override suspend fun waitForTransitionStart() { + repository.transitionStatus.filter { it is TransitionStarted }.first() + } } diff --git a/packages/SystemUI/src/com/android/systemui/util/Utils.java b/packages/SystemUI/src/com/android/systemui/util/Utils.java index fa6d0552c9e9..7861ded6cd24 100644 --- a/packages/SystemUI/src/com/android/systemui/util/Utils.java +++ b/packages/SystemUI/src/com/android/systemui/util/Utils.java @@ -82,6 +82,14 @@ public class Utils { } /** + * Returns {@code true} if the device is a foldable device + */ + public static boolean isDeviceFoldable(Context context) { + return context.getResources() + .getIntArray(com.android.internal.R.array.config_foldedDeviceStates).length != 0; + } + + /** * Allow the media player to be shown in the QS area, controlled by 2 flags. * On by default, but can be disabled by setting either flag to 0/false. */ diff --git a/packages/SystemUI/src/com/android/systemui/util/time/MeasureTimeUtil.kt b/packages/SystemUI/src/com/android/systemui/util/time/MeasureTimeUtil.kt new file mode 100644 index 000000000000..f13196857167 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/time/MeasureTimeUtil.kt @@ -0,0 +1,32 @@ +/* + * 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.util.time + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Executes the given [block] and returns elapsed time using provided [systemClock] in milliseconds. + */ +@OptIn(ExperimentalContracts::class) +inline fun measureTimeMillis(systemClock: SystemClock, block: () -> Unit): Long { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + val start = systemClock.currentTimeMillis() + block() + return systemClock.currentTimeMillis() - start +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt new file mode 100644 index 000000000000..ee2e5addd0e6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt @@ -0,0 +1,324 @@ +/* + * 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.unfold + +import android.content.Context +import android.content.res.Resources +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.display.data.repository.DeviceStateRepository +import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.ScreenPowerState +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.power.shared.model.WakefulnessModel +import com.android.systemui.power.shared.model.WakefulnessState +import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_CLOSED +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_HALF_OPEN +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent +import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl +import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractorImpl +import com.android.systemui.util.animation.data.repository.AnimationStatusRepository +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +@SmallTest +class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { + private lateinit var displaySwitchLatencyTracker: DisplaySwitchLatencyTracker + @Captor private lateinit var loggerArgumentCaptor: ArgumentCaptor<DisplaySwitchLatencyEvent> + + private val mockContext = mock<Context>() + private val resources = mock<Resources>() + private val foldStateRepository = mock<DeviceStateRepository>() + private val powerInteractor = mock<PowerInteractor>() + private val animationStatusRepository = mock<AnimationStatusRepository>() + private val keyguardInteractor = mock<KeyguardInteractor>() + private val displaySwitchLatencyLogger = mock<DisplaySwitchLatencyLogger>() + + private val nonEmptyClosedDeviceStatesArray: IntArray = IntArray(2) { 0 } + private val testDispatcher: TestDispatcher = StandardTestDispatcher() + private val testScope: TestScope = TestScope(testDispatcher) + private val isAsleep = MutableStateFlow(false) + private val isAodAvailable = MutableStateFlow(false) + private val deviceState = MutableStateFlow(DeviceState.UNFOLDED) + private val screenPowerState = MutableStateFlow(ScreenPowerState.SCREEN_ON) + private val areAnimationEnabled = MutableStateFlow(true) + private val lastWakefulnessEvent = MutableStateFlow(WakefulnessModel()) + private val systemClock = FakeSystemClock() + private val unfoldTransitionProgressProvider = TestUnfoldTransitionProvider() + private val unfoldTransitionRepository = + UnfoldTransitionRepositoryImpl(Optional.of(unfoldTransitionProgressProvider)) + private val unfoldTransitionInteractor = + UnfoldTransitionInteractorImpl(unfoldTransitionRepository) + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + whenever(mockContext.resources).thenReturn(resources) + whenever(resources.getIntArray(R.array.config_foldedDeviceStates)) + .thenReturn(nonEmptyClosedDeviceStatesArray) + whenever(foldStateRepository.state).thenReturn(deviceState) + whenever(powerInteractor.isAsleep).thenReturn(isAsleep) + whenever(animationStatusRepository.areAnimationsEnabled()).thenReturn(areAnimationEnabled) + whenever(powerInteractor.screenPowerState).thenReturn(screenPowerState) + whenever(keyguardInteractor.isAodAvailable).thenReturn(isAodAvailable) + whenever(powerInteractor.detailedWakefulness).thenReturn(lastWakefulnessEvent) + + displaySwitchLatencyTracker = + DisplaySwitchLatencyTracker( + mockContext, + foldStateRepository, + powerInteractor, + unfoldTransitionInteractor, + animationStatusRepository, + keyguardInteractor, + testDispatcher.asExecutor(), + testScope.backgroundScope, + displaySwitchLatencyLogger, + systemClock + ) + } + + @Test + fun unfold_logsLatencyTillTransitionStarted() { + testScope.runTest { + areAnimationEnabled.emit(true) + + displaySwitchLatencyTracker.start() + deviceState.emit(DeviceState.FOLDED) + screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + systemClock.advanceTime(50) + runCurrent() + deviceState.emit(DeviceState.HALF_FOLDED) + runCurrent() + systemClock.advanceTime(50) + screenPowerState.emit(ScreenPowerState.SCREEN_ON) + systemClock.advanceTime(200) + unfoldTransitionProgressProvider.onTransitionStarted() + runCurrent() + deviceState.emit(DeviceState.UNFOLDED) + + verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) + val loggedEvent = loggerArgumentCaptor.value + val expectedLoggedEvent = + DisplaySwitchLatencyEvent( + latencyMs = 250, + fromFoldableDeviceState = FOLDABLE_DEVICE_STATE_CLOSED, + toFoldableDeviceState = FOLDABLE_DEVICE_STATE_HALF_OPEN + ) + assertThat(loggedEvent).isEqualTo(expectedLoggedEvent) + } + } + + @Test + fun unfold_progressUnavailable_logsLatencyTillScreenTurnedOn() { + testScope.runTest { + val unfoldTransitionInteractorWithEmptyProgressProvider = + UnfoldTransitionInteractorImpl(UnfoldTransitionRepositoryImpl(Optional.empty())) + displaySwitchLatencyTracker = + DisplaySwitchLatencyTracker( + mockContext, + foldStateRepository, + powerInteractor, + unfoldTransitionInteractorWithEmptyProgressProvider, + animationStatusRepository, + keyguardInteractor, + testDispatcher.asExecutor(), + testScope.backgroundScope, + displaySwitchLatencyLogger, + systemClock + ) + areAnimationEnabled.emit(true) + + displaySwitchLatencyTracker.start() + deviceState.emit(DeviceState.FOLDED) + screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + systemClock.advanceTime(50) + runCurrent() + deviceState.emit(DeviceState.HALF_FOLDED) + systemClock.advanceTime(50) + runCurrent() + screenPowerState.emit(ScreenPowerState.SCREEN_ON) + systemClock.advanceTime(50) + runCurrent() + systemClock.advanceTime(200) + unfoldTransitionProgressProvider.onTransitionStarted() + runCurrent() + deviceState.emit(DeviceState.UNFOLDED) + + verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) + val loggedEvent = loggerArgumentCaptor.value + val expectedLoggedEvent = + DisplaySwitchLatencyEvent( + latencyMs = 50, + fromFoldableDeviceState = FOLDABLE_DEVICE_STATE_CLOSED, + toFoldableDeviceState = FOLDABLE_DEVICE_STATE_HALF_OPEN + ) + assertThat(loggedEvent).isEqualTo(expectedLoggedEvent) + } + } + + @Test + fun unfold_animationDisabled_logsLatencyTillScreenTurnedOn() { + testScope.runTest { + areAnimationEnabled.emit(false) + + displaySwitchLatencyTracker.start() + deviceState.emit(DeviceState.FOLDED) + screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + systemClock.advanceTime(50) + runCurrent() + deviceState.emit(DeviceState.HALF_FOLDED) + systemClock.advanceTime(50) + runCurrent() + screenPowerState.emit(ScreenPowerState.SCREEN_ON) + systemClock.advanceTime(50) + runCurrent() + unfoldTransitionProgressProvider.onTransitionStarted() + systemClock.advanceTime(200) + runCurrent() + deviceState.emit(DeviceState.UNFOLDED) + + verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) + val loggedEvent = loggerArgumentCaptor.value + val expectedLoggedEvent = + DisplaySwitchLatencyEvent( + latencyMs = 50, + fromFoldableDeviceState = FOLDABLE_DEVICE_STATE_CLOSED, + toFoldableDeviceState = FOLDABLE_DEVICE_STATE_HALF_OPEN + ) + assertThat(loggedEvent).isEqualTo(expectedLoggedEvent) + } + } + + @Test + fun foldWhileStayingAwake_logsLatency() { + testScope.runTest { + areAnimationEnabled.emit(true) + deviceState.emit(DeviceState.UNFOLDED) + screenPowerState.emit(ScreenPowerState.SCREEN_ON) + + displaySwitchLatencyTracker.start() + deviceState.emit(DeviceState.HALF_FOLDED) + systemClock.advanceTime(50) + runCurrent() + deviceState.emit(DeviceState.FOLDED) + screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + runCurrent() + systemClock.advanceTime(200) + screenPowerState.emit(ScreenPowerState.SCREEN_ON) + runCurrent() + + verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) + val loggedEvent = loggerArgumentCaptor.value + val expectedLoggedEvent = + DisplaySwitchLatencyEvent( + latencyMs = 200, + fromFoldableDeviceState = FOLDABLE_DEVICE_STATE_HALF_OPEN, + toFoldableDeviceState = FOLDABLE_DEVICE_STATE_CLOSED + ) + assertThat(loggedEvent).isEqualTo(expectedLoggedEvent) + } + } + + @Test + fun foldToAod_capturesToStateAsAod() { + testScope.runTest { + areAnimationEnabled.emit(true) + deviceState.emit(DeviceState.UNFOLDED) + isAodAvailable.emit(true) + + displaySwitchLatencyTracker.start() + deviceState.emit(DeviceState.HALF_FOLDED) + systemClock.advanceTime(50) + runCurrent() + deviceState.emit(DeviceState.FOLDED) + lastWakefulnessEvent.emit( + WakefulnessModel( + internalWakefulnessState = WakefulnessState.ASLEEP, + lastSleepReason = WakeSleepReason.FOLD + ) + ) + screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + runCurrent() + systemClock.advanceTime(200) + screenPowerState.emit(ScreenPowerState.SCREEN_ON) + runCurrent() + + verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) + val loggedEvent = loggerArgumentCaptor.value + val expectedLoggedEvent = + DisplaySwitchLatencyEvent( + latencyMs = 200, + fromFoldableDeviceState = FOLDABLE_DEVICE_STATE_HALF_OPEN, + toFoldableDeviceState = FOLDABLE_DEVICE_STATE_CLOSED, + toState = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD + ) + assertThat(loggedEvent).isEqualTo(expectedLoggedEvent) + } + } + + @Test + fun fold_notAFoldable_shouldNotLogLatency() { + testScope.runTest { + areAnimationEnabled.emit(true) + deviceState.emit(DeviceState.UNFOLDED) + whenever(resources.getIntArray(R.array.config_foldedDeviceStates)) + .thenReturn(IntArray(0)) + + displaySwitchLatencyTracker.start() + deviceState.emit(DeviceState.HALF_FOLDED) + systemClock.advanceTime(50) + runCurrent() + deviceState.emit(DeviceState.FOLDED) + screenPowerState.emit(ScreenPowerState.SCREEN_OFF) + runCurrent() + systemClock.advanceTime(200) + screenPowerState.emit(ScreenPowerState.SCREEN_ON) + runCurrent() + + verify(displaySwitchLatencyLogger, never()).log(any()) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/FoldStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldStateRepositoryTest.kt index 065132300564..ab779a75518e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/FoldStateRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldStateRepositoryTest.kt @@ -13,13 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.unfold.updates +package com.android.systemui.unfold import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.unfold.updates.FoldStateRepository.FoldUpdate +import com.android.systemui.unfold.data.repository.FoldStateRepository.FoldUpdate +import com.android.systemui.unfold.data.repository.FoldStateRepositoryImpl +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.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.mock diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 4200f05ad64b..975db3b179ac 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -80,7 +80,7 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { override val lastDozeTapToWakePosition = _lastDozeTapToWakePosition.asStateFlow() private val _isAodAvailable = MutableStateFlow(false) - override val isAodAvailable: Flow<Boolean> = _isAodAvailable + override val isAodAvailable: StateFlow<Boolean> = _isAodAvailable private val _isDreaming = MutableStateFlow(false) override val isDreaming: Flow<Boolean> = _isDreaming diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt index f7fb01465a40..1b7e71a42c22 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/UnfoldSharedModule.kt @@ -27,8 +27,6 @@ import com.android.systemui.unfold.progress.PhysicsBasedUnfoldTransitionProgress import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder import com.android.systemui.unfold.updates.DeviceFoldStateProvider import com.android.systemui.unfold.updates.FoldStateProvider -import com.android.systemui.unfold.updates.FoldStateRepository -import com.android.systemui.unfold.updates.FoldStateRepositoryImpl import com.android.systemui.unfold.updates.RotationChangeProvider import com.android.systemui.unfold.updates.hinge.EmptyHingeAngleProvider import com.android.systemui.unfold.updates.hinge.HingeAngleProvider @@ -68,10 +66,6 @@ class UnfoldSharedModule { fun unfoldKeyguardVisibilityManager( impl: UnfoldKeyguardVisibilityManagerImpl ): UnfoldKeyguardVisibilityManager = impl - - @Provides - @Singleton - fun foldStateRepository(impl: FoldStateRepositoryImpl): FoldStateRepository = impl } @Module |