Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 30dd32ba authored by Jeff DeCew's avatar Jeff DeCew
Browse files

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

Test: atest ListenerSetTest
Change-Id: Ic1320b4f6c424322451f7def11346865bf878f99
parent 093a2030
Loading
Loading
Loading
Loading
+4 −3
Original line number Diff line number Diff line
@@ -44,6 +44,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;
@@ -83,11 +84,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) {
+7 −8
Original line number Diff line number Diff line
@@ -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);
        }
    }
+47 −0
Original line number Diff line number Diff line
/*
 * 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()
}
+135 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}
 No newline at end of file