diff options
7 files changed, 643 insertions, 58 deletions
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 5b989cb6abc4..8b47a56f9440 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1960,6 +1960,16 @@ flag { } flag { + name: "unfold_latency_tracking_fix" + namespace: "systemui" + description: "New implementation to track unfold latency that excludes broken cases" + bug: "390649568" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "ui_rich_ongoing_force_expanded" namespace: "systemui" description: "Force promoted notifications to always be expanded" diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt index fecf1fd2f222..354edac75452 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt @@ -20,9 +20,12 @@ import android.content.Context import android.content.res.Resources import android.hardware.devicestate.DeviceStateManager import android.os.PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD +import android.os.PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.R +import com.android.internal.util.LatencyTracker +import com.android.internal.util.LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractorImpl @@ -44,8 +47,10 @@ import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_OFF import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON import com.android.systemui.shared.system.SysUiStatsLog import com.android.systemui.statusbar.policy.FakeConfigurationController +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.COOL_DOWN_DURATION 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.Companion.SCREEN_EVENT_TIMEOUT import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor @@ -56,11 +61,13 @@ import com.android.systemui.util.mockito.capture import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.Optional +import kotlin.time.Duration.Companion.milliseconds 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.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -73,6 +80,7 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations import org.mockito.kotlin.mock +import org.mockito.kotlin.times @RunWith(AndroidJUnit4::class) @SmallTest @@ -88,6 +96,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { private val animationStatusRepository = kosmos.fakeAnimationStatusRepository private val keyguardInteractor = mock<KeyguardInteractor>() private val displaySwitchLatencyLogger = mock<DisplaySwitchLatencyLogger>() + private val latencyTracker = mock<LatencyTracker>() private val deviceStateManager = kosmos.deviceStateManager private val closedDeviceState = kosmos.foldedDeviceStateList.first() @@ -142,6 +151,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { displaySwitchLatencyLogger, systemClock, deviceStateManager, + latencyTracker, ) } @@ -195,6 +205,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { displaySwitchLatencyLogger, systemClock, deviceStateManager, + latencyTracker, ) displaySwitchLatencyTracker.start() @@ -370,6 +381,256 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { } } + @Test + fun unfoldingDevice_startsLatencyTracking() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + + verify(latencyTracker).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun foldingDevice_doesntTrackLatency() { + testScope.runTest { + setDeviceState(UNFOLDED) + displaySwitchLatencyTracker.start() + runCurrent() + + startFolding() + + verify(latencyTracker, never()).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun foldedState_doesntStartTrackingOnScreenOn() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verify(latencyTracker, never()).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_endsLatencyTrackingWhenTransitionStarts() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + unfoldTransitionProgressProvider.onTransitionStarted() + runCurrent() + + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_animationsDisabled_endsLatencyTrackingWhenScreenOn() { + testScope.runTest { + animationStatusRepository.onAnimationStatusChanged(enabled = false) + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_doesntEndLatencyTrackingWhenScreenOn() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun unfoldingDevice_animationsDisabled_endsLatencyTrackingWhenDeviceGoesToSleep() { + testScope.runTest { + animationStatusRepository.onAnimationStatusChanged(enabled = false) + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + powerInteractor.setAsleepForTest(sleepReason = GO_TO_SLEEP_REASON_POWER_BUTTON) + runCurrent() + + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_cancelsTrackingWhenNewDeviceStateEmitted() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + finishFolding() + + verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_cancelsTrackingForManyStateChanges() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + startUnfolding() + startFolding() + startUnfolding() + finishUnfolding() + + verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_startsOneTrackingForManyStateChanges() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + startUnfolding() + startFolding() + startUnfolding() + + verify(latencyTracker, times(1)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun interruptedDisplaySwitchFinished_inCoolDownPeriod_trackingDisabled() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + finishFolding() + + advanceTimeBy(COOL_DOWN_DURATION.minus(10.milliseconds)) + startUnfolding() + finishUnfolding() + + verify(latencyTracker, times(1)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun interruptedDisplaySwitchFinished_coolDownPassed_trackingWorksAsUsual() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + finishFolding() + + advanceTimeBy(COOL_DOWN_DURATION.plus(10.milliseconds)) + startUnfolding() + finishUnfolding() + + verify(latencyTracker, times(2)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_coolDownExtendedByStartEvents() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + advanceTimeBy(COOL_DOWN_DURATION.minus(10.milliseconds)) + startUnfolding() + advanceTimeBy(20.milliseconds) + + startFolding() + finishUnfolding() + + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchInterrupted_coolDownExtendedByAnyEndEvent() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + startFolding() + startUnfolding() + advanceTimeBy(COOL_DOWN_DURATION - 10.milliseconds) + powerInteractor.setScreenPowerState(SCREEN_ON) + advanceTimeBy(20.milliseconds) + + startFolding() + finishUnfolding() + + verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + @Test + fun displaySwitchTimedOut_trackingCancelled() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + + startUnfolding() + advanceTimeBy(SCREEN_EVENT_TIMEOUT + 10.milliseconds) + finishUnfolding() + + verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + } + } + + private suspend fun TestScope.startInFoldedState(tracker: DisplaySwitchLatencyTracker) { + setDeviceState(FOLDED) + tracker.start() + runCurrent() + } + + private suspend fun TestScope.startUnfolding() { + setDeviceState(HALF_FOLDED) + powerInteractor.setScreenPowerState(SCREEN_OFF) + runCurrent() + } + + private suspend fun TestScope.startFolding() { + setDeviceState(FOLDED) + powerInteractor.setScreenPowerState(SCREEN_OFF) + runCurrent() + } + + private fun TestScope.finishFolding() { + powerInteractor.setScreenPowerState(SCREEN_ON) + runCurrent() + } + + private fun TestScope.finishUnfolding() { + unfoldTransitionProgressProvider.onTransitionStarted() + runCurrent() + } + private suspend fun setDeviceState(state: DeviceState) { foldStateRepository.emit(state) } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index fcc3ea9f7d58..fed77090c477 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -18,6 +18,7 @@ package com.android.systemui.dagger import com.android.keyguard.KeyguardBiometricLockoutLogger import com.android.systemui.CoreStartable +import com.android.systemui.Flags.unfoldLatencyTrackingFix import com.android.systemui.LatencyTester import com.android.systemui.SliceBroadcastRelayHandler import com.android.systemui.accessibility.Magnification @@ -60,6 +61,7 @@ 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.unfold.NoCooldownDisplaySwitchLatencyTracker import com.android.systemui.usb.StorageNotification import com.android.systemui.util.NotificationChannels import com.android.systemui.util.StartBinderLoggerModule @@ -67,8 +69,10 @@ import com.android.systemui.wallpapers.dagger.WallpaperModule import com.android.systemui.wmshell.WMShell import dagger.Binds import dagger.Module +import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap +import javax.inject.Provider /** * DEPRECATED: DO NOT ADD THINGS TO THIS FILE. @@ -148,12 +152,6 @@ 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 @@ -353,4 +351,15 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(ComplicationTypesUpdater::class) abstract fun bindComplicationTypesUpdater(updater: ComplicationTypesUpdater): CoreStartable + + companion object { + @Provides + @IntoMap + @ClassKey(DisplaySwitchLatencyTracker::class) + fun provideDisplaySwitchLatencyTracker( + noCoolDownVariant: Provider<NoCooldownDisplaySwitchLatencyTracker>, + coolDownVariant: Provider<DisplaySwitchLatencyTracker>, + ): CoreStartable = + if (unfoldLatencyTrackingFix()) coolDownVariant.get() else noCoolDownVariant.get() + } } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt index f5aac720fd47..cd401d5deb6e 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt @@ -19,8 +19,11 @@ package com.android.systemui.unfold import android.content.Context import android.hardware.devicestate.DeviceStateManager import android.util.Log +import androidx.annotation.VisibleForTesting import com.android.app.tracing.TraceUtils.traceAsync import com.android.app.tracing.instantForTrack +import com.android.internal.util.LatencyTracker +import com.android.internal.util.LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -30,10 +33,12 @@ 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.DisplaySwitchLatencyEvent import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg +import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import com.android.systemui.util.Compile import com.android.systemui.util.Utils.isDeviceFoldable @@ -42,17 +47,23 @@ import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.race import com.android.systemui.util.time.SystemClock import com.android.systemui.util.time.measureTimeMillis -import java.time.Duration import java.util.concurrent.Executor import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import com.android.app.tracing.coroutines.launchTraced as launch +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.timeout +import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout /** @@ -73,63 +84,96 @@ constructor( @Application private val applicationScope: CoroutineScope, private val displaySwitchLatencyLogger: DisplaySwitchLatencyLogger, private val systemClock: SystemClock, - private val deviceStateManager: DeviceStateManager + private val deviceStateManager: DeviceStateManager, + private val latencyTracker: LatencyTracker, ) : CoreStartable { private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher() private val isAodEnabled: Boolean get() = keyguardInteractor.isAodAvailable.value + private val displaySwitchStarted = + 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 + } + + private var startOrEndEvent: Flow<Any> = merge(displaySwitchStarted, anyEndEventFlow()) + + private var isCoolingDown = false + override fun start() { if (!isDeviceFoldable(context.resources, deviceStateManager)) { return } applicationScope.launch(context = 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 + displaySwitchStarted.collectLatest { (previousState, newState) -> + if (isCoolingDown) return@collectLatest + if (previousState == DeviceState.FOLDED) { + latencyTracker.onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD) + instantForTrack(TAG) { "unfold latency tracking started" } } - .flatMapLatest { foldableDeviceState -> - flow { - var displaySwitchLatencyEvent = DisplaySwitchLatencyEvent() - val toFoldableDeviceState = foldableDeviceState.newValue.toStatsInt() - displaySwitchLatencyEvent = - displaySwitchLatencyEvent.withBeforeFields( - foldableDeviceState.previousValue.toStatsInt() - ) - + try { + withTimeout(SCREEN_EVENT_TIMEOUT) { + val event = + DisplaySwitchLatencyEvent().withBeforeFields(previousState.toStatsInt()) val displaySwitchTimeMs = measureTimeMillis(systemClock) { - try { - withTimeout(SCREEN_EVENT_TIMEOUT) { - traceAsync(TAG, "displaySwitch") { - waitForDisplaySwitch(toFoldableDeviceState) - } - } - } catch (e: TimeoutCancellationException) { - Log.e(TAG, "Wait for display switch timed out") + traceAsync(TAG, "displaySwitch") { + waitForDisplaySwitch(newState.toStatsInt()) } } - - displaySwitchLatencyEvent = - displaySwitchLatencyEvent.withAfterFields( - toFoldableDeviceState, - displaySwitchTimeMs.toInt(), - getCurrentState() - ) - emit(displaySwitchLatencyEvent) + if (previousState == DeviceState.FOLDED) { + latencyTracker.onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD) + } + logDisplaySwitchEvent(event, newState, displaySwitchTimeMs) } + } catch (e: TimeoutCancellationException) { + instantForTrack(TAG) { "tracking timed out" } + latencyTracker.onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + } catch (e: CancellationException) { + instantForTrack(TAG) { "new state interrupted, entering cool down" } + latencyTracker.onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD) + startCoolDown() } - .collect { displaySwitchLatencyLogger.log(it) } + } } } + @OptIn(FlowPreview::class) + private fun startCoolDown() { + if (isCoolingDown) return + isCoolingDown = true + applicationScope.launch(context = backgroundDispatcher) { + val startTime = systemClock.elapsedRealtime() + try { + startOrEndEvent.timeout(COOL_DOWN_DURATION).collect() + } catch (e: TimeoutCancellationException) { + instantForTrack(TAG) { + "cool down finished, lasted ${systemClock.elapsedRealtime() - startTime} ms" + } + isCoolingDown = false + } + } + } + + private fun logDisplaySwitchEvent( + event: DisplaySwitchLatencyEvent, + toFoldableDeviceState: DeviceState, + displaySwitchTimeMs: Long, + ) { + displaySwitchLatencyLogger.log( + event.withAfterFields( + toFoldableDeviceState.toStatsInt(), + displaySwitchTimeMs.toInt(), + getCurrentState(), + ) + ) + } + private fun DeviceState.toStatsInt(): Int = when (this) { DeviceState.FOLDED -> FOLDABLE_DEVICE_STATE_CLOSED @@ -152,9 +196,20 @@ constructor( } } + private fun anyEndEventFlow(): Flow<Any> { + val unfoldStatus = + unfoldTransitionInteractor.unfoldTransitionStatus.filter { it is TransitionStarted } + // dropping first emission as we're only interested in new emissions, not current state + val screenOn = + powerInteractor.screenPowerState.drop(1).filter { it == ScreenPowerState.SCREEN_ON } + val goToSleep = + powerInteractor.detailedWakefulness.drop(1).filter { sleepWithScreenOff(it) } + return merge(screenOn, goToSleep, unfoldStatus) + } + private fun shouldWaitForTransitionStart( toFoldableDeviceState: Int, - isTransitionEnabled: Boolean + isTransitionEnabled: Boolean, ): Boolean = (toFoldableDeviceState != FOLDABLE_DEVICE_STATE_CLOSED && isTransitionEnabled) private suspend fun waitForScreenTurnedOn() { @@ -165,12 +220,13 @@ constructor( private suspend fun waitForGoToSleepWithScreenOff() { traceAsync(TAG, "waitForGoToSleepWithScreenOff()") { - powerInteractor.detailedWakefulness - .filter { it.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled } - .first() + powerInteractor.detailedWakefulness.filter { sleepWithScreenOff(it) }.first() } } + private fun sleepWithScreenOff(model: WakefulnessModel) = + model.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled + private fun getCurrentState(): Int = when { isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD @@ -205,7 +261,7 @@ constructor( private fun DisplaySwitchLatencyEvent.withAfterFields( toFoldableDeviceState: Int, displaySwitchTimeMs: Int, - toState: Int + toState: Int, ): DisplaySwitchLatencyEvent { log { "toFoldableDeviceState=$toFoldableDeviceState, " + @@ -217,7 +273,7 @@ constructor( return copy( toFoldableDeviceState = toFoldableDeviceState, latencyMs = displaySwitchTimeMs, - toState = toState + toState = toState, ) } @@ -250,14 +306,15 @@ constructor( val hallSensorToFirstHingeAngleChangeMs: Int = VALUE_UNKNOWN, val hallSensorToDeviceStateChangeMs: Int = VALUE_UNKNOWN, val onScreenTurningOnToOnDrawnMs: Int = VALUE_UNKNOWN, - val onDrawnToOnScreenTurnedOnMs: 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 val SCREEN_EVENT_TIMEOUT = Duration.ofMillis(15000).toMillis() + @VisibleForTesting val SCREEN_EVENT_TIMEOUT = 15.seconds + @VisibleForTesting val COOL_DOWN_DURATION = 2.seconds private const val FOLDABLE_DEVICE_STATE_UNKNOWN = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_UNKNOWN diff --git a/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt new file mode 100644 index 000000000000..6ac0bb168f18 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt @@ -0,0 +1,243 @@ +/* + * 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.hardware.devicestate.DeviceStateManager +import android.util.Log +import com.android.app.tracing.TraceUtils.traceAsync +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.app.tracing.instantForTrack +import com.android.systemui.CoreStartable +import com.android.systemui.Flags.unfoldLatencyTrackingFix +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.power.shared.model.WakefulnessState +import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent +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.kotlin.race +import com.android.systemui.util.time.SystemClock +import com.android.systemui.util.time.measureTimeMillis +import java.time.Duration +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.TimeoutCancellationException +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.withTimeout + +/** + * Old version of [DisplaySwitchLatencyTracker] tracking only [DisplaySwitchLatencyEvent]. Which + * version is used for tracking depends on [unfoldLatencyTrackingFix] flag. + */ +@SysUISingleton +class NoCooldownDisplaySwitchLatencyTracker +@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, + private val deviceStateManager: DeviceStateManager, +) : CoreStartable { + + private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher() + private val isAodEnabled: Boolean + get() = keyguardInteractor.isAodAvailable.value + + override fun start() { + if (!isDeviceFoldable(context.resources, deviceStateManager)) { + return + } + applicationScope.launch(context = 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) { + try { + withTimeout(SCREEN_EVENT_TIMEOUT) { + traceAsync(TAG, "displaySwitch") { + waitForDisplaySwitch(toFoldableDeviceState) + } + } + } catch (e: TimeoutCancellationException) { + Log.e(TAG, "Wait for display switch timed out") + } + } + + 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 (shouldWaitForTransitionStart(toFoldableDeviceState, isTransitionEnabled)) { + traceAsync(TAG, "waitForTransitionStart()") { + unfoldTransitionInteractor.waitForTransitionStart() + } + } else { + race({ waitForScreenTurnedOn() }, { waitForGoToSleepWithScreenOff() }) + } + } + + private fun shouldWaitForTransitionStart( + 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 suspend fun waitForGoToSleepWithScreenOff() { + traceAsync(TAG, "waitForGoToSleepWithScreenOff()") { + powerInteractor.detailedWakefulness + .filter { it.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled } + .first() + } + } + + private fun getCurrentState(): Int = + when { + isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD + isStateScreenOff() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__SCREEN_OFF + else -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__UNKNOWN + } + + private fun isStateAod(): Boolean = (isAsleepDueToFold() && isAodEnabled) + + private fun isStateScreenOff(): Boolean = (isAsleepDueToFold() && !isAodEnabled) + + private fun isAsleepDueToFold(): Boolean { + val lastWakefulnessEvent = powerInteractor.detailedWakefulness.value + + return (lastWakefulnessEvent.isAsleep() && + (lastWakefulnessEvent.lastSleepReason == WakeSleepReason.FOLD)) + } + + 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, + ) + } + + 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 val SCREEN_EVENT_TIMEOUT = Duration.ofMillis(15000).toMillis() + + 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/UnfoldLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt index f806a5c52d5a..9248cc801227 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt @@ -22,6 +22,7 @@ import android.hardware.devicestate.DeviceStateManager import android.os.Trace import android.util.Log import com.android.internal.util.LatencyTracker +import com.android.systemui.Flags.unfoldLatencyTrackingFix import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.keyguard.ScreenLifecycle import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener @@ -63,7 +64,7 @@ constructor( /** Registers for relevant events only if the device is foldable. */ fun init() { - if (!isFoldable) { + if (unfoldLatencyTrackingFix() || !isFoldable) { return } deviceStateManager.registerCallback(uiBgExecutor, foldStateListener) @@ -85,7 +86,7 @@ constructor( if (DEBUG) { Log.d( TAG, - "onScreenTurnedOn: folded = $folded, isTransitionEnabled = $isTransitionEnabled" + "onScreenTurnedOn: folded = $folded, isTransitionEnabled = $isTransitionEnabled", ) } @@ -109,7 +110,7 @@ constructor( if (DEBUG) { Log.d( TAG, - "onTransitionStarted: folded = $folded, isTransitionEnabled = $isTransitionEnabled" + "onTransitionStarted: folded = $folded, isTransitionEnabled = $isTransitionEnabled", ) } @@ -161,7 +162,7 @@ constructor( Log.d( TAG, "Starting ACTION_SWITCH_DISPLAY_UNFOLD, " + - "isTransitionEnabled = $isTransitionEnabled" + "isTransitionEnabled = $isTransitionEnabled", ) } } 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 885a2b0d7305..c2f86a37c6d8 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 @@ -21,6 +21,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository +import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionInProgress import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted @@ -48,6 +49,9 @@ constructor( val isAvailable: Boolean get() = repository.isAvailable + /** Flow of latest [UnfoldTransitionStatus] changes */ + val unfoldTransitionStatus: Flow<UnfoldTransitionStatus> = repository.transitionStatus + /** * This mapping emits 1 when the device is completely unfolded and 0.0 when the device is * completely folded. |