Merge changes from topic "b241229236_stable_sort_flagged" into tm-qpr-dev am: 00f287b876

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19194469

Change-Id: I7db0aa4cb3784ebbe465131f8edc8a238159da9f
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
TreeHugger Robot
2022-10-19 20:46:59 +00:00
committed by Automerger Merge Worker
9 changed files with 774 additions and 30 deletions

View File

@@ -75,7 +75,9 @@ public class Flags {
public static final UnreleasedFlag STABILITY_INDEX_FIX = new UnreleasedFlag(114, true); public static final UnreleasedFlag STABILITY_INDEX_FIX = new UnreleasedFlag(114, true);
// next id: 115 public static final UnreleasedFlag SEMI_STABLE_SORT = new UnreleasedFlag(115, true);
// next id: 116
/***************************************/ /***************************************/
// 200 - keyguard/lockscreen // 200 - keyguard/lockscreen

View File

@@ -57,4 +57,8 @@ class NotifPipelineFlags @Inject constructor(
val isStabilityIndexFixEnabled: Boolean by lazy { val isStabilityIndexFixEnabled: Boolean by lazy {
featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX) featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX)
} }
val isSemiStableSortEnabled: Boolean by lazy {
featureFlags.isEnabled(Flags.SEMI_STABLE_SORT)
}
} }

View File

@@ -68,6 +68,9 @@ data class ListAttachState private constructor(
*/ */
var stableIndex: Int = -1 var stableIndex: Int = -1
/** Access the index of the [section] or -1 if the entry does not have one */
val sectionIndex: Int get() = section?.index ?: -1
/** Copies the state of another instance. */ /** Copies the state of another instance. */
fun clone(other: ListAttachState) { fun clone(other: ListAttachState) {
parent = other.parent parent = other.parent
@@ -95,11 +98,13 @@ data class ListAttachState private constructor(
* This can happen if the entry is removed from a group that was broken up or if the entry was * This can happen if the entry is removed from a group that was broken up or if the entry was
* filtered out during any of the filtering steps. * filtered out during any of the filtering steps.
*/ */
fun detach() { fun detach(includingStableIndex: Boolean) {
parent = null parent = null
section = null section = null
promoter = null promoter = null
// stableIndex = -1 // TODO(b/241229236): Clear this once we fix the stability fragility if (includingStableIndex) {
stableIndex = -1
}
} }
companion object { companion object {

View File

@@ -54,6 +54,9 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener; import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener; import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState; import com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState;
import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort;
import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort.StableOrder;
import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper;
import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger; import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator;
@@ -96,12 +99,14 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
// used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated
// TODO replace temp with collection pool for readability // TODO replace temp with collection pool for readability
private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>(); private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>();
private final NotifPipelineFlags mFlags; private NotifPipelineFlags mFlags;
private final boolean mAlwaysLogList; private final boolean mAlwaysLogList;
private List<ListEntry> mNotifList = new ArrayList<>(); private List<ListEntry> mNotifList = new ArrayList<>();
private List<ListEntry> mNewNotifList = new ArrayList<>(); private List<ListEntry> mNewNotifList = new ArrayList<>();
private final SemiStableSort mSemiStableSort = new SemiStableSort();
private final StableOrder<ListEntry> mStableOrder = this::getStableOrderRank;
private final PipelineState mPipelineState = new PipelineState(); private final PipelineState mPipelineState = new PipelineState();
private final Map<String, GroupEntry> mGroups = new ArrayMap<>(); private final Map<String, GroupEntry> mGroups = new ArrayMap<>();
private Collection<NotificationEntry> mAllEntries = Collections.emptyList(); private Collection<NotificationEntry> mAllEntries = Collections.emptyList();
@@ -529,7 +534,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
List<NotifFilter> filters) { List<NotifFilter> filters) {
Trace.beginSection("ShadeListBuilder.filterNotifs"); Trace.beginSection("ShadeListBuilder.filterNotifs");
final long now = mSystemClock.uptimeMillis(); final long now = mSystemClock.uptimeMillis();
for (ListEntry entry : entries) { for (ListEntry entry : entries) {
if (entry instanceof GroupEntry) { if (entry instanceof GroupEntry) {
final GroupEntry groupEntry = (GroupEntry) entry; final GroupEntry groupEntry = (GroupEntry) entry;
@@ -960,7 +965,8 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
* filtered out during any of the filtering steps. * filtered out during any of the filtering steps.
*/ */
private void annulAddition(ListEntry entry) { private void annulAddition(ListEntry entry) {
entry.getAttachState().detach(); // NOTE(b/241229236): Don't clear stableIndex until we fix stability fragility
entry.getAttachState().detach(/* includingStableIndex= */ mFlags.isSemiStableSortEnabled());
} }
private void assignSections() { private void assignSections() {
@@ -980,7 +986,16 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
private void sortListAndGroups() { private void sortListAndGroups() {
Trace.beginSection("ShadeListBuilder.sortListAndGroups"); Trace.beginSection("ShadeListBuilder.sortListAndGroups");
// Assign sections to top-level elements and sort their children if (mFlags.isSemiStableSortEnabled()) {
sortWithSemiStableSort();
} else {
sortWithLegacyStability();
}
Trace.endSection();
}
private void sortWithLegacyStability() {
// Sort all groups and the top level list
for (ListEntry entry : mNotifList) { for (ListEntry entry : mNotifList) {
if (entry instanceof GroupEntry) { if (entry instanceof GroupEntry) {
GroupEntry parent = (GroupEntry) entry; GroupEntry parent = (GroupEntry) entry;
@@ -993,16 +1008,15 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
// Check for suppressed order changes // Check for suppressed order changes
if (!getStabilityManager().isEveryChangeAllowed()) { if (!getStabilityManager().isEveryChangeAllowed()) {
mForceReorderable = true; mForceReorderable = true;
boolean isSorted = isShadeSorted(); boolean isSorted = isShadeSortedLegacy();
mForceReorderable = false; mForceReorderable = false;
if (!isSorted) { if (!isSorted) {
getStabilityManager().onEntryReorderSuppressed(); getStabilityManager().onEntryReorderSuppressed();
} }
} }
Trace.endSection();
} }
private boolean isShadeSorted() { private boolean isShadeSortedLegacy() {
if (!isSorted(mNotifList, mTopLevelComparator)) { if (!isSorted(mNotifList, mTopLevelComparator)) {
return false; return false;
} }
@@ -1016,6 +1030,43 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
return true; return true;
} }
private void sortWithSemiStableSort() {
// Sort each group's children
boolean allSorted = true;
for (ListEntry entry : mNotifList) {
if (entry instanceof GroupEntry) {
GroupEntry parent = (GroupEntry) entry;
allSorted &= sortGroupChildren(parent.getRawChildren());
}
}
// Sort each section within the top level list
mNotifList.sort(mTopLevelComparator);
if (!getStabilityManager().isEveryChangeAllowed()) {
for (List<ListEntry> subList : getSectionSubLists(mNotifList)) {
allSorted &= mSemiStableSort.stabilizeTo(subList, mStableOrder, mNewNotifList);
}
applyNewNotifList();
}
assignIndexes(mNotifList);
if (!allSorted) {
// Report suppressed order changes
getStabilityManager().onEntryReorderSuppressed();
}
}
private Iterable<List<ListEntry>> getSectionSubLists(List<ListEntry> entries) {
return ShadeListBuilderHelper.INSTANCE.getSectionSubLists(entries);
}
private boolean sortGroupChildren(List<NotificationEntry> entries) {
if (getStabilityManager().isEveryChangeAllowed()) {
entries.sort(mGroupChildrenComparator);
return true;
} else {
return mSemiStableSort.sort(entries, mStableOrder, mGroupChildrenComparator);
}
}
/** Determine whether the items in the list are sorted according to the comparator */ /** Determine whether the items in the list are sorted according to the comparator */
@VisibleForTesting @VisibleForTesting
public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) { public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) {
@@ -1212,7 +1263,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
o2.getSectionIndex()); o2.getSectionIndex());
if (cmp != 0) return cmp; if (cmp != 0) return cmp;
cmp = Integer.compare( cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
getStableOrderIndex(o1), getStableOrderIndex(o1),
getStableOrderIndex(o2)); getStableOrderIndex(o2));
if (cmp != 0) return cmp; if (cmp != 0) return cmp;
@@ -1241,7 +1292,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> { private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> {
int cmp = Integer.compare( int cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
getStableOrderIndex(o1), getStableOrderIndex(o1),
getStableOrderIndex(o2)); getStableOrderIndex(o2));
if (cmp != 0) return cmp; if (cmp != 0) return cmp;
@@ -1272,9 +1323,25 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
// let the stability manager constrain or allow reordering // let the stability manager constrain or allow reordering
return -1; return -1;
} }
// NOTE(b/241229236): Can't use cleared section index until we fix stability fragility
return entry.getPreviousAttachState().getStableIndex(); return entry.getPreviousAttachState().getStableIndex();
} }
@Nullable
private Integer getStableOrderRank(ListEntry entry) {
if (getStabilityManager().isEntryReorderingAllowed(entry)) {
// let the stability manager constrain or allow reordering
return null;
}
if (entry.getAttachState().getSectionIndex()
!= entry.getPreviousAttachState().getSectionIndex()) {
// stable index is only valid within the same section; otherwise we allow reordering
return null;
}
final int stableIndex = entry.getPreviousAttachState().getStableIndex();
return stableIndex == -1 ? null : stableIndex;
}
private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) { private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) {
final NotifFilter filter = findRejectingFilter(entry, now, filters); final NotifFilter filter = findRejectingFilter(entry, now, filters);
entry.getAttachState().setExcludingFilter(filter); entry.getAttachState().setExcludingFilter(filter);

View File

@@ -0,0 +1,200 @@
/*
* Copyright (C) 2022 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.statusbar.notification.collection.listbuilder
import androidx.annotation.VisibleForTesting
import kotlin.math.sign
class SemiStableSort {
val preallocatedWorkspace by lazy { ArrayList<Any>() }
val preallocatedAdditions by lazy { ArrayList<Any>() }
val preallocatedMapToIndex by lazy { HashMap<Any, Int>() }
val preallocatedMapToIndexComparator: Comparator<Any> by lazy {
Comparator.comparingInt { item -> preallocatedMapToIndex[item] ?: -1 }
}
/**
* Sort the given [items] such that items which have a [stableOrder] will all be in that order,
* items without a [stableOrder] will be sorted according to the comparator, and the two sets of
* items will be combined to have the fewest elements out of order according to the [comparator]
* . The result will be placed into the original [items] list.
*/
fun <T : Any> sort(
items: MutableList<T>,
stableOrder: StableOrder<in T>,
comparator: Comparator<in T>,
): Boolean =
withWorkspace<T, Boolean> { workspace ->
val ordered =
sortTo(
items,
stableOrder,
comparator,
workspace,
)
items.clear()
items.addAll(workspace)
return ordered
}
/**
* Sort the given [items] such that items which have a [stableOrder] will all be in that order,
* items without a [stableOrder] will be sorted according to the comparator, and the two sets of
* items will be combined to have the fewest elements out of order according to the [comparator]
* . The result will be put into [output].
*/
fun <T : Any> sortTo(
items: Iterable<T>,
stableOrder: StableOrder<in T>,
comparator: Comparator<in T>,
output: MutableList<T>,
): Boolean {
if (DEBUG) println("\n> START from ${items.map { it to stableOrder.getRank(it) }}")
// If array already has elements, use subList to ensure we only append
val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size)
items.filterTo(result) { stableOrder.getRank(it) != null }
result.sortBy { stableOrder.getRank(it)!! }
val isOrdered = result.isSorted(comparator)
withAdditions<T> { additions ->
items.filterTo(additions) { stableOrder.getRank(it) == null }
additions.sortWith(comparator)
insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator)
}
return isOrdered
}
/**
* Rearrange the [sortedItems] to enforce that items are in the [stableOrder], and store the
* result in [output]. Items with a [stableOrder] will be in that order, items without a
* [stableOrder] will remain in same relative order as the input, and the two sets of items will
* be combined to have the fewest elements moved from their locations in the original.
*/
fun <T : Any> stabilizeTo(
sortedItems: Iterable<T>,
stableOrder: StableOrder<in T>,
output: MutableList<T>,
): Boolean {
// Append to the output array if present
val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size)
sortedItems.filterTo(result) { stableOrder.getRank(it) != null }
val stableRankComparator = compareBy<T> { stableOrder.getRank(it)!! }
val isOrdered = result.isSorted(stableRankComparator)
if (!isOrdered) {
result.sortWith(stableRankComparator)
}
if (result.isEmpty()) {
sortedItems.filterTo(result) { stableOrder.getRank(it) == null }
return isOrdered
}
withAdditions<T> { additions ->
sortedItems.filterTo(additions) { stableOrder.getRank(it) == null }
if (additions.isNotEmpty()) {
withIndexOfComparator(sortedItems) { comparator ->
insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator)
}
}
}
return isOrdered
}
private inline fun <T : Any, R> withWorkspace(block: (ArrayList<T>) -> R): R {
preallocatedWorkspace.clear()
val result = block(preallocatedWorkspace as ArrayList<T>)
preallocatedWorkspace.clear()
return result
}
private inline fun <T : Any> withAdditions(block: (ArrayList<T>) -> Unit) {
preallocatedAdditions.clear()
block(preallocatedAdditions as ArrayList<T>)
preallocatedAdditions.clear()
}
private inline fun <T : Any> withIndexOfComparator(
sortedItems: Iterable<T>,
block: (Comparator<in T>) -> Unit
) {
preallocatedMapToIndex.clear()
sortedItems.forEachIndexed { i, item -> preallocatedMapToIndex[item] = i }
block(preallocatedMapToIndexComparator as Comparator<in T>)
preallocatedMapToIndex.clear()
}
companion object {
/**
* This is the core of the algorithm.
*
* Insert [preSortedAdditions] (the elements to be inserted) into [existing] without
* changing the relative order of any elements already in [existing], even though those
* elements may be mis-ordered relative to the [comparator], such that the total number of
* elements which are ordered incorrectly according to the [comparator] is fewest.
*/
private fun <T> insertPreSortedElementsWithFewestMisOrderings(
existing: MutableList<T>,
preSortedAdditions: Iterable<T>,
comparator: Comparator<in T>,
) {
if (DEBUG) println(" To $existing insert $preSortedAdditions with fewest misordering")
var iStart = 0
preSortedAdditions.forEach { toAdd ->
if (DEBUG) println(" need to add $toAdd to $existing, starting at $iStart")
var cmpSum = 0
var cmpSumMax = 0
var iCmpSumMax = iStart
if (DEBUG) print(" ")
for (i in iCmpSumMax until existing.size) {
val cmp = comparator.compare(toAdd, existing[i]).sign
cmpSum += cmp
if (cmpSum > cmpSumMax) {
cmpSumMax = cmpSum
iCmpSumMax = i + 1
}
if (DEBUG) print("sum[$i]=$cmpSum, ")
}
if (DEBUG) println("inserting $toAdd at $iCmpSumMax")
existing.add(iCmpSumMax, toAdd)
iStart = iCmpSumMax + 1
}
}
/** Determines if a list is correctly sorted according to the given comparator */
@VisibleForTesting
fun <T> List<T>.isSorted(comparator: Comparator<T>): Boolean {
if (this.size <= 1) {
return true
}
val iterator = this.iterator()
var previous = iterator.next()
var current: T?
while (iterator.hasNext()) {
current = iterator.next()
if (comparator.compare(previous, current) > 0) {
return false
}
previous = current
}
return true
}
}
fun interface StableOrder<T> {
fun getRank(item: T): Int?
}
}
val DEBUG = false

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2022 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.statusbar.notification.collection.listbuilder
import com.android.systemui.statusbar.notification.collection.ListEntry
object ShadeListBuilderHelper {
fun getSectionSubLists(entries: List<ListEntry>): Iterable<List<ListEntry>> =
getContiguousSubLists(entries, minLength = 1) { it.sectionIndex }
inline fun <T : Any, K : Any> getContiguousSubLists(
itemList: List<T>,
minLength: Int = 1,
key: (T) -> K,
): Iterable<List<T>> {
val subLists = mutableListOf<List<T>>()
val numEntries = itemList.size
var currentSectionStartIndex = 0
var currentSectionKey: K? = null
for (i in 0 until numEntries) {
val sectionKey = key(itemList[i])
if (currentSectionKey == null) {
currentSectionKey = sectionKey
} else if (currentSectionKey != sectionKey) {
val length = i - currentSectionStartIndex
if (length >= minLength) {
subLists.add(itemList.subList(currentSectionStartIndex, i))
}
currentSectionStartIndex = i
currentSectionKey = sectionKey
}
}
val length = numEntries - currentSectionStartIndex
if (length >= minLength) {
subLists.add(itemList.subList(currentSectionStartIndex, numEntries))
}
return subLists
}
}

View File

@@ -34,6 +34,7 @@ import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@@ -136,6 +137,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
allowTestableLooperAsMainThread(); allowTestableLooperAsMainThread();
when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
mListBuilder = new ShadeListBuilder( mListBuilder = new ShadeListBuilder(
mDumpManager, mDumpManager,
@@ -1996,22 +1998,89 @@ public class ShadeListBuilderTest extends SysuiTestCase {
} }
@Test @Test
public void testStableOrdering() { public void testActiveOrdering_withLegacyStability() {
when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
}
@Test
public void testStableOrdering_withLegacyStability() {
when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
mStabilityManager.setAllowEntryReordering(false); mStabilityManager.setAllowEntryReordering(false);
assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG"); // X assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG"); // no change assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG", false); // X
assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG"); // Z and X assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // no change
assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG"); // Z and X + gap assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG", false); // Z and X
verify(mStabilityManager, times(4)).onEntryReorderSuppressed(); assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG", false); // Z and X + gap
}
@Test
public void testStableOrdering() {
when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
mStabilityManager.setAllowEntryReordering(false);
// No input or output
assertOrder("", "", "", true);
// Remove everything
assertOrder("ABCDEFG", "", "", true);
// Literally no changes
assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true);
// No stable order
assertOrder("", "ABCDEFG", "ABCDEFG", true);
// F moved after A, and...
assertOrder("ABCDEFG", "AFBCDEG", "ABCDEFG", false); // No other changes
assertOrder("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false); // Insert X before F
assertOrder("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false); // Insert X after F
assertOrder("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false); // Insert X where F was
// B moved after F, and...
assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // No other changes
assertOrder("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false); // Insert X before B
assertOrder("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false); // Insert X after B
assertOrder("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false); // Insert X where B was
// Swap F and B, and...
assertOrder("ABCDEFG", "AFCDEBG", "ABCDEFG", false); // No other changes
assertOrder("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false); // Insert X before F
assertOrder("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false); // Insert X after F
assertOrder("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false); // Insert X between CD (or: ABCXDEFG)
assertOrder("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false); // Insert X between DE (or: ABCDEFXG)
assertOrder("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false); // Insert X before B
assertOrder("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false); // Insert X after B
// Remove a bunch of entries at once
assertOrder("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true);
// Remove a bunch of entries and scramble
assertOrder("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false);
// Add a bunch of entries at once
assertOrder("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true);
// Add a bunch of entries and reverse originals
// NOTE: Some of these don't have obviously correct answers
assertOrder("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false); // appended
assertOrder("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false); // prepended
assertOrder("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false); // closer to back: append
assertOrder("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false); // closer to front: prepend
assertOrder("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false); // split new entries
// Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout
assertOrder("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false);
} }
@Test @Test
public void testActiveOrdering() { public void testActiveOrdering() {
assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG"); // X when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG"); // no change assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG"); // Z and X assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG"); // Z and X + gap assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
verify(mStabilityManager, never()).onEntryReorderSuppressed(); assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
} }
@Test @Test
@@ -2062,6 +2131,52 @@ public class ShadeListBuilderTest extends SysuiTestCase {
); );
} }
@Test
public void stableOrderingDisregardedWithSectionChange() {
when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
// GIVEN the first sectioner's packages can be changed from run-to-run
List<String> mutableSectionerPackages = new ArrayList<>();
mutableSectionerPackages.add(PACKAGE_1);
mListBuilder.setSectioners(asList(
new PackageSectioner(mutableSectionerPackages, null),
new PackageSectioner(List.of(PACKAGE_1, PACKAGE_2, PACKAGE_3), null)));
mStabilityManager.setAllowEntryReordering(false);
// WHEN the list is originally built with reordering disabled (and section changes allowed)
addNotif(0, PACKAGE_1).setRank(4);
addNotif(1, PACKAGE_1).setRank(5);
addNotif(2, PACKAGE_2).setRank(1);
addNotif(3, PACKAGE_2).setRank(2);
addNotif(4, PACKAGE_3).setRank(3);
dispatchBuild();
// VERIFY the order and that entry reordering has not been suppressed
verifyBuiltList(
notif(0),
notif(1),
notif(2),
notif(3),
notif(4)
);
verify(mStabilityManager, never()).onEntryReorderSuppressed();
// WHEN the first section now claims PACKAGE_3 notifications
mutableSectionerPackages.add(PACKAGE_3);
dispatchBuild();
// VERIFY the re-sectioned notification is inserted at #1 of the first section, which
// is the correct position based on its rank, rather than #3 in the new section simply
// because it was #3 in its previous section.
verifyBuiltList(
notif(4),
notif(0),
notif(1),
notif(2),
notif(3)
);
verify(mStabilityManager, never()).onEntryReorderSuppressed();
}
@Test @Test
public void testStableChildOrdering() { public void testStableChildOrdering() {
// WHEN the list is originally built with reordering disabled // WHEN the list is originally built with reordering disabled
@@ -2335,26 +2450,35 @@ public class ShadeListBuilderTest extends SysuiTestCase {
return addGroupChildWithTag(index, packageId, groupId, null); return addGroupChildWithTag(index, packageId, groupId, null);
} }
private void assertOrder(String visible, String active, String expected) { private void assertOrder(String visible, String active, String expected,
boolean isOrderedCorrectly) {
StringBuilder differenceSb = new StringBuilder(); StringBuilder differenceSb = new StringBuilder();
NotifSection section = new NotifSection(mock(NotifSectioner.class), 0);
for (char c : active.toCharArray()) { for (char c : active.toCharArray()) {
if (visible.indexOf(c) < 0) differenceSb.append(c); if (visible.indexOf(c) < 0) differenceSb.append(c);
} }
String difference = differenceSb.toString(); String difference = differenceSb.toString();
int globalIndex = 0;
for (int i = 0; i < visible.length(); i++) { for (int i = 0; i < visible.length(); i++) {
addNotif(i, String.valueOf(visible.charAt(i))) final char c = visible.charAt(i);
.setRank(active.indexOf(visible.charAt(i))) // Skip notifications which aren't active anymore
if (!active.contains(String.valueOf(c))) continue;
addNotif(globalIndex++, String.valueOf(c))
.setRank(active.indexOf(c))
.setSection(section)
.setStableIndex(i); .setStableIndex(i);
} }
for (int i = 0; i < difference.length(); i++) { for (char c : difference.toCharArray()) {
addNotif(i + visible.length(), String.valueOf(difference.charAt(i))) addNotif(globalIndex++, String.valueOf(c))
.setRank(active.indexOf(difference.charAt(i))) .setRank(active.indexOf(c))
.setSection(section)
.setStableIndex(-1); .setStableIndex(-1);
} }
clearInvocations(mStabilityManager);
dispatchBuild(); dispatchBuild();
StringBuilder resultSb = new StringBuilder(); StringBuilder resultSb = new StringBuilder();
for (int i = 0; i < expected.length(); i++) { for (int i = 0; i < expected.length(); i++) {
@@ -2364,6 +2488,9 @@ public class ShadeListBuilderTest extends SysuiTestCase {
assertEquals("visible [" + visible + "] active [" + active + "]", assertEquals("visible [" + visible + "] active [" + active + "]",
expected, resultSb.toString()); expected, resultSb.toString());
mEntrySet.clear(); mEntrySet.clear();
verify(mStabilityManager, isOrderedCorrectly ? never() : times(1))
.onEntryReorderSuppressed();
} }
private int nextId(String packageName) { private int nextId(String packageName) {

View File

@@ -0,0 +1,210 @@
/*
* Copyright (C) 2022 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.statusbar.notification.collection.listbuilder
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import android.util.Log
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class SemiStableSortTest : SysuiTestCase() {
var shuffleInput: Boolean = false
var testStabilizeTo: Boolean = false
var sorter: SemiStableSort? = null
@Before
fun setUp() {
shuffleInput = false
sorter = null
}
private fun stringStabilizeTo(
stableOrder: String,
activeOrder: String,
): Pair<String, Boolean> {
val actives = activeOrder.toMutableList()
val result = mutableListOf<Char>()
return (sorter ?: SemiStableSort())
.stabilizeTo(
actives,
{ ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } },
result,
)
.let { ordered -> result.joinToString("") to ordered }
}
private fun stringSort(
stableOrder: String,
activeOrder: String,
): Pair<String, Boolean> {
val actives = activeOrder.toMutableList()
if (shuffleInput) {
actives.shuffle()
}
return (sorter ?: SemiStableSort())
.sort(
actives,
{ ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } },
compareBy { activeOrder.indexOf(it) },
)
.let { ordered -> actives.joinToString("") to ordered }
}
private fun testCase(
stableOrder: String,
activeOrder: String,
expected: String,
expectOrdered: Boolean,
) {
val (mergeResult, ordered) =
if (testStabilizeTo) stringStabilizeTo(stableOrder, activeOrder)
else stringSort(stableOrder, activeOrder)
val resultPass = expected == mergeResult
val orderedPass = ordered == expectOrdered
val pass = resultPass && orderedPass
val resultSuffix =
if (resultPass) "result=$expected" else "expected=$expected got=$mergeResult"
val orderedSuffix =
if (orderedPass) "ordered=$ordered" else "expected ordered to be $expectOrdered"
val readableResult = "stable=$stableOrder active=$activeOrder $resultSuffix $orderedSuffix"
Log.d("SemiStableSortTest", "${if (pass) "PASS" else "FAIL"}: $readableResult")
if (!pass) {
throw AssertionError("Test case failed: $readableResult")
}
}
private fun runAllTestCases() {
// No input or output
testCase("", "", "", true)
// Remove everything
testCase("ABCDEFG", "", "", true)
// Literally no changes
testCase("ABCDEFG", "ABCDEFG", "ABCDEFG", true)
// No stable order
testCase("", "ABCDEFG", "ABCDEFG", true)
// F moved after A, and...
testCase("ABCDEFG", "AFBCDEG", "ABCDEFG", false) // No other changes
testCase("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false) // Insert X before F
testCase("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false) // Insert X after F
testCase("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false) // Insert X where F was
// B moved after F, and...
testCase("ABCDEFG", "ACDEFBG", "ABCDEFG", false) // No other changes
testCase("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false) // Insert X before B
testCase("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false) // Insert X after B
testCase("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false) // Insert X where B was
// Swap F and B, and...
testCase("ABCDEFG", "AFCDEBG", "ABCDEFG", false) // No other changes
testCase("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false) // Insert X before F
testCase("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false) // Insert X after F
testCase("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false) // Insert X between CD (Alt: ABCXDEFG)
testCase("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false) // Insert X between DE (Alt: ABCDEFXG)
testCase("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false) // Insert X before B
testCase("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false) // Insert X after B
// Remove a bunch of entries at once
testCase("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true)
// Remove a bunch of entries and scramble
testCase("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false)
// Add a bunch of entries at once
testCase("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true)
// Add a bunch of entries and reverse originals
// NOTE: Some of these don't have obviously correct answers
testCase("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false) // appended
testCase("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false) // prepended
testCase("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false) // closer to back: append
testCase("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false) // closer to front: prepend
testCase("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false) // split new entries
// Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout
testCase("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false)
}
@Test
fun testSort() {
testStabilizeTo = false
shuffleInput = false
sorter = null
runAllTestCases()
}
@Test
fun testSortWithSingleInstance() {
testStabilizeTo = false
shuffleInput = false
sorter = SemiStableSort()
runAllTestCases()
}
@Test
fun testSortWithShuffledInput() {
testStabilizeTo = false
shuffleInput = true
sorter = null
runAllTestCases()
}
@Test
fun testStabilizeTo() {
testStabilizeTo = true
sorter = null
runAllTestCases()
}
@Test
fun testStabilizeToWithSingleInstance() {
testStabilizeTo = true
sorter = SemiStableSort()
runAllTestCases()
}
@Test
fun testIsSorted() {
val intCmp = Comparator<Int> { x, y -> Integer.compare(x, y) }
SemiStableSort.apply {
assertTrue(emptyList<Int>().isSorted(intCmp))
assertTrue(listOf(1).isSorted(intCmp))
assertTrue(listOf(1, 2).isSorted(intCmp))
assertTrue(listOf(1, 2, 3).isSorted(intCmp))
assertTrue(listOf(1, 2, 3, 4).isSorted(intCmp))
assertTrue(listOf(1, 2, 3, 4, 5).isSorted(intCmp))
assertTrue(listOf(1, 1, 1, 1, 1).isSorted(intCmp))
assertTrue(listOf(1, 1, 2, 2, 3, 3).isSorted(intCmp))
assertFalse(listOf(2, 1).isSorted(intCmp))
assertFalse(listOf(2, 1, 2).isSorted(intCmp))
assertFalse(listOf(1, 2, 1).isSorted(intCmp))
assertFalse(listOf(1, 2, 3, 2, 5).isSorted(intCmp))
assertFalse(listOf(5, 2, 3, 4, 5).isSorted(intCmp))
assertFalse(listOf(1, 2, 3, 4, 1).isSorted(intCmp))
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2022 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.statusbar.notification.collection.listbuilder
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper.getContiguousSubLists
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class ShadeListBuilderHelperTest : SysuiTestCase() {
@Test
fun testGetContiguousSubLists() {
assertThat(getContiguousSubLists("AAAAAA".toList()) { it })
.containsExactly(
listOf('A', 'A', 'A', 'A', 'A', 'A'),
)
.inOrder()
assertThat(getContiguousSubLists("AAABBB".toList()) { it })
.containsExactly(
listOf('A', 'A', 'A'),
listOf('B', 'B', 'B'),
)
.inOrder()
assertThat(getContiguousSubLists("AAABAA".toList()) { it })
.containsExactly(
listOf('A', 'A', 'A'),
listOf('B'),
listOf('A', 'A'),
)
.inOrder()
assertThat(getContiguousSubLists("AAABAA".toList(), minLength = 2) { it })
.containsExactly(
listOf('A', 'A', 'A'),
listOf('A', 'A'),
)
.inOrder()
assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList()) { it })
.containsExactly(
listOf('A', 'A', 'A'),
listOf('B', 'B', 'B', 'B'),
listOf('C', 'C'),
listOf('D'),
listOf('E', 'E', 'E'),
)
.inOrder()
assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList(), minLength = 2) { it })
.containsExactly(
listOf('A', 'A', 'A'),
listOf('B', 'B', 'B', 'B'),
listOf('C', 'C'),
listOf('E', 'E', 'E'),
)
.inOrder()
}
}