Loading packages/SystemUI/multivalentTests/src/com/android/systemui/communal/util/UserTouchActivityNotifierTest.kt 0 → 100644 +74 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.communal.util import android.testing.AndroidTestingRunner import android.view.MotionEvent import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import org.junit.runner.RunWith @RunWith(AndroidTestingRunner::class) @SmallTest class UserTouchActivityNotifierTest : SysuiTestCase() { private val kosmos: Kosmos = testKosmos().useUnconfinedTestDispatcher() @Test fun firstEventTriggersNotify() = kosmos.runTest { sendEventAndVerify(0, MotionEvent.ACTION_MOVE, true) } @Test fun secondEventTriggersRateLimited() = kosmos.runTest { var eventTime = 0L sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true) eventTime += 50 sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, false) eventTime += USER_TOUCH_ACTIVITY_RATE_LIMIT sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true) } @Test fun overridingActionNotifies() = kosmos.runTest { var eventTime = 0L sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true) sendEventAndVerify(eventTime, MotionEvent.ACTION_DOWN, true) sendEventAndVerify(eventTime, MotionEvent.ACTION_UP, true) sendEventAndVerify(eventTime, MotionEvent.ACTION_CANCEL, true) } private fun sendEventAndVerify(eventTime: Long, action: Int, shouldBeHandled: Boolean) { kosmos.fakePowerRepository.userTouchRegistered = false val motionEvent = MotionEvent.obtain(0, eventTime, action, 0f, 0f, 0) kosmos.userTouchActivityNotifier.notifyActivity(motionEvent) if (shouldBeHandled) { assertThat(kosmos.fakePowerRepository.userTouchRegistered).isTrue() } else { assertThat(kosmos.fakePowerRepository.userTouchRegistered).isFalse() } } } packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +9 −0 Original line number Diff line number Diff line Loading @@ -108,6 +108,9 @@ interface CommunalModule { const val LAUNCHER_PACKAGE = "launcher_package" const val SWIPE_TO_HUB = "swipe_to_hub" const val SHOW_UMO = "show_umo" const val TOUCH_NOTIFICATION_RATE_LIMIT = "TOUCH_NOTIFICATION_RATE_LIMIT" const val TOUCH_NOTIFIFCATION_RATE_LIMIT_MS = 100 @Provides @Communal Loading Loading @@ -159,5 +162,11 @@ interface CommunalModule { fun provideShowUmo(@Main resources: Resources): Boolean { return resources.getBoolean(R.bool.config_showUmoOnHub) } @Provides @Named(TOUCH_NOTIFICATION_RATE_LIMIT) fun providesRateLimit(): Int { return TOUCH_NOTIFIFCATION_RATE_LIMIT_MS } } } packages/SystemUI/src/com/android/systemui/communal/util/UserTouchActivityNotifier.kt 0 → 100644 +58 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.communal.util import android.view.MotionEvent import com.android.systemui.communal.dagger.CommunalModule.Companion.TOUCH_NOTIFICATION_RATE_LIMIT import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.power.domain.interactor.PowerInteractor import javax.inject.Inject import javax.inject.Named import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** * {@link UserTouchActivityNotifier} helps rate limit the user activity notifications sent to {@link * PowerManager} from a single touch source. */ class UserTouchActivityNotifier @Inject constructor( @Background private val scope: CoroutineScope, private val powerInteractor: PowerInteractor, @Named(TOUCH_NOTIFICATION_RATE_LIMIT) private val rateLimitMs: Int, ) { private var lastNotification: Long? = null fun notifyActivity(event: MotionEvent) { val metered = when (event.action) { MotionEvent.ACTION_CANCEL -> false MotionEvent.ACTION_UP -> false MotionEvent.ACTION_DOWN -> false else -> true } if (metered && lastNotification?.let { event.eventTime - it < rateLimitMs } == true) { return } lastNotification = event.eventTime scope.launch { powerInteractor.onUserTouch() } } } packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +6 −13 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ import android.content.Context import android.content.res.Configuration import android.graphics.Rect import android.os.PowerManager import android.os.SystemClock import android.util.ArraySet import android.view.GestureDetector import android.view.MotionEvent Loading Loading @@ -54,6 +53,7 @@ import com.android.systemui.communal.ui.compose.CommunalContainer import com.android.systemui.communal.ui.compose.CommunalContent import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors import com.android.systemui.communal.util.UserTouchActivityNotifier import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor Loading Loading @@ -101,6 +101,7 @@ constructor( private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController, private val keyguardMediaController: KeyguardMediaController, private val lockscreenSmartspaceController: LockscreenSmartspaceController, private val userTouchActivityNotifier: UserTouchActivityNotifier, @CommunalTouchLog logBuffer: LogBuffer, private val userActivityNotifier: UserActivityNotifier, ) : LifecycleOwner { Loading Loading @@ -646,8 +647,8 @@ constructor( // result in broken states. return true } var handled = hubShowing try { var handled = false if (!touchTakenByKeyguardGesture) { communalContainerWrapper?.dispatchTouchEvent(ev) { if (it) { Loading @@ -655,18 +656,10 @@ constructor( } } } return handled || hubShowing return handled } finally { if (Flags.bouncerUiRevamp()) { userActivityNotifier.notifyUserActivity( event = PowerManager.USER_ACTIVITY_EVENT_TOUCH ) } else { powerManager.userActivity( SystemClock.uptimeMillis(), PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0, ) if (handled) { userTouchActivityNotifier.notifyActivity(ev) } } } Loading packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +18 −0 Original line number Diff line number Diff line Loading @@ -49,6 +49,7 @@ import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.ui.compose.CommunalContent import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors import com.android.systemui.communal.util.userTouchActivityNotifier import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor Loading @@ -64,6 +65,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.controller.keyguardMediaController import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.res.R import com.android.systemui.scene.shared.model.sceneDataSourceDelegator import com.android.systemui.shade.domain.interactor.shadeInteractor Loading Loading @@ -137,6 +139,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) Loading Loading @@ -178,6 +181,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) Loading Loading @@ -208,6 +212,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) Loading @@ -234,6 +239,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) Loading Loading @@ -538,6 +544,18 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { verify(containerView).onTouchEvent(UP_EVENT) } @Test fun onTouchEvent_touchHandled_notifyUserActivity() = kosmos.runTest { // Communal is open. goToScene(CommunalScenes.Communal) // Touch event is sent to the container view. assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() verify(containerView).onTouchEvent(DOWN_EVENT) assertThat(fakePowerRepository.userTouchRegistered).isTrue() } @Test fun onTouchEvent_editActivityShowing_touchesConsumedButNotDispatched() = kosmos.runTest { Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/util/UserTouchActivityNotifierTest.kt 0 → 100644 +74 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.communal.util import android.testing.AndroidTestingRunner import android.view.MotionEvent import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import org.junit.runner.RunWith @RunWith(AndroidTestingRunner::class) @SmallTest class UserTouchActivityNotifierTest : SysuiTestCase() { private val kosmos: Kosmos = testKosmos().useUnconfinedTestDispatcher() @Test fun firstEventTriggersNotify() = kosmos.runTest { sendEventAndVerify(0, MotionEvent.ACTION_MOVE, true) } @Test fun secondEventTriggersRateLimited() = kosmos.runTest { var eventTime = 0L sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true) eventTime += 50 sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, false) eventTime += USER_TOUCH_ACTIVITY_RATE_LIMIT sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true) } @Test fun overridingActionNotifies() = kosmos.runTest { var eventTime = 0L sendEventAndVerify(eventTime, MotionEvent.ACTION_MOVE, true) sendEventAndVerify(eventTime, MotionEvent.ACTION_DOWN, true) sendEventAndVerify(eventTime, MotionEvent.ACTION_UP, true) sendEventAndVerify(eventTime, MotionEvent.ACTION_CANCEL, true) } private fun sendEventAndVerify(eventTime: Long, action: Int, shouldBeHandled: Boolean) { kosmos.fakePowerRepository.userTouchRegistered = false val motionEvent = MotionEvent.obtain(0, eventTime, action, 0f, 0f, 0) kosmos.userTouchActivityNotifier.notifyActivity(motionEvent) if (shouldBeHandled) { assertThat(kosmos.fakePowerRepository.userTouchRegistered).isTrue() } else { assertThat(kosmos.fakePowerRepository.userTouchRegistered).isFalse() } } }
packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +9 −0 Original line number Diff line number Diff line Loading @@ -108,6 +108,9 @@ interface CommunalModule { const val LAUNCHER_PACKAGE = "launcher_package" const val SWIPE_TO_HUB = "swipe_to_hub" const val SHOW_UMO = "show_umo" const val TOUCH_NOTIFICATION_RATE_LIMIT = "TOUCH_NOTIFICATION_RATE_LIMIT" const val TOUCH_NOTIFIFCATION_RATE_LIMIT_MS = 100 @Provides @Communal Loading Loading @@ -159,5 +162,11 @@ interface CommunalModule { fun provideShowUmo(@Main resources: Resources): Boolean { return resources.getBoolean(R.bool.config_showUmoOnHub) } @Provides @Named(TOUCH_NOTIFICATION_RATE_LIMIT) fun providesRateLimit(): Int { return TOUCH_NOTIFIFCATION_RATE_LIMIT_MS } } }
packages/SystemUI/src/com/android/systemui/communal/util/UserTouchActivityNotifier.kt 0 → 100644 +58 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.communal.util import android.view.MotionEvent import com.android.systemui.communal.dagger.CommunalModule.Companion.TOUCH_NOTIFICATION_RATE_LIMIT import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.power.domain.interactor.PowerInteractor import javax.inject.Inject import javax.inject.Named import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** * {@link UserTouchActivityNotifier} helps rate limit the user activity notifications sent to {@link * PowerManager} from a single touch source. */ class UserTouchActivityNotifier @Inject constructor( @Background private val scope: CoroutineScope, private val powerInteractor: PowerInteractor, @Named(TOUCH_NOTIFICATION_RATE_LIMIT) private val rateLimitMs: Int, ) { private var lastNotification: Long? = null fun notifyActivity(event: MotionEvent) { val metered = when (event.action) { MotionEvent.ACTION_CANCEL -> false MotionEvent.ACTION_UP -> false MotionEvent.ACTION_DOWN -> false else -> true } if (metered && lastNotification?.let { event.eventTime - it < rateLimitMs } == true) { return } lastNotification = event.eventTime scope.launch { powerInteractor.onUserTouch() } } }
packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +6 −13 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ import android.content.Context import android.content.res.Configuration import android.graphics.Rect import android.os.PowerManager import android.os.SystemClock import android.util.ArraySet import android.view.GestureDetector import android.view.MotionEvent Loading Loading @@ -54,6 +53,7 @@ import com.android.systemui.communal.ui.compose.CommunalContainer import com.android.systemui.communal.ui.compose.CommunalContent import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors import com.android.systemui.communal.util.UserTouchActivityNotifier import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor Loading Loading @@ -101,6 +101,7 @@ constructor( private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController, private val keyguardMediaController: KeyguardMediaController, private val lockscreenSmartspaceController: LockscreenSmartspaceController, private val userTouchActivityNotifier: UserTouchActivityNotifier, @CommunalTouchLog logBuffer: LogBuffer, private val userActivityNotifier: UserActivityNotifier, ) : LifecycleOwner { Loading Loading @@ -646,8 +647,8 @@ constructor( // result in broken states. return true } var handled = hubShowing try { var handled = false if (!touchTakenByKeyguardGesture) { communalContainerWrapper?.dispatchTouchEvent(ev) { if (it) { Loading @@ -655,18 +656,10 @@ constructor( } } } return handled || hubShowing return handled } finally { if (Flags.bouncerUiRevamp()) { userActivityNotifier.notifyUserActivity( event = PowerManager.USER_ACTIVITY_EVENT_TOUCH ) } else { powerManager.userActivity( SystemClock.uptimeMillis(), PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0, ) if (handled) { userTouchActivityNotifier.notifyActivity(ev) } } } Loading
packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +18 −0 Original line number Diff line number Diff line Loading @@ -49,6 +49,7 @@ import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.ui.compose.CommunalContent import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors import com.android.systemui.communal.util.userTouchActivityNotifier import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor Loading @@ -64,6 +65,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.controller.keyguardMediaController import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.res.R import com.android.systemui.scene.shared.model.sceneDataSourceDelegator import com.android.systemui.shade.domain.interactor.shadeInteractor Loading Loading @@ -137,6 +139,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) Loading Loading @@ -178,6 +181,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) Loading Loading @@ -208,6 +212,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) Loading @@ -234,6 +239,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { notificationStackScrollLayoutController, keyguardMediaController, lockscreenSmartspaceController, userTouchActivityNotifier, logcatLogBuffer("GlanceableHubContainerControllerTest"), kosmos.userActivityNotifier, ) Loading Loading @@ -538,6 +544,18 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { verify(containerView).onTouchEvent(UP_EVENT) } @Test fun onTouchEvent_touchHandled_notifyUserActivity() = kosmos.runTest { // Communal is open. goToScene(CommunalScenes.Communal) // Touch event is sent to the container view. assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() verify(containerView).onTouchEvent(DOWN_EVENT) assertThat(fakePowerRepository.userTouchRegistered).isTrue() } @Test fun onTouchEvent_editActivityShowing_touchesConsumedButNotDispatched() = kosmos.runTest { Loading