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

Commit f5ac6754 authored by helencheuk's avatar helencheuk
Browse files

[Contextual Edu] Add PKT education interactor

- Add education interactors to provide info about which gesture needs edu, and what types of edu(Toast/Notification) is needed
- Add related kosmos classes

Test: KeyboardTouchpadEduInteractorTest
Bug: 317496783
Flag: com.android.systemui.keyboard_touchpad_contextual_education
Change-Id: I4a81461a0be2564f549d6780026c652bdc891082
parent 78297ef2
Loading
Loading
Loading
Loading
+78 −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.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.education.data.repository.contextualEducationRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.shared.education.GestureType
import com.android.systemui.shared.education.GestureType.BACK_GESTURE
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val repository = kosmos.contextualEducationRepository
    private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor

    @Before
    fun setup() {
        underTest.start()
    }

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

    @Test
    fun noEducationInfoBeforeMaxSignalCountReached() =
        testScope.runTest {
            repository.incrementSignalCount(BACK_GESTURE)
            val model by collectLastValue(underTest.educationTriggered)
            assertThat(model).isNull()
        }

    @Test
    fun noEducationInfoWhenShortcutTriggeredPreviously() =
        testScope.runTest {
            val model by collectLastValue(underTest.educationTriggered)
            repository.updateShortcutTriggerTime(BACK_GESTURE)
            tryTriggeringEducation(BACK_GESTURE)
            assertThat(model).isNull()
        }

    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)
        }
    }
}
+19 −2
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ 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.domain.interactor.ContextualEducationInteractor
import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor
import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor
import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractorImpl
import com.android.systemui.shared.education.GestureType
@@ -73,7 +74,7 @@ interface ContextualEducationModule {
                implLazy.get()
            } else {
                // No-op implementation when the flag is disabled.
                return NoOpCoreStartable
                return NoOpContextualEducationInteractor
            }
        }

@@ -88,6 +89,18 @@ interface ContextualEducationModule {
                return NoOpKeyboardTouchpadEduStatsInteractor
            }
        }

        @Provides
        fun provideKeyboardTouchpadEduInteractor(
            implLazy: Lazy<KeyboardTouchpadEduInteractor>
        ): CoreStartable {
            return if (Flags.keyboardTouchpadContextualEducation()) {
                implLazy.get()
            } else {
                // No-op implementation when the flag is disabled.
                return NoOpKeyboardTouchpadEduInteractor
            }
        }
    }

    private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor {
@@ -96,7 +109,11 @@ interface ContextualEducationModule {
        override fun updateShortcutTriggerTime(gestureType: GestureType) {}
    }

    private object NoOpCoreStartable : CoreStartable {
    private object NoOpContextualEducationInteractor : CoreStartable {
        override fun start() {}
    }

    private object NoOpKeyboardTouchpadEduInteractor : CoreStartable {
        override fun start() {}
    }
}
+17 −0
Original line number Diff line number Diff line
@@ -19,12 +19,17 @@ package com.android.systemui.education.domain.interactor
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.education.data.model.GestureEduModel
import com.android.systemui.education.data.repository.ContextualEducationRepository
import com.android.systemui.shared.education.GestureType
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch

/**
@@ -36,16 +41,28 @@ class ContextualEducationInteractor
@Inject
constructor(
    @Background private val backgroundScope: CoroutineScope,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val selectedUserInteractor: SelectedUserInteractor,
    private val repository: ContextualEducationRepository,
) : CoreStartable {

    val backGestureModelFlow = readEduModelsOnSignalCountChanged(GestureType.BACK_GESTURE)

    override fun start() {
        backgroundScope.launch {
            selectedUserInteractor.selectedUser.collectLatest { repository.setUser(it) }
        }
    }

    private fun readEduModelsOnSignalCountChanged(gestureType: GestureType): Flow<GestureEduModel> {
        return repository
            .readGestureEduModelFlow(gestureType)
            .distinctUntilChanged(
                areEquivalent = { old, new -> old.signalCount == new.signalCount }
            )
            .flowOn(backgroundDispatcher)
    }

    suspend fun incrementSignalCount(gestureType: GestureType) =
        repository.incrementSignalCount(gestureType)

+72 −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.domain.interactor

import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
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.shared.education.GestureType.BACK_GESTURE
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch

/** Allow listening to new contextual education triggered */
@SysUISingleton
class KeyboardTouchpadEduInteractor
@Inject
constructor(
    @Background private val backgroundScope: CoroutineScope,
    private val contextualEducationInteractor: ContextualEducationInteractor
) : CoreStartable {

    companion object {
        const val MAX_SIGNAL_COUNT: Int = 2
    }

    private val _educationTriggered = MutableStateFlow<EducationInfo?>(null)
    val educationTriggered = _educationTriggered.asStateFlow()

    override fun start() {
        backgroundScope.launch {
            contextualEducationInteractor.backGestureModelFlow
                .mapNotNull { getEduType(it) }
                .collect { _educationTriggered.value = EducationInfo(BACK_GESTURE, it) }
        }
    }

    private fun getEduType(model: GestureEduModel): EducationUiType? {
        if (isEducationNeeded(model)) {
            return EducationUiType.Toast
        } else {
            return null
        }
    }

    private fun isEducationNeeded(model: GestureEduModel): Boolean {
        // Todo: b/354884305 - add complete education logic to show education in correct scenarios
        val shortcutWasTriggered = model.lastShortcutTriggeredTime == null
        val signalCountReached = model.signalCount >= MAX_SIGNAL_COUNT

        return shortcutWasTriggered && signalCountReached
    }
}
+30 −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.shared.model

import com.android.systemui.shared.education.GestureType

/**
 * Model for education triggered. [gestureType] indicates what gesture it is trying to educate about
 * and [educationUiType] is how we educate user in the UI
 */
data class EducationInfo(val gestureType: GestureType, val educationUiType: EducationUiType)

enum class EducationUiType {
    Toast,
    Notification,
}
Loading