diff options
18 files changed, 1151 insertions, 209 deletions
diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java index fd5517d29d74..f28574ecb3b2 100644 --- a/core/java/android/view/AttachedSurfaceControl.java +++ b/core/java/android/view/AttachedSurfaceControl.java @@ -27,9 +27,6 @@ import android.window.SurfaceSyncGroup; import com.android.window.flags.Flags; -import java.util.concurrent.Executor; -import java.util.function.Consumer; - /** * Provides an interface to the root-Surface of a View Hierarchy or Window. This * is used in combination with the {@link android.view.SurfaceControl} API to enable @@ -197,42 +194,6 @@ public interface AttachedSurfaceControl { } /** - * Add a trusted presentation listener on the SurfaceControl associated with this window. - * - * @param t Transaction that the trusted presentation listener is added on. This should - * be applied by the caller. - * @param thresholds The {@link SurfaceControl.TrustedPresentationThresholds} that will specify - * when the to invoke the callback. - * @param executor The {@link Executor} where the callback will be invoked on. - * @param listener The {@link Consumer} that will receive the callbacks when entered or - * exited the threshold. - * - * @see SurfaceControl.Transaction#setTrustedPresentationCallback(SurfaceControl, - * SurfaceControl.TrustedPresentationThresholds, Executor, Consumer) - * - * @hide b/287076178 un-hide with API bump - */ - default void addTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, - @NonNull SurfaceControl.TrustedPresentationThresholds thresholds, - @NonNull Executor executor, @NonNull Consumer<Boolean> listener) { - } - - /** - * Remove a trusted presentation listener on the SurfaceControl associated with this window. - * - * @param t Transaction that the trusted presentation listener removed on. This should - * be applied by the caller. - * @param listener The {@link Consumer} that was previously registered with - * addTrustedPresentationCallback that should be removed. - * - * @see SurfaceControl.Transaction#clearTrustedPresentationCallback(SurfaceControl) - * @hide b/287076178 un-hide with API bump - */ - default void removeTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, - @NonNull Consumer<Boolean> listener) { - } - - /** * Transfer the currently in progress touch gesture from the host to the requested * {@link SurfaceControlViewHost.SurfacePackage}. This requires that the * SurfaceControlViewHost was created with the current host's inputToken. diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 17bbee6d020f..36b74e39072a 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -73,6 +73,8 @@ import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; import android.window.ScreenCapture; import android.window.WindowContextInfo; +import android.window.ITrustedPresentationListener; +import android.window.TrustedPresentationThresholds; /** * System private interface to the window manager. @@ -1075,4 +1077,10 @@ interface IWindowManager @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MONITOR_INPUT)") void unregisterDecorViewGestureListener(IDecorViewGestureListener listener, int displayId); + + void registerTrustedPresentationListener(in IBinder window, in ITrustedPresentationListener listener, + in TrustedPresentationThresholds thresholds, int id); + + + void unregisterTrustedPresentationListener(in ITrustedPresentationListener listener, int id); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index cac5387116a1..8acab2a43823 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -11913,18 +11913,6 @@ public final class ViewRootImpl implements ViewParent, scheduleTraversals(); } - @Override - public void addTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, - @NonNull SurfaceControl.TrustedPresentationThresholds thresholds, - @NonNull Executor executor, @NonNull Consumer<Boolean> listener) { - t.setTrustedPresentationCallback(getSurfaceControl(), thresholds, executor, listener); - } - - @Override - public void removeTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, - @NonNull Consumer<Boolean> listener) { - t.clearTrustedPresentationCallback(getSurfaceControl()); - } private void logAndTrace(String msg) { if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 046ea77f196d..f668088e6b44 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -122,7 +122,9 @@ import android.view.WindowInsets.Side.InsetsSide; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.accessibility.AccessibilityNodeInfo; +import android.window.ITrustedPresentationListener; import android.window.TaskFpsCallback; +import android.window.TrustedPresentationThresholds; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -5884,4 +5886,35 @@ public interface WindowManager extends ViewManager { default boolean replaceContentOnDisplayWithSc(int displayId, @NonNull SurfaceControl sc) { throw new UnsupportedOperationException(); } + + /** + * Add a trusted presentation listener associated with a window. If the listener has already + * been registered, an AndroidRuntimeException will be thrown. + * + * @param window The Window to add the trusted presentation listener for + * @param thresholds The {@link TrustedPresentationThresholds} that will specify + * when the to invoke the callback. + * @param executor The {@link Executor} where the callback will be invoked on. + * @param listener The {@link ITrustedPresentationListener} that will receive the callbacks + * when entered or exited trusted presentation per the thresholds. + * + * @hide b/287076178 un-hide with API bump + */ + default void registerTrustedPresentationListener(@NonNull IBinder window, + @NonNull TrustedPresentationThresholds thresholds, @NonNull Executor executor, + @NonNull Consumer<Boolean> listener) { + throw new UnsupportedOperationException(); + } + + /** + * Removes a presentation listener associated with a window. If the listener was not previously + * registered, the call will be a noop. + * + * @hide + * @see #registerTrustedPresentationListener(IBinder, + * TrustedPresentationThresholds, Executor, Consumer) + */ + default void unregisterTrustedPresentationListener(@NonNull Consumer<Boolean> listener) { + throw new UnsupportedOperationException(); + } } diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index 214f1ec3d1ec..a7d814e9ab8c 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -30,9 +30,13 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; import android.util.AndroidRuntimeException; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; +import android.util.Pair; import android.view.inputmethod.InputMethodManager; +import android.window.ITrustedPresentationListener; +import android.window.TrustedPresentationThresholds; import com.android.internal.util.FastPrintWriter; @@ -43,6 +47,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.WeakHashMap; import java.util.concurrent.Executor; +import java.util.function.Consumer; import java.util.function.IntConsumer; /** @@ -143,6 +148,9 @@ public final class WindowManagerGlobal { private Runnable mSystemPropertyUpdater; + private final TrustedPresentationListener mTrustedPresentationListener = + new TrustedPresentationListener(); + private WindowManagerGlobal() { } @@ -324,7 +332,7 @@ public final class WindowManagerGlobal { final Context context = view.getContext(); if (context != null && (context.getApplicationInfo().flags - & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) { + & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) { wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; } } @@ -482,7 +490,7 @@ public final class WindowManagerGlobal { if (who != null) { WindowLeaked leak = new WindowLeaked( what + " " + who + " has leaked window " - + root.getView() + " that was originally added here"); + + root.getView() + " that was originally added here"); leak.setStackTrace(root.getLocation().getStackTrace()); Log.e(TAG, "", leak); } @@ -790,6 +798,86 @@ public final class WindowManagerGlobal { } } + public void registerTrustedPresentationListener(@NonNull IBinder window, + @NonNull TrustedPresentationThresholds thresholds, Executor executor, + @NonNull Consumer<Boolean> listener) { + mTrustedPresentationListener.addListener(window, thresholds, listener, executor); + } + + public void unregisterTrustedPresentationListener(@NonNull Consumer<Boolean> listener) { + mTrustedPresentationListener.removeListener(listener); + } + + private final class TrustedPresentationListener extends + ITrustedPresentationListener.Stub { + private static int sId = 0; + private final ArrayMap<Consumer<Boolean>, Pair<Integer, Executor>> mListeners = + new ArrayMap<>(); + + private final Object mTplLock = new Object(); + + private void addListener(IBinder window, TrustedPresentationThresholds thresholds, + Consumer<Boolean> listener, Executor executor) { + synchronized (mTplLock) { + if (mListeners.containsKey(listener)) { + throw new AndroidRuntimeException("Trying to add duplicate listener"); + } + int id = sId++; + mListeners.put(listener, new Pair<>(id, executor)); + try { + WindowManagerGlobal.getWindowManagerService() + .registerTrustedPresentationListener(window, this, thresholds, id); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + } + + private void removeListener(Consumer<Boolean> listener) { + synchronized (mTplLock) { + var removedListener = mListeners.remove(listener); + if (removedListener == null) { + Log.i(TAG, "listener " + listener + " does not exist."); + return; + } + + try { + WindowManagerGlobal.getWindowManagerService() + .unregisterTrustedPresentationListener(this, removedListener.first); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + } + + @Override + public void onTrustedPresentationChanged(int[] inTrustedStateListenerIds, + int[] outOfTrustedStateListenerIds) { + ArrayList<Runnable> firedListeners = new ArrayList<>(); + synchronized (mTplLock) { + mListeners.forEach((listener, idExecutorPair) -> { + final var listenerId = idExecutorPair.first; + final var executor = idExecutorPair.second; + for (int id : inTrustedStateListenerIds) { + if (listenerId == id) { + firedListeners.add(() -> executor.execute( + () -> listener.accept(/*presentationState*/true))); + } + } + for (int id : outOfTrustedStateListenerIds) { + if (listenerId == id) { + firedListeners.add(() -> executor.execute( + () -> listener.accept(/*presentationState*/false))); + } + } + }); + } + for (int i = 0; i < firedListeners.size(); i++) { + firedListeners.get(i).run(); + } + } + } + /** @hide */ public void addWindowlessRoot(ViewRootImpl impl) { synchronized (mLock) { @@ -801,7 +889,7 @@ public final class WindowManagerGlobal { public void removeWindowlessRoot(ViewRootImpl impl) { synchronized (mLock) { mWindowlessRoots.remove(impl); - } + } } public void setRecentsAppBehindSystemBars(boolean behindSystemBars) { diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index d7b74b3bcfe2..b4b1fde89a46 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -37,6 +37,7 @@ import android.os.StrictMode; import android.util.Log; import android.window.ITaskFpsCallback; import android.window.TaskFpsCallback; +import android.window.TrustedPresentationThresholds; import android.window.WindowContext; import android.window.WindowMetricsController; import android.window.WindowProvider; @@ -508,4 +509,17 @@ public final class WindowManagerImpl implements WindowManager { } return false; } + + @Override + public void registerTrustedPresentationListener(@NonNull IBinder window, + @NonNull TrustedPresentationThresholds thresholds, @NonNull Executor executor, + @NonNull Consumer<Boolean> listener) { + mGlobal.registerTrustedPresentationListener(window, thresholds, executor, listener); + } + + @Override + public void unregisterTrustedPresentationListener(@NonNull Consumer<Boolean> listener) { + mGlobal.unregisterTrustedPresentationListener(listener); + + } } diff --git a/core/java/android/window/ITrustedPresentationListener.aidl b/core/java/android/window/ITrustedPresentationListener.aidl new file mode 100644 index 000000000000..b33128abb7e5 --- /dev/null +++ b/core/java/android/window/ITrustedPresentationListener.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 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.window; + +/** + * @hide + */ +oneway interface ITrustedPresentationListener { + void onTrustedPresentationChanged(in int[] enteredTrustedStateIds, in int[] exitedTrustedStateIds); +}
\ No newline at end of file diff --git a/core/java/android/window/TrustedPresentationListener.java b/core/java/android/window/TrustedPresentationListener.java new file mode 100644 index 000000000000..02fd6d98fb0d --- /dev/null +++ b/core/java/android/window/TrustedPresentationListener.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 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.window; + +/** + * @hide + */ +public interface TrustedPresentationListener { + + void onTrustedPresentationChanged(boolean inTrustedPresentationState); + +} diff --git a/core/java/android/window/TrustedPresentationThresholds.aidl b/core/java/android/window/TrustedPresentationThresholds.aidl new file mode 100644 index 000000000000..d7088bf0fddc --- /dev/null +++ b/core/java/android/window/TrustedPresentationThresholds.aidl @@ -0,0 +1,3 @@ +package android.window; + +parcelable TrustedPresentationThresholds; diff --git a/core/java/android/window/TrustedPresentationThresholds.java b/core/java/android/window/TrustedPresentationThresholds.java new file mode 100644 index 000000000000..801d35c49228 --- /dev/null +++ b/core/java/android/window/TrustedPresentationThresholds.java @@ -0,0 +1,127 @@ +/* + * Copyright 2023 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.window; + +import android.annotation.FloatRange; +import android.annotation.IntRange; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; + +/** + * @hide + */ +public final class TrustedPresentationThresholds implements Parcelable { + /** + * The min alpha the {@link SurfaceControl} is required to have to be considered inside the + * threshold. + */ + @FloatRange(from = 0f, fromInclusive = false, to = 1f) + public final float mMinAlpha; + + /** + * The min fraction of the SurfaceControl that was presented to the user to be considered + * inside the threshold. + */ + @FloatRange(from = 0f, fromInclusive = false, to = 1f) + public final float mMinFractionRendered; + + /** + * The time in milliseconds required for the {@link SurfaceControl} to be in the threshold. + */ + @IntRange(from = 1) + public final int mStabilityRequirementMs; + + private void checkValid() { + if (mMinAlpha <= 0 || mMinFractionRendered <= 0 || mStabilityRequirementMs < 1) { + throw new IllegalArgumentException( + "TrustedPresentationThresholds values are invalid"); + } + } + + /** + * Creates a new TrustedPresentationThresholds. + * + * @param minAlpha The min alpha the {@link SurfaceControl} is required to + * have to be considered inside the + * threshold. + * @param minFractionRendered The min fraction of the SurfaceControl that was presented + * to the user to be considered + * inside the threshold. + * @param stabilityRequirementMs The time in milliseconds required for the + * {@link SurfaceControl} to be in the threshold. + */ + public TrustedPresentationThresholds( + @FloatRange(from = 0f, fromInclusive = false, to = 1f) float minAlpha, + @FloatRange(from = 0f, fromInclusive = false, to = 1f) float minFractionRendered, + @IntRange(from = 1) int stabilityRequirementMs) { + this.mMinAlpha = minAlpha; + this.mMinFractionRendered = minFractionRendered; + this.mStabilityRequirementMs = stabilityRequirementMs; + checkValid(); + } + + @Override + public String toString() { + return "TrustedPresentationThresholds { " + + "minAlpha = " + mMinAlpha + ", " + + "minFractionRendered = " + mMinFractionRendered + ", " + + "stabilityRequirementMs = " + mStabilityRequirementMs + + " }"; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeFloat(mMinAlpha); + dest.writeFloat(mMinFractionRendered); + dest.writeInt(mStabilityRequirementMs); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * @hide + */ + TrustedPresentationThresholds(@NonNull Parcel in) { + mMinAlpha = in.readFloat(); + mMinFractionRendered = in.readFloat(); + mStabilityRequirementMs = in.readInt(); + + checkValid(); + } + + /** + * @hide + */ + public static final @NonNull Creator<TrustedPresentationThresholds> CREATOR = + new Creator<TrustedPresentationThresholds>() { + @Override + public TrustedPresentationThresholds[] newArray(int size) { + return new TrustedPresentationThresholds[size]; + } + + @Override + public TrustedPresentationThresholds createFromParcel(@NonNull Parcel in) { + return new TrustedPresentationThresholds(in); + } + }; +} diff --git a/core/java/com/android/internal/protolog/ProtoLogGroup.java b/core/java/com/android/internal/protolog/ProtoLogGroup.java index 4bb7c33b41e2..8c2a52560050 100644 --- a/core/java/com/android/internal/protolog/ProtoLogGroup.java +++ b/core/java/com/android/internal/protolog/ProtoLogGroup.java @@ -93,6 +93,7 @@ public enum ProtoLogGroup implements IProtoLogGroup { WM_DEBUG_DREAM(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, Consts.TAG_WM), WM_DEBUG_DIMMER(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), + WM_DEBUG_TPL(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), TEST_GROUP(true, true, false, "WindowManagerProtoLogTest"); private final boolean mEnabled; diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 2237ba1924db..19128212094d 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -595,6 +595,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-1518132958": { + "message": "fractionRendered boundsOverSource=%f", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "-1517908912": { "message": "requestScrollCapture: caught exception dispatching to window.token=%s", "level": "WARN", @@ -961,6 +967,12 @@ "group": "WM_DEBUG_CONTENT_RECORDING", "at": "com\/android\/server\/wm\/ContentRecorder.java" }, + "-1209762265": { + "message": "Registering listener=%s with id=%d for window=%s with %s", + "level": "DEBUG", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "-1209252064": { "message": "Clear animatingExit: reason=clearAnimatingFlags win=%s", "level": "DEBUG", @@ -1333,6 +1345,12 @@ "group": "WM_DEBUG_WINDOW_TRANSITIONS", "at": "com\/android\/server\/wm\/Transition.java" }, + "-888703350": { + "message": "Skipping %s", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "-883738232": { "message": "Adding more than one toast window for UID at a time.", "level": "WARN", @@ -2803,6 +2821,12 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "360319850": { + "message": "fractionRendered scale=%f", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "364992694": { "message": "freezeDisplayRotation: current rotation=%d, new rotation=%d, caller=%s", "level": "VERBOSE", @@ -2983,6 +3007,12 @@ "group": "WM_DEBUG_BACK_PREVIEW", "at": "com\/android\/server\/wm\/BackNavigationController.java" }, + "532771960": { + "message": "Adding untrusted state listener=%s with id=%d", + "level": "DEBUG", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "535103992": { "message": "Wallpaper may change! Adjusting", "level": "VERBOSE", @@ -3061,6 +3091,12 @@ "group": "WM_DEBUG_DREAM", "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java" }, + "605179032": { + "message": "checkIfInThreshold fractionRendered=%f alpha=%f currTimeMs=%d", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "608694300": { "message": " NEW SURFACE SESSION %s", "level": "INFO", @@ -3289,6 +3325,12 @@ "group": "WM_SHOW_TRANSACTIONS", "at": "com\/android\/server\/wm\/WindowState.java" }, + "824532141": { + "message": "lastState=%s newState=%s alpha=%f minAlpha=%f fractionRendered=%f minFractionRendered=%f", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "829434921": { "message": "Draw state now committed in %s", "level": "VERBOSE", @@ -3583,6 +3625,12 @@ "group": "WM_SHOW_SURFACE_ALLOC", "at": "com\/android\/server\/wm\/ScreenRotationAnimation.java" }, + "1090378847": { + "message": "Checking %d windows", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1100065297": { "message": "Attempted to get IME policy of a display that does not exist: %d", "level": "WARN", @@ -3715,6 +3763,12 @@ "group": "WM_DEBUG_FOCUS", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "1251721200": { + "message": "unregister failed, couldn't find deathRecipient for %s with id=%d", + "level": "ERROR", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1252594551": { "message": "Window types in WindowContext and LayoutParams.type should match! Type from LayoutParams is %d, but type from WindowContext is %d", "level": "WARN", @@ -3853,6 +3907,12 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/TaskDisplayArea.java" }, + "1382634842": { + "message": "Unregistering listener=%s with id=%d", + "level": "DEBUG", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1393721079": { "message": "Starting remote display change: from [rot = %d], to [%dx%d, rot = %d]", "level": "VERBOSE", @@ -3901,6 +3961,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "1445704347": { + "message": "coveredRegionsAbove updated with %s frame:%s region:%s", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1448683958": { "message": "Override pending remote transitionSet=%b adapter=%s", "level": "INFO", @@ -4201,6 +4267,12 @@ "group": "WM_DEBUG_RECENTS_ANIMATIONS", "at": "com\/android\/server\/wm\/RecentsAnimation.java" }, + "1786463281": { + "message": "Adding trusted state listener=%s with id=%d", + "level": "DEBUG", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1789321832": { "message": "Then token:%s is invalid. It might be removed", "level": "WARN", @@ -4375,6 +4447,12 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/RootWindowContainer.java" }, + "1955470028": { + "message": "computeFractionRendered: visibleRegion=%s screenBounds=%s contentSize=%s scale=%f,%f", + "level": "VERBOSE", + "group": "WM_DEBUG_TPL", + "at": "com\/android\/server\/wm\/TrustedPresentationListenerController.java" + }, "1964565370": { "message": "Starting remote animation", "level": "INFO", @@ -4659,6 +4737,9 @@ "WM_DEBUG_TASKS": { "tag": "WindowManager" }, + "WM_DEBUG_TPL": { + "tag": "WindowManager" + }, "WM_DEBUG_WALLPAPER": { "tag": "WindowManager" }, diff --git a/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java new file mode 100644 index 000000000000..e82dc37c2b6a --- /dev/null +++ b/services/core/java/com/android/server/wm/TrustedPresentationListenerController.java @@ -0,0 +1,448 @@ +/* + * Copyright 2023 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.server.wm; + +import static android.graphics.Matrix.MSCALE_X; +import static android.graphics.Matrix.MSCALE_Y; +import static android.graphics.Matrix.MSKEW_X; +import static android.graphics.Matrix.MSKEW_Y; + +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TPL; + +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.IntArray; +import android.util.Pair; +import android.util.Size; +import android.view.InputWindowHandle; +import android.window.ITrustedPresentationListener; +import android.window.TrustedPresentationThresholds; +import android.window.WindowInfosListener; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.server.wm.utils.RegionUtils; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Optional; + +/** + * Class to handle TrustedPresentationListener registrations in a thread safe manner. This class + * also takes care of cleaning up listeners when the remote process dies. + */ +public class TrustedPresentationListenerController { + + // Should only be accessed by the posting to the handler + private class Listeners { + private final class ListenerDeathRecipient implements IBinder.DeathRecipient { + IBinder mListenerBinder; + int mInstances; + + ListenerDeathRecipient(IBinder listenerBinder) { + mListenerBinder = listenerBinder; + mInstances = 0; + try { + mListenerBinder.linkToDeath(this, 0); + } catch (RemoteException ignore) { + } + } + + void addInstance() { + mInstances++; + } + + // return true if there are no instances alive + boolean removeInstance() { + mInstances--; + if (mInstances > 0) { + return false; + } + mListenerBinder.unlinkToDeath(this, 0); + return true; + } + + public void binderDied() { + mHandler.post(() -> { + mUniqueListeners.remove(mListenerBinder); + removeListeners(mListenerBinder, Optional.empty()); + }); + } + } + + // tracks binder deaths for cleanup + ArrayMap<IBinder, ListenerDeathRecipient> mUniqueListeners = new ArrayMap<>(); + ArrayMap<IBinder /*window*/, ArrayList<TrustedPresentationInfo>> mWindowToListeners = + new ArrayMap<>(); + + void register(IBinder window, ITrustedPresentationListener listener, + TrustedPresentationThresholds thresholds, int id) { + var listenersForWindow = mWindowToListeners.computeIfAbsent(window, + iBinder -> new ArrayList<>()); + listenersForWindow.add(new TrustedPresentationInfo(thresholds, id, listener)); + + // register death listener + var listenerBinder = listener.asBinder(); + var deathRecipient = mUniqueListeners.computeIfAbsent(listenerBinder, + ListenerDeathRecipient::new); + deathRecipient.addInstance(); + } + + void unregister(ITrustedPresentationListener trustedPresentationListener, int id) { + var listenerBinder = trustedPresentationListener.asBinder(); + var deathRecipient = mUniqueListeners.get(listenerBinder); + if (deathRecipient == null) { + ProtoLog.e(WM_DEBUG_TPL, "unregister failed, couldn't find" + + " deathRecipient for %s with id=%d", trustedPresentationListener, id); + return; + } + + if (deathRecipient.removeInstance()) { + mUniqueListeners.remove(listenerBinder); + } + removeListeners(listenerBinder, Optional.of(id)); + } + + boolean isEmpty() { + return mWindowToListeners.isEmpty(); + } + + ArrayList<TrustedPresentationInfo> get(IBinder windowToken) { + return mWindowToListeners.get(windowToken); + } + + private void removeListeners(IBinder listenerBinder, Optional<Integer> id) { + for (int i = mWindowToListeners.size() - 1; i >= 0; i--) { + var listeners = mWindowToListeners.valueAt(i); + for (int j = listeners.size() - 1; j >= 0; j--) { + var listener = listeners.get(j); + if (listener.mListener.asBinder() == listenerBinder && (id.isEmpty() + || listener.mId == id.get())) { + listeners.remove(j); + } + } + if (listeners.isEmpty()) { + mWindowToListeners.removeAt(i); + } + } + } + } + + private final Object mHandlerThreadLock = new Object(); + private HandlerThread mHandlerThread; + private Handler mHandler; + + private WindowInfosListener mWindowInfosListener; + + Listeners mRegisteredListeners = new Listeners(); + + private InputWindowHandle[] mLastWindowHandles; + + private final Object mIgnoredWindowTokensLock = new Object(); + + private final ArraySet<IBinder> mIgnoredWindowTokens = new ArraySet<>(); + + private void startHandlerThreadIfNeeded() { + synchronized (mHandlerThreadLock) { + if (mHandler == null) { + mHandlerThread = new HandlerThread("WindowInfosListenerForTpl"); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + } + } + } + + void addIgnoredWindowTokens(IBinder token) { + synchronized (mIgnoredWindowTokensLock) { + mIgnoredWindowTokens.add(token); + } + } + + void removeIgnoredWindowTokens(IBinder token) { + synchronized (mIgnoredWindowTokensLock) { + mIgnoredWindowTokens.remove(token); + } + } + + void registerListener(IBinder window, ITrustedPresentationListener listener, + TrustedPresentationThresholds thresholds, int id) { + startHandlerThreadIfNeeded(); + mHandler.post(() -> { + ProtoLog.d(WM_DEBUG_TPL, "Registering listener=%s with id=%d for window=%s with %s", + listener, id, window, thresholds); + + mRegisteredListeners.register(window, listener, thresholds, id); + registerWindowInfosListener(); + // Update the initial state for the new registered listener + computeTpl(mLastWindowHandles); + }); + } + + void unregisterListener(ITrustedPresentationListener listener, int id) { + startHandlerThreadIfNeeded(); + mHandler.post(() -> { + ProtoLog.d(WM_DEBUG_TPL, "Unregistering listener=%s with id=%d", + listener, id); + + mRegisteredListeners.unregister(listener, id); + if (mRegisteredListeners.isEmpty()) { + unregisterWindowInfosListener(); + } + }); + } + + void dump(PrintWriter pw) { + final String innerPrefix = " "; + pw.println("TrustedPresentationListenerController:"); + pw.println(innerPrefix + "Active unique listeners (" + + mRegisteredListeners.mUniqueListeners.size() + "):"); + for (int i = 0; i < mRegisteredListeners.mWindowToListeners.size(); i++) { + pw.println( + innerPrefix + " window=" + mRegisteredListeners.mWindowToListeners.keyAt(i)); + final var listeners = mRegisteredListeners.mWindowToListeners.valueAt(i); + for (int j = 0; j < listeners.size(); j++) { + final var listener = listeners.get(j); + pw.println(innerPrefix + innerPrefix + " listener=" + listener.mListener.asBinder() + + " id=" + listener.mId + + " thresholds=" + listener.mThresholds); + } + } + } + + private void registerWindowInfosListener() { + if (mWindowInfosListener != null) { + return; + } + + mWindowInfosListener = new WindowInfosListener() { + @Override + public void onWindowInfosChanged(InputWindowHandle[] windowHandles, + DisplayInfo[] displayInfos) { + mHandler.post(() -> computeTpl(windowHandles)); + } + }; + mLastWindowHandles = mWindowInfosListener.register().first; + } + + private void unregisterWindowInfosListener() { + if (mWindowInfosListener == null) { + return; + } + + mWindowInfosListener.unregister(); + mWindowInfosListener = null; + mLastWindowHandles = null; + } + + private void computeTpl(InputWindowHandle[] windowHandles) { + mLastWindowHandles = windowHandles; + if (mLastWindowHandles == null || mLastWindowHandles.length == 0 + || mRegisteredListeners.isEmpty()) { + return; + } + + Rect tmpRect = new Rect(); + Matrix tmpInverseMatrix = new Matrix(); + float[] tmpMatrix = new float[9]; + Region coveredRegionsAbove = new Region(); + long currTimeMs = System.currentTimeMillis(); + ProtoLog.v(WM_DEBUG_TPL, "Checking %d windows", mLastWindowHandles.length); + + ArrayMap<ITrustedPresentationListener, Pair<IntArray, IntArray>> listenerUpdates = + new ArrayMap<>(); + ArraySet<IBinder> ignoredWindowTokens; + synchronized (mIgnoredWindowTokensLock) { + ignoredWindowTokens = new ArraySet<>(mIgnoredWindowTokens); + } + for (var windowHandle : mLastWindowHandles) { + if (ignoredWindowTokens.contains(windowHandle.getWindowToken())) { + ProtoLog.v(WM_DEBUG_TPL, "Skipping %s", windowHandle.name); + continue; + } + tmpRect.set(windowHandle.frame); + var listeners = mRegisteredListeners.get(windowHandle.getWindowToken()); + if (listeners != null) { + Region region = new Region(); + region.op(tmpRect, coveredRegionsAbove, Region.Op.DIFFERENCE); + windowHandle.transform.invert(tmpInverseMatrix); + tmpInverseMatrix.getValues(tmpMatrix); + float scaleX = (float) Math.sqrt(tmpMatrix[MSCALE_X] * tmpMatrix[MSCALE_X] + + tmpMatrix[MSKEW_X] * tmpMatrix[MSKEW_X]); + float scaleY = (float) Math.sqrt(tmpMatrix[MSCALE_Y] * tmpMatrix[MSCALE_Y] + + tmpMatrix[MSKEW_Y] * tmpMatrix[MSKEW_Y]); + + float fractionRendered = computeFractionRendered(region, new RectF(tmpRect), + windowHandle.contentSize, + scaleX, scaleY); + + checkIfInThreshold(listeners, listenerUpdates, fractionRendered, windowHandle.alpha, + currTimeMs); + } + + coveredRegionsAbove.op(tmpRect, Region.Op.UNION); + ProtoLog.v(WM_DEBUG_TPL, "coveredRegionsAbove updated with %s frame:%s region:%s", + windowHandle.name, tmpRect.toShortString(), coveredRegionsAbove); + } + + for (int i = 0; i < listenerUpdates.size(); i++) { + var updates = listenerUpdates.valueAt(i); + var listener = listenerUpdates.keyAt(i); + try { + listener.onTrustedPresentationChanged(updates.first.toArray(), + updates.second.toArray()); + } catch (RemoteException ignore) { + } + } + } + + private void addListenerUpdate( + ArrayMap<ITrustedPresentationListener, Pair<IntArray, IntArray>> listenerUpdates, + ITrustedPresentationListener listener, int id, boolean presentationState) { + var updates = listenerUpdates.get(listener); + if (updates == null) { + updates = new Pair<>(new IntArray(), new IntArray()); + listenerUpdates.put(listener, updates); + } + if (presentationState) { + updates.first.add(id); + } else { + updates.second.add(id); + } + } + + + private void checkIfInThreshold( + ArrayList<TrustedPresentationInfo> listeners, + ArrayMap<ITrustedPresentationListener, Pair<IntArray, IntArray>> listenerUpdates, + float fractionRendered, float alpha, long currTimeMs) { + ProtoLog.v(WM_DEBUG_TPL, "checkIfInThreshold fractionRendered=%f alpha=%f currTimeMs=%d", + fractionRendered, alpha, currTimeMs); + for (int i = 0; i < listeners.size(); i++) { + var trustedPresentationInfo = listeners.get(i); + var listener = trustedPresentationInfo.mListener; + boolean lastState = trustedPresentationInfo.mLastComputedTrustedPresentationState; + boolean newState = + (alpha >= trustedPresentationInfo.mThresholds.mMinAlpha) && (fractionRendered + >= trustedPresentationInfo.mThresholds.mMinFractionRendered); + trustedPresentationInfo.mLastComputedTrustedPresentationState = newState; + + ProtoLog.v(WM_DEBUG_TPL, + "lastState=%s newState=%s alpha=%f minAlpha=%f fractionRendered=%f " + + "minFractionRendered=%f", + lastState, newState, alpha, trustedPresentationInfo.mThresholds.mMinAlpha, + fractionRendered, trustedPresentationInfo.mThresholds.mMinFractionRendered); + + if (lastState && !newState) { + // We were in the trusted presentation state, but now we left it, + // emit the callback if needed + if (trustedPresentationInfo.mLastReportedTrustedPresentationState) { + trustedPresentationInfo.mLastReportedTrustedPresentationState = false; + addListenerUpdate(listenerUpdates, listener, + trustedPresentationInfo.mId, /*presentationState*/ false); + ProtoLog.d(WM_DEBUG_TPL, "Adding untrusted state listener=%s with id=%d", + listener, trustedPresentationInfo.mId); + } + // Reset the timer + trustedPresentationInfo.mEnteredTrustedPresentationStateTime = -1; + } else if (!lastState && newState) { + // We were not in the trusted presentation state, but we entered it, begin the timer + // and make sure this gets called at least once more! + trustedPresentationInfo.mEnteredTrustedPresentationStateTime = currTimeMs; + mHandler.postDelayed(() -> { + computeTpl(mLastWindowHandles); + }, (long) (trustedPresentationInfo.mThresholds.mStabilityRequirementMs * 1.5)); + } + + // Has the timer elapsed, but we are still in the state? Emit a callback if needed + if (!trustedPresentationInfo.mLastReportedTrustedPresentationState && newState && ( + currTimeMs - trustedPresentationInfo.mEnteredTrustedPresentationStateTime + > trustedPresentationInfo.mThresholds.mStabilityRequirementMs)) { + trustedPresentationInfo.mLastReportedTrustedPresentationState = true; + addListenerUpdate(listenerUpdates, listener, + trustedPresentationInfo.mId, /*presentationState*/ true); + ProtoLog.d(WM_DEBUG_TPL, "Adding trusted state listener=%s with id=%d", + listener, trustedPresentationInfo.mId); + } + } + } + + private float computeFractionRendered(Region visibleRegion, RectF screenBounds, + Size contentSize, + float sx, float sy) { + ProtoLog.v(WM_DEBUG_TPL, + "computeFractionRendered: visibleRegion=%s screenBounds=%s contentSize=%s " + + "scale=%f,%f", + visibleRegion, screenBounds, contentSize, sx, sy); + + if (contentSize.getWidth() == 0 || contentSize.getHeight() == 0) { + return -1; + } + if (screenBounds.width() == 0 || screenBounds.height() == 0) { + return -1; + } + + float fractionRendered = Math.min(sx * sy, 1.0f); + ProtoLog.v(WM_DEBUG_TPL, "fractionRendered scale=%f", fractionRendered); + + float boundsOverSourceW = screenBounds.width() / (float) contentSize.getWidth(); + float boundsOverSourceH = screenBounds.height() / (float) contentSize.getHeight(); + fractionRendered *= boundsOverSourceW * boundsOverSourceH; + ProtoLog.v(WM_DEBUG_TPL, "fractionRendered boundsOverSource=%f", fractionRendered); + // Compute the size of all the rects since they may be disconnected. + float[] visibleSize = new float[1]; + RegionUtils.forEachRect(visibleRegion, rect -> { + float size = rect.width() * rect.height(); + visibleSize[0] += size; + }); + + fractionRendered *= visibleSize[0] / (screenBounds.width() * screenBounds.height()); + return fractionRendered; + } + + private static class TrustedPresentationInfo { + boolean mLastComputedTrustedPresentationState = false; + boolean mLastReportedTrustedPresentationState = false; + long mEnteredTrustedPresentationStateTime = -1; + final TrustedPresentationThresholds mThresholds; + + final ITrustedPresentationListener mListener; + final int mId; + + private TrustedPresentationInfo(TrustedPresentationThresholds thresholds, int id, + ITrustedPresentationListener listener) { + mThresholds = thresholds; + mId = id; + mListener = listener; + checkValid(thresholds); + } + + private void checkValid(TrustedPresentationThresholds thresholds) { + if (thresholds.mMinAlpha <= 0 || thresholds.mMinFractionRendered <= 0 + || thresholds.mStabilityRequirementMs < 1) { + throw new IllegalArgumentException( + "TrustedPresentationThresholds values are invalid"); + } + } + } +} diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index dd2b48bb5a3d..eaed3f94c5f5 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -303,9 +303,11 @@ import android.window.AddToSurfaceSyncGroupResult; import android.window.ClientWindowFrames; import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; +import android.window.ITrustedPresentationListener; import android.window.ScreenCapture; import android.window.SystemPerformanceHinter; import android.window.TaskSnapshot; +import android.window.TrustedPresentationThresholds; import android.window.WindowContainerToken; import android.window.WindowContextInfo; @@ -764,6 +766,9 @@ public class WindowManagerService extends IWindowManager.Stub private final SurfaceSyncGroupController mSurfaceSyncGroupController = new SurfaceSyncGroupController(); + final TrustedPresentationListenerController mTrustedPresentationListenerController = + new TrustedPresentationListenerController(); + @VisibleForTesting final class SettingsObserver extends ContentObserver { private final Uri mDisplayInversionEnabledUri = @@ -7171,6 +7176,7 @@ public class WindowManagerService extends IWindowManager.Stub pw.println(separator); } mSystemPerformanceHinter.dump(pw, ""); + mTrustedPresentationListenerController.dump(pw); } } @@ -9762,4 +9768,17 @@ public class WindowManagerService extends IWindowManager.Stub Binder.restoreCallingIdentity(origId); } } + + @Override + public void registerTrustedPresentationListener(IBinder window, + ITrustedPresentationListener listener, + TrustedPresentationThresholds thresholds, int id) { + mTrustedPresentationListenerController.registerListener(window, listener, thresholds, id); + } + + @Override + public void unregisterTrustedPresentationListener(ITrustedPresentationListener listener, + int id) { + mTrustedPresentationListenerController.unregisterListener(listener, id); + } } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index e1f1f662c5aa..7bc7e2cb780b 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1189,6 +1189,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP ProtoLog.v(WM_DEBUG_ADD_REMOVE, "Adding %s to %s", this, parentWindow); parentWindow.addChild(this, sWindowSubLayerComparator); } + + if (token.mRoundedCornerOverlay) { + mWmService.mTrustedPresentationListenerController.addIgnoredWindowTokens( + getWindowToken()); + } } @Override @@ -2393,6 +2398,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } mWmService.postWindowRemoveCleanupLocked(this); + + mWmService.mTrustedPresentationListenerController.removeIgnoredWindowTokens( + getWindowToken()); } @Override diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml index c3074bb0fee8..a8d3fa110844 100644 --- a/services/tests/wmtests/AndroidManifest.xml +++ b/services/tests/wmtests/AndroidManifest.xml @@ -99,7 +99,7 @@ android:theme="@style/WhiteBackgroundTheme" android:exported="true"/> - <activity android:name="com.android.server.wm.TrustedPresentationCallbackTest$TestActivity" + <activity android:name="com.android.server.wm.TrustedPresentationListenerTest$TestActivity" android:exported="true" android:showWhenLocked="true" android:turnScreenOn="true" /> diff --git a/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java b/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java deleted file mode 100644 index c5dd447b5b0c..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationCallbackTest.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (C) 2023 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.server.wm; - -import static android.server.wm.ActivityManagerTestBase.createFullscreenActivityScenarioRule; -import static android.server.wm.BuildUtils.HW_TIMEOUT_MULTIPLIER; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import android.app.Activity; -import android.platform.test.annotations.Presubmit; -import android.server.wm.CtsWindowInfoUtils; -import android.view.SurfaceControl; -import android.view.SurfaceControl.TrustedPresentationThresholds; - -import androidx.annotation.GuardedBy; -import androidx.test.ext.junit.rules.ActivityScenarioRule; - -import com.android.server.wm.utils.CommonUtils; - -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestName; - -import java.util.function.Consumer; - -/** - * TODO (b/287076178): Move these tests to - * {@link android.view.surfacecontrol.cts.TrustedPresentationCallbackTest} when API is made public - */ -@Presubmit -public class TrustedPresentationCallbackTest { - private static final String TAG = "TrustedPresentationCallbackTest"; - private static final int STABILITY_REQUIREMENT_MS = 500; - private static final long WAIT_TIME_MS = HW_TIMEOUT_MULTIPLIER * 2000L; - - private static final float FRACTION_VISIBLE = 0.1f; - - private final Object mResultsLock = new Object(); - @GuardedBy("mResultsLock") - private boolean mResult; - @GuardedBy("mResultsLock") - private boolean mReceivedResults; - - @Rule - public TestName mName = new TestName(); - - @Rule - public ActivityScenarioRule<TestActivity> mActivityRule = createFullscreenActivityScenarioRule( - TestActivity.class); - - private TestActivity mActivity; - - @Before - public void setup() { - mActivityRule.getScenario().onActivity(activity -> mActivity = activity); - } - - @After - public void tearDown() { - CommonUtils.waitUntilActivityRemoved(mActivity); - } - - @Test - public void testAddTrustedPresentationListenerOnWindow() throws InterruptedException { - TrustedPresentationThresholds thresholds = new TrustedPresentationThresholds( - 1 /* minAlpha */, FRACTION_VISIBLE, STABILITY_REQUIREMENT_MS); - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - mActivity.getWindow().getRootSurfaceControl().addTrustedPresentationCallback(t, thresholds, - Runnable::run, inTrustedPresentationState -> { - synchronized (mResultsLock) { - mResult = inTrustedPresentationState; - mReceivedResults = true; - mResultsLock.notify(); - } - }); - t.apply(); - synchronized (mResultsLock) { - assertResults(); - } - } - - @Test - public void testRemoveTrustedPresentationListenerOnWindow() throws InterruptedException { - TrustedPresentationThresholds thresholds = new TrustedPresentationThresholds( - 1 /* minAlpha */, FRACTION_VISIBLE, STABILITY_REQUIREMENT_MS); - Consumer<Boolean> trustedPresentationCallback = inTrustedPresentationState -> { - synchronized (mResultsLock) { - mResult = inTrustedPresentationState; - mReceivedResults = true; - mResultsLock.notify(); - } - }; - SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - mActivity.getWindow().getRootSurfaceControl().addTrustedPresentationCallback(t, thresholds, - Runnable::run, trustedPresentationCallback); - t.apply(); - - synchronized (mResultsLock) { - if (!mReceivedResults) { - mResultsLock.wait(WAIT_TIME_MS); - } - assertResults(); - // reset the state - mReceivedResults = false; - } - - mActivity.getWindow().getRootSurfaceControl().removeTrustedPresentationCallback(t, - trustedPresentationCallback); - t.apply(); - - synchronized (mResultsLock) { - if (!mReceivedResults) { - mResultsLock.wait(WAIT_TIME_MS); - } - // Ensure we waited the full time and never received a notify on the result from the - // callback. - assertFalse("Should never have received a callback", mReceivedResults); - // results shouldn't have changed. - assertTrue(mResult); - } - } - - @GuardedBy("mResultsLock") - private void assertResults() throws InterruptedException { - mResultsLock.wait(WAIT_TIME_MS); - - if (!mReceivedResults) { - CtsWindowInfoUtils.dumpWindowsOnScreen(TAG, "test " + mName.getMethodName()); - } - // Make sure we received the results and not just timed out - assertTrue("Timed out waiting for results", mReceivedResults); - assertTrue(mResult); - } - - public static class TestActivity extends Activity { - } -} diff --git a/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java b/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java new file mode 100644 index 000000000000..96b66bfd3bc0 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/TrustedPresentationListenerTest.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2023 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.server.wm; + +import static android.server.wm.ActivityManagerTestBase.createFullscreenActivityScenarioRule; +import static android.server.wm.BuildUtils.HW_TIMEOUT_MULTIPLIER; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +import android.app.Activity; +import android.os.SystemClock; +import android.platform.test.annotations.Presubmit; +import android.server.wm.CtsWindowInfoUtils; +import android.util.AndroidRuntimeException; +import android.util.Log; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.WindowManager; +import android.window.TrustedPresentationThresholds; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.test.ext.junit.rules.ActivityScenarioRule; + +import com.android.server.wm.utils.CommonUtils; + +import junit.framework.Assert; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * TODO (b/287076178): Move these tests to + * {@link android.view.surfacecontrol.cts.TrustedPresentationListenerTest} when API is made public + */ +@Presubmit +public class TrustedPresentationListenerTest { + private static final String TAG = "TrustedPresentationListenerTest"; + private static final int STABILITY_REQUIREMENT_MS = 500; + private static final long WAIT_TIME_MS = HW_TIMEOUT_MULTIPLIER * 2000L; + + private static final float FRACTION_VISIBLE = 0.1f; + + private final List<Boolean> mResults = Collections.synchronizedList(new ArrayList<>()); + private CountDownLatch mReceivedResults = new CountDownLatch(1); + + private TrustedPresentationThresholds mThresholds = new TrustedPresentationThresholds( + 1 /* minAlpha */, FRACTION_VISIBLE, STABILITY_REQUIREMENT_MS); + + @Rule + public TestName mName = new TestName(); + + @Rule + public ActivityScenarioRule<TestActivity> mActivityRule = createFullscreenActivityScenarioRule( + TestActivity.class); + + private TestActivity mActivity; + + private SurfaceControlViewHost.SurfacePackage mSurfacePackage = null; + + @Before + public void setup() { + mActivityRule.getScenario().onActivity(activity -> mActivity = activity); + mDefaultListener = new Listener(mReceivedResults); + } + + @After + public void tearDown() { + if (mSurfacePackage != null) { + new SurfaceControl.Transaction().remove(mSurfacePackage.getSurfaceControl()).apply( + true); + mSurfacePackage.release(); + } + CommonUtils.waitUntilActivityRemoved(mActivity); + + } + + private class Listener implements Consumer<Boolean> { + final CountDownLatch mLatch; + + Listener(CountDownLatch latch) { + mLatch = latch; + } + + @Override + public void accept(Boolean inTrustedPresentationState) { + Log.d(TAG, "onTrustedPresentationChanged " + inTrustedPresentationState); + mResults.add(inTrustedPresentationState); + mLatch.countDown(); + } + } + + private Consumer<Boolean> mDefaultListener; + + @Test + public void testAddTrustedPresentationListenerOnWindow() { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, Runnable::run, + mDefaultListener); + assertResults(List.of(true)); + } + + @Test + public void testRemoveTrustedPresentationListenerOnWindow() throws InterruptedException { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, Runnable::run, + mDefaultListener); + assertResults(List.of(true)); + // reset the latch + mReceivedResults = new CountDownLatch(1); + + windowManager.unregisterTrustedPresentationListener(mDefaultListener); + mReceivedResults.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS); + // Ensure we waited the full time and never received a notify on the result from the + // callback. + assertEquals("Should never have received a callback", mReceivedResults.getCount(), 1); + // results shouldn't have changed. + assertEquals(mResults, List.of(true)); + } + + @Test + public void testRemovingUnknownListenerIsANoop() { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + assertNotNull(windowManager); + windowManager.unregisterTrustedPresentationListener(mDefaultListener); + } + + @Test + public void testAddDuplicateListenerThrowsException() { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + assertNotNull(windowManager); + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, + Runnable::run, mDefaultListener); + assertThrows(AndroidRuntimeException.class, + () -> windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, + Runnable::run, mDefaultListener)); + } + + @Test + public void testAddDuplicateThresholds() { + mReceivedResults = new CountDownLatch(2); + mDefaultListener = new Listener(mReceivedResults); + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, + Runnable::run, mDefaultListener); + + Consumer<Boolean> mNewListener = new Listener(mReceivedResults); + + windowManager.registerTrustedPresentationListener( + mActivity.getWindow().getDecorView().getWindowToken(), mThresholds, + Runnable::run, mNewListener); + assertResults(List.of(true, true)); + } + + private void waitForViewAttach(View view) { + final CountDownLatch viewAttached = new CountDownLatch(1); + view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(@NonNull View v) { + viewAttached.countDown(); + } + + @Override + public void onViewDetachedFromWindow(@NonNull View v) { + + } + }); + try { + viewAttached.await(2000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (!wait(viewAttached, 2000 /* waitTimeMs */)) { + fail("Couldn't attach view=" + view); + } + } + + @Test + public void testAddListenerToScvh() { + WindowManager windowManager = mActivity.getSystemService(WindowManager.class); + + var embeddedView = new View(mActivity); + mActivityRule.getScenario().onActivity(activity -> { + var attachedSurfaceControl = + mActivity.getWindow().getDecorView().getRootSurfaceControl(); + var scvh = new SurfaceControlViewHost(mActivity, mActivity.getDisplay(), + attachedSurfaceControl.getHostToken()); + mSurfacePackage = scvh.getSurfacePackage(); + scvh.setView(embeddedView, mActivity.getWindow().getDecorView().getWidth(), + mActivity.getWindow().getDecorView().getHeight()); + attachedSurfaceControl.buildReparentTransaction( + mSurfacePackage.getSurfaceControl()); + }); + + waitForViewAttach(embeddedView); + windowManager.registerTrustedPresentationListener(embeddedView.getWindowToken(), + mThresholds, + Runnable::run, mDefaultListener); + + assertResults(List.of(true)); + } + + private boolean wait(CountDownLatch latch, long waitTimeMs) { + while (true) { + long now = SystemClock.uptimeMillis(); + try { + return latch.await(waitTimeMs, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + long elapsedTime = SystemClock.uptimeMillis() - now; + waitTimeMs = Math.max(0, waitTimeMs - elapsedTime); + } + } + + } + + @GuardedBy("mResultsLock") + private void assertResults(List<Boolean> results) { + if (!wait(mReceivedResults, WAIT_TIME_MS)) { + try { + CtsWindowInfoUtils.dumpWindowsOnScreen(TAG, "test " + mName.getMethodName()); + } catch (InterruptedException e) { + Log.d(TAG, "Couldn't dump windows", e); + } + Assert.fail("Timed out waiting for results mReceivedResults.count=" + + mReceivedResults.getCount() + "mReceivedResults=" + mReceivedResults); + } + + // Make sure we received the results + assertEquals(results.toArray(), mResults.toArray()); + } + + public static class TestActivity extends Activity { + } +} |