Merge "Send FSIs on ranking update when they are no longer suppressed by DND." into tm-qpr-dev

This commit is contained in:
Yuri Lin
2022-12-13 21:46:38 +00:00
committed by Android (Google) Code Review
4 changed files with 170 additions and 13 deletions

View File

@@ -33,6 +33,8 @@ class NotifPipelineFlags @Inject constructor(
fun fullScreenIntentRequiresKeyguard(): Boolean =
featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD)
fun fsiOnDNDUpdate(): Boolean = featureFlags.isEnabled(Flags.FSI_ON_DND_UPDATE)
val isStabilityIndexFixEnabled: Boolean by lazy {
featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX)
}

View File

@@ -22,6 +22,7 @@ import android.util.ArraySet
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.NotificationRemoteInputManager
import com.android.systemui.statusbar.notification.NotifPipelineFlags
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -38,6 +39,7 @@ import com.android.systemui.statusbar.notification.collection.render.NodeControl
import com.android.systemui.statusbar.notification.dagger.IncomingHeader
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision
import com.android.systemui.statusbar.notification.logKey
import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP
import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -70,11 +72,13 @@ class HeadsUpCoordinator @Inject constructor(
private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider,
private val mRemoteInputManager: NotificationRemoteInputManager,
private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider,
private val mFlags: NotifPipelineFlags,
@IncomingHeader private val mIncomingHeaderController: NodeController,
@Main private val mExecutor: DelayableExecutor,
) : Coordinator {
private val mEntriesBindingUntil = ArrayMap<String, Long>()
private val mEntriesUpdateTimes = ArrayMap<String, Long>()
private val mFSIUpdateCandidates = ArrayMap<String, Long>()
private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null
private lateinit var mNotifPipeline: NotifPipeline
private var mNow: Long = -1
@@ -278,7 +282,7 @@ class HeadsUpCoordinator @Inject constructor(
mPostedEntries.clear()
// Also take this opportunity to clean up any stale entry update times
cleanUpEntryUpdateTimes()
cleanUpEntryTimes()
}
/**
@@ -384,8 +388,15 @@ class HeadsUpCoordinator @Inject constructor(
override fun onEntryAdded(entry: NotificationEntry) {
// First check whether this notification should launch a full screen intent, and
// launch it if needed.
if (mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) {
val fsiDecision = mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry)
if (fsiDecision != null && fsiDecision.shouldLaunch) {
mNotificationInterruptStateProvider.logFullScreenIntentDecision(entry, fsiDecision)
mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
} else if (mFlags.fsiOnDNDUpdate() &&
fsiDecision.equals(FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)) {
// If DND was the only reason this entry was suppressed, note it for potential
// reconsideration on later ranking updates.
addForFSIReconsideration(entry, mSystemClock.currentTimeMillis())
}
// shouldHeadsUp includes check for whether this notification should be filtered
@@ -488,11 +499,32 @@ class HeadsUpCoordinator @Inject constructor(
if (!isNewEnoughForRankingUpdate(entry)) continue
// The only entries we consider alerting for here are entries that have never
// interrupted and that now say they should heads up; if they've alerted in the
// past, we don't want to incorrectly alert a second time if there wasn't an
// interrupted and that now say they should heads up or FSI; if they've alerted in
// the past, we don't want to incorrectly alert a second time if there wasn't an
// explicit notification update.
if (entry.hasInterrupted()) continue
// Before potentially allowing heads-up, check for any candidates for a FSI launch.
// Any entry that is a candidate meets two criteria:
// - was suppressed from FSI launch only by a DND suppression
// - is within the recency window for reconsideration
// If any of these entries are no longer suppressed, launch the FSI now.
if (mFlags.fsiOnDNDUpdate() && isCandidateForFSIReconsideration(entry)) {
val decision =
mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry)
if (decision.shouldLaunch) {
// Log both the launch of the full screen and also that this was via a
// ranking update.
mLogger.logEntryUpdatedToFullScreen(entry.key)
mNotificationInterruptStateProvider.logFullScreenIntentDecision(
entry, decision)
mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
// if we launch the FSI then this is no longer a candidate for HUN
continue
}
}
// The cases where we should consider this notification to be updated:
// - if this entry is not present in PostedEntries, and is now in a shouldHeadsUp
// state
@@ -527,6 +559,15 @@ class HeadsUpCoordinator @Inject constructor(
mEntriesUpdateTimes[entry.key] = time
}
/**
* Add the entry to the list of entries potentially considerable for FSI ranking update, where
* the provided time is the time the entry was added.
*/
@VisibleForTesting
fun addForFSIReconsideration(entry: NotificationEntry, time: Long) {
mFSIUpdateCandidates[entry.key] = time
}
/**
* Checks whether the entry is new enough to be updated via ranking update.
* We want to avoid updating an entry too long after it was originally posted/updated when we're
@@ -541,17 +582,38 @@ class HeadsUpCoordinator @Inject constructor(
return (mSystemClock.currentTimeMillis() - updateTime) <= MAX_RANKING_UPDATE_DELAY_MS
}
private fun cleanUpEntryUpdateTimes() {
/**
* Checks whether the entry is present new enough for reconsideration for full screen launch.
* The time window is the same as for ranking update, but this doesn't allow a potential update
* to an entry with full screen intent to count for timing purposes.
*/
private fun isCandidateForFSIReconsideration(entry: NotificationEntry): Boolean {
val addedTime = mFSIUpdateCandidates[entry.key] ?: return false
return (mSystemClock.currentTimeMillis() - addedTime) <= MAX_RANKING_UPDATE_DELAY_MS
}
private fun cleanUpEntryTimes() {
// Because we won't update entries that are older than this amount of time anyway, clean
// up any entries that are too old to notify.
// up any entries that are too old to notify from both the general and FSI specific lists.
// Anything newer than this time is still within the window.
val timeThreshold = mSystemClock.currentTimeMillis() - MAX_RANKING_UPDATE_DELAY_MS
val toRemove = ArraySet<String>()
for ((key, updateTime) in mEntriesUpdateTimes) {
if (updateTime == null ||
(mSystemClock.currentTimeMillis() - updateTime) > MAX_RANKING_UPDATE_DELAY_MS) {
if (updateTime == null || timeThreshold > updateTime) {
toRemove.add(key)
}
}
mEntriesUpdateTimes.removeAll(toRemove)
val toRemoveForFSI = ArraySet<String>()
for ((key, addedTime) in mFSIUpdateCandidates) {
if (addedTime == null || timeThreshold > addedTime) {
toRemoveForFSI.add(key)
}
}
mFSIUpdateCandidates.removeAll(toRemoveForFSI)
}
/** When an action is pressed on a notification, end HeadsUp lifetime extension. */

View File

@@ -70,6 +70,14 @@ class HeadsUpCoordinatorLogger constructor(
})
}
fun logEntryUpdatedToFullScreen(key: String) {
buffer.log(TAG, LogLevel.DEBUG, {
str1 = key
}, {
"updating entry to launch full screen intent: $str1"
})
}
fun logSummaryMarkedInterrupted(summaryKey: String, childKey: String) {
buffer.log(TAG, LogLevel.DEBUG, {
str1 = summaryKey

View File

@@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.logcatLogBuffer
import com.android.systemui.statusbar.NotificationRemoteInputManager
import com.android.systemui.statusbar.notification.NotifPipelineFlags
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -38,6 +39,7 @@ import com.android.systemui.statusbar.notification.collection.provider.LaunchFul
import com.android.systemui.statusbar.notification.collection.render.NodeController
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision
import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback
import com.android.systemui.statusbar.phone.NotificationGroupTestHelper
import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -88,6 +90,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
private val mEndLifetimeExtension: OnEndLifetimeExtensionCallback = mock()
private val mHeaderController: NodeController = mock()
private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider = mock()
private val mFlags: NotifPipelineFlags = mock()
private lateinit var mEntry: NotificationEntry
private lateinit var mGroupSummary: NotificationEntry
@@ -113,6 +116,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
mNotificationInterruptStateProvider,
mRemoteInputManager,
mLaunchFullScreenIntentProvider,
mFlags,
mHeaderController,
mExecutor)
mCoordinator.attach(mNotifPipeline)
@@ -246,14 +250,14 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
@Test
fun testOnEntryAdded_shouldFullScreen() {
setShouldFullScreen(mEntry)
setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_EXPECTED_NOT_TO_HUN)
mCollectionListener.onEntryAdded(mEntry)
verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry)
}
@Test
fun testOnEntryAdded_shouldNotFullScreen() {
setShouldFullScreen(mEntry, should = false)
setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FULL_SCREEN_INTENT)
mCollectionListener.onEntryAdded(mEntry)
verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
}
@@ -805,15 +809,96 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
verify(mHeadsUpManager, never()).showNotification(any())
}
@Test
fun testOnRankingApplied_noFSIOnUpdateWhenFlagOff() {
// Ensure the feature flag is off
whenever(mFlags.fsiOnDNDUpdate()).thenReturn(false)
// GIVEN that mEntry was previously suppressed from full-screen only by DND
setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
mCollectionListener.onEntryAdded(mEntry)
// and it is then updated to allow full screen
setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
mCollectionListener.onRankingApplied()
// THEN it should not full screen because the feature is off
verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
}
@Test
fun testOnRankingApplied_updateToFullScreen() {
// Turn on the feature
whenever(mFlags.fsiOnDNDUpdate()).thenReturn(true)
// GIVEN that mEntry was previously suppressed from full-screen only by DND
setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
mCollectionListener.onEntryAdded(mEntry)
// at this point, it should not have full screened
verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
// and it is then updated to allow full screen AND HUN
setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
setShouldHeadsUp(mEntry)
whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
mCollectionListener.onRankingApplied()
mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
// THEN it should full screen but it should NOT HUN
verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry)
verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
verify(mHeadsUpManager, never()).showNotification(any())
}
@Test
fun testOnRankingApplied_noFSIWhenAlsoSuppressedForOtherReasons() {
// Feature on
whenever(mFlags.fsiOnDNDUpdate()).thenReturn(true)
// GIVEN that mEntry is suppressed by DND (functionally), but not *only* DND
setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_BY_DND)
mCollectionListener.onEntryAdded(mEntry)
// and it is updated to full screen later
setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
mCollectionListener.onRankingApplied()
// THEN it should still not full screen because something else was blocking it before
verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
}
@Test
fun testOnRankingApplied_noFSIWhenTooOld() {
// Feature on
whenever(mFlags.fsiOnDNDUpdate()).thenReturn(true)
// GIVEN that mEntry is suppressed only by DND
setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
mCollectionListener.onEntryAdded(mEntry)
// but it's >10s old
mCoordinator.addForFSIReconsideration(mEntry, mSystemClock.currentTimeMillis() - 10000)
// and it is updated to full screen later
setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_EXPECTED_NOT_TO_HUN)
mCollectionListener.onRankingApplied()
// THEN it should still not full screen because it's too old
verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
}
private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) {
whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should)
whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any()))
.thenReturn(should)
}
private fun setShouldFullScreen(entry: NotificationEntry, should: Boolean = true) {
whenever(mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
.thenReturn(should)
private fun setShouldFullScreen(entry: NotificationEntry, decision: FullScreenIntentDecision) {
whenever(mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry))
.thenReturn(decision)
}
private fun finishBind(entry: NotificationEntry) {