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

Commit 154128d1 authored by András Kurucz's avatar András Kurucz
Browse files

Create HeadsUpInteractor backed by HeadsUpManager

Fixes: 328390331
Test: atest NotificationListViewModelTest
HeadsUpNotificationInteractorTest SceneContainerStartableTest
Flag: ACONFIG com.android.systemui.notifications_heads_up_refactor DEVELOPMENT

Change-Id: I76d466cff68c71a489a0d5b4ad82eb90563b464c
parent 776ed34c
Loading
Loading
Loading
Loading
+19 −2
Original line number Diff line number Diff line
@@ -51,6 +51,8 @@ import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -175,10 +177,12 @@ class SceneContainerStartableTest : SysuiTestCase() {
            transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone)
            assertThat(isVisible).isFalse()

            kosmos.headsUpNotificationRepository.hasPinnedHeadsUp.value = true
            kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
                buildNotificationRows(isPinned = true)
            assertThat(isVisible).isTrue()

            kosmos.headsUpNotificationRepository.hasPinnedHeadsUp.value = false
            kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
                buildNotificationRows(isPinned = false)
            assertThat(isVisible).isFalse()
        }

@@ -1070,4 +1074,17 @@ class SceneContainerStartableTest : SysuiTestCase() {

        return transitionStateFlow
    }

    private fun buildNotificationRows(isPinned: Boolean = false): Set<HeadsUpRowRepository> =
        setOf(
            fakeHeadsUpRowRepository(key = "0", isPinned = isPinned),
            fakeHeadsUpRowRepository(key = "1", isPinned = isPinned),
            fakeHeadsUpRowRepository(key = "2", isPinned = isPinned),
            fakeHeadsUpRowRepository(key = "3", isPinned = isPinned),
        )

    private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean) =
        FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply {
            this.isPinned.value = isPinned
        }
}
+269 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 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.
 */

@file:OptIn(ExperimentalCoroutinesApi::class)

package com.android.systemui.statusbar.notification.domain.interactor

import android.platform.test.annotations.EnableFlags
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.kosmos.testScope
import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
class HeadsUpNotificationInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val repository = kosmos.headsUpNotificationRepository

    private val underTest = kosmos.headsUpNotificationInteractor

    @Test
    fun hasPinnedRows_emptyList_false() =
        testScope.runTest {
            val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)

            assertThat(hasPinnedRows).isFalse()
        }

    @Test
    fun hasPinnedRows_noPinnedRows_false() =
        testScope.runTest {
            val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
            // WHEN no pinned rows are set
            repository.setNotifications(
                fakeHeadsUpRowRepository("key 0"),
                fakeHeadsUpRowRepository("key 1"),
                fakeHeadsUpRowRepository("key 2"),
            )
            runCurrent()

            // THEN hasPinnedRows is false
            assertThat(hasPinnedRows).isFalse()
        }

    @Test
    fun hasPinnedRows_hasPinnedRows_true() =
        testScope.runTest {
            val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
            // WHEN a pinned rows is set
            repository.setNotifications(
                fakeHeadsUpRowRepository("key 0", isPinned = true),
                fakeHeadsUpRowRepository("key 1"),
                fakeHeadsUpRowRepository("key 2"),
            )
            runCurrent()

            // THEN hasPinnedRows is true
            assertThat(hasPinnedRows).isTrue()
        }

    @Test
    fun hasPinnedRows_rowGetsPinned_true() =
        testScope.runTest {
            val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
            // GIVEN no rows are pinned
            val rows =
                arrayListOf(
                    fakeHeadsUpRowRepository("key 0"),
                    fakeHeadsUpRowRepository("key 1"),
                    fakeHeadsUpRowRepository("key 2"),
                )
            repository.setNotifications(rows)
            runCurrent()

            // WHEN a row gets pinned
            rows[0].isPinned.value = true
            runCurrent()

            // THEN hasPinnedRows updates to true
            assertThat(hasPinnedRows).isTrue()
        }

    @Test
    fun hasPinnedRows_rowGetsUnPinned_false() =
        testScope.runTest {
            val hasPinnedRows by collectLastValue(underTest.hasPinnedRows)
            // GIVEN one row is pinned
            val rows =
                arrayListOf(
                    fakeHeadsUpRowRepository("key 0", isPinned = true),
                    fakeHeadsUpRowRepository("key 1"),
                    fakeHeadsUpRowRepository("key 2"),
                )
            repository.setNotifications(rows)
            runCurrent()

            // THEN that row gets unpinned
            rows[0].isPinned.value = false
            runCurrent()

            // THEN hasPinnedRows updates to false
            assertThat(hasPinnedRows).isFalse()
        }

    @Test
    fun pinnedRows_noRows_isEmpty() =
        testScope.runTest {
            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)

            assertThat(pinnedHeadsUpRows).isEmpty()
        }

    @Test
    fun pinnedRows_noPinnedRows_isEmpty() =
        testScope.runTest {
            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
            // WHEN no rows are pinned
            repository.setNotifications(
                fakeHeadsUpRowRepository("key 0"),
                fakeHeadsUpRowRepository("key 1"),
                fakeHeadsUpRowRepository("key 2"),
            )
            runCurrent()

            // THEN all rows are filtered
            assertThat(pinnedHeadsUpRows).isEmpty()
        }

    @Test
    fun pinnedRows_hasPinnedRows_containsPinnedRows() =
        testScope.runTest {
            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
            // WHEN some rows are pinned
            val rows =
                arrayListOf(
                    fakeHeadsUpRowRepository("key 0", isPinned = true),
                    fakeHeadsUpRowRepository("key 1", isPinned = true),
                    fakeHeadsUpRowRepository("key 2"),
                )
            repository.setNotifications(rows)
            runCurrent()

            // THEN the unpinned rows are filtered
            assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1])
        }

    @Test
    fun pinnedRows_rowGetsPinned_containsPinnedRows() =
        testScope.runTest {
            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
            // GIVEN some rows are pinned
            val rows =
                arrayListOf(
                    fakeHeadsUpRowRepository("key 0", isPinned = true),
                    fakeHeadsUpRowRepository("key 1", isPinned = true),
                    fakeHeadsUpRowRepository("key 2"),
                )
            repository.setNotifications(rows)
            runCurrent()

            // WHEN all rows gets pinned
            rows[2].isPinned.value = true
            runCurrent()

            // THEN no rows are filtered
            assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1], rows[2])
        }

    @Test
    fun pinnedRows_allRowsPinned_containsAllRows() =
        testScope.runTest {
            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
            // WHEN all rows are pinned
            val rows =
                arrayListOf(
                    fakeHeadsUpRowRepository("key 0", isPinned = true),
                    fakeHeadsUpRowRepository("key 1", isPinned = true),
                    fakeHeadsUpRowRepository("key 2", isPinned = true),
                )
            repository.setNotifications(rows)
            runCurrent()

            // THEN no rows are filtered
            assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1], rows[2])
        }

    @Test
    fun pinnedRows_rowGetsUnPinned_containsPinnedRows() =
        testScope.runTest {
            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
            // GIVEN all rows are pinned
            val rows =
                arrayListOf(
                    fakeHeadsUpRowRepository("key 0", isPinned = true),
                    fakeHeadsUpRowRepository("key 1", isPinned = true),
                    fakeHeadsUpRowRepository("key 2", isPinned = true),
                )
            repository.setNotifications(rows)
            runCurrent()

            // WHEN a row gets unpinned
            rows[0].isPinned.value = false
            runCurrent()

            // THEN the unpinned row is filtered
            assertThat(pinnedHeadsUpRows).containsExactly(rows[1], rows[2])
        }

    @Test
    fun pinnedRows_rowGetsPinnedAndUnPinned_containsTheSameInstance() =
        testScope.runTest {
            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)

            val rows =
                arrayListOf(
                    fakeHeadsUpRowRepository("key 0"),
                    fakeHeadsUpRowRepository("key 1"),
                    fakeHeadsUpRowRepository("key 2"),
                )
            repository.setNotifications(rows)
            runCurrent()

            rows[0].isPinned.value = true
            runCurrent()
            assertThat(pinnedHeadsUpRows).containsExactly(rows[0])

            rows[0].isPinned.value = false
            runCurrent()
            assertThat(pinnedHeadsUpRows).isEmpty()

            rows[0].isPinned.value = true
            runCurrent()
            assertThat(pinnedHeadsUpRows).containsExactly(rows[0])
        }

    private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) =
        FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply {
            this.isPinned.value = isPinned
        }
}
+8 −0
Original line number Diff line number Diff line
@@ -193,6 +193,7 @@ import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefac
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
import com.android.systemui.statusbar.notification.stack.AmbientState;
import com.android.systemui.statusbar.notification.stack.AnimationProperties;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
@@ -4381,6 +4382,10 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump

        @Override
        public void onHeadsUpPinned(NotificationEntry entry) {
            if (NotificationsHeadsUpRefactor.isEnabled()) {
                return;
            }

            if (!isKeyguardShowing()) {
                mNotificationStackScrollLayoutController.generateHeadsUpAnimation(entry, true);
            }
@@ -4388,6 +4393,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump

        @Override
        public void onHeadsUpUnPinned(NotificationEntry entry) {
            if (NotificationsHeadsUpRefactor.isEnabled()) {
                return;
            }

            // When we're unpinning the notification via active edge they remain heads-upped,
            // we need to make sure that an animation happens in this case, otherwise the
+3 −6
Original line number Diff line number Diff line
@@ -15,8 +15,8 @@
 */
package com.android.systemui.statusbar.notification.data

import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepository
import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepositoryImpl
import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
import dagger.Binds
import dagger.Module

@@ -27,8 +27,5 @@ import dagger.Module
        ]
)
interface NotificationDataLayerModule {
    @Binds
    fun bindHeadsUpNotificationRepository(
        impl: HeadsUpNotificationRepositoryImpl
    ): HeadsUpNotificationRepository
    @Binds fun bindHeadsUpNotificationRepository(impl: HeadsUpManagerPhone): HeadsUpRepository
}
+41 −0
Original line number Diff line number Diff line
@@ -16,44 +16,26 @@

package com.android.systemui.statusbar.notification.data.repository

import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow

class HeadsUpNotificationRepositoryImpl
@Inject
constructor(
    headsUpManager: HeadsUpManager,
) : HeadsUpNotificationRepository {
    override val hasPinnedHeadsUp: Flow<Boolean> = conflatedCallbackFlow {
        val listener =
            object : OnHeadsUpChangedListener {
                override fun onHeadsUpPinnedModeChanged(inPinnedMode: Boolean) {
                    trySend(headsUpManager.hasPinnedHeadsUp())
                }

                override fun onHeadsUpPinned(entry: NotificationEntry?) {
                    trySend(headsUpManager.hasPinnedHeadsUp())
                }
/**
 * A repository of currently displayed heads up notifications.
 *
 * This repository serves as a boundary between the
 * [com.android.systemui.statusbar.policy.HeadsUpManager] and the modern notifications presentation
 * codebase.
 */
interface HeadsUpRepository {

                override fun onHeadsUpUnPinned(entry: NotificationEntry?) {
                    trySend(headsUpManager.hasPinnedHeadsUp())
                }
    /**
     * True if we are exiting the headsUp pinned mode, and some notifications might still be
     * animating out. This is used to keep the touchable regions in a reasonable state.
     */
    val headsUpAnimatingAway: Flow<Boolean>

                override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
                    trySend(headsUpManager.hasPinnedHeadsUp())
                }
            }
        trySend(headsUpManager.hasPinnedHeadsUp())
        headsUpManager.addListener(listener)
        awaitClose { headsUpManager.removeListener(listener) }
    }
}
    /** The heads up row that should be displayed on top. */
    val topHeadsUpRow: Flow<HeadsUpRowRepository?>

interface HeadsUpNotificationRepository {
    val hasPinnedHeadsUp: Flow<Boolean>
    /** Set of currently active top-level heads up rows to be displayed. */
    val activeHeadsUpRows: Flow<Set<HeadsUpRowRepository>>
}
Loading