Merge "Fix occasional bad placement of Conversation header" into rvc-dev

This commit is contained in:
Steve Elliott
2020-06-15 14:56:14 +00:00
committed by Android (Google) Code Review
5 changed files with 264 additions and 212 deletions

View File

@@ -68,7 +68,7 @@ public class LogModule {
public static LogBuffer provideNotificationSectionLogBuffer(
LogcatEchoTracker bufferFilter,
DumpManager dumpManager) {
LogBuffer buffer = new LogBuffer("NotifSectionLog", 500, 10, bufferFilter);
LogBuffer buffer = new LogBuffer("NotifSectionLog", 1000, 10, bufferFilter);
buffer.attach(dumpManager);
return buffer;
}

View File

@@ -41,6 +41,7 @@ import com.android.systemui.statusbar.notification.row.StackScrollerDecorView
import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.SectionProvider
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.children
import com.android.systemui.util.takeUntil
import com.android.systemui.util.foldToSparseArray
import javax.inject.Inject
@@ -197,7 +198,7 @@ class NotificationSectionsManager @Inject internal constructor(
else -> null
}
private fun logShadeContents() = parent.children.forEachIndexed { i, child ->
private fun logShadeChild(i: Int, child: View) {
when {
child === incomingHeaderView -> logger.logIncomingHeader(i)
child === mediaControlsView -> logger.logMediaControls(i)
@@ -216,6 +217,7 @@ class NotificationSectionsManager @Inject internal constructor(
}
}
}
private fun logShadeContents() = parent.children.forEachIndexed(::logShadeChild)
private val isUsingMultipleSections: Boolean
get() = sectionsFeatureManager.getNumberOfBuckets() > 1
@@ -223,6 +225,57 @@ class NotificationSectionsManager @Inject internal constructor(
@VisibleForTesting
fun updateSectionBoundaries() = updateSectionBoundaries("test")
private interface SectionUpdateState<out T : ExpandableView> {
val header: T
var currentPosition: Int?
var targetPosition: Int?
fun adjustViewPosition()
}
private fun <T : ExpandableView> expandableViewHeaderState(header: T): SectionUpdateState<T> =
object : SectionUpdateState<T> {
override val header = header
override var currentPosition: Int? = null
override var targetPosition: Int? = null
override fun adjustViewPosition() {
val target = targetPosition
val current = currentPosition
if (target == null) {
if (current != null) {
parent.removeView(header)
}
} else {
if (current == null) {
// If the header is animating away, it will still have a parent, so
// detach it first
// TODO: We should really cancel the active animations here. This will
// happen automatically when the view's intro animation starts, but
// it's a fragile link.
header.transientContainer?.removeTransientView(header)
header.transientContainer = null
parent.addView(header, target)
} else {
parent.changeViewPosition(header, target)
}
}
}
}
private fun <T : StackScrollerDecorView> decorViewHeaderState(
header: T
): SectionUpdateState<T> {
val inner = expandableViewHeaderState(header)
return object : SectionUpdateState<T> by inner {
override fun adjustViewPosition() {
inner.adjustViewPosition()
if (targetPosition != null && currentPosition == null) {
header.isContentVisible = true
}
}
}
}
/**
* Should be called whenever notifs are added, removed, or updated. Updates section boundary
* bookkeeping and adds/moves/removes section headers if appropriate.
@@ -238,233 +291,136 @@ class NotificationSectionsManager @Inject internal constructor(
// Then, once we find the start of a new section, we track that position as the "target" for
// the section header, adjusted for the case where existing headers are in front of that
// target, but won't be once they are moved / removed after the pass has completed.
val showHeaders = statusBarStateController.state != StatusBarState.KEYGUARD
val usingPeopleFiltering = sectionsFeatureManager.isFilteringEnabled()
val usingMediaControls = sectionsFeatureManager.isMediaControlsEnabled()
val mediaState = mediaControlsView?.let(::expandableViewHeaderState)
val incomingState = incomingHeaderView?.let(::decorViewHeaderState)
val peopleState = peopleHeaderView?.let(::decorViewHeaderState)
val alertingState = alertingHeaderView?.let(::decorViewHeaderState)
val gentleState = silentHeaderView?.let(::decorViewHeaderState)
fun getSectionState(view: View): SectionUpdateState<ExpandableView>? = when {
view === mediaControlsView -> mediaState
view === incomingHeaderView -> incomingState
view === peopleHeaderView -> peopleState
view === alertingHeaderView -> alertingState
view === silentHeaderView -> gentleState
else -> null
}
val headersOrdered = sequenceOf(
mediaState, incomingState, peopleState, alertingState, gentleState
).filterNotNull()
var peopleNotifsPresent = false
var currentMediaControlsIdx = -1
val mediaControlsTarget = if (usingMediaControls) 0 else -1
var currentIncomingHeaderIdx = -1
var incomingHeaderTarget = -1
var currentPeopleHeaderIdx = -1
var peopleHeaderTarget = -1
var currentAlertingHeaderIdx = -1
var alertingHeaderTarget = -1
var currentGentleHeaderIdx = -1
var gentleHeaderTarget = -1
var lastNotifIndex = 0
var lastIncomingIndex = -1
var prev: ExpandableNotificationRow? = null
var nextBucket: Int? = null
var inIncomingSection = false
for ((i, child) in parent.children.withIndex()) {
when {
// Track the existing positions of the headers
child === incomingHeaderView -> {
logger.logIncomingHeader(i)
currentIncomingHeaderIdx = i
}
child === mediaControlsView -> {
logger.logMediaControls(i)
currentMediaControlsIdx = i
}
child === peopleHeaderView -> {
logger.logConversationsHeader(i)
currentPeopleHeaderIdx = i
}
child === alertingHeaderView -> {
logger.logAlertingHeader(i)
currentAlertingHeaderIdx = i
}
child === silentHeaderView -> {
logger.logSilentHeader(i)
currentGentleHeaderIdx = i
}
child !is ExpandableNotificationRow -> logger.logOther(i, child.javaClass)
else -> {
lastNotifIndex = i
// Is there a section discontinuity? This usually occurs due to HUNs
if (prev?.entry?.bucket?.let { it > child.entry.bucket } == true) {
// Remove existing headers, and move the Incoming header if necessary
incomingHeaderTarget = when {
!showHeaders -> -1
incomingHeaderTarget != -1 -> incomingHeaderTarget
peopleHeaderTarget != -1 -> peopleHeaderTarget
alertingHeaderTarget != -1 -> alertingHeaderTarget
gentleHeaderTarget != -1 -> gentleHeaderTarget
else -> 0
}
peopleHeaderTarget = -1
alertingHeaderTarget = -1
gentleHeaderTarget = -1
// Walk backwards changing all previous notifications to the Incoming
// section
for (j in i - 1 downTo lastIncomingIndex + 1) {
val prevChild = parent.getChildAt(j)
if (prevChild is ExpandableNotificationRow) {
prevChild.entry.bucket = BUCKET_HEADS_UP
}
}
// Track the new bottom of the Incoming section
lastIncomingIndex = i - 1
}
val isHeadsUp = child.isHeadsUp
when (child.entry.bucket) {
BUCKET_FOREGROUND_SERVICE -> logger.logForegroundService(i, isHeadsUp)
BUCKET_PEOPLE -> {
logger.logConversation(i, isHeadsUp)
peopleNotifsPresent = true
if (showHeaders && peopleHeaderTarget == -1) {
peopleHeaderTarget = i
// Offset the target if there are other headers before this that
// will be moved.
if (currentIncomingHeaderIdx != -1 && incomingHeaderTarget == -1) {
peopleHeaderTarget--
}
if (currentPeopleHeaderIdx != -1) {
peopleHeaderTarget--
}
if (currentAlertingHeaderIdx != -1) {
peopleHeaderTarget--
}
if (currentGentleHeaderIdx != -1) {
peopleHeaderTarget--
}
}
}
BUCKET_ALERTING -> {
logger.logAlerting(i, isHeadsUp)
if (showHeaders && usingPeopleFiltering && alertingHeaderTarget == -1) {
alertingHeaderTarget = i
// Offset the target if there are other headers before this that
// will be moved.
if (currentIncomingHeaderIdx != -1 && incomingHeaderTarget == -1) {
alertingHeaderTarget--
}
if (currentPeopleHeaderIdx != -1 && peopleHeaderTarget == -1) {
// People header will be removed
alertingHeaderTarget--
}
if (currentAlertingHeaderIdx != -1) {
alertingHeaderTarget--
}
if (currentGentleHeaderIdx != -1) {
alertingHeaderTarget--
}
}
}
BUCKET_SILENT -> {
logger.logSilent(i, isHeadsUp)
if (showHeaders && gentleHeaderTarget == -1) {
gentleHeaderTarget = i
// Offset the target if there are other headers before this that
// will be moved.
if (currentIncomingHeaderIdx != -1 && incomingHeaderTarget == -1) {
gentleHeaderTarget--
}
if (currentPeopleHeaderIdx != -1 && peopleHeaderTarget == -1) {
// People header will be removed
gentleHeaderTarget--
}
if (currentAlertingHeaderIdx != -1 && alertingHeaderTarget == -1) {
// Alerting header will be removed
gentleHeaderTarget--
}
if (currentGentleHeaderIdx != -1) {
gentleHeaderTarget--
}
}
}
}
prev = child
// Iterating backwards allows for easier construction of the Incoming section, as opposed
// to backtracking when a discontinuity in the sections is discovered.
// Iterating to -1 in order to support the case where a header is at the very top of the
// shade.
for (i in parent.childCount - 1 downTo -1) {
val child: View? = parent.getChildAt(i)
child?.let {
logShadeChild(i, child)
// If this child is a header, update the tracked positions
getSectionState(child)?.let { state ->
state.currentPosition = i
// If headers that should appear above this one in the shade already have a
// target index, then we need to decrement them in order to account for this one
// being either removed, or moved below them.
headersOrdered.takeUntil { it === state }
.forEach { it.targetPosition = it.targetPosition?.minus(1) }
}
}
val row = child as? ExpandableNotificationRow
// Is there a section discontinuity? This usually occurs due to HUNs
inIncomingSection = inIncomingSection || nextBucket?.let { next ->
row?.entry?.bucket?.let { curr -> next < curr }
} == true
if (inIncomingSection) {
// Update the bucket to reflect that it's being placed in the Incoming section
row?.entry?.bucket = BUCKET_HEADS_UP
}
// Insert a header in front of the next row, if there's a boundary between it and this
// row, or if it is the topmost row.
val isSectionBoundary = nextBucket != null &&
(child == null || row != null && nextBucket != row.entry.bucket)
if (isSectionBoundary && showHeaders) {
when (nextBucket) {
BUCKET_HEADS_UP -> incomingState?.targetPosition = i + 1
BUCKET_PEOPLE -> peopleState?.targetPosition = i + 1
BUCKET_ALERTING -> alertingState?.targetPosition = i + 1
BUCKET_SILENT -> gentleState?.targetPosition = i + 1
}
}
row ?: continue
// Check if there are any people notifications
peopleNotifsPresent = peopleNotifsPresent || row.entry.bucket == BUCKET_PEOPLE
if (nextBucket == null) {
lastNotifIndex = i
}
nextBucket = row.entry.bucket
}
if (showHeaders && usingPeopleFiltering && peopleHubVisible && peopleHeaderTarget == -1) {
// Insert the people header even if there are no people visible, in order to show
// the hub. Put it directly above the next header.
peopleHeaderTarget = when {
alertingHeaderTarget != -1 -> alertingHeaderTarget
gentleHeaderTarget != -1 -> gentleHeaderTarget
else -> lastNotifIndex // Put it at the end of the list.
}
if (showHeaders && usingPeopleFiltering && peopleHubVisible) {
peopleState?.targetPosition = peopleState?.targetPosition
// Insert the people header even if there are no people visible, in order to
// show the hub. Put it directly above the next header.
?: alertingState?.targetPosition
?: gentleState?.targetPosition
// Put it at the end of the list.
?: lastNotifIndex
// Offset the target to account for the current position of the people header.
if (currentPeopleHeaderIdx != -1 && currentPeopleHeaderIdx < peopleHeaderTarget) {
peopleHeaderTarget--
peopleState?.targetPosition = peopleState?.currentPosition?.let { current ->
peopleState?.targetPosition?.let { target ->
if (current < target) target - 1 else target
}
}
}
mediaState?.targetPosition = if (usingMediaControls) 0 else null
logger.logStr("New header target positions:")
logger.logIncomingHeader(incomingHeaderTarget)
logger.logMediaControls(mediaControlsTarget)
logger.logConversationsHeader(peopleHeaderTarget)
logger.logAlertingHeader(alertingHeaderTarget)
logger.logSilentHeader(gentleHeaderTarget)
logger.logMediaControls(mediaState?.targetPosition ?: -1)
logger.logIncomingHeader(incomingState?.targetPosition ?: -1)
logger.logConversationsHeader(peopleState?.targetPosition ?: -1)
logger.logAlertingHeader(alertingState?.targetPosition ?: -1)
logger.logSilentHeader(gentleState?.targetPosition ?: -1)
// Add headers in reverse order to preserve indices
silentHeaderView?.let {
adjustHeaderVisibilityAndPosition(gentleHeaderTarget, it, currentGentleHeaderIdx)
}
alertingHeaderView?.let {
adjustHeaderVisibilityAndPosition(alertingHeaderTarget, it, currentAlertingHeaderIdx)
}
peopleHeaderView?.let {
adjustHeaderVisibilityAndPosition(peopleHeaderTarget, it, currentPeopleHeaderIdx)
}
incomingHeaderView?.let {
adjustHeaderVisibilityAndPosition(incomingHeaderTarget, it, currentIncomingHeaderIdx)
}
mediaControlsView?.let {
adjustViewPosition(mediaControlsTarget, it, currentMediaControlsIdx)
}
// Update headers in reverse order to preserve indices, otherwise movements earlier in the
// list will affect the target indices of the headers later in the list.
headersOrdered.asIterable().reversed().forEach { it.adjustViewPosition() }
logger.logStr("Final order:")
logShadeContents()
logger.logStr("Section boundary update complete")
// Update headers to reflect state of section contents
silentHeaderView?.setAreThereDismissableGentleNotifs(
parent.hasActiveClearableNotifications(NotificationStackScrollLayout.ROWS_GENTLE)
)
peopleHeaderView?.canSwipe = showHeaders && peopleHubVisible && !peopleNotifsPresent
if (peopleHeaderTarget != currentPeopleHeaderIdx) {
peopleHeaderView?.resetTranslation()
silentHeaderView?.run {
val hasActiveClearableNotifications = this@NotificationSectionsManager.parent
.hasActiveClearableNotifications(NotificationStackScrollLayout.ROWS_GENTLE)
setAreThereDismissableGentleNotifs(hasActiveClearableNotifications)
}
}
private fun adjustHeaderVisibilityAndPosition(
targetPosition: Int,
header: StackScrollerDecorView,
currentPosition: Int
) {
adjustViewPosition(targetPosition, header, currentPosition)
if (targetPosition != -1 && currentPosition == -1) {
header.isContentVisible = true
}
}
private fun adjustViewPosition(
targetPosition: Int,
view: ExpandableView,
currentPosition: Int
) {
if (targetPosition == -1) {
if (currentPosition != -1) {
parent.removeView(view)
}
} else {
if (currentPosition == -1) {
// If the header is animating away, it will still have a parent, so detach it first
// TODO: We should really cancel the active animations here. This will happen
// automatically when the view's intro animation starts, but it's a fragile link.
view.transientContainer?.removeTransientView(view)
view.transientContainer = null
parent.addView(view, targetPosition)
} else {
parent.changeViewPosition(view, targetPosition)
peopleHeaderView?.run {
canSwipe = showHeaders && peopleHubVisible && !peopleNotifsPresent
peopleState?.targetPosition?.let { targetPosition ->
if (targetPosition != peopleState.currentPosition) {
resetTranslation()
}
}
}
}

View File

@@ -22,4 +22,14 @@ import android.view.ViewGroup
val ViewGroup.children
get() = sequence {
for (i in 0 until childCount) yield(getChildAt(i))
}
}
/** Inclusive version of [Iterable.takeWhile] */
fun <T> Sequence<T>.takeUntil(pred: (T) -> Boolean): Sequence<T> = sequence {
for (x in this@takeUntil) {
yield(x)
if (pred(x)) {
break
}
}
}

View File

@@ -18,6 +18,22 @@ package com.android.systemui.util
import android.util.SparseArray
/**
* Transforms a sequence of Key/Value pairs into a SparseArray.
*
* See [kotlin.collections.toMap].
*/
fun <T> Sequence<Pair<Int, T>>.toSparseArray(size: Int = -1): SparseArray<T> {
val sparseArray = when {
size < 0 -> SparseArray<T>()
else -> SparseArray<T>(size)
}
for ((i, v) in this) {
sparseArray.put(i, v)
}
return sparseArray
}
/**
* Transforms an [Array] into a [SparseArray], by applying each element to [keySelector] in order to
* generate the index at which it will be placed. If two elements produce the same index, the latter

View File

@@ -403,11 +403,11 @@ public class NotificationSectionsManagerTest extends SysuiTestCase {
enablePeopleFiltering();
setupMockStack(
PERSON.headsUp(), // personHeaderTarget = 0
INCOMING_HEADER, // currentIncomingHeaderIdx = 1
ALERTING.headsUp(), // alertingHeaderTarget = 1
PEOPLE_HEADER, // currentPeopleHeaderIdx = 3
PERSON //
PERSON.headsUp(),
INCOMING_HEADER,
ALERTING.headsUp(),
PEOPLE_HEADER,
PERSON
);
mSectionsManager.updateSectionBoundaries();
@@ -520,6 +520,70 @@ public class NotificationSectionsManagerTest extends SysuiTestCase {
ChildType.GENTLE);
}
@Test
public void testRemoveIncomingHeader() {
enablePeopleFiltering();
enableMediaControls();
setupMockStack(
MEDIA_CONTROLS,
INCOMING_HEADER,
PERSON,
ALERTING,
PEOPLE_HEADER,
ALERTING_HEADER,
ALERTING,
ALERTING,
GENTLE_HEADER,
GENTLE,
GENTLE
);
mSectionsManager.updateSectionBoundaries();
verifyMockStack(
ChildType.MEDIA_CONTROLS,
ChildType.PEOPLE_HEADER,
ChildType.PERSON,
ChildType.ALERTING_HEADER,
ChildType.ALERTING,
ChildType.ALERTING,
ChildType.ALERTING,
ChildType.GENTLE_HEADER,
ChildType.GENTLE,
ChildType.GENTLE
);
}
@Test
public void testExpandIncomingSection() {
enablePeopleFiltering();
setupMockStack(
INCOMING_HEADER,
PERSON,
ALERTING,
PEOPLE_HEADER,
ALERTING,
PERSON,
ALERTING_HEADER,
ALERTING
);
mSectionsManager.updateSectionBoundaries();
verifyMockStack(
ChildType.INCOMING_HEADER,
ChildType.HEADS_UP,
ChildType.HEADS_UP,
ChildType.HEADS_UP,
ChildType.PEOPLE_HEADER,
ChildType.PERSON,
ChildType.ALERTING_HEADER,
ChildType.ALERTING
);
}
private void enablePeopleFiltering() {
when(mSectionsFeatureManager.isFilteringEnabled()).thenReturn(true);
}
@@ -657,7 +721,13 @@ public class NotificationSectionsManagerTest extends SysuiTestCase {
final List<View> children = new ArrayList<>();
when(mNssl.getChildCount()).thenAnswer(invocation -> children.size());
when(mNssl.getChildAt(anyInt()))
.thenAnswer(invocation -> children.get(invocation.getArgument(0)));
.thenAnswer(invocation -> {
Integer index = invocation.getArgument(0);
if (index == null || index < 0 || index >= children.size()) {
return null;
}
return children.get(index);
});
when(mNssl.indexOfChild(any()))
.thenAnswer(invocation -> children.indexOf(invocation.getArgument(0)));
doAnswer(invocation -> {