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

Commit 762af00c authored by helencheuk's avatar helencheuk
Browse files

[Contextual Edu] Display edu notification

Display education notification when UiType received is Notification in ContextualEduUiCoordinator
Add title and content for the notification. Tutorial will be launched when user taps the edu notification.

Add userId to GestureEduModel and EducationInfo for 2 reasons:
1) "educationTriggered" in KeyboardTouchpadEduInteractor is a StateFlow which only emits values that are distinct from the previous item.
All users shares the same KeyboardTouchpadEduInteractor so it is necessary to know which user the education is for, otherwise same education of 2 different users will not both be emitted for UI to display.
e.g. If BACK notification of user0 and user10 are set to be value of "_educationTriggered" consecutively without userId, ContextualEduUiCoordinator will only receive the 1st value to display and not receive the 2nd one.
2) notifyAsUser is used to display notification in ContextualEduUiCoordinator, we need to pass in userId to call this method

New variable userId is added in UserContextualEducationRepository because of its responsibility to maintain edu data based on user change.

Bug: 358633150
Test: ContextualEduUiCoordinatorTest
Test: ContextualEducationRepositoryTest
Flag: com.android.systemui.keyboard_touchpad_contextual_education

Change-Id: I84d1f550b3741421fb35463aa0b95acd8308b4b8
parent 1d94f565
Loading
Loading
Loading
Loading
+12 −1
Original line number Diff line number Diff line
@@ -51,6 +51,7 @@ class ContextualEducationRepositoryTest : SysuiTestCase() {
    }

    private val testUserId = 1111
    private val secondTestUserId = 1112

    // For deleting any test files created after the test
    @get:Rule val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@@ -73,11 +74,20 @@ class ContextualEducationRepositoryTest : SysuiTestCase() {
            assertThat(model?.signalCount).isEqualTo(1)

            // User is changed.
            underTest.setUser(1112)
            underTest.setUser(secondTestUserId)
            // Assert count is 0 after user is changed.
            assertThat(model?.signalCount).isEqualTo(0)
        }

    @Test
    fun changeUserIdForNewUser() =
        testScope.runTest {
            val model by collectLastValue(underTest.readGestureEduModelFlow(BACK))
            assertThat(model?.userId).isEqualTo(testUserId)
            underTest.setUser(secondTestUserId)
            assertThat(model?.userId).isEqualTo(secondTestUserId)
        }

    @Test
    fun dataChangedOnUpdate() =
        testScope.runTest {
@@ -88,6 +98,7 @@ class ContextualEducationRepositoryTest : SysuiTestCase() {
                    lastShortcutTriggeredTime = kosmos.fakeEduClock.instant(),
                    lastEducationTime = kosmos.fakeEduClock.instant(),
                    usageSessionStartTime = kosmos.fakeEduClock.instant(),
                    userId = testUserId
                )
            underTest.updateGestureEduModel(BACK) { newModel }
            val model by collectLastValue(underTest.readGestureEduModelFlow(BACK))
+2 −1
Original line number Diff line number Diff line
@@ -109,7 +109,8 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
                .isEqualTo(
                    GestureEduModel(
                        signalCount = 1,
                        usageSessionStartTime = secondSignalReceivedTime
                        usageSessionStartTime = secondSignalReceivedTime,
                        userId = 0
                    )
                )
        }
+68 −5
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.education.domain.ui.view

import android.app.Notification
import android.app.NotificationManager
import android.content.applicationContext
import android.widget.Toast
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -30,27 +32,35 @@ import com.android.systemui.education.ui.view.ContextualEduUiCoordinator
import com.android.systemui.education.ui.viewmodel.ContextualEduViewModel
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.kotlin.any
import org.mockito.kotlin.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
class ContextualEduUiCoordinatorTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val interactor = kosmos.contextualEducationInteractor
    private lateinit var underTest: ContextualEduUiCoordinator
    @Mock private lateinit var toast: Toast

    @Mock private lateinit var notificationManager: NotificationManager
    @get:Rule val mockitoRule = MockitoJUnit.rule()
    private var toastContent = ""

    @Before
    fun setUp() {
@@ -60,23 +70,76 @@ class ContextualEduUiCoordinatorTest : SysuiTestCase() {
                kosmos.keyboardTouchpadEduInteractor
            )
        underTest =
            ContextualEduUiCoordinator(kosmos.applicationCoroutineScope, viewModel) { _ -> toast }
            ContextualEduUiCoordinator(
                kosmos.applicationCoroutineScope,
                viewModel,
                kosmos.applicationContext,
                notificationManager
            ) { content ->
                toastContent = content
                toast
            }
        underTest.start()
        kosmos.keyboardTouchpadEduInteractor.start()
    }

    @Test
    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
    fun showToastOnNewEdu() =
        testScope.runTest {
            triggerEducation(BACK)
            runCurrent()
            verify(toast).show()
        }

    private suspend fun triggerEducation(gestureType: GestureType) {
    @Test
    fun showNotificationOn2ndEdu() =
        testScope.runTest {
            triggerEducation(BACK)
            triggerEducation(BACK)
            verify(notificationManager).notifyAsUser(any(), anyInt(), any(), any())
        }

    @Test
    fun verifyBackEduToastContent() =
        testScope.runTest {
            triggerEducation(BACK)
            assertThat(toastContent).isEqualTo(context.getString(R.string.back_edu_toast_content))
        }

    @Test
    fun verifyBackEduNotificationContent() =
        testScope.runTest {
            val notificationCaptor = ArgumentCaptor.forClass(Notification::class.java)
            triggerEducation(BACK)
            triggerEducation(BACK)
            verify(notificationManager)
                .notifyAsUser(any(), anyInt(), notificationCaptor.capture(), any())
            verifyNotificationContent(
                R.string.back_edu_notification_title,
                R.string.back_edu_notification_content,
                notificationCaptor.value
            )
        }

    private fun verifyNotificationContent(
        titleResId: Int,
        contentResId: Int,
        notification: Notification
    ) {
        val expectedContent = context.getString(contentResId)
        val expectedTitle = context.getString(titleResId)
        val actualContent = notification.getString(Notification.EXTRA_TEXT)
        val actualTitle = notification.getString(Notification.EXTRA_TITLE)
        assertThat(actualContent).isEqualTo(expectedContent)
        assertThat(actualTitle).isEqualTo(expectedTitle)
    }

    private fun Notification.getString(key: String): String =
        this.extras?.getCharSequence(key).toString()

    private suspend fun TestScope.triggerEducation(gestureType: GestureType) {
        for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
            interactor.incrementSignalCount(gestureType)
        }
        runCurrent()
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -28,4 +28,5 @@ data class GestureEduModel(
    val lastShortcutTriggeredTime: Instant? = null,
    val usageSessionStartTime: Instant? = null,
    val lastEducationTime: Instant? = null,
    val userId: Int
)
+5 −0
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import com.android.systemui.education.data.model.GestureEduModel
import java.time.Instant
import javax.inject.Inject
import javax.inject.Provider
import kotlin.properties.Delegates.notNull
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
@@ -79,6 +80,8 @@ constructor(
        const val DATASTORE_DIR = "education/USER%s_ContextualEducation"
    }

    private var userId by notNull<Int>()

    private var dataStoreScope: CoroutineScope? = null

    private val datastore = MutableStateFlow<DataStore<Preferences>?>(null)
@@ -89,6 +92,7 @@ constructor(
    override fun setUser(userId: Int) {
        dataStoreScope?.cancel()
        val newDsScope = dataStoreScopeProvider.get()
        this.userId = userId
        datastore.value =
            PreferenceDataStoreFactory.create(
                produceFile = {
@@ -123,6 +127,7 @@ constructor(
                preferences[getLastEducationTimeKey(gestureType)]?.let {
                    Instant.ofEpochSecond(it)
                },
            userId = userId
        )
    }

Loading