summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt2
-rw-r--r--core/java/android/view/ListenerWrapper.java56
-rw-r--r--core/java/android/view/WindowManagerGlobal.java33
-rw-r--r--core/java/android/view/flags/view_flags.aconfig8
-rw-r--r--core/java/android/view/inspector/WindowInspector.java24
-rw-r--r--core/java/android/view/translation/ListenerGroup.java87
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;
+ }
+}