Add ListenerSet and use in places which currently copy-on-iterate.

Fixes: 204127880
Test: atest ListenerSetTest
Merged-In: Ic1320b4f6c424322451f7def11346865bf878f99
Change-Id: Ic1320b4f6c424322451f7def11346865bf878f99
This commit is contained in:
Jeff DeCew
2021-10-06 19:17:43 +00:00
parent 4ddf9d094c
commit 6a9caed07f
4 changed files with 193 additions and 11 deletions

View File

@@ -45,6 +45,7 @@ import com.android.systemui.dagger.qualifiers.RootView;
import com.android.systemui.keyguard.DismissCallbackRegistry;
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.ListenerSet;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -84,11 +85,11 @@ public class KeyguardBouncer {
private final Runnable mRemoveViewRunnable = this::removeView;
private final KeyguardBypassController mKeyguardBypassController;
private KeyguardHostViewController mKeyguardViewController;
private final List<KeyguardResetCallback> mResetCallbacks = new ArrayList<>();
private final ListenerSet<KeyguardResetCallback> mResetCallbacks = new ListenerSet<>();
private final Runnable mResetRunnable = ()-> {
if (mKeyguardViewController != null) {
mKeyguardViewController.resetSecurityContainer();
for (KeyguardResetCallback callback : new ArrayList<>(mResetCallbacks)) {
for (KeyguardResetCallback callback : mResetCallbacks) {
callback.onKeyguardReset();
}
}
@@ -602,7 +603,7 @@ public class KeyguardBouncer {
}
public void addKeyguardResetCallback(KeyguardResetCallback callback) {
mResetCallbacks.add(callback);
mResetCallbacks.addIfAbsent(callback);
}
public void removeKeyguardResetCallback(KeyguardResetCallback callback) {

View File

@@ -38,11 +38,10 @@ import com.android.systemui.R;
import com.android.systemui.statusbar.AlertingNotificationManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
import com.android.systemui.util.ListenerSet;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
/**
* A manager which handles heads up notifications which is a special mode where
@@ -52,7 +51,7 @@ public abstract class HeadsUpManager extends AlertingNotificationManager {
private static final String TAG = "HeadsUpManager";
private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
protected final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
protected final ListenerSet<OnHeadsUpChangedListener> mListeners = new ListenerSet<>();
protected final Context mContext;
@@ -118,7 +117,7 @@ public abstract class HeadsUpManager extends AlertingNotificationManager {
* Adds an OnHeadUpChangedListener to observe events.
*/
public void addListener(@NonNull OnHeadsUpChangedListener listener) {
mListeners.add(listener);
mListeners.addIfAbsent(listener);
}
/**
@@ -158,7 +157,7 @@ public abstract class HeadsUpManager extends AlertingNotificationManager {
NotificationPeekEvent.NOTIFICATION_PEEK, entry.getSbn().getUid(),
entry.getSbn().getPackageName(), entry.getSbn().getInstanceId());
}
for (OnHeadsUpChangedListener listener : new ArrayList<>(mListeners)) {
for (OnHeadsUpChangedListener listener : mListeners) {
if (isPinned) {
listener.onHeadsUpPinned(entry);
} else {
@@ -178,7 +177,7 @@ public abstract class HeadsUpManager extends AlertingNotificationManager {
entry.setHeadsUp(true);
setEntryPinned((HeadsUpEntry) alertEntry, shouldHeadsUpBecomePinned(entry));
EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 1 /* visible */);
for (OnHeadsUpChangedListener listener : new ArrayList<>(mListeners)) {
for (OnHeadsUpChangedListener listener : mListeners) {
listener.onHeadsUpStateChanged(entry, true);
}
}
@@ -189,7 +188,7 @@ public abstract class HeadsUpManager extends AlertingNotificationManager {
entry.setHeadsUp(false);
setEntryPinned((HeadsUpEntry) alertEntry, false /* isPinned */);
EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 0 /* visible */);
for (OnHeadsUpChangedListener listener : new ArrayList<>(mListeners)) {
for (OnHeadsUpChangedListener listener : mListeners) {
listener.onHeadsUpStateChanged(entry, false);
}
}
@@ -207,7 +206,7 @@ public abstract class HeadsUpManager extends AlertingNotificationManager {
if (mHasPinnedNotification) {
MetricsLogger.count(mContext, "note_peek", 1);
}
for (OnHeadsUpChangedListener listener : new ArrayList<>(mListeners)) {
for (OnHeadsUpChangedListener listener : mListeners) {
listener.onHeadsUpPinnedModeChanged(hasPinnedNotification);
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2021 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 java.util.concurrent.CopyOnWriteArrayList
/**
* A collection of listeners, observers, callbacks, etc.
*
* This container is optimized for infrequent mutation and frequent iteration, with thread safety
* and reentrant-safety guarantees as well.
*/
class ListenerSet<E> : Iterable<E> {
private val listeners: CopyOnWriteArrayList<E> = CopyOnWriteArrayList()
/**
* A thread-safe, reentrant-safe method to add a listener.
* Does nothing if the listener is already in the set.
*/
fun addIfAbsent(element: E): Boolean = listeners.addIfAbsent(element)
/**
* A thread-safe, reentrant-safe method to remove a listener.
*/
fun remove(element: E): Boolean = listeners.remove(element)
/**
* Returns an iterator over the listeners currently in the set. Note that to ensure
* [ConcurrentModificationException] is never thrown, this iterator will not reflect changes
* made to the set after the iterator is constructed.
*/
override fun iterator(): Iterator<E> = listeners.iterator()
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright (C) 2021 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 android.test.suitebuilder.annotation.SmallTest
import androidx.test.runner.AndroidJUnit4
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class ListenerSetTest : SysuiTestCase() {
var runnableSet: ListenerSet<Runnable> = ListenerSet()
@Before
fun setup() {
runnableSet = ListenerSet()
}
@Test
fun addIfAbsent_doesNotDoubleAdd() {
// setup & preconditions
val runnable1 = Runnable { }
val runnable2 = Runnable { }
assertThat(runnableSet.toList()).isEmpty()
// Test that an element can be added
assertThat(runnableSet.addIfAbsent(runnable1)).isTrue()
assertThat(runnableSet.toList()).containsExactly(runnable1)
// Test that a second element can be added
assertThat(runnableSet.addIfAbsent(runnable2)).isTrue()
assertThat(runnableSet.toList()).containsExactly(runnable1, runnable2)
// Test that re-adding the first element does nothing and returns false
assertThat(runnableSet.addIfAbsent(runnable1)).isFalse()
assertThat(runnableSet.toList()).containsExactly(runnable1, runnable2)
}
@Test
fun remove_removesListener() {
// setup and preconditions
val runnable1 = Runnable { }
val runnable2 = Runnable { }
assertThat(runnableSet.toList()).isEmpty()
runnableSet.addIfAbsent(runnable1)
runnableSet.addIfAbsent(runnable2)
assertThat(runnableSet.toList()).containsExactly(runnable1, runnable2)
// Test that removing the first runnable only removes that one runnable
assertThat(runnableSet.remove(runnable1)).isTrue()
assertThat(runnableSet.toList()).containsExactly(runnable2)
// Test that removing a non-present runnable does not error
assertThat(runnableSet.remove(runnable1)).isFalse()
assertThat(runnableSet.toList()).containsExactly(runnable2)
// Test that removing the other runnable succeeds
assertThat(runnableSet.remove(runnable2)).isTrue()
assertThat(runnableSet.toList()).isEmpty()
}
@Test
fun remove_isReentrantSafe() {
// 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 runnable2 = Runnable {
runnablesCalled.add(2)
}
assertThat(runnableSet.toList()).isEmpty()
runnableSet.addIfAbsent(runnable1)
runnableSet.addIfAbsent(runnable2)
assertThat(runnableSet.toList()).containsExactly(runnable1, runnable2)
// Test that both runnables are called and 1 was removed
for (runnable in runnableSet) {
runnable.run()
}
assertThat(runnablesCalled).containsExactly(1, 2)
assertThat(runnableSet.toList()).containsExactly(runnable2)
}
@Test
fun addIfAbsent_isReentrantSafe() {
// Setup and preconditions
val runnablesCalled = mutableListOf<Int>()
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)
}
assertThat(runnableSet.toList()).isEmpty()
runnableSet.addIfAbsent(runnable1)
runnableSet.addIfAbsent(runnable2)
assertThat(runnableSet.toList()).containsExactly(runnable1, runnable2)
// Test that both original runnables are called and 99 was added but not called
for (runnable in runnableSet) {
runnable.run()
}
assertThat(runnablesCalled).containsExactly(1, 2)
assertThat(runnableSet.toList()).containsExactly(runnable1, runnable2, runnable99)
}
}