diff options
6 files changed, 359 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt new file mode 100644 index 000000000000..4eb7d44ebfa6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt @@ -0,0 +1,110 @@ +/* + * 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.keyguard + +import android.annotation.WorkerThread +import android.content.ComponentCallbacks2 +import android.os.Trace +import android.util.Log +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.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.shared.model.WakefulnessState +import com.android.systemui.utils.GlobalWindowManager +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** + * Releases cached resources on allocated by keyguard. + * + * We release most resources when device goes idle since that's the least likely time it'll cause + * jank during use. Idle in this case means after lockscreen -> AoD transition completes or when the + * device screen is turned off, depending on settings. + */ +@SysUISingleton +class ResourceTrimmer +@Inject +constructor( + private val keyguardInteractor: KeyguardInteractor, + private val globalWindowManager: GlobalWindowManager, + @Application private val applicationScope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, +) : CoreStartable, WakefulnessLifecycle.Observer { + + override fun start() { + Log.d(LOG_TAG, "Resource trimmer registered.") + applicationScope.launch(bgDispatcher) { + // We need to wait for the AoD transition (and animation) to complete. + // This means we're waiting for isDreaming (== implies isDoze) and dozeAmount == 1f + // signal. This is to make sure we don't clear font caches during animation which + // would jank and leave stale data in memory. + val isDozingFully = + keyguardInteractor.dozeAmount.map { it == 1f }.distinctUntilChanged() + combine( + keyguardInteractor.wakefulnessModel.map { it.state }, + keyguardInteractor.isDreaming, + isDozingFully, + ::Triple + ) + .distinctUntilChanged() + .collect { onWakefulnessUpdated(it.first, it.second, it.third) } + } + } + + @WorkerThread + private fun onWakefulnessUpdated( + wakefulness: WakefulnessState, + isDreaming: Boolean, + isDozingFully: Boolean + ) { + if (DEBUG) { + Log.d( + LOG_TAG, + "Wakefulness: $wakefulness Dreaming: $isDreaming DozeAmount: $isDozingFully" + ) + } + // There are three scenarios: + // * No dozing and no AoD at all - where we go directly to ASLEEP with isDreaming = false + // and dozeAmount == 0f + // * Dozing without Aod - where we go to ASLEEP with isDreaming = true and dozeAmount jumps + // to 1f + // * AoD - where we go to ASLEEP with iDreaming = true and dozeAmount slowly increases + // to 1f + val dozeDisabledAndScreenOff = wakefulness == WakefulnessState.ASLEEP && !isDreaming + val dozeEnabledAndDozeAnimationCompleted = + wakefulness == WakefulnessState.ASLEEP && isDreaming && isDozingFully + if (dozeDisabledAndScreenOff || dozeEnabledAndDozeAnimationCompleted) { + Trace.beginSection("ResourceTrimmer#trimMemory") + Log.d(LOG_TAG, "SysUI asleep, trimming memory.") + globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) + Trace.endSection() + } + } + + companion object { + private const val LOG_TAG = "ResourceTrimmer" + private val DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 255556c80121..d7c039d9b519 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -87,6 +87,7 @@ import java.util.concurrent.Executor; KeyguardRepositoryModule.class, KeyguardFaceAuthModule.class, StartKeyguardTransitionModule.class, + ResourceTrimmerModule.class, }) public class KeyguardModule { /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/ResourceTrimmerModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/ResourceTrimmerModule.java new file mode 100644 index 000000000000..d693326f0dba --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/ResourceTrimmerModule.java @@ -0,0 +1,38 @@ +/* + * 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.keyguard.dagger; + +import com.android.systemui.CoreStartable; +import com.android.systemui.keyguard.ResourceTrimmer; + +import dagger.Binds; +import dagger.Module; +import dagger.multibindings.ClassKey; +import dagger.multibindings.IntoMap; + +/** + * Binds {@link ResourceTrimmer} into {@link CoreStartable} set. + */ +@Module +public abstract class ResourceTrimmerModule { + + /** Bind ResourceTrimmer into CoreStarteables. */ + @Binds + @IntoMap + @ClassKey(ResourceTrimmer.class) + public abstract CoreStartable bindResourceTrimmer(ResourceTrimmer resourceTrimmer); +} diff --git a/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt b/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt new file mode 100644 index 000000000000..038fddc1f7a9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt @@ -0,0 +1,16 @@ +package com.android.systemui.utils + +import android.view.WindowManagerGlobal +import javax.inject.Inject + +/** Interface to talk to [WindowManagerGlobal] */ +class GlobalWindowManager @Inject constructor() { + /** + * Sends a trim memory command to [WindowManagerGlobal]. + * + * @param level One of levels from [ComponentCallbacks2] starting with TRIM_ + */ + fun trimMemory(level: Int) { + WindowManagerGlobal.getInstance().trimMemory(level) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt new file mode 100644 index 000000000000..931f82ce9f6b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt @@ -0,0 +1,190 @@ +package com.android.systemui.keyguard + +import android.content.ComponentCallbacks2 +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.keyguard.data.repository.FakeCommandQueue +import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.shared.model.WakeSleepReason +import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.keyguard.shared.model.WakefulnessState +import com.android.systemui.utils.GlobalWindowManager +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +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.Mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ResourceTrimmerTest : SysuiTestCase() { + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + private val keyguardRepository = FakeKeyguardRepository() + + @Mock private lateinit var globalWindowManager: GlobalWindowManager + @Mock private lateinit var featureFlags: FeatureFlags + private lateinit var resourceTrimmer: ResourceTrimmer + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + keyguardRepository.setWakefulnessModel( + WakefulnessModel(WakefulnessState.AWAKE, WakeSleepReason.OTHER, WakeSleepReason.OTHER) + ) + keyguardRepository.setDozeAmount(0f) + + val interactor = + KeyguardInteractor( + keyguardRepository, + FakeCommandQueue(), + featureFlags, + FakeKeyguardBouncerRepository() + ) + resourceTrimmer = + ResourceTrimmer( + interactor, + globalWindowManager, + testScope.backgroundScope, + testDispatcher + ) + resourceTrimmer.start() + } + + @Test + fun noChange_noOutputChanges() = + testScope.runTest { + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + } + + @Test + fun dozeAodDisabled_sleep_trimsMemory() = + testScope.runTest { + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + testScope.runCurrent() + verify(globalWindowManager, times(1)) + .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) + } + + @Test + fun dozeEnabled_sleepWithFullDozeAmount_trimsMemory() = + testScope.runTest { + keyguardRepository.setDreaming(true) + keyguardRepository.setDozeAmount(1f) + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + testScope.runCurrent() + verify(globalWindowManager, times(1)) + .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) + } + + @Test + fun dozeEnabled_sleepWithoutFullDozeAmount_doesntTrimMemory() = + testScope.runTest { + keyguardRepository.setDreaming(true) + keyguardRepository.setDozeAmount(0f) + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + } + + @Test + fun aodEnabled_sleepWithFullDozeAmount_trimsMemoryOnce() { + testScope.runTest { + keyguardRepository.setDreaming(true) + keyguardRepository.setDozeAmount(0f) + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + + generateSequence(0f) { it + 0.1f } + .takeWhile { it < 1f } + .forEach { + keyguardRepository.setDozeAmount(it) + testScope.runCurrent() + } + verifyZeroInteractions(globalWindowManager) + + keyguardRepository.setDozeAmount(1f) + testScope.runCurrent() + verify(globalWindowManager, times(1)) + .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) + } + } + + @Test + fun aodEnabled_deviceWakesHalfWayThrough_doesNotTrimMemory() { + testScope.runTest { + keyguardRepository.setDreaming(true) + keyguardRepository.setDozeAmount(0f) + keyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.ASLEEP, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + + generateSequence(0f) { it + 0.1f } + .takeWhile { it < 0.8f } + .forEach { + keyguardRepository.setDozeAmount(it) + testScope.runCurrent() + } + verifyZeroInteractions(globalWindowManager) + + generateSequence(0.8f) { it - 0.1f } + .takeWhile { it >= 0f } + .forEach { + keyguardRepository.setDozeAmount(it) + testScope.runCurrent() + } + + keyguardRepository.setDozeAmount(0f) + testScope.runCurrent() + verifyZeroInteractions(globalWindowManager) + } + } +} 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 fd8c4b81063d..b52a76839a99 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 @@ -147,6 +147,10 @@ class FakeKeyguardRepository : KeyguardRepository { _isAodAvailable.value = isAodAvailable } + fun setDreaming(isDreaming: Boolean) { + _isDreaming.value = isDreaming + } + fun setDreamingWithOverlay(isDreaming: Boolean) { _isDreamingWithOverlay.value = isDreaming } |