Make caption a insets source
This patch introduced setCaptionInsets, and set the Insets in
ViewRootImpl when dispatch the insets if there's a caption.
Modification is made in Window and DecorCaptionView to make caption
overlay with the app content, and pass the value to ViewRootImpl to
apply when dispatch. It is necessary to trigger a dispatch when caption
enabled status chanaged, otherwise sometimes it will not be updated.
Because caption is now updated locally on the client side.
Some old logic to deal with the overlay caption without insets are
removed, including the touch event dispatch override, the color
override.
Bug: 134531136
Test: go/wm-smoke
Test: Manually change the value in dispatchApplyInsets, can observe a
blank content area when there's a caption bar.
Test: atest InsetsStateTest
Test: atest InsetsControllerTest
Change-Id: I356344a13c8569512d8f51f7ea19a5603f778252
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index e6bd843..40a460d 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -16,6 +16,7 @@
package android.view;
+import static android.view.InsetsState.ITYPE_CAPTION_BAR;
import static android.view.InsetsState.ITYPE_IME;
import static android.view.InsetsState.toInternalType;
import static android.view.InsetsState.toPublicType;
@@ -367,6 +368,7 @@
private int mLastLegacySystemUiFlags;
private DisplayCutout mLastDisplayCutout;
private boolean mStartingAnimation;
+ private int mCaptionInsetsHeight = 0;
private SyncRtSurfaceTransactionApplier mApplier;
@@ -460,7 +462,8 @@
@VisibleForTesting
public boolean onStateChanged(InsetsState state) {
- boolean localStateChanged = !mState.equals(state);
+ boolean localStateChanged = !mState.equals(state, true /* excludingCaptionInsets */)
+ || !captionInsetsUnchanged();
if (!localStateChanged && mLastDispachedState.equals(state)) {
return false;
}
@@ -470,7 +473,7 @@
if (localStateChanged) {
mViewRoot.notifyInsetsChanged();
}
- if (!mState.equals(mLastDispachedState)) {
+ if (!mState.equals(mLastDispachedState, true /* excludingCaptionInsets */)) {
sendStateToWindowManager();
}
return true;
@@ -488,6 +491,23 @@
mState.removeSource(source.getType());
}
}
+ if (mCaptionInsetsHeight != 0) {
+ mState.getSource(ITYPE_CAPTION_BAR).setFrame(new Rect(mFrame.left, mFrame.top,
+ mFrame.right, mFrame.top + mCaptionInsetsHeight));
+ }
+ }
+
+ private boolean captionInsetsUnchanged() {
+ if (mState.peekSource(ITYPE_CAPTION_BAR) == null
+ && mCaptionInsetsHeight == 0) {
+ return true;
+ }
+ if (mState.peekSource(ITYPE_CAPTION_BAR) != null
+ && mCaptionInsetsHeight
+ == mState.peekSource(ITYPE_CAPTION_BAR).getFrame().height()) {
+ return true;
+ }
+ return false;
}
/**
@@ -964,6 +984,7 @@
InsetsState tmpState = new InsetsState();
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
+ if (consumer.getType() == ITYPE_CAPTION_BAR) continue;
if (consumer.getControl() != null) {
tmpState.addSource(mState.getSource(consumer.getType()));
}
@@ -1105,6 +1126,11 @@
}
@Override
+ public void setCaptionInsetsHeight(int height) {
+ mCaptionInsetsHeight = height;
+ }
+
+ @Override
public void setSystemBarsBehavior(@Behavior int behavior) {
mViewRoot.mWindowAttributes.privateFlags |= PRIVATE_FLAG_BEHAVIOR_CONTROLLED;
if (mViewRoot.mWindowAttributes.insetsFlags.behavior != behavior) {
diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java
index 294faaf..033ccef 100644
--- a/core/java/android/view/InsetsSource.java
+++ b/core/java/android/view/InsetsSource.java
@@ -16,6 +16,7 @@
package android.view;
+import static android.view.InsetsState.ITYPE_CAPTION_BAR;
import static android.view.InsetsState.ITYPE_IME;
import android.annotation.NonNull;
@@ -118,6 +119,12 @@
if (!getIntersection(frame, relativeFrame, mTmpFrame)) {
return Insets.NONE;
}
+ // During drag-move and drag-resizing, the caption insets position may not get updated
+ // before the app frame get updated. To layout the app content correctly during drag events,
+ // we always return the insets with the corresponding height covering the top.
+ if (getType() == ITYPE_CAPTION_BAR) {
+ return Insets.of(0, frame.height(), 0, 0);
+ }
// TODO: Currently, non-floating IME always intersects at bottom due to issues with cutout.
// However, we should let the policy decide from the server.
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index 40e6f57..c515466 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -45,6 +45,8 @@
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -498,6 +500,19 @@
@Override
public boolean equals(Object o) {
+ return equals(o, false);
+ }
+
+ /**
+ * An equals method can exclude the caption insets. This is useful because we assemble the
+ * caption insets information on the client side, and when we communicate with server, it's
+ * excluded.
+ * @param excludingCaptionInsets {@code true} if we want to compare two InsetsState objects but
+ * ignore the caption insets source value.
+ * @return {@code true} if the two InsetsState objects are equal, {@code false} otherwise.
+ */
+ @VisibleForTesting
+ public boolean equals(Object o, boolean excludingCaptionInsets) {
if (this == o) { return true; }
if (o == null || getClass() != o.getClass()) { return false; }
@@ -506,11 +521,24 @@
if (!mDisplayFrame.equals(state.mDisplayFrame)) {
return false;
}
- if (mSources.size() != state.mSources.size()) {
+ int size = mSources.size();
+ int otherSize = state.mSources.size();
+ if (excludingCaptionInsets) {
+ if (mSources.get(ITYPE_CAPTION_BAR) != null) {
+ size--;
+ }
+ if (state.mSources.get(ITYPE_CAPTION_BAR) != null) {
+ otherSize--;
+ }
+ }
+ if (size != otherSize) {
return false;
}
for (int i = mSources.size() - 1; i >= 0; i--) {
InsetsSource source = mSources.valueAt(i);
+ if (excludingCaptionInsets) {
+ if (source.getType() == ITYPE_CAPTION_BAR) continue;
+ }
InsetsSource otherSource = state.mSources.get(source.getType());
if (otherSource == null) {
return false;
diff --git a/core/java/android/view/PendingInsetsController.java b/core/java/android/view/PendingInsetsController.java
index 229ee03..a106b2c 100644
--- a/core/java/android/view/PendingInsetsController.java
+++ b/core/java/android/view/PendingInsetsController.java
@@ -42,6 +42,7 @@
private InsetsController mReplayedInsetsController;
private ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners
= new ArrayList<>();
+ private int mCaptionInsetsHeight = 0;
@Override
public void show(int types) {
@@ -80,6 +81,11 @@
}
@Override
+ public void setCaptionInsetsHeight(int height) {
+ mCaptionInsetsHeight = height;
+ }
+
+ @Override
public void setSystemBarsBehavior(int behavior) {
if (mReplayedInsetsController != null) {
mReplayedInsetsController.setSystemBarsBehavior(behavior);
@@ -134,6 +140,9 @@
if (mAppearanceMask != 0) {
controller.setSystemBarsAppearance(mAppearance, mAppearanceMask);
}
+ if (mCaptionInsetsHeight != 0) {
+ controller.setCaptionInsetsHeight(mCaptionInsetsHeight);
+ }
int size = mRequests.size();
for (int i = 0; i < size; i++) {
mRequests.get(i).replay(controller);
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index d69357b..49bff6d 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -28768,6 +28768,11 @@
publicAlternatives = "Use {@link WindowInsets#getInsets(int)}")
final Rect mStableInsets = new Rect();
+ /**
+ * Current caption insets to the display coordinate.
+ */
+ final Rect mCaptionInsets = new Rect();
+
final DisplayCutout.ParcelableWrapper mDisplayCutout =
new DisplayCutout.ParcelableWrapper(DisplayCutout.NO_CUTOUT);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 50202ae..51304dc 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -158,6 +158,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.IResultReceiver;
import com.android.internal.os.SomeArgs;
+import com.android.internal.policy.DecorView;
import com.android.internal.policy.PhoneFallbackEventHandler;
import com.android.internal.util.Preconditions;
import com.android.internal.view.BaseSurfaceHolder;
@@ -2221,6 +2222,19 @@
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
+ private boolean updateCaptionInsets() {
+ if (!(mView instanceof DecorView)) return false;
+ final int captionInsetsHeight = ((DecorView) mView).getCaptionInsetsHeight();
+ final Rect captionFrame = new Rect();
+ if (captionInsetsHeight != 0) {
+ captionFrame.set(mWinFrame.left, mWinFrame.top, mWinFrame.right,
+ mWinFrame.top + captionInsetsHeight);
+ }
+ if (mAttachInfo.mCaptionInsets.equals(captionFrame)) return false;
+ mAttachInfo.mCaptionInsets.set(captionFrame);
+ return true;
+ }
+
private boolean shouldDispatchCutout() {
return mWindowAttributes.layoutInDisplayCutoutMode
== LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
@@ -2592,6 +2606,9 @@
mAttachInfo.mAlwaysConsumeSystemBars = mPendingAlwaysConsumeSystemBars;
dispatchApplyInsets = true;
}
+ if (updateCaptionInsets()) {
+ dispatchApplyInsets = true;
+ }
if (dispatchApplyInsets || mLastSystemUiVisibility !=
mAttachInfo.mSystemUiVisibility || mApplyInsetsRequested) {
mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 0c5c183..ae9afaba 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -49,9 +49,6 @@
import android.transition.TransitionManager;
import android.util.Pair;
import android.view.View.OnApplyWindowInsetsListener;
-import android.view.ViewGroup.LayoutParams;
-import android.view.WindowInsets.Side.InsetsSide;
-import android.view.WindowInsets.Type.InsetsType;
import android.view.accessibility.AccessibilityEvent;
import java.util.Collections;
@@ -323,7 +320,7 @@
@UnsupportedAppUsage
private boolean mDestroyed;
- private boolean mOverlayWithDecorCaptionEnabled = false;
+ private boolean mOverlayWithDecorCaptionEnabled = true;
private boolean mCloseOnSwipeEnabled = false;
// The current window attributes.
diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java
index fde184c..9b2a6cb 100644
--- a/core/java/android/view/WindowInsets.java
+++ b/core/java/android/view/WindowInsets.java
@@ -17,6 +17,7 @@
package android.view;
+import static android.view.WindowInsets.Type.CAPTION_BAR;
import static android.view.WindowInsets.Type.DISPLAY_CUTOUT;
import static android.view.WindowInsets.Type.FIRST;
import static android.view.WindowInsets.Type.IME;
diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java
index 0282eca..439223c 100644
--- a/core/java/android/view/WindowInsetsController.java
+++ b/core/java/android/view/WindowInsetsController.java
@@ -196,6 +196,15 @@
@Appearance int getSystemBarsAppearance();
/**
+ * Notify the caption insets height change. The information will be used on the client side to,
+ * make sure the InsetsState has the correct caption insets.
+ *
+ * @param height the height of caption bar insets.
+ * @hide
+ */
+ void setCaptionInsetsHeight(int height);
+
+ /**
* Controls the behavior of system bars.
*
* @param behavior Determines how the bars behave when being hidden by the application.
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 51b73fc..d2508f36 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -78,7 +78,6 @@
import android.view.Gravity;
import android.view.InputQueue;
import android.view.InsetsState;
-import android.view.InsetsController;
import android.view.InsetsState.InternalInsetsType;
import android.view.KeyEvent;
import android.view.KeyboardShortcutGroup;
@@ -1174,6 +1173,12 @@
false /* matchVertical */, statusBarNeedsLeftInset, statusBarSideInset,
animate && !disallowAnimate,
mForceWindowDrawsBarBackgrounds, state);
+
+ if (mHasCaption) {
+ final int captionColor = calculateStatusBarColor();
+ mDecorCaptionView.getCaption().setBackgroundColor(captionColor);
+ updateDecorCaptionShade();
+ }
}
// When we expand the window with FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS or
@@ -1355,7 +1360,7 @@
: state.attributes.isPresent(insetsState, mWindow.getAttributes().flags, force);
boolean show = state.attributes.isVisible(state.present, color,
mWindow.getAttributes().flags, force);
- boolean showView = show && !isResizing() && size > 0;
+ boolean showView = show && !isResizing() && !mHasCaption && size > 0;
boolean visibilityChanged = false;
View view = state.view;
@@ -2021,6 +2026,7 @@
if (getForeground() != null) {
drawableChanged();
}
+ getWindowInsetsController().setCaptionInsetsHeight(getCaptionInsetsHeight());
}
}
@@ -2094,6 +2100,7 @@
mDecorCaptionView.onConfigurationChanged(displayWindowDecor);
enableCaption(displayWindowDecor);
}
+ getWindowInsetsController().setCaptionInsetsHeight(getCaptionInsetsHeight());
}
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
@@ -2182,11 +2189,11 @@
inflater = inflater.from(context);
final DecorCaptionView view = (DecorCaptionView) inflater.inflate(R.layout.decor_caption,
null);
- setDecorCaptionShade(context, view);
+ setDecorCaptionShade(view);
return view;
}
- private void setDecorCaptionShade(Context context, DecorCaptionView view) {
+ private void setDecorCaptionShade(DecorCaptionView view) {
final int shade = mWindow.getDecorCaptionShade();
switch (shade) {
case DECOR_CAPTION_SHADE_LIGHT:
@@ -2196,15 +2203,10 @@
setDarkDecorCaptionShade(view);
break;
default: {
- TypedValue value = new TypedValue();
- context.getTheme().resolveAttribute(R.attr.colorPrimary, value, true);
- // We invert the shade depending on brightness of the theme. Dark shade for light
- // theme and vice versa. Thanks to this the buttons should be visible on the
- // background.
- if (Color.luminance(value.data) < 0.5) {
- setLightDecorCaptionShade(view);
- } else {
+ if ((getWindowSystemUiVisibility() & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0) {
setDarkDecorCaptionShade(view);
+ } else {
+ setLightDecorCaptionShade(view);
}
break;
}
@@ -2213,7 +2215,7 @@
void updateDecorCaptionShade() {
if (mDecorCaptionView != null) {
- setDecorCaptionShade(getContext(), mDecorCaptionView);
+ setDecorCaptionShade(mDecorCaptionView);
}
}
@@ -2484,6 +2486,15 @@
}
/**
+ * @hide
+ * @return the height of insets covering the top of window content area.
+ */
+ public int getCaptionInsetsHeight() {
+ if (!mWindow.isOverlayWithDecorCaptionEnabled()) return 0;
+ return getCaptionHeight();
+ }
+
+ /**
* Converts a DIP measure into physical pixels.
* @param dip The dip value.
* @return Returns the number of pixels.
diff --git a/core/java/com/android/internal/widget/DecorCaptionView.java b/core/java/com/android/internal/widget/DecorCaptionView.java
index b5d787c2..7a01024 100644
--- a/core/java/com/android/internal/widget/DecorCaptionView.java
+++ b/core/java/com/android/internal/widget/DecorCaptionView.java
@@ -17,7 +17,6 @@
package com.android.internal.widget;
import android.content.Context;
-import android.graphics.Color;
import android.graphics.Rect;
import android.os.RemoteException;
import android.util.AttributeSet;
@@ -53,8 +52,7 @@
* <li>..</li>
* </ul>
*
- * Although this ViewGroup has only two direct sub-Views, its behavior is more complex due to
- * overlaying caption on the content and drawing.
+ * Here describe the behavior of overlaying caption on the content and drawing.
*
* First, no matter where the content View gets added, it will always be the first child and the
* caption will be the second. This way the caption will always be drawn on top of the content when
@@ -66,11 +64,9 @@
* <li>DecorCaptionView.onInterceptTouchEvent() will try intercepting the touch events if the
* down action is performed on top close or maximize buttons; the reason for that is we want these
* buttons to always work.</li>
- * <li>The content View will receive the touch event. Mind that content is actually underneath the
- * caption, so we need to introduce our own dispatch ordering. We achieve this by overriding
- * {@link #buildTouchDispatchChildList()}.</li>
- * <li>If the touch event is not consumed by the content View, it will go to the caption View
- * and the dragging logic will be executed.</li>
+ * <li>The caption view will try to consume the event to apply the dragging logic.</li>
+ * <li>If the touch event is not consumed by the caption, the content View will receive the touch
+ * event</li>
* </ul>
*/
public class DecorCaptionView extends ViewGroup implements View.OnTouchListener,
@@ -137,11 +133,6 @@
mOwner = owner;
mShow = show;
mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled();
- if (mOverlayWithAppContent) {
- // The caption is covering the content, so we make its background transparent to make
- // the content visible.
- mCaption.setBackgroundColor(Color.TRANSPARENT);
- }
updateCaptionVisibility();
// By changing the outline provider to BOUNDS, the window can remove its
// background without removing the shadow.
@@ -236,18 +227,6 @@
}
@Override
- public ArrayList<View> buildTouchDispatchChildList() {
- mTouchDispatchList.ensureCapacity(3);
- if (mCaption != null) {
- mTouchDispatchList.add(mCaption);
- }
- if (mContent != null) {
- mTouchDispatchList.add(mContent);
- }
- return mTouchDispatchList;
- }
-
- @Override
public boolean shouldDelayChildPressedState() {
return false;
}
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index efdb51d..cbb379b 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -21,6 +21,7 @@
import static android.view.InsetsController.ANIMATION_TYPE_SHOW;
import static android.view.InsetsSourceConsumer.ShowResult.IME_SHOW_DELAYED;
import static android.view.InsetsSourceConsumer.ShowResult.SHOW_IMMEDIATELY;
+import static android.view.InsetsState.ITYPE_CAPTION_BAR;
import static android.view.InsetsState.ITYPE_IME;
import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
import static android.view.InsetsState.ITYPE_STATUS_BAR;
@@ -88,7 +89,6 @@
@Presubmit
@RunWith(AndroidJUnit4.class)
public class InsetsControllerTest {
-
private InsetsController mController;
private SurfaceSession mSession = new SurfaceSession();
private SurfaceControl mLeash;
@@ -665,6 +665,26 @@
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
+ @Test
+ public void testCaptionInsetsStateAssemble() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ mController.onFrameChanged(new Rect(0, 0, 100, 300));
+ final InsetsState state = new InsetsState(mController.getState(), true);
+ final Rect captionFrame = new Rect(0, 0, 100, 100);
+ mController.setCaptionInsetsHeight(100);
+ mController.onStateChanged(state);
+ final InsetsState currentState = new InsetsState(mController.getState());
+ // The caption bar source should be synced with the info in mAttachInfo.
+ assertEquals(captionFrame, currentState.peekSource(ITYPE_CAPTION_BAR).getFrame());
+ assertTrue(currentState.equals(state, true /* excludingCaptionInsets*/));
+ mController.setCaptionInsetsHeight(0);
+ mController.onStateChanged(state);
+ // The caption bar source should not be there at all, because we don't add empty
+ // caption to the state from the server.
+ assertNull(mController.getState().peekSource(ITYPE_CAPTION_BAR));
+ });
+ }
+
private void waitUntilNextFrame() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
Choreographer.getMainThreadInstance().postCallback(Choreographer.CALLBACK_COMMIT,
diff --git a/core/tests/coretests/src/android/view/InsetsStateTest.java b/core/tests/coretests/src/android/view/InsetsStateTest.java
index 721dc98..2884777 100644
--- a/core/tests/coretests/src/android/view/InsetsStateTest.java
+++ b/core/tests/coretests/src/android/view/InsetsStateTest.java
@@ -153,6 +153,35 @@
}
}
+
+ @Test
+ public void testCalculateInsets_captionStatusBarOverlap() throws Exception {
+ try (InsetsModeSession session =
+ new InsetsModeSession(ViewRootImpl.NEW_INSETS_MODE_FULL)) {
+ mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));
+ mState.getSource(ITYPE_STATUS_BAR).setVisible(true);
+ mState.getSource(ITYPE_CAPTION_BAR).setFrame(new Rect(0, 0, 100, 300));
+ mState.getSource(ITYPE_CAPTION_BAR).setVisible(true);
+
+ Rect visibleInsets = mState.calculateVisibleInsets(
+ new Rect(0, 0, 100, 400), SOFT_INPUT_ADJUST_NOTHING);
+ assertEquals(new Rect(0, 300, 0, 0), visibleInsets);
+ }
+ }
+
+ @Test
+ public void testCalculateInsets_captionBarOffset() throws Exception {
+ try (InsetsModeSession session =
+ new InsetsModeSession(ViewRootImpl.NEW_INSETS_MODE_FULL)) {
+ mState.getSource(ITYPE_CAPTION_BAR).setFrame(new Rect(0, 0, 100, 300));
+ mState.getSource(ITYPE_CAPTION_BAR).setVisible(true);
+
+ Rect visibleInsets = mState.calculateVisibleInsets(
+ new Rect(0, 0, 150, 400), SOFT_INPUT_ADJUST_NOTHING);
+ assertEquals(new Rect(0, 300, 0, 0), visibleInsets);
+ }
+ }
+
@Test
public void testStripForDispatch() {
mState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 100, 100));