Add CopyOnLoopListenerSet which may be faster

Bug: 341479400
Test: atest SystemUITests
Flag: EXEMPT bugfix / unused utility
Change-Id: Ice43df54311aa1d045e1f007f92c65b95cf38196
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/CopyOnLoopListenerSetTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/CopyOnLoopListenerSetTest.kt
new file mode 100644
index 0000000..b08d799
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/CopyOnLoopListenerSetTest.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 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.util
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CopyOnLoopListenerSetTest : ListenerSetTest() {
+    override fun makeRunnableListenerSet(): IListenerSet<Runnable> = CopyOnLoopListenerSet()
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/ListenerSetTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/ListenerSetTest.kt
similarity index 89%
rename from packages/SystemUI/tests/src/com/android/systemui/util/ListenerSetTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/util/ListenerSetTest.kt
index 1404a4f..a4f031b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/ListenerSetTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/ListenerSetTest.kt
@@ -34,8 +34,8 @@
     @Test
     fun addIfAbsent_doesNotDoubleAdd() {
         // setup & preconditions
-        val runnable1 = Runnable { }
-        val runnable2 = Runnable { }
+        val runnable1 = Runnable {}
+        val runnable2 = Runnable {}
         assertThat(runnableSet).isEmpty()
 
         // Test that an element can be added
@@ -53,7 +53,7 @@
 
     @Test
     fun isEmpty_changes() {
-        val runnable = Runnable { }
+        val runnable = Runnable {}
         assertThat(runnableSet).isEmpty()
         assertThat(runnableSet.isEmpty()).isTrue()
         assertThat(runnableSet.isNotEmpty()).isFalse()
@@ -74,17 +74,17 @@
         assertThat(runnableSet).isEmpty()
         assertThat(runnableSet.size).isEqualTo(0)
 
-        assertThat(runnableSet.addIfAbsent(Runnable { })).isTrue()
+        assertThat(runnableSet.addIfAbsent(Runnable {})).isTrue()
         assertThat(runnableSet.size).isEqualTo(1)
 
-        assertThat(runnableSet.addIfAbsent(Runnable { })).isTrue()
+        assertThat(runnableSet.addIfAbsent(Runnable {})).isTrue()
         assertThat(runnableSet.size).isEqualTo(2)
     }
 
     @Test
     fun contains_worksAsExpected() {
-        val runnable1 = Runnable { }
-        val runnable2 = Runnable { }
+        val runnable1 = Runnable {}
+        val runnable2 = Runnable {}
         assertThat(runnableSet).isEmpty()
         assertThat(runnable1 in runnableSet).isFalse()
         assertThat(runnable2 in runnableSet).isFalse()
@@ -112,8 +112,8 @@
 
     @Test
     fun containsAll_worksAsExpected() {
-        val runnable1 = Runnable { }
-        val runnable2 = Runnable { }
+        val runnable1 = Runnable {}
+        val runnable2 = Runnable {}
 
         assertThat(runnableSet).isEmpty()
         assertThat(runnableSet.containsAll(listOf())).isTrue()
@@ -143,8 +143,8 @@
     @Test
     fun remove_removesListener() {
         // setup and preconditions
-        val runnable1 = Runnable { }
-        val runnable2 = Runnable { }
+        val runnable1 = Runnable {}
+        val runnable2 = Runnable {}
         assertThat(runnableSet).isEmpty()
         runnableSet.addIfAbsent(runnable1)
         runnableSet.addIfAbsent(runnable2)
@@ -168,15 +168,14 @@
         // Setup and preconditions
         val runnablesCalled = mutableListOf<Int>()
         // runnable1 is configured to remove itself
-        val runnable1 = object : Runnable {
-            override fun run() {
-                runnableSet.remove(this)
-                runnablesCalled.add(1)
+        val runnable1 =
+            object : Runnable {
+                override fun run() {
+                    runnableSet.remove(this)
+                    runnablesCalled.add(1)
+                }
             }
-        }
-        val runnable2 = Runnable {
-            runnablesCalled.add(2)
-        }
+        val runnable2 = Runnable { runnablesCalled.add(2) }
         assertThat(runnableSet).isEmpty()
         runnableSet.addIfAbsent(runnable1)
         runnableSet.addIfAbsent(runnable2)
@@ -194,17 +193,13 @@
     fun addIfAbsent_isReentrantSafe() {
         // Setup and preconditions
         val runnablesCalled = mutableListOf<Int>()
-        val runnable99 = Runnable {
-            runnablesCalled.add(99)
-        }
+        val runnable99 = Runnable { runnablesCalled.add(99) }
         // runnable1 is configured to add runnable99
         val runnable1 = Runnable {
             runnableSet.addIfAbsent(runnable99)
             runnablesCalled.add(1)
         }
-        val runnable2 = Runnable {
-            runnablesCalled.add(2)
-        }
+        val runnable2 = Runnable { runnablesCalled.add(2) }
         assertThat(runnableSet).isEmpty()
         runnableSet.addIfAbsent(runnable1)
         runnableSet.addIfAbsent(runnable2)
@@ -217,4 +212,4 @@
         assertThat(runnablesCalled).containsExactly(1, 2)
         assertThat(runnableSet).containsExactly(runnable1, runnable2, runnable99)
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/NamedListenerSetTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/NamedListenerSetTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/util/NamedListenerSetTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/util/NamedListenerSetTest.kt
diff --git a/packages/SystemUI/src/com/android/systemui/util/CopyOnLoopListenerSet.kt b/packages/SystemUI/src/com/android/systemui/util/CopyOnLoopListenerSet.kt
new file mode 100644
index 0000000..8a7ab80
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/CopyOnLoopListenerSet.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 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.util
+
+/**
+ * A collection of listeners, observers, callbacks, etc.
+ *
+ * This container is optimized for frequent mutation and infrequent iteration, with reentrant-safety
+ * guarantees but without thread-safety guarantees. Specifically, to ensure that
+ * [ConcurrentModificationException] is not thrown when listeners mutate the set, this iterator will
+ * not reflect changes made to the set after the iterator is constructed.
+ */
+class CopyOnLoopListenerSet<E : Any>
+/** Private constructor takes the internal list so that we can use auto-delegation */
+private constructor(private val listeners: ArrayList<E>) :
+    Collection<E> by listeners, IListenerSet<E> {
+
+    /** Create a new instance */
+    constructor() : this(ArrayList())
+
+    @Suppress("UNCHECKED_CAST")
+    override fun iterator(): Iterator<E> = listeners.toArray().iterator() as Iterator<E>
+
+    override fun addIfAbsent(element: E): Boolean =
+        if (element in listeners) {
+            false
+        } else {
+            listeners.add(element)
+        }
+
+    override fun remove(element: E): Boolean = listeners.remove(element)
+}