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

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

Merge "[SB][Notif] Up-rank notification chips whose app was recently opened." into main

parents 01c0d2b4 4861bb78
Loading
Loading
Loading
Loading
+113 −0
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.log.core.Logger
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -136,6 +137,118 @@ class ActivityManagerRepositoryTest : SysuiTestCase() {
            assertThat(latest).isFalse()
        }

    @Test
    fun createAppVisibilityFlow_fetchesInitialValue_trueWithLastVisibleTime() =
        kosmos.runTest {
            whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_FOREGROUND)
            fakeSystemClock.setCurrentTimeMillis(5000)

            val latest by
                collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))

            assertThat(latest!!.isAppCurrentlyVisible).isTrue()
            assertThat(latest!!.lastAppVisibleTime).isEqualTo(5000)
        }

    @Test
    fun createAppVisibilityFlow_fetchesInitialValue_falseWithoutLastVisibleTime() =
        kosmos.runTest {
            whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE)
            fakeSystemClock.setCurrentTimeMillis(5000)

            val latest by
                collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))

            assertThat(latest!!.isAppCurrentlyVisible).isFalse()
            assertThat(latest!!.lastAppVisibleTime).isNull()
        }

    @Test
    fun createAppVisibilityFlow_getsImportanceUpdates_updatesLastVisibleTimeOnlyWhenVisible() =
        kosmos.runTest {
            whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE)
            fakeSystemClock.setCurrentTimeMillis(5000)
            val latest by
                collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))

            assertThat(latest!!.isAppCurrentlyVisible).isFalse()
            assertThat(latest!!.lastAppVisibleTime).isNull()

            val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>()
            verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any())
            val listener = listenerCaptor.firstValue

            // WHEN the app becomes visible
            fakeSystemClock.setCurrentTimeMillis(7000)
            listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND)

            // THEN the status and lastAppVisibleTime are updated
            assertThat(latest!!.isAppCurrentlyVisible).isTrue()
            assertThat(latest!!.lastAppVisibleTime).isEqualTo(7000)

            // WHEN the app is no longer visible
            listener.onUidImportance(THIS_UID, IMPORTANCE_TOP_SLEEPING)

            // THEN the lastAppVisibleTime is preserved
            assertThat(latest!!.isAppCurrentlyVisible).isFalse()
            assertThat(latest!!.lastAppVisibleTime).isEqualTo(7000)

            // WHEN the app is visible again
            fakeSystemClock.setCurrentTimeMillis(9000)
            listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND)

            // THEN the lastAppVisibleTime is updated
            assertThat(latest!!.isAppCurrentlyVisible).isTrue()
            assertThat(latest!!.lastAppVisibleTime).isEqualTo(9000)
        }

    @Test
    fun createAppVisibilityFlow_ignoresUpdatesForOtherUids() =
        kosmos.runTest {
            val latest by
                collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))

            val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>()
            verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any())
            val listener = listenerCaptor.firstValue

            listener.onUidImportance(THIS_UID, IMPORTANCE_GONE)
            assertThat(latest!!.isAppCurrentlyVisible).isFalse()

            // WHEN another UID becomes foreground
            listener.onUidImportance(THIS_UID + 2, IMPORTANCE_FOREGROUND)

            // THEN this UID still stays not visible
            assertThat(latest!!.isAppCurrentlyVisible).isFalse()
        }

    @Test
    fun createAppVisibilityFlow_securityExceptionOnUidRegistration_ok() =
        kosmos.runTest {
            whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE)
            whenever(activityManager.addOnUidImportanceListener(any(), any()))
                .thenThrow(SecurityException())

            val latest by
                collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))

            // Verify no crash, and we get a value emitted
            assertThat(latest!!.isAppCurrentlyVisible).isFalse()
        }

    /** Regression test for b/216248574. */
    @Test
    fun createAppVisibilityFlow_getUidImportanceThrowsException_ok() =
        kosmos.runTest {
            whenever(activityManager.getUidImportance(any())).thenThrow(SecurityException())

            val latest by
                collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG))

            // Verify no crash, and we get a value emitted
            assertThat(latest!!.isAppCurrentlyVisible).isFalse()
        }

    companion object {
        private const val THIS_UID = 558
        private const val LOG_TAG = "LogTag"
+50 −17
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.testKosmos
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -183,7 +184,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                        statusBarChipIcon = null,
                        promotedContent = PROMOTED_CONTENT,
                    ),
                    32L,
                    creationTime = 32L,
                )

            val latest by collectLastValue(underTest.notificationChip)
@@ -246,7 +247,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
                    statusBarChipIcon = mock(),
                    promotedContent = PROMOTED_CONTENT,
                )
            val underTest = factory.create(startingNotif, 123L)
            val underTest = factory.create(startingNotif, creationTime = 123L)
            val latest by collectLastValue(underTest.notificationChip)
            assertThat(latest).isNotNull()

@@ -306,9 +307,10 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
        }

    @Test
    fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrue() =
    fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrueWithTime() =
        kosmos.runTest {
            activityManagerRepository.fake.startingIsAppVisibleValue = true
            fakeSystemClock.setCurrentTimeMillis(9000)

            val underTest =
                factory.create(
@@ -325,12 +327,14 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {

            assertThat(latest).isNotNull()
            assertThat(latest!!.isAppVisible).isTrue()
            assertThat(latest!!.lastAppVisibleTime).isEqualTo(9000)
        }

    @Test
    fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalse() =
    fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalseWithNoTime() =
        kosmos.runTest {
            activityManagerRepository.fake.startingIsAppVisibleValue = false
            fakeSystemClock.setCurrentTimeMillis(9000)

            val underTest =
                factory.create(
@@ -347,11 +351,15 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {

            assertThat(latest).isNotNull()
            assertThat(latest!!.isAppVisible).isFalse()
            assertThat(latest!!.lastAppVisibleTime).isNull()
        }

    @Test
    fun notificationChip_updatesWhenAppIsVisible() =
        kosmos.runTest {
            activityManagerRepository.fake.startingIsAppVisibleValue = false
            fakeSystemClock.setCurrentTimeMillis(9000)

            val underTest =
                factory.create(
                    activeNotificationModel(
@@ -365,32 +373,39 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {

            val latest by collectLastValue(underTest.notificationChip)

            activityManagerRepository.fake.setIsAppVisible(UID, false)
            activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = false)
            assertThat(latest!!.isAppVisible).isFalse()
            assertThat(latest!!.lastAppVisibleTime).isNull()

            activityManagerRepository.fake.setIsAppVisible(UID, true)
            fakeSystemClock.setCurrentTimeMillis(11000)
            activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = true)
            assertThat(latest!!.isAppVisible).isTrue()
            assertThat(latest!!.lastAppVisibleTime).isEqualTo(11000)

            activityManagerRepository.fake.setIsAppVisible(UID, false)
            fakeSystemClock.setCurrentTimeMillis(13000)
            activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = false)
            assertThat(latest!!.isAppVisible).isFalse()
            assertThat(latest!!.lastAppVisibleTime).isEqualTo(11000)

            fakeSystemClock.setCurrentTimeMillis(15000)
            activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = true)
            assertThat(latest!!.isAppVisible).isTrue()
            assertThat(latest!!.lastAppVisibleTime).isEqualTo(15000)
        }

    // Note: This test is theoretically impossible because the notification key should contain the
    // UID, so if the UID changes then the key would also change and a new interactor would be
    // created. But, test it just in case.
    @Test
    fun notificationChip_updatedUid_rechecksAppVisibility_oldObserverUnregistered() =
    fun notificationChip_updatedUid_newUidIsIgnoredButOtherDataNotIgnored() =
        kosmos.runTest {
            activityManagerRepository.fake.startingIsAppVisibleValue = false

            val hiddenUid = 100
            val shownUid = 101
            val originalUid = 100
            val newUid = 101

            val underTest =
                factory.create(
                    activeNotificationModel(
                        key = "notif",
                        uid = hiddenUid,
                        uid = originalUid,
                        statusBarChipIcon = mock(),
                        promotedContent = PROMOTED_CONTENT,
                    ),
@@ -402,16 +417,34 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {

            // WHEN the notif gets a new UID that starts as visible
            activityManagerRepository.fake.startingIsAppVisibleValue = true
            val newPromotedContentBuilder =
                PromotedNotificationContentModel.Builder("notif").apply {
                    this.shortCriticalText = "Arrived"
                }
            val newPromotedContent = newPromotedContentBuilder.build()
            underTest.setNotification(
                activeNotificationModel(
                    key = "notif",
                    uid = shownUid,
                    uid = newUid,
                    statusBarChipIcon = mock(),
                    promotedContent = PROMOTED_CONTENT,
                    promotedContent = newPromotedContent,
                )
            )

            // THEN we re-fetch the app visibility state with the new UID
            // THEN we do update other fields like promoted content
            assertThat(latest!!.promotedContent).isEqualTo(newPromotedContent)

            // THEN we don't fetch the app visibility state for the new UID
            assertThat(latest!!.isAppVisible).isFalse()

            // AND don't listen to updates for the new UID
            activityManagerRepository.fake.setIsAppVisible(newUid, isAppVisible = false)
            activityManagerRepository.fake.setIsAppVisible(newUid, isAppVisible = true)
            assertThat(latest!!.isAppVisible).isFalse()

            // AND we still use updates from the old UID
            // TODO(b/364653005): This particular behavior isn't great, can we do better?
            activityManagerRepository.fake.setIsAppVisible(originalUid, isAppVisible = true)
            assertThat(latest!!.isAppVisible).isTrue()
        }

+241 −19

File changed.

Preview size limit exceeded, changes collapsed.

+29 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.activity.data.model

/** Describes an app's previous and current visibility to the user. */
data class AppVisibilityModel(
    /** True if the app is currently visible to the user and false otherwise. */
    val isAppCurrentlyVisible: Boolean = false,
    /**
     * The last time this app became visible to the user, in
     * [com.android.systemui.util.time.SystemClock.currentTimeMillis] units. Null if the app hasn't
     * become visible since the flow started collection.
     */
    val lastAppVisibleTime: Long? = null,
)
+46 −0
Original line number Diff line number Diff line
@@ -18,9 +18,11 @@ package com.android.systemui.activity.data.repository

import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
import com.android.systemui.activity.data.model.AppVisibilityModel
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.core.Logger
import com.android.systemui.util.time.SystemClock
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
@@ -29,9 +31,23 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.scan

/** Repository for interfacing with [ActivityManager]. */
interface ActivityManagerRepository {

    /**
     * Given a UID, creates a flow that emits details about when the process with the given UID was
     * and is visible to the user.
     *
     * @param identifyingLogTag a tag identifying who created this flow, used for logging.
     */
    fun createAppVisibilityFlow(
        creationUid: Int,
        logger: Logger,
        identifyingLogTag: String,
    ): Flow<AppVisibilityModel>

    /**
     * Given a UID, creates a flow that emits true when the process with the given UID is visible to
     * the user and false otherwise.
@@ -50,8 +66,38 @@ class ActivityManagerRepositoryImpl
@Inject
constructor(
    @Background private val backgroundContext: CoroutineContext,
    private val systemClock: SystemClock,
    private val activityManager: ActivityManager,
) : ActivityManagerRepository {

    override fun createAppVisibilityFlow(
        creationUid: Int,
        logger: Logger,
        identifyingLogTag: String,
    ): Flow<AppVisibilityModel> {
        return createIsAppVisibleFlow(creationUid, logger, identifyingLogTag)
            .distinctUntilChanged()
            .scan(initial = AppVisibilityModel()) {
                oldState: AppVisibilityModel,
                newIsVisible: Boolean ->
                if (newIsVisible) {
                    val lastAppVisibleTime = systemClock.currentTimeMillis()
                    logger.d({ "$str1: Setting lastAppVisibleTime=$long1" }) {
                        str1 = identifyingLogTag
                        long1 = lastAppVisibleTime
                    }
                    AppVisibilityModel(
                        isAppCurrentlyVisible = true,
                        lastAppVisibleTime = lastAppVisibleTime,
                    )
                } else {
                    // Reset the current status while maintaining the lastAppVisibleTime
                    oldState.copy(isAppCurrentlyVisible = false)
                }
            }
            .distinctUntilChanged()
    }

    override fun createIsAppVisibleFlow(
        creationUid: Int,
        logger: Logger,
Loading