Loading packages/SystemUI/AndroidManifest.xml +3 −0 Original line number Diff line number Diff line Loading @@ -370,6 +370,9 @@ <uses-permission android:name="android.permission.MONITOR_STICKY_MODIFIER_STATE" /> <!-- Listen to keyboard shortcut events from input manager --> <uses-permission android:name="android.permission.MANAGE_KEY_GESTURES" /> <!-- To follow the grammatical gender preference --> <uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" /> Loading packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +31 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,9 @@ package com.android.systemui.education.domain.interactor import android.content.pm.UserInfo import android.hardware.input.InputManager import android.hardware.input.KeyGestureEvent import android.view.KeyEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase Loading @@ -41,6 +44,9 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.kotlin.any import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) Loading Loading @@ -203,6 +209,31 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { assertThat(model.keyboardFirstConnectionTime).isEqualTo(newUserFirstConnectionTime) } @Test fun updateShortcutTimeOnKeyboardShortcutTriggered() = testScope.runTest { // runCurrent() to trigger inputManager#registerKeyGestureEventListener in the // interactor runCurrent() val listenerCaptor = ArgumentCaptor.forClass(InputManager.KeyGestureEventListener::class.java) verify(kosmos.mockEduInputManager) .registerKeyGestureEventListener(any(), listenerCaptor.capture()) val backGestureEvent = KeyGestureEvent( /* deviceId= */ 1, intArrayOf(KeyEvent.KEYCODE_ESCAPE), KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_BACK ) listenerCaptor.value.onKeyGestureEvent(backGestureEvent) val model by collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK)) assertThat(model?.lastShortcutTriggeredTime).isEqualTo(eduClock.instant()) } private suspend fun triggerMaxEducationSignals(gestureType: GestureType) { // Increment max number of signal to try triggering education for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) { Loading packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt +40 −0 Original line number Diff line number Diff line Loading @@ -16,8 +16,16 @@ package com.android.systemui.education.domain.interactor import android.hardware.input.InputManager import android.hardware.input.InputManager.KeyGestureEventListener import android.hardware.input.KeyGestureEvent import com.android.systemui.CoreStartable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.contextualeducation.GestureType import com.android.systemui.contextualeducation.GestureType.ALL_APPS import com.android.systemui.contextualeducation.GestureType.BACK import com.android.systemui.contextualeducation.GestureType.HOME import com.android.systemui.contextualeducation.GestureType.OVERVIEW import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.dagger.ContextualEducationModule.EduClock Loading @@ -25,10 +33,14 @@ import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.shared.model.EducationInfo import com.android.systemui.education.shared.model.EducationUiType import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import java.time.Clock import java.util.concurrent.Executor import javax.inject.Inject import kotlin.time.Duration.Companion.hours import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch Loading @@ -41,10 +53,12 @@ constructor( @Background private val backgroundScope: CoroutineScope, private val contextualEducationInteractor: ContextualEducationInteractor, private val userInputDeviceRepository: UserInputDeviceRepository, private val inputManager: InputManager, @EduClock private val clock: Clock, ) : CoreStartable { companion object { const val TAG = "KeyboardTouchpadEduInteractor" const val MAX_SIGNAL_COUNT: Int = 2 val usageSessionDuration = 72.hours } Loading @@ -52,6 +66,26 @@ constructor( private val _educationTriggered = MutableStateFlow<EducationInfo?>(null) val educationTriggered = _educationTriggered.asStateFlow() private val keyboardShortcutTriggered: Flow<GestureType> = conflatedCallbackFlow { val listener = KeyGestureEventListener { event -> val shortcutType = when (event.keyGestureType) { KeyGestureEvent.KEY_GESTURE_TYPE_BACK -> BACK KeyGestureEvent.KEY_GESTURE_TYPE_HOME -> HOME KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS -> OVERVIEW KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS -> ALL_APPS else -> null } if (shortcutType != null) { trySendWithFailureLogging(shortcutType, TAG) } } inputManager.registerKeyGestureEventListener(Executor(Runnable::run), listener) awaitClose { inputManager.unregisterKeyGestureEventListener(listener) } } override fun start() { backgroundScope.launch { contextualEducationInteractor.backGestureModelFlow.collect { Loading Loading @@ -89,6 +123,12 @@ constructor( } } } backgroundScope.launch { keyboardShortcutTriggered.collect { contextualEducationInteractor.updateShortcutTriggerTime(it) } } } private fun isEducationNeeded(model: GestureEduModel): Boolean { Loading packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt +6 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.education.domain.interactor import android.hardware.input.InputManager import com.android.systemui.education.data.repository.fakeEduClock import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository import com.android.systemui.keyboard.data.repository.keyboardRepository Loading @@ -24,6 +25,7 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.touchpad.data.repository.touchpadRepository import com.android.systemui.user.data.repository.userRepository import org.mockito.kotlin.mock var Kosmos.keyboardTouchpadEduInteractor by Kosmos.Fixture { Loading @@ -37,10 +39,13 @@ var Kosmos.keyboardTouchpadEduInteractor by touchpadRepository, userRepository ), clock = fakeEduClock clock = fakeEduClock, inputManager = mockEduInputManager ) } var Kosmos.mockEduInputManager by Kosmos.Fixture { mock<InputManager>() } var Kosmos.keyboardTouchpadEduStatsInteractor by Kosmos.Fixture { KeyboardTouchpadEduStatsInteractorImpl( Loading Loading
packages/SystemUI/AndroidManifest.xml +3 −0 Original line number Diff line number Diff line Loading @@ -370,6 +370,9 @@ <uses-permission android:name="android.permission.MONITOR_STICKY_MODIFIER_STATE" /> <!-- Listen to keyboard shortcut events from input manager --> <uses-permission android:name="android.permission.MANAGE_KEY_GESTURES" /> <!-- To follow the grammatical gender preference --> <uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" /> Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +31 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,9 @@ package com.android.systemui.education.domain.interactor import android.content.pm.UserInfo import android.hardware.input.InputManager import android.hardware.input.KeyGestureEvent import android.view.KeyEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase Loading @@ -41,6 +44,9 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.kotlin.any import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) Loading Loading @@ -203,6 +209,31 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { assertThat(model.keyboardFirstConnectionTime).isEqualTo(newUserFirstConnectionTime) } @Test fun updateShortcutTimeOnKeyboardShortcutTriggered() = testScope.runTest { // runCurrent() to trigger inputManager#registerKeyGestureEventListener in the // interactor runCurrent() val listenerCaptor = ArgumentCaptor.forClass(InputManager.KeyGestureEventListener::class.java) verify(kosmos.mockEduInputManager) .registerKeyGestureEventListener(any(), listenerCaptor.capture()) val backGestureEvent = KeyGestureEvent( /* deviceId= */ 1, intArrayOf(KeyEvent.KEYCODE_ESCAPE), KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_BACK ) listenerCaptor.value.onKeyGestureEvent(backGestureEvent) val model by collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK)) assertThat(model?.lastShortcutTriggeredTime).isEqualTo(eduClock.instant()) } private suspend fun triggerMaxEducationSignals(gestureType: GestureType) { // Increment max number of signal to try triggering education for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) { Loading
packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt +40 −0 Original line number Diff line number Diff line Loading @@ -16,8 +16,16 @@ package com.android.systemui.education.domain.interactor import android.hardware.input.InputManager import android.hardware.input.InputManager.KeyGestureEventListener import android.hardware.input.KeyGestureEvent import com.android.systemui.CoreStartable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.contextualeducation.GestureType import com.android.systemui.contextualeducation.GestureType.ALL_APPS import com.android.systemui.contextualeducation.GestureType.BACK import com.android.systemui.contextualeducation.GestureType.HOME import com.android.systemui.contextualeducation.GestureType.OVERVIEW import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.dagger.ContextualEducationModule.EduClock Loading @@ -25,10 +33,14 @@ import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.shared.model.EducationInfo import com.android.systemui.education.shared.model.EducationUiType import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import java.time.Clock import java.util.concurrent.Executor import javax.inject.Inject import kotlin.time.Duration.Companion.hours import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch Loading @@ -41,10 +53,12 @@ constructor( @Background private val backgroundScope: CoroutineScope, private val contextualEducationInteractor: ContextualEducationInteractor, private val userInputDeviceRepository: UserInputDeviceRepository, private val inputManager: InputManager, @EduClock private val clock: Clock, ) : CoreStartable { companion object { const val TAG = "KeyboardTouchpadEduInteractor" const val MAX_SIGNAL_COUNT: Int = 2 val usageSessionDuration = 72.hours } Loading @@ -52,6 +66,26 @@ constructor( private val _educationTriggered = MutableStateFlow<EducationInfo?>(null) val educationTriggered = _educationTriggered.asStateFlow() private val keyboardShortcutTriggered: Flow<GestureType> = conflatedCallbackFlow { val listener = KeyGestureEventListener { event -> val shortcutType = when (event.keyGestureType) { KeyGestureEvent.KEY_GESTURE_TYPE_BACK -> BACK KeyGestureEvent.KEY_GESTURE_TYPE_HOME -> HOME KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS -> OVERVIEW KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS -> ALL_APPS else -> null } if (shortcutType != null) { trySendWithFailureLogging(shortcutType, TAG) } } inputManager.registerKeyGestureEventListener(Executor(Runnable::run), listener) awaitClose { inputManager.unregisterKeyGestureEventListener(listener) } } override fun start() { backgroundScope.launch { contextualEducationInteractor.backGestureModelFlow.collect { Loading Loading @@ -89,6 +123,12 @@ constructor( } } } backgroundScope.launch { keyboardShortcutTriggered.collect { contextualEducationInteractor.updateShortcutTriggerTime(it) } } } private fun isEducationNeeded(model: GestureEduModel): Boolean { Loading
packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt +6 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.education.domain.interactor import android.hardware.input.InputManager import com.android.systemui.education.data.repository.fakeEduClock import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository import com.android.systemui.keyboard.data.repository.keyboardRepository Loading @@ -24,6 +25,7 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.touchpad.data.repository.touchpadRepository import com.android.systemui.user.data.repository.userRepository import org.mockito.kotlin.mock var Kosmos.keyboardTouchpadEduInteractor by Kosmos.Fixture { Loading @@ -37,10 +39,13 @@ var Kosmos.keyboardTouchpadEduInteractor by touchpadRepository, userRepository ), clock = fakeEduClock clock = fakeEduClock, inputManager = mockEduInputManager ) } var Kosmos.mockEduInputManager by Kosmos.Fixture { mock<InputManager>() } var Kosmos.keyboardTouchpadEduStatsInteractor by Kosmos.Fixture { KeyboardTouchpadEduStatsInteractorImpl( Loading