Migrate WindowContext to WindowContextListener mechanism

1. Use registerWindowContextListener instead of
   addWindowTokenWithOptions
2. Clean up the logic of sending the config to the client in WindowToken
3. Add a mechanism in WMS#addWindow, when a WindowContext adds a view,
   the server side will switch to register the WindowToken for the
   WindowContext.

In this way, we won't add a WindowToken until adding the fist view.
Also, we could apply an existing window token by overriding
WindowManager.LayoutParams.token.

Bug: 159767464
Bug: 153369119
Test: atest WindowContextTests WindowContextPolicyTests
Test: atest WindowContextTest
Test: atest WindowContextListenerControllerTests

Change-Id: I2396187c445306d18101706c4521edd9cf92facf
This commit is contained in:
Charles Chen
2020-12-04 16:22:01 +08:00
parent c43b7aacb0
commit 9854a1e315
5 changed files with 198 additions and 149 deletions

View File

@@ -15,8 +15,7 @@
*/
package android.app;
import static android.view.WindowManagerGlobal.ADD_OKAY;
import static android.view.WindowManagerGlobal.ADD_TOO_MANY_TOKENS;
import static android.view.WindowManagerImpl.createWindowContextWindowManager;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -28,8 +27,8 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.view.Display;
import android.view.IWindowManager;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import android.view.WindowManagerImpl;
import com.android.internal.annotations.VisibleForTesting;
@@ -46,10 +45,10 @@ import java.lang.ref.Reference;
*/
@UiContext
public class WindowContext extends ContextWrapper {
private final WindowManagerImpl mWindowManager;
private final WindowManager mWindowManager;
private final IWindowManager mWms;
private final WindowTokenClient mToken;
private boolean mOwnsToken;
private boolean mListenerRegistered;
/**
* Default constructor. Will generate a {@link WindowTokenClient} and attach this context to
@@ -86,25 +85,14 @@ public class WindowContext extends ContextWrapper {
mToken.attachContext(this);
mWindowManager = new WindowManagerImpl(this);
mWindowManager.setDefaultToken(mToken);
mWindowManager = createWindowContextWindowManager(this);
int result;
try {
// Register the token with WindowManager. This will also call back with the current
// config back to the client.
result = mWms.addWindowTokenWithOptions(
mToken, type, getDisplayId(), options, getPackageName());
mListenerRegistered = mWms.registerWindowContextListener(mToken, type, getDisplayId(),
options);
} catch (RemoteException e) {
mOwnsToken = false;
throw e.rethrowFromSystemServer();
}
if (result == ADD_TOO_MANY_TOKENS) {
throw new UnsupportedOperationException("createWindowContext failed! Too many unused "
+ "window contexts. Please see Context#createWindowContext documentation for "
+ "detail.");
}
mOwnsToken = result == ADD_OKAY;
Reference.reachabilityFence(this);
}
@@ -131,10 +119,10 @@ public class WindowContext extends ContextWrapper {
/** Used for test to invoke because we can't invoke finalize directly. */
@VisibleForTesting
public void release() {
if (mOwnsToken) {
if (mListenerRegistered) {
mListenerRegistered = false;
try {
mWms.removeWindowToken(mToken, getDisplayId());
mOwnsToken = false;
mWms.unregisterWindowContextListener(mToken);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}

View File

@@ -75,6 +75,7 @@ import android.view.Display;
import android.view.DisplayAdjustments;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams.WindowType;
import android.view.autofill.AutofillManager.AutofillClient;
@@ -6101,18 +6102,19 @@ public abstract class Context {
*
* // WindowManager.LayoutParams initialization
* ...
* // The types used in addView and createWindowContext must match.
* mParams.type = TYPE_APPLICATION_OVERLAY;
* ...
*
* mWindowContext.getSystemService(WindowManager.class).addView(overlayView, mParams);
* windowContext.getSystemService(WindowManager.class).addView(overlayView, mParams);
* </pre>
*
* <p>
* This context's configuration and resources are adjusted to a display area where the windows
* with provided type will be added. <b>Note that all windows associated with the same context
* will have an affinity and can only be moved together between different displays or areas on a
* display.</b> If there is a need to add different window types, or non-associated windows,
* separate Contexts should be used.
* This context's configuration and resources are adjusted to an area of the display where
* the windows with provided type will be added. <b>Note that all windows associated with the
* same context will have an affinity and can only be moved together between different displays
* or areas on a display.</b> If there is a need to add different window types, or
* non-associated windows, separate Contexts should be used.
* </p>
* <p>
* Creating a window context is an expensive operation. Misuse of this API may lead to a huge
@@ -6120,7 +6122,43 @@ public abstract class Context {
* An approach is to create one window context with specific window type and display and
* use it everywhere it's needed.
* </p>
* <p>
* After {@link Build.VERSION_CODES#S}, window context provides the capability to receive
* configuration changes for existing token by overriding the
* {@link android.view.WindowManager.LayoutParams#token token} of the
* {@link android.view.WindowManager.LayoutParams} passed in
* {@link WindowManager#addView(View, LayoutParams)}. This is useful when an application needs
* to attach its window to an existing activity for window token sharing use-case.
* </p>
* <p>
* Note that the window context in {@link Build.VERSION_CODES#R} didn't have this
* capability. This is a no-op for the window context in {@link Build.VERSION_CODES#R}.
* </p>
* Below is sample code to <b>attach an existing token to a window context:</b>
* <pre class="prettyprint">
* final DisplayManager dm = anyContext.getSystemService(DisplayManager.class);
* final Display primaryDisplay = dm.getDisplay(DEFAULT_DISPLAY);
* final Context windowContext = anyContext.createWindowContext(primaryDisplay,
* TYPE_APPLICATION, null);
*
* // Get an existing token.
* final IBinder existingToken = activity.getWindow().getAttributes().token;
*
* // The types used in addView() and createWindowContext() must match.
* final WindowManager.LayoutParams params = new WindowManager.LayoutParams(TYPE_APPLICATION);
* params.token = existingToken;
*
* // After WindowManager#addView(), the server side will extract the provided token from
* // LayoutParams#token (existingToken in the sample code), and switch to propagate
* // configuration changes from the node associated with the provided token.
* windowContext.getSystemService(WindowManager.class).addView(overlayView, mParams);
* </pre>
* <p>
* Note that using {@link android.app.Application} or {@link android.app.Service} context for
* UI-related queries may result in layout or continuity issues on devices with variable screen
* sizes (e.g. foldables) or in multi-window modes, since these non-UI contexts may not reflect
* the {@link Configuration} changes for the visual container.
* </p>
* @param type Window type in {@link WindowManager.LayoutParams}
* @param options A bundle used to pass window-related options
* @return A {@link Context} that can be used to create
@@ -6132,9 +6170,7 @@ public abstract class Context {
* @see #LAYOUT_INFLATER_SERVICE
* @see #WALLPAPER_SERVICE
* @throws UnsupportedOperationException if this {@link Context} does not attach to a display,
* such as {@link android.app.Application Application} or {@link android.app.Service Service},
* or the current number of window contexts without adding any view by
* {@link WindowManager#addView} <b>exceeds five</b>.
* such as {@link android.app.Application Application} or {@link android.app.Service Service}.
*/
@UiContext
@NonNull

View File

@@ -148,6 +148,9 @@
<!-- WindowMetricsTest permissions -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- WindowContextTest permissions -->
<uses-permission android:name="android.permission.MANAGE_APP_TOKENS" />
<!-- Allow use of PendingIntent.getIntent() -->
<uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT" />

View File

@@ -18,29 +18,38 @@ package android.app;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.content.Intent;
import android.hardware.display.DisplayManager;
import android.os.Binder;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import android.view.Display;
import android.view.IWindowManager;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams.WindowType;
import android.view.WindowManagerGlobal;
import android.view.WindowManagerImpl;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Tests for {@link WindowContext}
*
@@ -54,20 +63,14 @@ import org.junit.runner.RunWith;
@SmallTest
@Presubmit
public class WindowContextTest {
@Rule
public ActivityTestRule<EmptyActivity> mActivityRule =
new ActivityTestRule<>(EmptyActivity.class, false /* initialTouchMode */,
false /* launchActivity */);
private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
private final WindowContext mWindowContext = createWindowContext();
@Test
public void testWindowContextRelease_doRemoveWindowToken() throws Throwable {
final IBinder token = mWindowContext.getWindowContextToken();
final IWindowManager wms = WindowManagerGlobal.getWindowManagerService();
assertTrue("Token must be registered to WMS", wms.isWindowToken(token));
mWindowContext.release();
assertFalse("Token must be unregistered to WMS", wms.isWindowToken(token));
}
private final IWindowManager mWms = WindowManagerGlobal.getWindowManagerService();
@Test
public void testCreateWindowContextWindowManagerAttachClientToken() {
@@ -83,12 +86,133 @@ public class WindowContextTest {
assertEquals(mWindowContext.getWindowContextToken(), params.mWindowContextToken);
}
/**
* Test the {@link WindowContext} life cycle behavior to add a new window token:
* <ul>
* <li>The window token is created before adding the first view.</li>
* <li>The window token is registered after adding the first view.</li>
* <li>The window token is removed after {@link WindowContext}'s release.</li>
* </ul>
*/
@Test
public void testCreateWindowContextNewTokenFromClient() throws Throwable {
final IBinder token = mWindowContext.getWindowContextToken();
// Test that the window token is not created yet.
assertFalse("Token must not be registered until adding the first window",
mWms.isWindowToken(token));
final WindowManager.LayoutParams params =
new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
final View testView = new View(mWindowContext);
final CountDownLatch latch = new CountDownLatch(1);
testView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
latch.countDown();
}
@Override
public void onViewDetachedFromWindow(View v) {}
});
mInstrumentation.runOnMainSync(() -> {
mWindowContext.getSystemService(WindowManager.class).addView(testView, params);
assertEquals(token, params.mWindowContextToken);
});
assertTrue(latch.await(4, TimeUnit.SECONDS));
// Verify that the window token of the window context is created after first addView().
assertTrue("Token must exist after adding the first view.",
mWms.isWindowToken(token));
mWindowContext.release();
// After the window context's release, the window token is also removed.
assertFalse("Token must be removed after release.", mWms.isWindowToken(token));
}
/**
* Verifies the behavior when window context attaches an {@link Activity} by override
* {@link WindowManager.LayoutParams#token}.
*
* The window context token should be overridden to
* {@link android.view.WindowManager.LayoutParams} and the {@link Activity}'s token must
* not be removed regardless of the release of window context.
*/
@Test
public void testCreateWindowContext_AttachActivity_TokenNotRemovedAfterRelease()
throws Throwable {
mActivityRule.launchActivity(new Intent());
final Activity activity = mActivityRule.getActivity();
final WindowManager.LayoutParams params = activity.getWindow().getAttributes();
final WindowContext windowContext = createWindowContext(params.type);
final IBinder token = windowContext.getWindowContextToken();
final View testView = new View(windowContext);
mInstrumentation.runOnMainSync(() -> {
windowContext.getSystemService(WindowManager.class).addView(testView, params);
assertEquals(token, params.mWindowContextToken);
});
windowContext.release();
// Even if the window context is released, the activity should still exist.
assertTrue("Token must exist even if the window context is released.",
mWms.isWindowToken(activity.getActivityToken()));
}
/**
* Verifies the behavior when window context attaches an existing token by override
* {@link WindowManager.LayoutParams#token}.
*
* The window context token should be overridden to
* {@link android.view.WindowManager.LayoutParams} and the {@link Activity}'s token must not be
* removed regardless of release of window context.
*/
@Test
public void testCreateWindowContext_AttachWindowToken_TokenNotRemovedAfterRelease()
throws Throwable {
final WindowContext windowContext = createWindowContext(TYPE_INPUT_METHOD);
final IBinder token = windowContext.getWindowContextToken();
final IBinder existingToken = new Binder();
mWms.addWindowToken(existingToken, TYPE_INPUT_METHOD, windowContext.getDisplayId());
final WindowManager.LayoutParams params =
new WindowManager.LayoutParams(TYPE_INPUT_METHOD);
params.token = existingToken;
final View testView = new View(windowContext);
mInstrumentation.runOnMainSync(() -> {
windowContext.getSystemService(WindowManager.class).addView(testView, params);
assertEquals(token, params.mWindowContextToken);
});
windowContext.release();
// Even if the window context is released, the existing token should still exist.
assertTrue("Token must exist even if the window context is released.",
mWms.isWindowToken(existingToken));
mWms.removeWindowToken(existingToken, DEFAULT_DISPLAY);
}
private WindowContext createWindowContext() {
return createWindowContext(TYPE_APPLICATION_OVERLAY);
}
private WindowContext createWindowContext(@WindowType int type) {
final Context instContext = mInstrumentation.getTargetContext();
final Display display = instContext.getSystemService(DisplayManager.class)
.getDisplay(DEFAULT_DISPLAY);
final Context context = instContext.createDisplayContext(display);
return new WindowContext(context, TYPE_APPLICATION_OVERLAY,
null /* options */);
return (WindowContext) instContext.createWindowContext(display, type, null /* options */);
}
}

View File

@@ -17,7 +17,6 @@
package com.android.server.wm;
import static android.os.Process.INVALID_UID;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
@@ -26,7 +25,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_MOVEMENT;
import static com.android.internal.protolog.ProtoLogGroup.WM_ERROR;
import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
@@ -41,7 +39,6 @@ import static com.android.server.wm.WindowTokenProto.WINDOW_CONTAINER;
import android.annotation.CallSuper;
import android.annotation.Nullable;
import android.app.IWindowToken;
import android.app.servertransaction.FixedRotationAdjustmentsItem;
import android.content.res.Configuration;
import android.graphics.Rect;
@@ -58,7 +55,6 @@ import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.WindowManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.server.policy.WindowManagerPolicy;
@@ -112,17 +108,10 @@ class WindowToken extends WindowContainer<WindowState> {
private FixedRotationTransformState mFixedRotationTransformState;
private Configuration mLastReportedConfig;
private int mLastReportedDisplay = INVALID_DISPLAY;
/**
* When set to {@code true}, this window token is created from {@link android.app.WindowContext}
*/
@VisibleForTesting
final boolean mFromClientToken;
private DeathRecipient mDeathRecipient;
private boolean mBinderDied = false;
private final boolean mFromClientToken;
private final int mOwnerUid;
@@ -188,30 +177,6 @@ class WindowToken extends WindowContainer<WindowState> {
}
}
private class DeathRecipient implements IBinder.DeathRecipient {
private boolean mHasUnlinkToDeath = false;
@Override
public void binderDied() {
synchronized (mWmService.mGlobalLock) {
mBinderDied = true;
removeImmediately();
}
}
void linkToDeath() throws RemoteException {
token.linkToDeath(DeathRecipient.this, 0);
}
void unlinkToDeath() {
if (mHasUnlinkToDeath) {
return;
}
token.unlinkToDeath(DeathRecipient.this, 0);
mHasUnlinkToDeath = true;
}
}
/**
* Compares two child window of this token and returns -1 if the first is lesser than the
* second in terms of z-order and 1 otherwise.
@@ -266,17 +231,6 @@ class WindowToken extends WindowContainer<WindowState> {
if (dc != null) {
dc.addWindowToken(token, this);
}
if (shouldReportToClient()) {
try {
mDeathRecipient = new DeathRecipient();
mDeathRecipient.linkToDeath();
} catch (RemoteException e) {
Slog.e(TAG, "Unable to add window token with type " + windowType + " on "
+ "display " + dc.getDisplayId(), e);
mDeathRecipient = null;
return;
}
}
}
void removeAllWindowsIfPossible() {
@@ -414,22 +368,6 @@ class WindowToken extends WindowContainer<WindowState> {
// Needs to occur after the token is removed from the display above to avoid attempt at
// duplicate removal of this window container from it's parent.
super.removeImmediately();
reportWindowTokenRemovedToClient();
}
// TODO(b/159767464): Remove after we migrate to listener approach.
private void reportWindowTokenRemovedToClient() {
if (!shouldReportToClient()) {
return;
}
mDeathRecipient.unlinkToDeath();
IWindowToken windowTokenClient = IWindowToken.Stub.asInterface(token);
try {
windowTokenClient.onWindowTokenRemoved();
} catch (RemoteException e) {
ProtoLog.w(WM_ERROR, "Could not report token removal to the window token client.");
}
}
@Override
@@ -441,51 +379,11 @@ class WindowToken extends WindowContainer<WindowState> {
// to another display before the window behind
// it is ready.
super.onDisplayChanged(dc);
reportConfigToWindowTokenClient();
}
@Override
public void onConfigurationChanged(Configuration newParentConfig) {
super.onConfigurationChanged(newParentConfig);
reportConfigToWindowTokenClient();
}
void reportConfigToWindowTokenClient() {
if (!shouldReportToClient()) {
return;
}
if (mLastReportedConfig == null) {
mLastReportedConfig = new Configuration();
}
final Configuration config = getConfiguration();
final int displayId = getDisplayContent().getDisplayId();
if (config.diff(mLastReportedConfig) == 0 && displayId == mLastReportedDisplay) {
// No changes since last reported time.
return;
}
mLastReportedConfig.setTo(config);
mLastReportedDisplay = displayId;
IWindowToken windowTokenClient = IWindowToken.Stub.asInterface(token);
try {
windowTokenClient.onConfigurationChanged(config, displayId);
} catch (RemoteException e) {
ProtoLog.w(WM_ERROR,
"Could not report config changes to the window token client.");
}
}
/**
* @return {@code true} if this {@link WindowToken} is not an {@link ActivityRecord} and
* registered from client side.
*/
private boolean shouldReportToClient() {
// Only report to client for WindowToken because Activities are updated through ATM
// callbacks.
return asActivityRecord() == null
// Report to {@link android.view.WindowTokenClient} if this token was registered from it.
&& mFromClientToken && !mBinderDied;
}
@Override