• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.unfold
18 
19 import android.content.Context
20 import android.hardware.devicestate.DeviceStateManager
21 import android.util.Log
22 import androidx.annotation.VisibleForTesting
23 import com.android.app.tracing.TraceUtils.traceAsync
24 import com.android.app.tracing.instantForTrack
25 import com.android.internal.util.LatencyTracker
26 import com.android.internal.util.LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD
27 import com.android.systemui.CoreStartable
28 import com.android.systemui.dagger.SysUISingleton
29 import com.android.systemui.dagger.qualifiers.Application
30 import com.android.systemui.display.data.repository.DeviceStateRepository
31 import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState
32 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
33 import com.android.systemui.power.domain.interactor.PowerInteractor
34 import com.android.systemui.power.shared.model.ScreenPowerState
35 import com.android.systemui.power.shared.model.WakeSleepReason
36 import com.android.systemui.power.shared.model.WakefulnessModel
37 import com.android.systemui.power.shared.model.WakefulnessState
38 import com.android.systemui.shared.system.SysUiStatsLog
39 import com.android.systemui.unfold.DisplaySwitchLatencyTracker.TrackingResult.CORRUPTED
40 import com.android.systemui.unfold.DisplaySwitchLatencyTracker.TrackingResult.SUCCESS
41 import com.android.systemui.unfold.DisplaySwitchLatencyTracker.TrackingResult.TIMED_OUT
42 import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
43 import com.android.systemui.unfold.data.repository.ScreenTimeoutPolicyRepository
44 import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted
45 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
46 import com.android.systemui.util.Compile
47 import com.android.systemui.util.Utils.isDeviceFoldable
48 import com.android.systemui.util.animation.data.repository.AnimationStatusRepository
49 import com.android.systemui.util.kotlin.WithPrev
50 import com.android.systemui.util.kotlin.pairwise
51 import com.android.systemui.util.kotlin.race
52 import com.android.systemui.util.time.SystemClock
53 import com.android.systemui.util.time.measureTimeMillis
54 import java.util.concurrent.Executor
55 import javax.inject.Inject
56 import kotlin.coroutines.cancellation.CancellationException
57 import kotlin.time.Duration.Companion.seconds
58 import kotlinx.coroutines.CoroutineScope
59 import kotlinx.coroutines.FlowPreview
60 import kotlinx.coroutines.TimeoutCancellationException
61 import kotlinx.coroutines.asCoroutineDispatcher
62 import kotlinx.coroutines.flow.Flow
63 import kotlinx.coroutines.flow.collectLatest
64 import kotlinx.coroutines.flow.drop
65 import kotlinx.coroutines.flow.filter
66 import kotlinx.coroutines.flow.first
67 import kotlinx.coroutines.flow.merge
68 import kotlinx.coroutines.flow.timeout
69 import kotlinx.coroutines.launch
70 import kotlinx.coroutines.withTimeout
71 
72 /**
73  * [DisplaySwitchLatencyTracker] tracks latency and related fields for display switch of a foldable
74  * device. This class populates [DisplaySwitchLatencyEvent] while an ongoing display switch event
75  */
76 @SysUISingleton
77 class DisplaySwitchLatencyTracker
78 @Inject
79 constructor(
80     private val context: Context,
81     private val deviceStateRepository: DeviceStateRepository,
82     private val powerInteractor: PowerInteractor,
83     private val screenTimeoutPolicyRepository: ScreenTimeoutPolicyRepository,
84     private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
85     private val animationStatusRepository: AnimationStatusRepository,
86     private val keyguardInteractor: KeyguardInteractor,
87     @UnfoldSingleThreadBg private val singleThreadBgExecutor: Executor,
88     @Application private val applicationScope: CoroutineScope,
89     private val displaySwitchLatencyLogger: DisplaySwitchLatencyLogger,
90     private val systemClock: SystemClock,
91     private val deviceStateManager: DeviceStateManager,
92     private val latencyTracker: LatencyTracker,
93 ) : CoreStartable {
94 
95     private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher()
96     private val isAodEnabled: Boolean
97         get() = keyguardInteractor.isAodAvailable.value
98 
99     private val displaySwitchStarted =
100         deviceStateRepository.state.pairwise().filter {
101             // Start tracking only when the foldable device is
102             // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or unfolding(FOLDED -> HALF_FOLD/UNFOLDED)
103             foldableDeviceState ->
104             foldableDeviceState.previousValue == DeviceState.FOLDED ||
105                 foldableDeviceState.newValue == DeviceState.FOLDED
106         }
107 
108     private var startOrEndEvent: Flow<Any> = merge(displaySwitchStarted, anyEndEventFlow())
109 
110     private var isCoolingDown = false
111 
112     override fun start() {
113         if (!isDeviceFoldable(context.resources, deviceStateManager)) {
114             return
115         }
116         applicationScope.launch(context = backgroundDispatcher) {
117             displaySwitchStarted.collectLatest { (previousState, newState) ->
118                 if (isCoolingDown) return@collectLatest
119                 if (previousState == DeviceState.FOLDED) {
120                     latencyTracker.onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
121                     instantForTrack(TAG) { "unfold latency tracking started" }
122                 }
123                 val event = DisplaySwitchLatencyEvent().withBeforeFields(previousState.toStatsInt())
124                 try {
125                     withTimeout(SCREEN_EVENT_TIMEOUT) {
126                         val displaySwitchTimeMs =
127                             measureTimeMillis(systemClock) {
128                                 traceAsync(TAG, "displaySwitch") {
129                                     waitForDisplaySwitch(newState.toStatsInt())
130                                 }
131                             }
132                         if (previousState == DeviceState.FOLDED) {
133                             latencyTracker.onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
134                         }
135                         logDisplaySwitchEvent(event, newState, displaySwitchTimeMs)
136                     }
137                 } catch (e: TimeoutCancellationException) {
138                     instantForTrack(TAG) { "tracking timed out" }
139                     latencyTracker.onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
140                     logDisplaySwitchEvent(
141                         event = event,
142                         toFoldableDeviceState = newState,
143                         displaySwitchTimeMs = SCREEN_EVENT_TIMEOUT.inWholeMilliseconds,
144                         trackingResult = TIMED_OUT,
145                     )
146                 } catch (e: CancellationException) {
147                     instantForTrack(TAG) { "new state interrupted, entering cool down" }
148                     latencyTracker.onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
149                     startCoolDown(event)
150                 }
151             }
152         }
153     }
154 
155     @OptIn(FlowPreview::class)
156     private fun startCoolDown(event: DisplaySwitchLatencyEvent) {
157         if (isCoolingDown) return
158         isCoolingDown = true
159         applicationScope.launch(context = backgroundDispatcher) {
160             val startTime = systemClock.elapsedRealtime()
161             var lastState: DeviceState? = null
162             try {
163                 startOrEndEvent.timeout(COOL_DOWN_DURATION).collect {
164                     if (it is WithPrev<*, *>) {
165                         lastState = it.newValue as? DeviceState
166                     }
167                 }
168             } catch (e: TimeoutCancellationException) {
169                 val totalCooldownTime = systemClock.elapsedRealtime() - startTime
170                 logDisplaySwitchEvent(
171                     event = event,
172                     toFoldableDeviceState = lastState ?: DeviceState.UNKNOWN,
173                     displaySwitchTimeMs = totalCooldownTime,
174                     trackingResult = CORRUPTED,
175                 )
176                 instantForTrack(TAG) { "cool down finished, lasted $totalCooldownTime ms" }
177                 isCoolingDown = false
178             }
179         }
180     }
181 
182     private fun logDisplaySwitchEvent(
183         event: DisplaySwitchLatencyEvent,
184         toFoldableDeviceState: DeviceState,
185         displaySwitchTimeMs: Long,
186         trackingResult: TrackingResult = SUCCESS,
187     ) {
188         displaySwitchLatencyLogger.log(
189             event.withAfterFields(
190                 toFoldableDeviceState,
191                 displaySwitchTimeMs,
192                 getCurrentState(),
193                 trackingResult,
194             )
195         )
196     }
197 
198     private fun DeviceState.toStatsInt(): Int =
199         when (this) {
200             DeviceState.FOLDED -> FOLDABLE_DEVICE_STATE_CLOSED
201             DeviceState.HALF_FOLDED -> FOLDABLE_DEVICE_STATE_HALF_OPEN
202             DeviceState.UNFOLDED -> FOLDABLE_DEVICE_STATE_OPEN
203             DeviceState.CONCURRENT_DISPLAY -> FOLDABLE_DEVICE_STATE_FLIPPED
204             else -> FOLDABLE_DEVICE_STATE_UNKNOWN
205         }
206 
207     private fun TrackingResult.toStatsInt(): Int =
208         when (this) {
209             SUCCESS -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TRACKING_RESULT__SUCCESS
210             CORRUPTED -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TRACKING_RESULT__CORRUPTED
211             TIMED_OUT -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TRACKING_RESULT__TIMED_OUT
212         }
213 
214     private suspend fun waitForDisplaySwitch(toFoldableDeviceState: Int) {
215         val isTransitionEnabled =
216             unfoldTransitionInteractor.isAvailable &&
217                 animationStatusRepository.areAnimationsEnabled().first()
218         if (shouldWaitForTransitionStart(toFoldableDeviceState, isTransitionEnabled)) {
219             traceAsync(TAG, "waitForTransitionStart()") {
220                 unfoldTransitionInteractor.waitForTransitionStart()
221             }
222         } else {
223             race({ waitForScreenTurnedOn() }, { waitForGoToSleepWithScreenOff() })
224         }
225     }
226 
227     private fun anyEndEventFlow(): Flow<Any> {
228         val unfoldStatus =
229             unfoldTransitionInteractor.unfoldTransitionStatus.filter { it is TransitionStarted }
230         // dropping first emission as we're only interested in new emissions, not current state
231         val screenOn =
232             powerInteractor.screenPowerState.drop(1).filter { it == ScreenPowerState.SCREEN_ON }
233         val goToSleep =
234             powerInteractor.detailedWakefulness.drop(1).filter { sleepWithScreenOff(it) }
235         return merge(screenOn, goToSleep, unfoldStatus)
236     }
237 
238     private fun shouldWaitForTransitionStart(
239         toFoldableDeviceState: Int,
240         isTransitionEnabled: Boolean,
241     ): Boolean = (toFoldableDeviceState != FOLDABLE_DEVICE_STATE_CLOSED && isTransitionEnabled)
242 
243     private suspend fun waitForScreenTurnedOn() {
244         traceAsync(TAG, "waitForScreenTurnedOn()") {
245             // dropping first as it's stateFlow and will always emit latest value but we're
246             // only interested in new states
247             powerInteractor.screenPowerState
248                 .drop(1)
249                 .filter { it == ScreenPowerState.SCREEN_ON }
250                 .first()
251         }
252     }
253 
254     private suspend fun waitForGoToSleepWithScreenOff() {
255         traceAsync(TAG, "waitForGoToSleepWithScreenOff()") {
256             powerInteractor.detailedWakefulness.filter { sleepWithScreenOff(it) }.first()
257         }
258     }
259 
260     private fun sleepWithScreenOff(model: WakefulnessModel) =
261         model.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled
262 
263     private fun getCurrentState(): Int =
264         when {
265             isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD
266             isStateScreenOff() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__SCREEN_OFF
267             else -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__UNKNOWN
268         }
269 
270     private fun isStateAod(): Boolean = (isAsleepDueToFold() && isAodEnabled)
271 
272     private fun isStateScreenOff(): Boolean = (isAsleepDueToFold() && !isAodEnabled)
273 
274     private fun isAsleepDueToFold(): Boolean {
275         val lastWakefulnessEvent = powerInteractor.detailedWakefulness.value
276 
277         return (lastWakefulnessEvent.isAsleep() &&
278             (lastWakefulnessEvent.lastSleepReason == WakeSleepReason.FOLD))
279     }
280 
281     private inline fun log(msg: () -> String) {
282         if (DEBUG) Log.d(TAG, msg())
283     }
284 
285     private fun DisplaySwitchLatencyEvent.withBeforeFields(
286         fromFoldableDeviceState: Int
287     ): DisplaySwitchLatencyEvent {
288         log { "fromFoldableDeviceState=$fromFoldableDeviceState" }
289         instantForTrack(TAG) { "fromFoldableDeviceState=$fromFoldableDeviceState" }
290 
291         val screenTimeoutActive = screenTimeoutPolicyRepository.screenTimeoutActive.value
292         val screenWakelockStatus =
293             if (screenTimeoutActive) {
294                 NO_SCREEN_WAKELOCKS
295             } else {
296                 HAS_SCREEN_WAKELOCKS
297             }
298 
299         return copy(
300             fromFoldableDeviceState = fromFoldableDeviceState,
301             screenWakelockStatus = screenWakelockStatus
302         )
303     }
304 
305     private fun DisplaySwitchLatencyEvent.withAfterFields(
306         toFoldableDeviceState: DeviceState,
307         displaySwitchTimeMs: Long,
308         toState: Int,
309         trackingResult: TrackingResult,
310     ): DisplaySwitchLatencyEvent {
311         log {
312             "trackingResult=$trackingResult, " +
313                 "toFoldableDeviceState=$toFoldableDeviceState, " +
314                 "toState=$toState, " +
315                 "latencyMs=$displaySwitchTimeMs"
316         }
317         instantForTrack(TAG) { "toFoldableDeviceState=$toFoldableDeviceState, toState=$toState" }
318 
319         return copy(
320             toFoldableDeviceState = toFoldableDeviceState.toStatsInt(),
321             latencyMs = displaySwitchTimeMs.toInt(),
322             toState = toState,
323             trackingResult = trackingResult.toStatsInt(),
324         )
325     }
326 
327     /**
328      * Stores values corresponding to all respective [DisplaySwitchLatencyTrackedField] in a single
329      * event of display switch for foldable devices.
330      *
331      * Once the data is captured in this data class and appropriate to log, it is logged through
332      * [DisplaySwitchLatencyLogger]
333      */
334     data class DisplaySwitchLatencyEvent(
335         val latencyMs: Int = VALUE_UNKNOWN,
336         val fromFoldableDeviceState: Int = FOLDABLE_DEVICE_STATE_UNKNOWN,
337         val fromState: Int = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_STATE__UNKNOWN,
338         val fromFocusedAppUid: Int = VALUE_UNKNOWN,
339         val fromPipAppUid: Int = VALUE_UNKNOWN,
340         val fromVisibleAppsUid: Set<Int> = setOf(),
341         val fromDensityDpi: Int = VALUE_UNKNOWN,
342         val toFoldableDeviceState: Int = FOLDABLE_DEVICE_STATE_UNKNOWN,
343         val toState: Int = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_STATE__UNKNOWN,
344         val toFocusedAppUid: Int = VALUE_UNKNOWN,
345         val toPipAppUid: Int = VALUE_UNKNOWN,
346         val toVisibleAppsUid: Set<Int> = setOf(),
347         val toDensityDpi: Int = VALUE_UNKNOWN,
348         val notificationCount: Int = VALUE_UNKNOWN,
349         val externalDisplayCount: Int = VALUE_UNKNOWN,
350         val throttlingLevel: Int =
351             SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__THROTTLING_LEVEL__NONE,
352         val vskinTemperatureC: Int = VALUE_UNKNOWN,
353         val hallSensorToFirstHingeAngleChangeMs: Int = VALUE_UNKNOWN,
354         val hallSensorToDeviceStateChangeMs: Int = VALUE_UNKNOWN,
355         val onScreenTurningOnToOnDrawnMs: Int = VALUE_UNKNOWN,
356         val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN,
357         val trackingResult: Int =
358             SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TRACKING_RESULT__UNKNOWN_RESULT,
359         val screenWakelockStatus: Int =
360             SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_UNKNOWN,
361     )
362 
363     enum class TrackingResult {
364         SUCCESS,
365         CORRUPTED,
366         TIMED_OUT,
367     }
368 
369     companion object {
370         private const val VALUE_UNKNOWN = -1
371         private const val TAG = "DisplaySwitchLatency"
372         private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE)
373         @VisibleForTesting val SCREEN_EVENT_TIMEOUT = 15.seconds
374         @VisibleForTesting val COOL_DOWN_DURATION = 2.seconds
375 
376         private const val FOLDABLE_DEVICE_STATE_UNKNOWN =
377             SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_UNKNOWN
378         const val FOLDABLE_DEVICE_STATE_CLOSED =
379             SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_CLOSED
380         const val FOLDABLE_DEVICE_STATE_HALF_OPEN =
381             SysUiStatsLog
382                 .DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_HALF_OPENED
383         private const val FOLDABLE_DEVICE_STATE_OPEN =
384             SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_OPENED
385         private const val FOLDABLE_DEVICE_STATE_FLIPPED =
386             SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_FLIPPED
387 
388         private const val HAS_SCREEN_WAKELOCKS =
389             SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_HAS_SCREEN_WAKELOCKS
390         private const val NO_SCREEN_WAKELOCKS =
391             SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_NO_WAKELOCKS
392     }
393 }
394