Loading packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt 0 → 100644 +110 −0 Original line number Diff line number Diff line /* * 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) } } packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +1 −0 Original line number Diff line number Diff line Loading @@ -87,6 +87,7 @@ import java.util.concurrent.Executor; KeyguardRepositoryModule.class, KeyguardFaceAuthModule.class, StartKeyguardTransitionModule.class, ResourceTrimmerModule.class, }) public class KeyguardModule { /** Loading packages/SystemUI/src/com/android/systemui/keyguard/dagger/ResourceTrimmerModule.java 0 → 100644 +38 −0 Original line number Diff line number Diff line /* * 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); } packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt 0 → 100644 +16 −0 Original line number Diff line number Diff line 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) } } packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt 0 → 100644 +190 −0 Original line number Diff line number Diff line 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) } } } Loading
packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt 0 → 100644 +110 −0 Original line number Diff line number Diff line /* * 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) } }
packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +1 −0 Original line number Diff line number Diff line Loading @@ -87,6 +87,7 @@ import java.util.concurrent.Executor; KeyguardRepositoryModule.class, KeyguardFaceAuthModule.class, StartKeyguardTransitionModule.class, ResourceTrimmerModule.class, }) public class KeyguardModule { /** Loading
packages/SystemUI/src/com/android/systemui/keyguard/dagger/ResourceTrimmerModule.java 0 → 100644 +38 −0 Original line number Diff line number Diff line /* * 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); }
packages/SystemUI/src/com/android/systemui/utils/GlobalWindowManager.kt 0 → 100644 +16 −0 Original line number Diff line number Diff line 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) } }
packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt 0 → 100644 +190 −0 Original line number Diff line number Diff line 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) } } }