1 /*
2  * 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.keyguard
18 
19 import android.annotation.WorkerThread
20 import android.content.ComponentCallbacks2
21 import android.graphics.HardwareRenderer
22 import android.os.Trace
23 import android.util.Log
24 import com.android.systemui.CoreStartable
25 import com.android.systemui.dagger.SysUISingleton
26 import com.android.systemui.dagger.qualifiers.Application
27 import com.android.systemui.dagger.qualifiers.Background
28 import com.android.systemui.flags.FeatureFlags
29 import com.android.systemui.flags.Flags
30 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
31 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
32 import com.android.systemui.keyguard.shared.model.TransitionState
33 import com.android.systemui.keyguard.shared.model.WakefulnessState
34 import com.android.systemui.utils.GlobalWindowManager
35 import javax.inject.Inject
36 import kotlinx.coroutines.CoroutineDispatcher
37 import kotlinx.coroutines.CoroutineScope
38 import kotlinx.coroutines.flow.combine
39 import kotlinx.coroutines.flow.distinctUntilChanged
40 import kotlinx.coroutines.flow.map
41 import kotlinx.coroutines.launch
42 
43 /**
44  * Releases cached resources on allocated by keyguard.
45  *
46  * We release most resources when device goes idle since that's the least likely time it'll cause
47  * jank during use. Idle in this case means after lockscreen -> AoD transition completes or when the
48  * device screen is turned off, depending on settings.
49  */
50 @SysUISingleton
51 class ResourceTrimmer
52 @Inject
53 constructor(
54     private val keyguardInteractor: KeyguardInteractor,
55     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
56     private val globalWindowManager: GlobalWindowManager,
57     @Application private val applicationScope: CoroutineScope,
58     @Background private val bgDispatcher: CoroutineDispatcher,
59     private val featureFlags: FeatureFlags,
60 ) : CoreStartable, WakefulnessLifecycle.Observer {
61 
62     override fun start() {
63         Log.d(LOG_TAG, "Resource trimmer registered.")
64         if (featureFlags.isEnabled(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)) {
65             applicationScope.launch(bgDispatcher) {
66                 // We need to wait for the AoD transition (and animation) to complete.
67                 // This means we're waiting for isDreaming (== implies isDoze) and dozeAmount == 1f
68                 // signal. This is to make sure we don't clear font caches during animation which
69                 // would jank and leave stale data in memory.
70                 val isDozingFully =
71                     keyguardInteractor.dozeAmount.map { it == 1f }.distinctUntilChanged()
72                 combine(
73                         keyguardInteractor.wakefulnessModel.map { it.state },
74                         keyguardInteractor.isDreaming,
75                         isDozingFully,
76                         ::Triple
77                     )
78                     .distinctUntilChanged()
79                     .collect { onWakefulnessUpdated(it.first, it.second, it.third) }
80             }
81         }
82 
83         applicationScope.launch(bgDispatcher) {
84             // We drop 1 to avoid triggering on initial collect().
85             keyguardTransitionInteractor.anyStateToGoneTransition.collect { transition ->
86                 if (transition.transitionState == TransitionState.FINISHED) {
87                     onKeyguardGone()
88                 }
89             }
90         }
91     }
92 
93     @WorkerThread
94     private fun onKeyguardGone() {
95         // We want to clear temporary caches we've created while rendering and animating
96         // lockscreen elements, especially clocks.
97         Log.d(LOG_TAG, "Sending TRIM_MEMORY_UI_HIDDEN.")
98         globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
99         if (featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK)) {
100             if (DEBUG) {
101                 Log.d(LOG_TAG, "Trimming font caches since keyguard went away.")
102             }
103             globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_FONT)
104         }
105     }
106 
107     @WorkerThread
108     private fun onWakefulnessUpdated(
109         wakefulness: WakefulnessState,
110         isDreaming: Boolean,
111         isDozingFully: Boolean
112     ) {
113         if (!featureFlags.isEnabled(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)) {
114             return
115         }
116 
117         if (DEBUG) {
118             Log.d(
119                 LOG_TAG,
120                 "Wakefulness: $wakefulness Dreaming: $isDreaming DozeAmount: $isDozingFully"
121             )
122         }
123         // There are three scenarios:
124         // * No dozing and no AoD at all - where we go directly to ASLEEP with isDreaming = false
125         //      and dozeAmount == 0f
126         // * Dozing without Aod - where we go to ASLEEP with isDreaming = true and dozeAmount jumps
127         //      to 1f
128         // * AoD - where we go to ASLEEP with iDreaming = true and dozeAmount slowly increases
129         //      to 1f
130         val dozeDisabledAndScreenOff = wakefulness == WakefulnessState.ASLEEP && !isDreaming
131         val dozeEnabledAndDozeAnimationCompleted =
132             wakefulness == WakefulnessState.ASLEEP && isDreaming && isDozingFully
133         if (dozeDisabledAndScreenOff || dozeEnabledAndDozeAnimationCompleted) {
134             Trace.beginSection("ResourceTrimmer#trimMemory")
135             Log.d(LOG_TAG, "SysUI asleep, trimming memory.")
136             globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
137             globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_ALL)
138             Trace.endSection()
139         }
140     }
141 
142     companion object {
143         private const val LOG_TAG = "ResourceTrimmer"
144         private val DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG)
145     }
146 }
147