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

Commit fbc74686 authored by Diego Vela's avatar Diego Vela
Browse files

Move initial value to be on main thread initially.

Move initial value to be on the main thread. This is done to avoid
holding the WindowManagerGlobal lock.
Move all array modifications to use a handler and the main thread. This
avoids holding on to the lock while modifying the list of listeners. The
list is expected to be small so it should not cause issues.

Flag: android.view.flags.root_view_changed_listener
Bug: 394397033
Test: atest CtsViewTestCases:WindowInspectorTest
Change-Id: I9acf8ce18af1876a6b2a2b021411fd51f51ccf01
parent f67d9630
Loading
Loading
Loading
Loading
+98 −0
Original line number Diff line number Diff line
@@ -14,56 +14,76 @@
 * limitations under the License.
 */

package android.view;
package android.util;

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 ListenerWrapper}. This class is not thread safe.
 * 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.
 * @param <T> the type of the value to be reported.
 * @hide
 */
public class ListenerGroup<T> {
    private final List<ListenerWrapper<T>> mListeners = new ArrayList<>();

    /**
     * The set of listeners to be managed. All modifications should be done on {@link #mHandler}.
     */
    private final ArrayMap<Consumer<T>, Executor> mListeners =
            new ArrayMap<>();
    @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) {
        mLastValue = value;
    public ListenerGroup(@NonNull T value, @NonNull Handler handler) {
        mLastValue = Objects.requireNonNull(value);
        mHandler = Objects.requireNonNull(handler);
    }

    /**
     * Relays the value to all the registered {@link java.util.function.Consumer}
     * 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}.
     */
    public void accept(@NonNull T value) {
        mLastValue = Objects.requireNonNull(value);
        Objects.requireNonNull(value);
        mHandler.post(() -> {
            mLastValue = value;
            for (int i = 0; i < mListeners.size(); i++) {
            mListeners.get(i).accept(value);
                final Consumer<T> consumer = mListeners.keyAt(i);
                final Executor executor = mListeners.get(consumer);
                executor.execute(() -> consumer.accept(value));
            }
        });
    }

    /**
     * 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.
     * 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.
     */
    public void addListener(@NonNull Executor executor, @NonNull Consumer<T> consumer) {
        if (isConsumerPresent(consumer)) {
        Objects.requireNonNull(executor, "Executor must not be null.");
        Objects.requireNonNull(consumer, "Consumer must not be null.");
        mHandler.post(() -> {
            if (mListeners.containsKey(consumer)) {
                return;
            }
        final ListenerWrapper<T> listenerWrapper = new ListenerWrapper<>(executor, consumer);
        mListeners.add(listenerWrapper);
        listenerWrapper.accept(mLastValue);
            mListeners.put(consumer, executor);
            executor.execute(() -> consumer.accept(mLastValue));
        });
    }

    /**
@@ -71,29 +91,8 @@ public class ListenerGroup<T> {
     * is a no op.
     */
    public void removeListener(@NonNull Consumer<T> 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;
        mHandler.post(() -> {
            mListeners.remove(consumer);
        });
    }
}
+0 −56
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);
    }
}
+3 −5
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ 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;
@@ -40,6 +41,7 @@ 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;
@@ -153,9 +155,8 @@ 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 ListenerGroup<>(new ArrayList<>(), new Handler(Looper.getMainLooper()));
    @UnsupportedAppUsage
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    @UnsupportedAppUsage
@@ -336,9 +337,6 @@ 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);
        }
    }
+3 −1
Original line number Diff line number Diff line
@@ -43,7 +43,9 @@ 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}.
     * 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.
     * If this {@link Consumer} is already registered, then this method is a no op.
     * @see #getGlobalWindowViews()
     */
+146 −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.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