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

Commit dcf293d0 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[OOBE] Update the notification when the device disconnects" into main

parents a110f901 eceea7f3
Loading
Loading
Loading
Loading
+62 −4
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.inputdevice.tutorial.domain.interactor

import android.app.Notification
import android.app.NotificationManager
import android.service.notification.StatusBarNotification
import androidx.annotation.StringRes
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -28,6 +29,7 @@ import com.android.systemui.inputdevice.tutorial.ui.TutorialNotificationCoordina
import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.res.R
import com.android.systemui.settings.userTracker
import com.android.systemui.statusbar.commandline.commandRegistry
@@ -35,6 +37,7 @@ import com.android.systemui.testKosmos
import com.android.systemui.touchpad.data.repository.FakeTouchpadRepository
import com.google.common.truth.Truth.assertThat
import kotlin.time.Duration.Companion.hours
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -44,23 +47,29 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.times
import org.mockito.junit.MockitoJUnit
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.firstValue
import org.mockito.kotlin.never
import org.mockito.kotlin.secondValue
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class TutorialNotificationCoordinatorTest : SysuiTestCase() {

    private lateinit var underTest: TutorialNotificationCoordinator
    private val kosmos = testKosmos()
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val testScope = kosmos.testScope
    private val keyboardRepository = FakeKeyboardRepository()
    private val touchpadRepository = FakeTouchpadRepository()
    private lateinit var repository: TutorialSchedulerRepository
    @Mock private lateinit var notificationManager: NotificationManager
    @Mock private lateinit var notification: StatusBarNotification
    @Captor private lateinit var notificationCaptor: ArgumentCaptor<Notification>
    @get:Rule val rule = MockitoJUnit.rule()

@@ -107,6 +116,7 @@ class TutorialNotificationCoordinatorTest : SysuiTestCase() {
    fun showTouchpadNotification() = runTestAndClear {
        touchpadRepository.setIsAnyTouchpadConnected(true)
        testScope.advanceTimeBy(LAUNCH_DELAY)
        mockExistingNotification()
        verifyNotification(
            R.string.launch_touchpad_tutorial_notification_title,
            R.string.launch_touchpad_tutorial_notification_content,
@@ -131,6 +141,45 @@ class TutorialNotificationCoordinatorTest : SysuiTestCase() {
            .notifyAsUser(eq(TAG), eq(NOTIFICATION_ID), any(), any())
    }

    @Test
    fun showKeyboardNotificationThenDisconnectKeyboard() = runTestAndClear {
        keyboardRepository.setIsAnyKeyboardConnected(true)
        testScope.advanceTimeBy(LAUNCH_DELAY)
        verifyNotification(
            R.string.launch_keyboard_tutorial_notification_title,
            R.string.launch_keyboard_tutorial_notification_content,
        )
        mockExistingNotification()

        // After the keyboard is disconnected, i.e. there is nothing connected, the notification
        // should be cancelled
        keyboardRepository.setIsAnyKeyboardConnected(false)
        verify(notificationManager).cancelAsUser(eq(TAG), eq(NOTIFICATION_ID), any())
    }

    @Test
    fun showKeyboardTouchpadNotificationThenDisconnectKeyboard() = runTestAndClear {
        keyboardRepository.setIsAnyKeyboardConnected(true)
        touchpadRepository.setIsAnyTouchpadConnected(true)
        testScope.advanceTimeBy(LAUNCH_DELAY)
        mockExistingNotification()
        keyboardRepository.setIsAnyKeyboardConnected(false)

        verify(notificationManager, times(2))
            .notifyAsUser(eq(TAG), eq(NOTIFICATION_ID), notificationCaptor.capture(), any())
        // Connect both device and the first notification is for both
        notificationCaptor.firstValue.verify(
            R.string.launch_keyboard_touchpad_tutorial_notification_title,
            R.string.launch_keyboard_touchpad_tutorial_notification_content,
        )
        // After the keyboard is disconnected, i.e. with only the touchpad left, the notification
        // should be update to the one for only touchpad
        notificationCaptor.secondValue.verify(
            R.string.launch_touchpad_tutorial_notification_title,
            R.string.launch_touchpad_tutorial_notification_content,
        )
    }

    private fun runTestAndClear(block: suspend () -> Unit) =
        testScope.runTest {
            try {
@@ -140,12 +189,21 @@ class TutorialNotificationCoordinatorTest : SysuiTestCase() {
            }
        }

    // Assume that there's an existing notification when the updater checks activeNotifications
    private fun mockExistingNotification() {
        whenever(notification.id).thenReturn(NOTIFICATION_ID)
        whenever(notificationManager.activeNotifications).thenReturn(arrayOf(notification))
    }

    private fun verifyNotification(@StringRes titleResId: Int, @StringRes contentResId: Int) {
        verify(notificationManager)
            .notifyAsUser(eq(TAG), eq(NOTIFICATION_ID), notificationCaptor.capture(), any())
        val notification = notificationCaptor.value
        val actualTitle = notification.getString(Notification.EXTRA_TITLE)
        val actualContent = notification.getString(Notification.EXTRA_TEXT)
        notificationCaptor.value.verify(titleResId, contentResId)
    }

    private fun Notification.verify(@StringRes titleResId: Int, @StringRes contentResId: Int) {
        val actualTitle = getString(Notification.EXTRA_TITLE)
        val actualContent = getString(Notification.EXTRA_TEXT)
        assertThat(actualTitle).isEqualTo(context.getString(titleResId))
        assertThat(actualContent).isEqualTo(context.getString(contentResId))
    }
+22 −0
Original line number Diff line number Diff line
@@ -39,6 +39,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
@@ -99,6 +101,25 @@ constructor(
        waitForDeviceConnection(deviceType)
    }

    // This flow is used by the notification updater once an initial notification is launched. It
    // listens to the device connection changes for both keyboard and touchpad. When either of the
    // device is disconnected, resolve the tutorial type base on the latest connection state.
    // Dropping the initial state because it's the existing notification. Filtering out BOTH because
    // we only care about disconnections.
    val tutorialTypeUpdates: Flow<TutorialType> =
        keyboardRepository.isAnyKeyboardConnected
            .combine(touchpadRepository.isAnyTouchpadConnected, ::Pair)
            .map { (keyboardConnected, touchpadConnected) ->
                when {
                    keyboardConnected && touchpadConnected -> TutorialType.BOTH
                    keyboardConnected -> TutorialType.KEYBOARD
                    touchpadConnected -> TutorialType.TOUCHPAD
                    else -> TutorialType.NONE
                }
            }
            .drop(1)
            .filter { it != TutorialType.BOTH }

    private suspend fun waitForDeviceConnection(deviceType: DeviceType) =
        isAnyDeviceConnected[deviceType]!!.filter { it }.first()

@@ -172,6 +193,7 @@ constructor(
                        pw.println(
                            "         launch time = ${repo.getScheduledTutorialLaunchTime(TOUCHPAD)}"
                        )
                        pw.println("Delay time = ${LAUNCH_DELAY.seconds} sec")
                    }
                "notify" -> {
                    if (args.size != 2) help(pw)
+28 −1
Original line number Diff line number Diff line
@@ -42,6 +42,9 @@ import com.android.systemui.res.R
import com.android.systemui.settings.UserTracker
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.merge

/** When the scheduler is due, show a notification to launch tutorial */
@@ -55,19 +58,43 @@ constructor(
    private val notificationManager: NotificationManager,
    private val userTracker: UserTracker,
) {
    private var updaterJob: Job? = null

    fun start() {
        backgroundScope.launch {
            merge(
                    tutorialSchedulerInteractor.tutorials,
                    tutorialSchedulerInteractor.commandTutorials,
                )
                .collect { showNotification(it) }
                .filter { it != TutorialType.NONE }
                .collectLatest {
                    showNotification(it)
                    updaterJob?.cancel()
                    updaterJob = backgroundScope.launch { updateWhenDeviceDisconnects() }
                }
        }
    }

    private suspend fun updateWhenDeviceDisconnects() {
        // Only update the notification when there is an active one (i.e. if the notification has
        // been dismissed by the user, or if the tutorial has been launched, there's no need to
        // update)
        tutorialSchedulerInteractor.tutorialTypeUpdates
            .filter { hasNotification() }
            .collect {
                if (it == TutorialType.NONE)
                    notificationManager.cancelAsUser(TAG, NOTIFICATION_ID, userTracker.userHandle)
                else showNotification(it)
            }
    }

    private fun hasNotification() =
        notificationManager.activeNotifications.any { it.id == NOTIFICATION_ID }

    // By sharing the same tag and id, we update the content of existing notification instead of
    // creating multiple notifications
    private fun showNotification(tutorialType: TutorialType) {
        // Safe guard - but this should never been reached
        if (tutorialType == TutorialType.NONE) return

        if (notificationManager.getNotificationChannel(CHANNEL_ID) == null)