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

Commit 9400a98b authored by Liana Kazanova (xWF)'s avatar Liana Kazanova (xWF) Committed by Android (Google) Code Review
Browse files

Merge "Revert "Move initial value to be on main thread initially."" into main

parents c5fe2eec fd7e13d8
Loading
Loading
Loading
Loading
+99 −0
Original line number Diff line number Diff line
@@ -14,76 +14,56 @@
 * limitations under the License.
 */

package android.util;
package android.view;

import android.annotation.NonNull;
import android.os.Handler;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * A utility class to manage a list of {@link Consumer} and {@link Executor} pairs. This class
 * is thread safe because all the effects are dispatched through the handler.
 * A utility class to manage a list of {@link ListenerWrapper}. This class is not thread safe.
 * @param <T> the type of the value to be reported.
 * @hide
 */
public class ListenerGroup<T> {

    /**
     * The set of listeners to be managed. All modifications should be done on {@link #mHandler}.
     */
    private final ArrayMap<Consumer<T>, Executor> mListeners =
            new ArrayMap<>();
    private final List<ListenerWrapper<T>> mListeners = new ArrayList<>();
    @NonNull
    private T mLastValue;
    @NonNull
    private final Handler mHandler;

    /**
     * Constructs a {@link ListenerGroup} that will replay the last reported value whenever a new
     * listener is registered.
     * @param value the initial value
     * @param handler a handler to synchronize access to shared resources.
     */
    public ListenerGroup(@NonNull T value, @NonNull Handler handler) {
        mLastValue = Objects.requireNonNull(value);
        mHandler = Objects.requireNonNull(handler);
    public ListenerGroup(@NonNull T value) {
        mLastValue = value;
    }

    /**
     * Relays the value to all the registered {@link java.util.function.Consumer}. The relay is
     * initiated on the {@link Handler} provided in the constructor and then switched to the
     * {@link Executor} that was registered with the {@link Consumer}.
     * Relays the value to all the registered {@link java.util.function.Consumer}
     */
    public void accept(@NonNull T value) {
        Objects.requireNonNull(value);
        mHandler.post(() -> {
            mLastValue = value;
        mLastValue = Objects.requireNonNull(value);
        for (int i = 0; i < mListeners.size(); i++) {
                final Consumer<T> consumer = mListeners.keyAt(i);
                final Executor executor = mListeners.get(consumer);
                executor.execute(() -> consumer.accept(value));
            mListeners.get(i).accept(value);
        }
        });
    }

    /**
     * Adds a {@link Consumer} to the group and replays the last reported value. The replay is
     * initiated from the {@link Handler} provided in the constructor and run on the
     * {@link Executor}. If the {@link Consumer} is already present then this is a no op.
     * Adds a {@link Consumer} to the group and replays the last reported value. If the
     * {@link Consumer} is already present then this is a no op.
     */
    public void addListener(@NonNull Executor executor, @NonNull Consumer<T> consumer) {
        Objects.requireNonNull(executor, "Executor must not be null.");
        Objects.requireNonNull(consumer, "Consumer must not be null.");
        mHandler.post(() -> {
            if (mListeners.containsKey(consumer)) {
        if (isConsumerPresent(consumer)) {
            return;
        }
            mListeners.put(consumer, executor);
            executor.execute(() -> consumer.accept(mLastValue));
        });
        final ListenerWrapper<T> listenerWrapper = new ListenerWrapper<>(executor, consumer);
        mListeners.add(listenerWrapper);
        listenerWrapper.accept(mLastValue);
    }

    /**
@@ -91,8 +71,29 @@ public class ListenerGroup<T> {
     * is a no op.
     */
    public void removeListener(@NonNull Consumer<T> consumer) {
        mHandler.post(() -> {
            mListeners.remove(consumer);
        });
        final int index = computeIndex(consumer);
        if (index > -1) {
            mListeners.remove(index);
        }
    }

    /**
     * Returns {@code true} if the {@link Consumer} is present in the list, {@code false}
     * otherwise.
     */
    public boolean isConsumerPresent(Consumer<T> consumer) {
        return computeIndex(consumer) > -1;
    }

    /**
     * Returns the index of the matching {@link ListenerWrapper} if present, {@code -1} otherwise.
     */
    private int computeIndex(Consumer<T> consumer) {
        for (int i = 0; i < mListeners.size(); i++) {
            if (mListeners.get(i).isConsumerSame(consumer)) {
                return i;
            }
        }
        return -1;
    }
}
+56 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 android.view;

import android.annotation.NonNull;

import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * A utilty class to bundle a {@link Consumer} and an {@link Executor}
 * @param <T> the type of value to be reported.
 * @hide
 */
public class ListenerWrapper<T> {

    @NonNull
    private final Consumer<T> mConsumer;
    @NonNull
    private final Executor mExecutor;

    public ListenerWrapper(@NonNull Executor executor, @NonNull Consumer<T> consumer) {
        mExecutor = Objects.requireNonNull(executor);
        mConsumer = Objects.requireNonNull(consumer);
    }

    /**
     * Relays the new value to the {@link Consumer} using the {@link  Executor}
     */
    public void accept(@NonNull T value) {
        mExecutor.execute(() -> mConsumer.accept(value));
    }

    /**
     * Returns {@code true} if the consumer matches the one provided in the constructor,
     * {@code false} otherwise.
     */
    public boolean isConsumerSame(@NonNull Consumer<T> consumer) {
        return mConsumer.equals(consumer);
    }
}
+5 −3
Original line number Diff line number Diff line
@@ -32,7 +32,6 @@ import android.content.res.TypedArray;
import android.graphics.HardwareRenderer;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
@@ -41,7 +40,6 @@ import android.os.SystemProperties;
import android.util.AndroidRuntimeException;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.ListenerGroup;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
@@ -155,8 +153,9 @@ public final class WindowManagerGlobal {
     * The {@link ListenerGroup} that is associated to {@link #mViews}.
     * @hide
     */
    @GuardedBy("mLock")
    private final ListenerGroup<List<View>> mWindowViewsListenerGroup =
            new ListenerGroup<>(new ArrayList<>(), new Handler(Looper.getMainLooper()));
            new ListenerGroup<>(new ArrayList<>());
    @UnsupportedAppUsage
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    @UnsupportedAppUsage
@@ -337,6 +336,9 @@ public final class WindowManagerGlobal {
    public void addWindowViewsListener(@NonNull Executor executor,
            @NonNull Consumer<List<View>> consumer) {
        synchronized (mLock) {
            if (mWindowViewsListenerGroup.isConsumerPresent(consumer)) {
                return;
            }
            mWindowViewsListenerGroup.addListener(executor, consumer);
        }
    }
+1 −3
Original line number Diff line number Diff line
@@ -43,9 +43,7 @@ public final class WindowInspector {

    /**
     * Adds a listener that is notified whenever the value of {@link #getGlobalWindowViews()}
     * changes. The current value is provided immediately using the provided {@link Executor}. The
     * value is initially relayed using the main thread and then posted to the {@link Executor}.
     * Using a direct {@link Executor} and doing heavy calculations can cause performance issues.
     * changes. The current value is provided immediately using the provided {@link Executor}.
     * If this {@link Consumer} is already registered, then this method is a no op.
     * @see #getGlobalWindowViews()
     */
+0 −146
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 android.util;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;

import android.os.Handler;
import android.os.Looper;

import androidx.test.filters.SmallTest;

import com.android.internal.annotations.GuardedBy;

import org.junit.Before;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;


@SmallTest
public class ListenerGroupTest {

    private Object mInitialObject = new Object();

    private ListenerGroup<Object> mListenerGroup;

    @Before
    public void setUp() throws InterruptedException {
        mListenerGroup = new ListenerGroup<>(mInitialObject, new Handler(Looper.getMainLooper()));
    }

    @Test
    public void test_added_listener_gets_initial_value() {
        final int valueCount = 1;
        final ValueListener valueListener = new ValueListener(valueCount);

        mListenerGroup.addListener(Runnable::run, valueListener);

        final boolean waitCompleted = valueListener.waitForValues();
        List<Object> values = valueListener.getValues();

        assertTrue("waitForValues did not complete.", waitCompleted);
        assertEquals("Value count does not match.", valueCount, values.size());
        assertEquals("First value does not match initial value", mInitialObject,
                valueListener.getValues().getFirst());
    }

    @Test
    public void test_added_listener_gets_receives_updates() {
        int valueCount = 2;
        Object nextValue = new Object();
        ValueListener valueListener = new ValueListener(2);

        mListenerGroup.addListener(Runnable::run, valueListener);
        mListenerGroup.accept(nextValue);

        boolean waitCompleted = valueListener.waitForValues();
        List<Object> values = valueListener.getValues();

        assertTrue("waitForValues did not complete.", waitCompleted);
        assertEquals("Value count does not match.", valueCount, values.size());
        assertEquals("Next value not received", nextValue,
                valueListener.getValues().getLast());
    }

    @Test
    public void test_removed_listener_stops_receiving_updates() {
        final int valueCount = 1;
        Object nextValue = new Object();
        ValueListener valueListener = new ValueListener(valueCount);
        ValueListener stopListener = new ValueListener(valueCount);

        mListenerGroup.addListener(Runnable::run, valueListener);
        mListenerGroup.removeListener(valueListener);

        mListenerGroup.accept(nextValue);
        mListenerGroup.addListener(Runnable::run, stopListener);

        boolean waitCompleted = valueListener.waitForValues() && stopListener.waitForValues();
        List<Object> values = valueListener.getValues();

        assertTrue("waitForValues did not complete.", waitCompleted);
        assertEquals("Value count does not match.", valueCount, values.size());
        assertEquals("Incorrect values received.", mInitialObject,
                values.getFirst());
        assertEquals("stopListener must receive next value", nextValue,
                stopListener.getValues().getFirst());
    }

    private static final class ValueListener implements Consumer<Object> {

        private final Object mLock = new Object();

        @GuardedBy("mLock")
        private final List<Object> mValues = new ArrayList<>();

        private final CountDownLatch mCountDownLatch;

        ValueListener(int valueCount) {
            mCountDownLatch = new CountDownLatch(valueCount);
        }

        @Override
        public void accept(Object o) {
            synchronized (mLock) {
                mValues.add(Objects.requireNonNull(o));
                mCountDownLatch.countDown();
            }
        }

        public boolean waitForValues() {
            try {
                return mCountDownLatch.await(16, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
                return false;
            }

        }

        public List<Object> getValues() {
            synchronized (mLock) {
                return new ArrayList<>(mValues);
            }
        }
    }
}
Loading