Loading packages/SystemUI/src/com/android/systemui/flags/Flags.java +5 −1 Original line number Diff line number Diff line Loading @@ -73,7 +73,11 @@ public class Flags { public static final UnreleasedFlag NOTIFICATION_DISMISSAL_FADE = new UnreleasedFlag(113, true); // next id: 114 public static final UnreleasedFlag STABILITY_INDEX_FIX = new UnreleasedFlag(114, true); public static final UnreleasedFlag SEMI_STABLE_SORT = new UnreleasedFlag(115, true); // next id: 116 /***************************************/ // 200 - keyguard/lockscreen Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt +8 −0 Original line number Diff line number Diff line Loading @@ -53,4 +53,12 @@ class NotifPipelineFlags @Inject constructor( fun fullScreenIntentRequiresKeyguard(): Boolean = featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD) val isStabilityIndexFixEnabled: Boolean by lazy { featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX) } val isSemiStableSortEnabled: Boolean by lazy { featureFlags.isEnabled(Flags.SEMI_STABLE_SORT) } } packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt +7 −2 Original line number Diff line number Diff line Loading @@ -68,6 +68,9 @@ data class ListAttachState private constructor( */ 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. */ fun clone(other: ListAttachState) { parent = other.parent Loading Loading @@ -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 * filtered out during any of the filtering steps. */ fun detach() { fun detach(includingStableIndex: Boolean) { parent = null section = null promoter = null // stableIndex = -1 // TODO(b/241229236): Clear this once we fix the stability fragility if (includingStableIndex) { stableIndex = -1 } } companion object { Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +101 −18 Original line number Diff line number Diff line Loading @@ -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.OnBeforeTransformGroupsListener; 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.pluggable.DefaultNotifStabilityManager; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator; Loading Loading @@ -96,11 +99,14 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated // TODO replace temp with collection pool for readability private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>(); private NotifPipelineFlags mFlags; private final boolean mAlwaysLogList; private List<ListEntry> mNotifList = 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 Map<String, GroupEntry> mGroups = new ArrayMap<>(); private Collection<NotificationEntry> mAllEntries = Collections.emptyList(); Loading Loading @@ -141,6 +147,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { ) { mSystemClock = systemClock; mLogger = logger; mFlags = flags; mAlwaysLogList = flags.isDevLoggingEnabled(); mInteractionTracker = interactionTracker; mChoreographer = pipelineChoreographer; Loading Loading @@ -958,7 +965,8 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { * filtered out during any of the filtering steps. */ 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() { Loading @@ -978,7 +986,16 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { private void 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) { if (entry instanceof GroupEntry) { GroupEntry parent = (GroupEntry) entry; Loading @@ -991,16 +1008,15 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // Check for suppressed order changes if (!getStabilityManager().isEveryChangeAllowed()) { mForceReorderable = true; boolean isSorted = isShadeSorted(); boolean isSorted = isShadeSortedLegacy(); mForceReorderable = false; if (!isSorted) { getStabilityManager().onEntryReorderSuppressed(); } } Trace.endSection(); } private boolean isShadeSorted() { private boolean isShadeSortedLegacy() { if (!isSorted(mNotifList, mTopLevelComparator)) { return false; } Loading @@ -1014,6 +1030,43 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { 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 */ @VisibleForTesting public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) { Loading @@ -1036,29 +1089,43 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { /** * Assign the index of each notification relative to the total order */ private static void assignIndexes(List<ListEntry> notifList) { private void assignIndexes(List<ListEntry> notifList) { if (notifList.size() == 0) return; NotifSection currentSection = requireNonNull(notifList.get(0).getSection()); int sectionMemberIndex = 0; for (int i = 0; i < notifList.size(); i++) { ListEntry entry = notifList.get(i); final ListEntry entry = notifList.get(i); NotifSection section = requireNonNull(entry.getSection()); if (section.getIndex() != currentSection.getIndex()) { sectionMemberIndex = 0; currentSection = section; } entry.getAttachState().setStableIndex(sectionMemberIndex); if (mFlags.isStabilityIndexFixEnabled()) { entry.getAttachState().setStableIndex(sectionMemberIndex++); if (entry instanceof GroupEntry) { GroupEntry parent = (GroupEntry) entry; for (int j = 0; j < parent.getChildren().size(); j++) { entry = parent.getChildren().get(j); final GroupEntry parent = (GroupEntry) entry; final NotificationEntry summary = parent.getSummary(); if (summary != null) { summary.getAttachState().setStableIndex(sectionMemberIndex++); } for (NotificationEntry child : parent.getChildren()) { child.getAttachState().setStableIndex(sectionMemberIndex++); } } } else { // This old implementation uses the same index number for the group as the first // child, and fails to assign an index to the summary. Remove once tested. entry.getAttachState().setStableIndex(sectionMemberIndex); sectionMemberIndex++; if (entry instanceof GroupEntry) { final GroupEntry parent = (GroupEntry) entry; for (NotificationEntry child : parent.getChildren()) { child.getAttachState().setStableIndex(sectionMemberIndex++); } } sectionMemberIndex++; } } } private void freeEmptyGroups() { Trace.beginSection("ShadeListBuilder.freeEmptyGroups"); Loading Loading @@ -1196,7 +1263,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { o2.getSectionIndex()); if (cmp != 0) return cmp; cmp = Integer.compare( cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare( getStableOrderIndex(o1), getStableOrderIndex(o2)); if (cmp != 0) return cmp; Loading Loading @@ -1225,7 +1292,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> { int cmp = Integer.compare( int cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare( getStableOrderIndex(o1), getStableOrderIndex(o2)); if (cmp != 0) return cmp; Loading Loading @@ -1256,9 +1323,25 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // let the stability manager constrain or allow reordering return -1; } // NOTE(b/241229236): Can't use cleared section index until we fix stability fragility 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) { final NotifFilter filter = findRejectingFilter(entry, now, filters); entry.getAttachState().setExcludingFilter(filter); Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt 0 → 100644 +200 −0 Original line number Diff line number Diff line /* * 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 Loading
packages/SystemUI/src/com/android/systemui/flags/Flags.java +5 −1 Original line number Diff line number Diff line Loading @@ -73,7 +73,11 @@ public class Flags { public static final UnreleasedFlag NOTIFICATION_DISMISSAL_FADE = new UnreleasedFlag(113, true); // next id: 114 public static final UnreleasedFlag STABILITY_INDEX_FIX = new UnreleasedFlag(114, true); public static final UnreleasedFlag SEMI_STABLE_SORT = new UnreleasedFlag(115, true); // next id: 116 /***************************************/ // 200 - keyguard/lockscreen Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt +8 −0 Original line number Diff line number Diff line Loading @@ -53,4 +53,12 @@ class NotifPipelineFlags @Inject constructor( fun fullScreenIntentRequiresKeyguard(): Boolean = featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD) val isStabilityIndexFixEnabled: Boolean by lazy { featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX) } val isSemiStableSortEnabled: Boolean by lazy { featureFlags.isEnabled(Flags.SEMI_STABLE_SORT) } }
packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt +7 −2 Original line number Diff line number Diff line Loading @@ -68,6 +68,9 @@ data class ListAttachState private constructor( */ 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. */ fun clone(other: ListAttachState) { parent = other.parent Loading Loading @@ -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 * filtered out during any of the filtering steps. */ fun detach() { fun detach(includingStableIndex: Boolean) { parent = null section = null promoter = null // stableIndex = -1 // TODO(b/241229236): Clear this once we fix the stability fragility if (includingStableIndex) { stableIndex = -1 } } companion object { Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +101 −18 Original line number Diff line number Diff line Loading @@ -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.OnBeforeTransformGroupsListener; 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.pluggable.DefaultNotifStabilityManager; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator; Loading Loading @@ -96,11 +99,14 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated // TODO replace temp with collection pool for readability private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>(); private NotifPipelineFlags mFlags; private final boolean mAlwaysLogList; private List<ListEntry> mNotifList = 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 Map<String, GroupEntry> mGroups = new ArrayMap<>(); private Collection<NotificationEntry> mAllEntries = Collections.emptyList(); Loading Loading @@ -141,6 +147,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { ) { mSystemClock = systemClock; mLogger = logger; mFlags = flags; mAlwaysLogList = flags.isDevLoggingEnabled(); mInteractionTracker = interactionTracker; mChoreographer = pipelineChoreographer; Loading Loading @@ -958,7 +965,8 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { * filtered out during any of the filtering steps. */ 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() { Loading @@ -978,7 +986,16 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { private void 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) { if (entry instanceof GroupEntry) { GroupEntry parent = (GroupEntry) entry; Loading @@ -991,16 +1008,15 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // Check for suppressed order changes if (!getStabilityManager().isEveryChangeAllowed()) { mForceReorderable = true; boolean isSorted = isShadeSorted(); boolean isSorted = isShadeSortedLegacy(); mForceReorderable = false; if (!isSorted) { getStabilityManager().onEntryReorderSuppressed(); } } Trace.endSection(); } private boolean isShadeSorted() { private boolean isShadeSortedLegacy() { if (!isSorted(mNotifList, mTopLevelComparator)) { return false; } Loading @@ -1014,6 +1030,43 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { 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 */ @VisibleForTesting public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) { Loading @@ -1036,29 +1089,43 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { /** * Assign the index of each notification relative to the total order */ private static void assignIndexes(List<ListEntry> notifList) { private void assignIndexes(List<ListEntry> notifList) { if (notifList.size() == 0) return; NotifSection currentSection = requireNonNull(notifList.get(0).getSection()); int sectionMemberIndex = 0; for (int i = 0; i < notifList.size(); i++) { ListEntry entry = notifList.get(i); final ListEntry entry = notifList.get(i); NotifSection section = requireNonNull(entry.getSection()); if (section.getIndex() != currentSection.getIndex()) { sectionMemberIndex = 0; currentSection = section; } entry.getAttachState().setStableIndex(sectionMemberIndex); if (mFlags.isStabilityIndexFixEnabled()) { entry.getAttachState().setStableIndex(sectionMemberIndex++); if (entry instanceof GroupEntry) { GroupEntry parent = (GroupEntry) entry; for (int j = 0; j < parent.getChildren().size(); j++) { entry = parent.getChildren().get(j); final GroupEntry parent = (GroupEntry) entry; final NotificationEntry summary = parent.getSummary(); if (summary != null) { summary.getAttachState().setStableIndex(sectionMemberIndex++); } for (NotificationEntry child : parent.getChildren()) { child.getAttachState().setStableIndex(sectionMemberIndex++); } } } else { // This old implementation uses the same index number for the group as the first // child, and fails to assign an index to the summary. Remove once tested. entry.getAttachState().setStableIndex(sectionMemberIndex); sectionMemberIndex++; if (entry instanceof GroupEntry) { final GroupEntry parent = (GroupEntry) entry; for (NotificationEntry child : parent.getChildren()) { child.getAttachState().setStableIndex(sectionMemberIndex++); } } sectionMemberIndex++; } } } private void freeEmptyGroups() { Trace.beginSection("ShadeListBuilder.freeEmptyGroups"); Loading Loading @@ -1196,7 +1263,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { o2.getSectionIndex()); if (cmp != 0) return cmp; cmp = Integer.compare( cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare( getStableOrderIndex(o1), getStableOrderIndex(o2)); if (cmp != 0) return cmp; Loading Loading @@ -1225,7 +1292,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> { int cmp = Integer.compare( int cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare( getStableOrderIndex(o1), getStableOrderIndex(o2)); if (cmp != 0) return cmp; Loading Loading @@ -1256,9 +1323,25 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { // let the stability manager constrain or allow reordering return -1; } // NOTE(b/241229236): Can't use cleared section index until we fix stability fragility 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) { final NotifFilter filter = findRejectingFilter(entry, now, filters); entry.getAttachState().setExcludingFilter(filter); Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt 0 → 100644 +200 −0 Original line number Diff line number Diff line /* * 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