Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 61b164ea authored by Helen Cheuk's avatar Helen Cheuk Committed by Android (Google) Code Review
Browse files

Merge "[Contextual Edu] Store keyboard shortcut trigger time" into main

parents be7f65de fc706282
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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" />

+31 −0
Original line number Diff line number Diff line
@@ -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
@@ -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)
@@ -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) {
+40 −0
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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
    }
@@ -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 {
@@ -89,6 +123,12 @@ constructor(
                }
            }
        }

        backgroundScope.launch {
            keyboardShortcutTriggered.collect {
                contextualEducationInteractor.updateShortcutTriggerTime(it)
            }
        }
    }

    private fun isEducationNeeded(model: GestureEduModel): Boolean {
+6 −1
Original line number Diff line number Diff line
@@ -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
@@ -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 {
@@ -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(