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

Commit 9f514d35 authored by helencheuk's avatar helencheuk
Browse files

[Contextual Edu] Store first connection time for touchpad and keyboard

Store first connection time for touchpad and keyboard to calculate initial delay of education in next CL

Bug: 360833649
Test: KeyboardTouchpadEduInteractorTest
Test: ContextualEducationRepositoryTest
Flag: com.android.systemui.keyboard_touchpad_contextual_education

Change-Id: Id927e561bc371d70c33d1f50941e568e3c674d6d
parent cc3ff7cb
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestableContext
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.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
@@ -105,6 +106,19 @@ class ContextualEducationRepositoryTest : SysuiTestCase() {
            assertThat(model).isEqualTo(newModel)
        }

    @Test
    fun eduDeviceConnectionTimeDataChangedOnUpdate() =
        testScope.runTest {
            val newModel =
                EduDeviceConnectionTime(
                    keyboardFirstConnectionTime = kosmos.fakeEduClock.instant(),
                    touchpadFirstConnectionTime = kosmos.fakeEduClock.instant(),
                )
            underTest.updateEduDeviceConnectionTime { newModel }
            val model by collectLastValue(underTest.readEduDeviceConnectionTime())
            assertThat(model).isEqualTo(newModel)
        }

    /** Test context which allows overriding getFilesDir path */
    private class TestContext(context: Context, private val folder: File) :
        SysuiTestableContext(context) {
+106 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.education.domain.interactor

import android.content.pm.UserInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -26,10 +27,15 @@ import com.android.systemui.education.data.model.GestureEduModel
import com.android.systemui.education.data.repository.contextualEducationRepository
import com.android.systemui.education.data.repository.fakeEduClock
import com.android.systemui.education.shared.model.EducationUiType
import com.android.systemui.keyboard.data.repository.keyboardRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.android.systemui.touchpad.data.repository.touchpadRepository
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.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -38,16 +44,23 @@ import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
@kotlinx.coroutines.ExperimentalCoroutinesApi
class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val contextualEduInteractor = kosmos.contextualEducationInteractor
    private val touchpadRepository = kosmos.touchpadRepository
    private val keyboardRepository = kosmos.keyboardRepository
    private val userRepository = kosmos.fakeUserRepository

    private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor
    private val eduClock = kosmos.fakeEduClock

    @Before
    fun setup() {
        underTest.start()
        contextualEduInteractor.start()
        userRepository.setUserInfos(USER_INFOS)
    }

    @Test
@@ -67,7 +80,6 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
        }

    @Test
    @kotlinx.coroutines.ExperimentalCoroutinesApi
    fun newEducationNotificationOn2ndEducation() =
        testScope.runTest {
            val model by collectLastValue(underTest.educationTriggered)
@@ -115,10 +127,103 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
                )
        }

    @Test
    fun newTouchpadConnectionTimeOnFirstTouchpadConnected() =
        testScope.runTest {
            setIsAnyTouchpadConnected(true)
            val model = contextualEduInteractor.getEduDeviceConnectionTime()
            assertThat(model.touchpadFirstConnectionTime).isEqualTo(eduClock.instant())
        }

    @Test
    fun unchangedTouchpadConnectionTimeOnSecondConnection() =
        testScope.runTest {
            val firstConnectionTime = eduClock.instant()
            setIsAnyTouchpadConnected(true)
            setIsAnyTouchpadConnected(false)

            eduClock.offset(1.hours)
            setIsAnyTouchpadConnected(true)

            val model = contextualEduInteractor.getEduDeviceConnectionTime()
            assertThat(model.touchpadFirstConnectionTime).isEqualTo(firstConnectionTime)
        }

    @Test
    fun newTouchpadConnectionTimeOnUserChanged() =
        testScope.runTest {
            // Touchpad connected for user 0
            setIsAnyTouchpadConnected(true)

            // Change user
            eduClock.offset(1.hours)
            val newUserFirstConnectionTime = eduClock.instant()
            userRepository.setSelectedUserInfo(USER_INFOS[0])
            runCurrent()

            val model = contextualEduInteractor.getEduDeviceConnectionTime()
            assertThat(model.touchpadFirstConnectionTime).isEqualTo(newUserFirstConnectionTime)
        }

    @Test
    fun newKeyboardConnectionTimeOnKeyboardConnected() =
        testScope.runTest {
            setIsAnyKeyboardConnected(true)
            val model = contextualEduInteractor.getEduDeviceConnectionTime()
            assertThat(model.keyboardFirstConnectionTime).isEqualTo(eduClock.instant())
        }

    @Test
    fun unchangedKeyboardConnectionTimeOnSecondConnection() =
        testScope.runTest {
            val firstConnectionTime = eduClock.instant()
            setIsAnyKeyboardConnected(true)
            setIsAnyKeyboardConnected(false)

            eduClock.offset(1.hours)
            setIsAnyKeyboardConnected(true)

            val model = contextualEduInteractor.getEduDeviceConnectionTime()
            assertThat(model.keyboardFirstConnectionTime).isEqualTo(firstConnectionTime)
        }

    @Test
    fun newKeyboardConnectionTimeOnUserChanged() =
        testScope.runTest {
            // Keyboard connected for user 0
            setIsAnyKeyboardConnected(true)

            // Change user
            eduClock.offset(1.hours)
            val newUserFirstConnectionTime = eduClock.instant()
            userRepository.setSelectedUserInfo(USER_INFOS[0])
            runCurrent()

            val model = contextualEduInteractor.getEduDeviceConnectionTime()
            assertThat(model.keyboardFirstConnectionTime).isEqualTo(newUserFirstConnectionTime)
        }

    private suspend fun triggerMaxEducationSignals(gestureType: GestureType) {
        // Increment max number of signal to try triggering education
        for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
            contextualEduInteractor.incrementSignalCount(gestureType)
        }
    }

    private fun TestScope.setIsAnyTouchpadConnected(isConnected: Boolean) {
        touchpadRepository.setIsAnyTouchpadConnected(isConnected)
        runCurrent()
    }

    private fun TestScope.setIsAnyKeyboardConnected(isConnected: Boolean) {
        keyboardRepository.setIsAnyKeyboardConnected(isConnected)
        runCurrent()
    }

    companion object {
        private val USER_INFOS =
            listOf(
                UserInfo(101, "Second User", 0),
            )
    }
}
+24 −0
Original line number Diff line number Diff line
/*
 * Copyright 2024 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.education.data.model

import java.time.Instant

data class EduDeviceConnectionTime(
    val keyboardFirstConnectionTime: Instant? = null,
    val touchpadFirstConnectionTime: Instant? = null
)
+46 −0
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import com.android.systemui.contextualeducation.GestureType
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
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 java.time.Instant
import javax.inject.Inject
@@ -53,10 +54,16 @@ interface ContextualEducationRepository {

    fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel>

    fun readEduDeviceConnectionTime(): Flow<EduDeviceConnectionTime>

    suspend fun updateGestureEduModel(
        gestureType: GestureType,
        transform: (GestureEduModel) -> GestureEduModel
    )

    suspend fun updateEduDeviceConnectionTime(
        transform: (EduDeviceConnectionTime) -> EduDeviceConnectionTime
    )
}

/**
@@ -76,6 +83,8 @@ constructor(
        const val LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX = "_LAST_SHORTCUT_TRIGGERED_TIME"
        const val USAGE_SESSION_START_TIME_SUFFIX = "_USAGE_SESSION_START_TIME"
        const val LAST_EDUCATION_TIME_SUFFIX = "_LAST_EDUCATION_TIME"
        const val KEYBOARD_FIRST_CONNECTION_TIME = "KEYBOARD_FIRST_CONNECTION_TIME"
        const val TOUCHPAD_FIRST_CONNECTION_TIME = "TOUCHPAD_FIRST_CONNECTION_TIME"

        const val DATASTORE_DIR = "education/USER%s_ContextualEducation"
    }
@@ -158,6 +167,37 @@ constructor(
        }
    }

    override fun readEduDeviceConnectionTime(): Flow<EduDeviceConnectionTime> =
        prefData.map { preferences -> getEduDeviceConnectionTime(preferences) }

    override suspend fun updateEduDeviceConnectionTime(
        transform: (EduDeviceConnectionTime) -> EduDeviceConnectionTime
    ) {
        datastore.filterNotNull().first().edit { preferences ->
            val currentModel = getEduDeviceConnectionTime(preferences)
            val updatedModel = transform(currentModel)
            setInstant(
                preferences,
                updatedModel.keyboardFirstConnectionTime,
                getKeyboardFirstConnectionTimeKey()
            )
            setInstant(
                preferences,
                updatedModel.touchpadFirstConnectionTime,
                getTouchpadFirstConnectionTimeKey()
            )
        }
    }

    private fun getEduDeviceConnectionTime(preferences: Preferences): EduDeviceConnectionTime {
        return EduDeviceConnectionTime(
            keyboardFirstConnectionTime =
                preferences[getKeyboardFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) },
            touchpadFirstConnectionTime =
                preferences[getTouchpadFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) }
        )
    }

    private fun getSignalCountKey(gestureType: GestureType): Preferences.Key<Int> =
        intPreferencesKey(gestureType.name + SIGNAL_COUNT_SUFFIX)

@@ -173,6 +213,12 @@ constructor(
    private fun getLastEducationTimeKey(gestureType: GestureType): Preferences.Key<Long> =
        longPreferencesKey(gestureType.name + LAST_EDUCATION_TIME_SUFFIX)

    private fun getKeyboardFirstConnectionTimeKey(): Preferences.Key<Long> =
        longPreferencesKey(KEYBOARD_FIRST_CONNECTION_TIME)

    private fun getTouchpadFirstConnectionTimeKey(): Preferences.Key<Long> =
        longPreferencesKey(TOUCHPAD_FIRST_CONNECTION_TIME)

    private fun setInstant(
        preferences: MutablePreferences,
        instant: Instant?,
+18 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import com.android.systemui.contextualeducation.GestureType.BACK
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.education.dagger.ContextualEducationModule.EduClock
import com.android.systemui.education.data.model.EduDeviceConnectionTime
import com.android.systemui.education.data.model.GestureEduModel
import com.android.systemui.education.data.repository.ContextualEducationRepository
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
@@ -32,6 +33,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch

@@ -67,6 +69,10 @@ constructor(
            .flowOn(backgroundDispatcher)
    }

    suspend fun getEduDeviceConnectionTime(): EduDeviceConnectionTime {
        return repository.readEduDeviceConnectionTime().first()
    }

    suspend fun incrementSignalCount(gestureType: GestureType) {
        repository.updateGestureEduModel(gestureType) {
            it.copy(
@@ -100,4 +106,16 @@ constructor(
            it.copy(usageSessionStartTime = clock.instant(), signalCount = 1)
        }
    }

    suspend fun updateKeyboardFirstConnectionTime() {
        repository.updateEduDeviceConnectionTime {
            it.copy(keyboardFirstConnectionTime = clock.instant())
        }
    }

    suspend fun updateTouchpadFirstConnectionTime() {
        repository.updateEduDeviceConnectionTime {
            it.copy(touchpadFirstConnectionTime = clock.instant())
        }
    }
}
Loading