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

Commit 8747a0a3 authored by András Kurucz's avatar András Kurucz Committed by Android (Google) Code Review
Browse files

Merge "Create HeadsUpInteractor backed by HeadsUpManager" into main

parents 79bf3310 154128d1
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