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 = fun fullScreenIntentRequiresKeyguard(): Boolean =
featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD) featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD)
fun fsiOnDNDUpdate(): Boolean = featureFlags.isEnabled(Flags.FSI_ON_DND_UPDATE)
val isStabilityIndexFixEnabled: Boolean by lazy { val isStabilityIndexFixEnabled: Boolean by lazy {
featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX) 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.internal.annotations.VisibleForTesting
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.NotificationRemoteInputManager 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.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline 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.dagger.IncomingHeader
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider 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.logKey
import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP
import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -70,11 +72,13 @@ class HeadsUpCoordinator @Inject constructor(
private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider, private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider,
private val mRemoteInputManager: NotificationRemoteInputManager, private val mRemoteInputManager: NotificationRemoteInputManager,
private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider, private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider,
private val mFlags: NotifPipelineFlags,
@IncomingHeader private val mIncomingHeaderController: NodeController, @IncomingHeader private val mIncomingHeaderController: NodeController,
@Main private val mExecutor: DelayableExecutor, @Main private val mExecutor: DelayableExecutor,
) : Coordinator { ) : Coordinator {
private val mEntriesBindingUntil = ArrayMap<String, Long>() private val mEntriesBindingUntil = ArrayMap<String, Long>()
private val mEntriesUpdateTimes = ArrayMap<String, Long>() private val mEntriesUpdateTimes = ArrayMap<String, Long>()
private val mFSIUpdateCandidates = ArrayMap<String, Long>()
private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null
private lateinit var mNotifPipeline: NotifPipeline private lateinit var mNotifPipeline: NotifPipeline
private var mNow: Long = -1 private var mNow: Long = -1
@@ -278,7 +282,7 @@ class HeadsUpCoordinator @Inject constructor(
mPostedEntries.clear() mPostedEntries.clear()
// Also take this opportunity to clean up any stale entry update times // 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) { override fun onEntryAdded(entry: NotificationEntry) {
// First check whether this notification should launch a full screen intent, and // First check whether this notification should launch a full screen intent, and
// launch it if needed. // 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) 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 // shouldHeadsUp includes check for whether this notification should be filtered
@@ -488,11 +499,32 @@ class HeadsUpCoordinator @Inject constructor(
if (!isNewEnoughForRankingUpdate(entry)) continue if (!isNewEnoughForRankingUpdate(entry)) continue
// The only entries we consider alerting for here are entries that have never // 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 // interrupted and that now say they should heads up or FSI; if they've alerted in
// past, we don't want to incorrectly alert a second time if there wasn't an // the past, we don't want to incorrectly alert a second time if there wasn't an
// explicit notification update. // explicit notification update.
if (entry.hasInterrupted()) continue 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: // 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 // - if this entry is not present in PostedEntries, and is now in a shouldHeadsUp
// state // state
@@ -527,6 +559,15 @@ class HeadsUpCoordinator @Inject constructor(
mEntriesUpdateTimes[entry.key] = time 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. * 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 * 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 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 // 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>() val toRemove = ArraySet<String>()
for ((key, updateTime) in mEntriesUpdateTimes) { for ((key, updateTime) in mEntriesUpdateTimes) {
if (updateTime == null || if (updateTime == null || timeThreshold > updateTime) {
(mSystemClock.currentTimeMillis() - updateTime) > MAX_RANKING_UPDATE_DELAY_MS) {
toRemove.add(key) toRemove.add(key)
} }
} }
mEntriesUpdateTimes.removeAll(toRemove) 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. */ /** 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) { fun logSummaryMarkedInterrupted(summaryKey: String, childKey: String) {
buffer.log(TAG, LogLevel.DEBUG, { buffer.log(TAG, LogLevel.DEBUG, {
str1 = summaryKey str1 = summaryKey

View File

@@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.logcatLogBuffer import com.android.systemui.dump.logcatLogBuffer
import com.android.systemui.statusbar.NotificationRemoteInputManager 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.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry 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.collection.render.NodeController
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider 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.notification.row.NotifBindPipeline.BindCallback
import com.android.systemui.statusbar.phone.NotificationGroupTestHelper import com.android.systemui.statusbar.phone.NotificationGroupTestHelper
import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -88,6 +90,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
private val mEndLifetimeExtension: OnEndLifetimeExtensionCallback = mock() private val mEndLifetimeExtension: OnEndLifetimeExtensionCallback = mock()
private val mHeaderController: NodeController = mock() private val mHeaderController: NodeController = mock()
private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider = mock() private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider = mock()
private val mFlags: NotifPipelineFlags = mock()
private lateinit var mEntry: NotificationEntry private lateinit var mEntry: NotificationEntry
private lateinit var mGroupSummary: NotificationEntry private lateinit var mGroupSummary: NotificationEntry
@@ -113,6 +116,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
mNotificationInterruptStateProvider, mNotificationInterruptStateProvider,
mRemoteInputManager, mRemoteInputManager,
mLaunchFullScreenIntentProvider, mLaunchFullScreenIntentProvider,
mFlags,
mHeaderController, mHeaderController,
mExecutor) mExecutor)
mCoordinator.attach(mNotifPipeline) mCoordinator.attach(mNotifPipeline)
@@ -246,14 +250,14 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
@Test @Test
fun testOnEntryAdded_shouldFullScreen() { fun testOnEntryAdded_shouldFullScreen() {
setShouldFullScreen(mEntry) setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_EXPECTED_NOT_TO_HUN)
mCollectionListener.onEntryAdded(mEntry) mCollectionListener.onEntryAdded(mEntry)
verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry) verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry)
} }
@Test @Test
fun testOnEntryAdded_shouldNotFullScreen() { fun testOnEntryAdded_shouldNotFullScreen() {
setShouldFullScreen(mEntry, should = false) setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FULL_SCREEN_INTENT)
mCollectionListener.onEntryAdded(mEntry) mCollectionListener.onEntryAdded(mEntry)
verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any()) verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
} }
@@ -805,15 +809,96 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
verify(mHeadsUpManager, never()).showNotification(any()) 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) { private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) {
whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should) whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should)
whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any())) whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any()))
.thenReturn(should) .thenReturn(should)
} }
private fun setShouldFullScreen(entry: NotificationEntry, should: Boolean = true) { private fun setShouldFullScreen(entry: NotificationEntry, decision: FullScreenIntentDecision) {
whenever(mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) whenever(mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry))
.thenReturn(should) .thenReturn(decision)
} }
private fun finishBind(entry: NotificationEntry) { private fun finishBind(entry: NotificationEntry) {