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

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

Merge "[Contextual Edu] Move business logic to ContextualEducationInteractor" into main

parents ce70511a 67401379
Loading
Loading
Loading
Loading
+15 −20
Original line number Diff line number Diff line
@@ -21,15 +21,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestableContext
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.contextualeducation.GestureType.BACK
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.education.data.model.GestureEduModel
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.google.common.truth.Truth.assertThat
import java.io.File
import java.time.Clock
import java.time.Instant
import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope
@@ -44,13 +43,13 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ContextualEducationRepositoryTest : SysuiTestCase() {

    private lateinit var underTest: ContextualEducationRepository
    private lateinit var underTest: UserContextualEducationRepository
    private val kosmos = Kosmos()
    private val testScope = kosmos.testScope
    private val dsScopeProvider: Provider<CoroutineScope> = Provider {
        TestScope(kosmos.testDispatcher).backgroundScope
    }
    private val clock: Clock = FakeEduClock(Instant.ofEpochMilli(1000))

    private val testUserId = 1111

    // For deleting any test files created after the test
@@ -61,8 +60,7 @@ 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())
        val userRepository = UserContextualEducationRepository(testContext, dsScopeProvider)
        underTest = ContextualEducationRepositoryImpl(clock, userRepository)
        underTest = UserContextualEducationRepository(testContext, dsScopeProvider)
        underTest.setUser(testUserId)
    }

@@ -70,7 +68,7 @@ class ContextualEducationRepositoryTest : SysuiTestCase() {
    fun changeRetrievedValueForNewUser() =
        testScope.runTest {
            // Update data for old user.
            underTest.incrementSignalCount(BACK)
            underTest.updateGestureEduModel(BACK) { it.copy(signalCount = 1) }
            val model by collectLastValue(underTest.readGestureEduModelFlow(BACK))
            assertThat(model?.signalCount).isEqualTo(1)

@@ -81,20 +79,17 @@ class ContextualEducationRepositoryTest : SysuiTestCase() {
        }

    @Test
    fun incrementSignalCount() =
        testScope.runTest {
            underTest.incrementSignalCount(BACK)
            val model by collectLastValue(underTest.readGestureEduModelFlow(BACK))
            assertThat(model?.signalCount).isEqualTo(1)
        }

    @Test
    fun dataAddedOnUpdateShortcutTriggerTime() =
    fun dataChangedOnUpdate() =
        testScope.runTest {
            val newModel =
                GestureEduModel(
                    signalCount = 2,
                    educationShownCount = 1,
                    lastShortcutTriggeredTime = kosmos.fakeEduClock.instant()
                )
            underTest.updateGestureEduModel(BACK) { newModel }
            val model by collectLastValue(underTest.readGestureEduModelFlow(BACK))
            assertThat(model?.lastShortcutTriggeredTime).isNull()
            underTest.updateShortcutTriggerTime(BACK)
            assertThat(model?.lastShortcutTriggeredTime).isEqualTo(clock.instant())
            assertThat(model).isEqualTo(newModel)
        }

    /** Test context which allows overriding getFilesDir path */
+5 −6
Original line number Diff line number Diff line
@@ -19,10 +19,9 @@ package com.android.systemui.education.domain.interactor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.contextualeducation.GestureType
import com.android.systemui.contextualeducation.GestureType.BACK
import com.android.systemui.education.data.repository.contextualEducationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -36,7 +35,7 @@ import org.junit.runner.RunWith
class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val repository = kosmos.contextualEducationRepository
    private val contextualEduInteractor = kosmos.contextualEducationInteractor
    private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor

    @Before
@@ -55,7 +54,7 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
    @Test
    fun noEducationInfoBeforeMaxSignalCountReached() =
        testScope.runTest {
            repository.incrementSignalCount(BACK)
            contextualEduInteractor.incrementSignalCount(BACK)
            val model by collectLastValue(underTest.educationTriggered)
            assertThat(model).isNull()
        }
@@ -64,7 +63,7 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
    fun noEducationInfoWhenShortcutTriggeredPreviously() =
        testScope.runTest {
            val model by collectLastValue(underTest.educationTriggered)
            repository.updateShortcutTriggerTime(BACK)
            contextualEduInteractor.updateShortcutTriggerTime(BACK)
            tryTriggeringEducation(BACK)
            assertThat(model).isNull()
        }
@@ -72,7 +71,7 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
    private suspend fun tryTriggeringEducation(gestureType: GestureType) {
        // Increment max number of signal to try triggering education
        for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
            repository.incrementSignalCount(gestureType)
            contextualEduInteractor.incrementSignalCount(gestureType)
        }
    }
}
+3 −3
Original line number Diff line number Diff line
@@ -18,10 +18,10 @@ package com.android.systemui.education.dagger

import com.android.systemui.CoreStartable
import com.android.systemui.Flags
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.contextualeducation.GestureType
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.education.data.repository.ContextualEducationRepository
import com.android.systemui.education.data.repository.ContextualEducationRepositoryImpl
import com.android.systemui.education.data.repository.UserContextualEducationRepository
import com.android.systemui.education.domain.interactor.ContextualEducationInteractor
import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor
import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor
@@ -42,7 +42,7 @@ import kotlinx.coroutines.SupervisorJob
interface ContextualEducationModule {
    @Binds
    fun bindContextualEducationRepository(
        impl: ContextualEducationRepositoryImpl
        impl: UserContextualEducationRepository
    ): ContextualEducationRepository

    @Qualifier annotation class EduDataStoreScope
+0 −66
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.repository

import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.contextualeducation.GestureType
import com.android.systemui.education.dagger.ContextualEducationModule.EduClock
import com.android.systemui.education.data.model.GestureEduModel
import java.time.Clock
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow

/** Encapsulates the functions of ContextualEducationRepository. */
interface ContextualEducationRepository {
    fun setUser(userId: Int)

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

    suspend fun incrementSignalCount(gestureType: GestureType)

    suspend fun updateShortcutTriggerTime(gestureType: GestureType)
}

/**
 * Provide methods to read and update on field level and allow setting datastore when user is
 * changed
 */
@SysUISingleton
class ContextualEducationRepositoryImpl
@Inject
constructor(
    @EduClock private val clock: Clock,
    private val userEduRepository: UserContextualEducationRepository
) : ContextualEducationRepository {
    /** To change data store when user is changed */
    override fun setUser(userId: Int) = userEduRepository.setUser(userId)

    override fun readGestureEduModelFlow(gestureType: GestureType) =
        userEduRepository.readGestureEduModelFlow(gestureType)

    override suspend fun incrementSignalCount(gestureType: GestureType) {
        userEduRepository.updateGestureEduModel(gestureType) {
            it.copy(signalCount = it.signalCount + 1)
        }
    }

    override suspend fun updateShortcutTriggerTime(gestureType: GestureType) {
        userEduRepository.updateGestureEduModel(gestureType) {
            it.copy(lastShortcutTriggeredTime = clock.instant())
        }
    }
}
+28 −11
Original line number Diff line number Diff line
@@ -25,9 +25,9 @@ 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.contextualeducation.GestureType
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.contextualeducation.GestureType
import com.android.systemui.education.dagger.ContextualEducationModule.EduDataStoreScope
import com.android.systemui.education.data.model.GestureEduModel
import java.time.Instant
@@ -43,10 +43,24 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map

/**
 * A contextual education repository to:
 * 1) store education data per user
 * 2) provide methods to read and update data on model-level
 * 3) provide method to enable changing datastore when user is changed
 * Allows to:
 * 1) read and update data on model-level
 * 2) change data store when user is changed
 */
interface ContextualEducationRepository {
    fun setUser(userId: Int)

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

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

/**
 * Implementation of [ContextualEducationRepository] that uses [androidx.datastore.preferences.core]
 * for storage. Data is stored per user.
 */
@SysUISingleton
class UserContextualEducationRepository
@@ -54,7 +68,7 @@ class UserContextualEducationRepository
constructor(
    @Application private val applicationContext: Context,
    @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope>
) {
) : ContextualEducationRepository {
    companion object {
        const val SIGNAL_COUNT_SUFFIX = "_SIGNAL_COUNT"
        const val NUMBER_OF_EDU_SHOWN_SUFFIX = "_NUMBER_OF_EDU_SHOWN"
@@ -70,7 +84,7 @@ constructor(
    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
    private val prefData: Flow<Preferences> = datastore.filterNotNull().flatMapLatest { it.data }

    internal fun setUser(userId: Int) {
    override fun setUser(userId: Int) {
        dataStoreScope?.cancel()
        val newDsScope = dataStoreScopeProvider.get()
        datastore.value =
@@ -85,7 +99,7 @@ constructor(
        dataStoreScope = newDsScope
    }

    internal fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> =
    override fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> =
        prefData.map { preferences -> getGestureEduModel(gestureType, preferences) }

    private fun getGestureEduModel(
@@ -97,12 +111,12 @@ constructor(
            educationShownCount = preferences[getEducationShownCountKey(gestureType)] ?: 0,
            lastShortcutTriggeredTime =
                preferences[getLastShortcutTriggeredTimeKey(gestureType)]?.let {
                    Instant.ofEpochMilli(it)
                    Instant.ofEpochSecond(it)
                },
        )
    }

    internal suspend fun updateGestureEduModel(
    override suspend fun updateGestureEduModel(
        gestureType: GestureType,
        transform: (GestureEduModel) -> GestureEduModel
    ) {
@@ -134,7 +148,10 @@ constructor(
        key: Preferences.Key<Long>
    ) {
        if (instant != null) {
            preferences[key] = instant.toEpochMilli()
            // Use epochSecond because an instant is defined as a signed long (64bit number) of
            // seconds. Using toEpochMilli() on Instant.MIN or Instant.MAX will throw exception
            // when converting to a long. So we use second instead of milliseconds for storage.
            preferences[key] = instant.epochSecond
        } else {
            preferences.remove(key)
        }
Loading