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

Commit c38c0e4f authored by helencheuk's avatar helencheuk
Browse files

[Contextual Edu] Add logic for all gesture types in SysUI

Add education logic for the rest of the gestures (home, overview, all apps) in SysUI

Bug: 366195470
Test: KeyboardTouchpadEduInteractorTest
Flag: com.android.systemui.keyboard_touchpad_contextual_education

Change-Id: I8c1cd8f5bfcd76ceeb4b4381ca7aa7f9e7ad8646
parent fb01df17
Loading
Loading
Loading
Loading
+10 −2
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import com.android.systemui.contextualeducation.GestureType.BACK
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.education.data.model.EduDeviceConnectionTime
import com.android.systemui.education.data.model.GestureEduModel
import com.android.systemui.education.domain.interactor.mockEduInputManager
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
@@ -62,7 +63,13 @@ class ContextualEducationRepositoryTest : SysuiTestCase() {
        // Create TestContext here because TemporaryFolder.create() is called in @Before. It is
        // needed before calling TemporaryFolder.newFolder().
        val testContext = TestContext(context, tmpFolder.newFolder())
        underTest = UserContextualEducationRepository(testContext, dsScopeProvider)
        underTest =
            UserContextualEducationRepository(
                testContext,
                dsScopeProvider,
                kosmos.mockEduInputManager,
                kosmos.testDispatcher
            )
        underTest.setUser(testUserId)
    }

@@ -99,7 +106,8 @@ class ContextualEducationRepositoryTest : SysuiTestCase() {
                    lastShortcutTriggeredTime = kosmos.fakeEduClock.instant(),
                    lastEducationTime = kosmos.fakeEduClock.instant(),
                    usageSessionStartTime = kosmos.fakeEduClock.instant(),
                    userId = testUserId
                    userId = testUserId,
                    gestureType = BACK
                )
            underTest.updateGestureEduModel(BACK) { newModel }
            val model by collectLastValue(underTest.readGestureEduModelFlow(BACK))
+49 −46
Original line number Diff line number Diff line
@@ -17,15 +17,13 @@
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
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.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.education.data.model.GestureEduModel
@@ -40,20 +38,21 @@ import com.android.systemui.user.data.repository.fakeUserRepository
import com.google.common.truth.Truth.assertThat
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Assume.assumeTrue
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
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters

@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWith(ParameterizedAndroidJunit4::class)
@kotlinx.coroutines.ExperimentalCoroutinesApi
class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val contextualEduInteractor = kosmos.contextualEducationInteractor
@@ -71,21 +70,27 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
        underTest.start()
        contextualEduInteractor.start()
        userRepository.setUserInfos(USER_INFOS)
        testScope.launch {
            contextualEduInteractor.updateKeyboardFirstConnectionTime()
            contextualEduInteractor.updateTouchpadFirstConnectionTime()
        }
    }

    @Test
    fun newEducationInfoOnMaxSignalCountReached() =
        testScope.runTest {
            triggerMaxEducationSignals(BACK)
            triggerMaxEducationSignals(gestureType)
            val model by collectLastValue(underTest.educationTriggered)
            assertThat(model?.gestureType).isEqualTo(BACK)

            assertThat(model?.gestureType).isEqualTo(gestureType)
        }

    @Test
    fun newEducationToastOn1stEducation() =
        testScope.runTest {
            val model by collectLastValue(underTest.educationTriggered)
            triggerMaxEducationSignals(BACK)
            triggerMaxEducationSignals(gestureType)

            assertThat(model?.educationUiType).isEqualTo(EducationUiType.Toast)
        }

@@ -93,12 +98,12 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
    fun newEducationNotificationOn2ndEducation() =
        testScope.runTest {
            val model by collectLastValue(underTest.educationTriggered)
            triggerMaxEducationSignals(BACK)
            triggerMaxEducationSignals(gestureType)
            // runCurrent() to trigger 1st education
            runCurrent()

            eduClock.offset(minDurationForNextEdu)
            triggerMaxEducationSignals(BACK)
            triggerMaxEducationSignals(gestureType)

            assertThat(model?.educationUiType).isEqualTo(EducationUiType.Notification)
        }
@@ -106,7 +111,7 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
    @Test
    fun noEducationInfoBeforeMaxSignalCountReached() =
        testScope.runTest {
            contextualEduInteractor.incrementSignalCount(BACK)
            contextualEduInteractor.incrementSignalCount(gestureType)
            val model by collectLastValue(underTest.educationTriggered)
            assertThat(model).isNull()
        }
@@ -115,8 +120,8 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
    fun noEducationInfoWhenShortcutTriggeredPreviously() =
        testScope.runTest {
            val model by collectLastValue(underTest.educationTriggered)
            contextualEduInteractor.updateShortcutTriggerTime(BACK)
            triggerMaxEducationSignals(BACK)
            contextualEduInteractor.updateShortcutTriggerTime(gestureType)
            triggerMaxEducationSignals(gestureType)
            assertThat(model).isNull()
        }

@@ -124,12 +129,12 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
    fun no2ndEducationBeforeMinEduIntervalReached() =
        testScope.runTest {
            val models by collectValues(underTest.educationTriggered)
            triggerMaxEducationSignals(BACK)
            triggerMaxEducationSignals(gestureType)
            runCurrent()

            // Offset a duration that is less than the required education interval
            eduClock.offset(1.seconds)
            triggerMaxEducationSignals(BACK)
            triggerMaxEducationSignals(gestureType)
            runCurrent()

            assertThat(models.filterNotNull().size).isEqualTo(1)
@@ -140,15 +145,15 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
        testScope.runTest {
            val models by collectValues(underTest.educationTriggered)
            // Trigger 2 educations
            triggerMaxEducationSignals(BACK)
            triggerMaxEducationSignals(gestureType)
            runCurrent()
            eduClock.offset(minDurationForNextEdu)
            triggerMaxEducationSignals(BACK)
            triggerMaxEducationSignals(gestureType)
            runCurrent()

            // Try triggering 3rd education
            eduClock.offset(minDurationForNextEdu)
            triggerMaxEducationSignals(BACK)
            triggerMaxEducationSignals(gestureType)

            assertThat(models.filterNotNull().size).isEqualTo(2)
        }
@@ -157,18 +162,21 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
    fun startNewUsageSessionWhen2ndSignalReceivedAfterSessionDeadline() =
        testScope.runTest {
            val model by
                collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK))
            contextualEduInteractor.incrementSignalCount(BACK)
                collectLastValue(
                    kosmos.contextualEducationRepository.readGestureEduModelFlow(gestureType)
                )
            contextualEduInteractor.incrementSignalCount(gestureType)
            eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration.plus(1.seconds))
            val secondSignalReceivedTime = eduClock.instant()
            contextualEduInteractor.incrementSignalCount(BACK)
            contextualEduInteractor.incrementSignalCount(gestureType)

            assertThat(model)
                .isEqualTo(
                    GestureEduModel(
                        signalCount = 1,
                        usageSessionStartTime = secondSignalReceivedTime,
                        userId = 0
                        userId = 0,
                        gestureType = gestureType
                    )
                )
        }
@@ -252,22 +260,9 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
    @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 allAppsKeyGestureEvent =
                KeyGestureEvent.Builder()
                    .setDeviceId(1)
                    .setModifierState(KeyEvent.META_META_ON)
                    .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS)
                    .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
                    .build()
            listenerCaptor.value.onKeyGestureEvent(allAppsKeyGestureEvent)
            // Only All Apps needs to update the keyboard shortcut
            assumeTrue(gestureType == ALL_APPS)
            kosmos.contextualEducationRepository.setKeyboardShortcutTriggered(ALL_APPS)

            val model by
                collectLastValue(
@@ -293,10 +288,18 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
        runCurrent()
    }

    private suspend fun setUpForDeviceConnection() {
        contextualEduInteractor.updateKeyboardFirstConnectionTime()
        contextualEduInteractor.updateTouchpadFirstConnectionTime()
    }

    companion object {
        private val USER_INFOS =
            listOf(
                UserInfo(101, "Second User", 0),
            )
        private val USER_INFOS = listOf(UserInfo(101, "Second User", 0))

        @JvmStatic
        @Parameters(name = "{0}")
        fun getGestureTypes(): List<GestureType> {
            return listOf(BACK, HOME, OVERVIEW, ALL_APPS)
        }
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -69,6 +70,11 @@ class ContextualEduUiCoordinatorTest : SysuiTestCase() {

    @Before
    fun setUp() {
        testScope.launch {
            interactor.updateKeyboardFirstConnectionTime()
            interactor.updateTouchpadFirstConnectionTime()
        }

        val viewModel =
            ContextualEduViewModel(
                kosmos.applicationContext.resources,
+2 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.education.data.model

import com.android.systemui.contextualeducation.GestureType
import java.time.Instant

/**
@@ -23,6 +24,7 @@ import java.time.Instant
 * gesture stores its own model separately.
 */
data class GestureEduModel(
    val gestureType: GestureType,
    val signalCount: Int = 0,
    val educationShownCount: Int = 0,
    val lastShortcutTriggeredTime: Instant? = null,
+43 −2
Original line number Diff line number Diff line
@@ -17,6 +17,8 @@
package com.android.systemui.education.data.repository

import android.content.Context
import android.hardware.input.InputManager
import android.hardware.input.KeyGestureEvent
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.MutablePreferences
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
@@ -25,23 +27,31 @@ import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.preferencesDataStoreFile
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.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.education.dagger.ContextualEducationModule.EduDataStoreScope
import com.android.systemui.education.data.model.EduDeviceConnectionTime
import com.android.systemui.education.data.model.GestureEduModel
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import java.time.Instant
import java.util.concurrent.Executor
import javax.inject.Inject
import javax.inject.Provider
import kotlin.properties.Delegates.notNull
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map

/**
@@ -64,6 +74,8 @@ interface ContextualEducationRepository {
    suspend fun updateEduDeviceConnectionTime(
        transform: (EduDeviceConnectionTime) -> EduDeviceConnectionTime
    )

    val keyboardShortcutTriggered: Flow<GestureType>
}

/**
@@ -75,9 +87,13 @@ class UserContextualEducationRepository
@Inject
constructor(
    @Application private val applicationContext: Context,
    @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope>
    @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope>,
    private val inputManager: InputManager,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
) : ContextualEducationRepository {
    companion object {
        const val TAG = "UserContextualEducationRepository"

        const val SIGNAL_COUNT_SUFFIX = "_SIGNAL_COUNT"
        const val NUMBER_OF_EDU_SHOWN_SUFFIX = "_NUMBER_OF_EDU_SHOWN"
        const val LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX = "_LAST_SHORTCUT_TRIGGERED_TIME"
@@ -98,6 +114,30 @@ constructor(
    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
    private val prefData: Flow<Preferences> = datastore.filterNotNull().flatMapLatest { it.data }

    override val keyboardShortcutTriggered: Flow<GestureType> =
        conflatedCallbackFlow {
                val listener =
                    InputManager.KeyGestureEventListener { event ->
                        // Only store keyboard shortcut time for gestures providing keyboard
                        // education
                        val shortcutType =
                            when (event.keyGestureType) {
                                KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS,
                                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) }
            }
            .flowOn(backgroundDispatcher)

    override fun setUser(userId: Int) {
        dataStoreScope?.cancel()
        val newDsScope = dataStoreScopeProvider.get()
@@ -136,7 +176,8 @@ constructor(
                preferences[getLastEducationTimeKey(gestureType)]?.let {
                    Instant.ofEpochSecond(it)
                },
            userId = userId
            userId = userId,
            gestureType = gestureType,
        )
    }

Loading