Merge "Send FSIs on ranking update when they are no longer suppressed by DND." into tm-qpr-dev
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user