diff options
| -rw-r--r-- | core/api/current.txt | 2 | ||||
| -rw-r--r-- | core/java/android/view/ListenerWrapper.java | 56 | ||||
| -rw-r--r-- | core/java/android/view/WindowManagerGlobal.java | 33 | ||||
| -rw-r--r-- | core/java/android/view/flags/view_flags.aconfig | 8 | ||||
| -rw-r--r-- | core/java/android/view/inspector/WindowInspector.java | 24 | ||||
| -rw-r--r-- | core/java/android/view/translation/ListenerGroup.java | 87 |
6 files changed, 210 insertions, 0 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 7bc0fb220e1a..1630d80346ce 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -58287,7 +58287,9 @@ package android.view.inspector { } public final class WindowInspector { + method @FlaggedApi("android.view.flags.root_view_changed_listener") public static void addGlobalWindowViewsListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.view.View>>); method @NonNull public static java.util.List<android.view.View> getGlobalWindowViews(); + method @FlaggedApi("android.view.flags.root_view_changed_listener") public static void removeGlobalWindowViewsListener(@NonNull java.util.function.Consumer<java.util.List<android.view.View>>); } } diff --git a/core/java/android/view/ListenerWrapper.java b/core/java/android/view/ListenerWrapper.java new file mode 100644 index 000000000000..fcf3fdb68112 --- /dev/null +++ b/core/java/android/view/ListenerWrapper.java @@ -0,0 +1,56 @@ +/* + * 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); + } +} diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index a5da0c3ce5b1..624216776f42 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -44,6 +44,7 @@ import android.util.Log; import android.util.Pair; import android.util.SparseArray; import android.view.inputmethod.InputMethodManager; +import android.view.translation.ListenerGroup; import android.window.ITrustedPresentationListener; import android.window.InputTransferToken; import android.window.TrustedPresentationThresholds; @@ -58,6 +59,7 @@ import java.io.FileOutputStream; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.List; import java.util.WeakHashMap; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -147,6 +149,12 @@ public final class WindowManagerGlobal { @UnsupportedAppUsage private final ArrayList<View> mViews = new ArrayList<View>(); + /** + * The {@link ListenerGroup} that is associated to {@link #mViews}. + * @hide + */ + @GuardedBy("mLock") + private final ListenerGroup<List<View>> mWindowViewsListenerGroup = new ListenerGroup<>(); @UnsupportedAppUsage private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); @UnsupportedAppUsage @@ -319,6 +327,29 @@ public final class WindowManagerGlobal { } } + /** + * Adds a listener that will be notified whenever {@link #getWindowViews()} changes. The + * current value is provided immediately. If it was registered previously then this is ano op. + */ + public void addWindowViewsListener(@NonNull Executor executor, + @NonNull Consumer<List<View>> consumer) { + synchronized (mLock) { + mWindowViewsListenerGroup.addListener(executor, consumer); + mWindowViewsListenerGroup.accept(getWindowViews()); + } + } + + /** + * Removes a listener that was registered in + * {@link #addWindowViewsListener(Executor, Consumer)}. If it was not registered previously, + * then this is a no op. + */ + public void removeWindowViewsListener(@NonNull Consumer<List<View>> consumer) { + synchronized (mLock) { + mWindowViewsListenerGroup.removeListener(consumer); + } + } + public View getWindowView(IBinder windowToken) { synchronized (mLock) { final int numViews = mViews.size(); @@ -454,6 +485,7 @@ public final class WindowManagerGlobal { // do this last because it fires off messages to start doing things try { root.setView(view, wparams, panelParentView, userId); + mWindowViewsListenerGroup.accept(getWindowViews()); } catch (RuntimeException e) { Log.e(TAG, "Couldn't add view: " + view, e); final int viewIndex = (index >= 0) ? index : (mViews.size() - 1); @@ -575,6 +607,7 @@ public final class WindowManagerGlobal { mDyingViews.remove(view); } allViewsRemoved = mRoots.isEmpty(); + mWindowViewsListenerGroup.accept(getWindowViews()); } // If we don't have any views anymore in our process, we no longer need the diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig index d06f885638b6..d97310494d34 100644 --- a/core/java/android/view/flags/view_flags.aconfig +++ b/core/java/android/view/flags/view_flags.aconfig @@ -159,3 +159,11 @@ flag { bug: "364653005" is_fixed_read_only: true } + +flag { + name: "root_view_changed_listener" + namespace: "windowing_sdk" + description: "Implement listener pattern for WindowInspector#getGlobalWindowViews." + bug: "394397033" + is_fixed_read_only: false +} diff --git a/core/java/android/view/inspector/WindowInspector.java b/core/java/android/view/inspector/WindowInspector.java index 69d004e860fd..3ebca3c9d9b6 100644 --- a/core/java/android/view/inspector/WindowInspector.java +++ b/core/java/android/view/inspector/WindowInspector.java @@ -16,11 +16,14 @@ package android.view.inspector; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.view.View; import android.view.WindowManagerGlobal; import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Provides access to window inspection information. @@ -37,4 +40,25 @@ public final class WindowInspector { public static List<View> getGlobalWindowViews() { return WindowManagerGlobal.getInstance().getWindowViews(); } + + /** + * Adds a listener that is notified whenever the list of global window views changes. If a + * {@link Consumer} is already registered this method is a no op. + * @see #getGlobalWindowViews() + */ + @FlaggedApi(android.view.flags.Flags.FLAG_ROOT_VIEW_CHANGED_LISTENER) + public static void addGlobalWindowViewsListener(@NonNull Executor executor, + @NonNull Consumer<List<View>> consumer) { + WindowManagerGlobal.getInstance().addWindowViewsListener(executor, consumer); + } + + /** + * Removes a listener from getting notifications of global window views changes. If the + * {@link Consumer} is not registered this method is a no op. + * @see #addGlobalWindowViewsListener(Executor, Consumer) + */ + @FlaggedApi(android.view.flags.Flags.FLAG_ROOT_VIEW_CHANGED_LISTENER) + public static void removeGlobalWindowViewsListener(@NonNull Consumer<List<View>> consumer) { + WindowManagerGlobal.getInstance().removeWindowViewsListener(consumer); + } } diff --git a/core/java/android/view/translation/ListenerGroup.java b/core/java/android/view/translation/ListenerGroup.java new file mode 100644 index 000000000000..bf506815f841 --- /dev/null +++ b/core/java/android/view/translation/ListenerGroup.java @@ -0,0 +1,87 @@ +/* + * 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.translation; + +import android.annotation.NonNull; +import android.view.ListenerWrapper; + +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. + * @param <T> the type of the value to be reported. + * @hide + */ +public class ListenerGroup<T> { + private final List<ListenerWrapper<T>> mListeners = new ArrayList<>(); + + /** + * Relays the value to all the registered {@link java.util.function.Consumer} + */ + public void accept(@NonNull T value) { + Objects.requireNonNull(value); + for (int i = 0; i < mListeners.size(); i++) { + mListeners.get(i).accept(value); + } + } + + /** + * Adds a {@link Consumer} to the group. If the {@link Consumer} is already present then this + * is a no op. + */ + public void addListener(@NonNull Executor executor, @NonNull Consumer<T> consumer) { + if (isContained(consumer)) { + return; + } + mListeners.add(new ListenerWrapper<>(executor, consumer)); + } + + /** + * Removes a {@link Consumer} from the group. If the {@link Consumer} was not present then this + * 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. + */ + private boolean isContained(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; + } +} |