/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.view; import static android.view.InsetsSourceProto.FRAME; import static android.view.InsetsSourceProto.TYPE; import static android.view.InsetsSourceProto.TYPE_NUMBER; import static android.view.InsetsSourceProto.VISIBLE; import static android.view.InsetsSourceProto.VISIBLE_FRAME; import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsets.Type.ime; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Insets; import android.graphics.Rect; import android.os.Parcel; import android.os.Parcelable; import android.util.proto.ProtoOutputStream; import android.view.WindowInsets.Type.InsetsType; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Objects; import java.util.StringJoiner; /** * Represents the state of a single entity generating insets for clients. * @hide */ public class InsetsSource implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "SIDE_", value = { SIDE_NONE, SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM, SIDE_UNKNOWN }) public @interface InternalInsetsSide {} static final int SIDE_NONE = 0; static final int SIDE_LEFT = 1; static final int SIDE_TOP = 2; static final int SIDE_RIGHT = 3; static final int SIDE_BOTTOM = 4; static final int SIDE_UNKNOWN = 5; /** The insets source ID of IME */ public static final int ID_IME = createId(null, 0, ime()); /** The insets source ID of the IME caption bar ("fake" IME navigation bar). */ public static final int ID_IME_CAPTION_BAR = InsetsSource.createId(null /* owner */, 1 /* index */, captionBar()); /** * Controls whether this source suppresses the scrim. If the scrim is ignored, the system won't * draw a semi-transparent scrim behind the system bar area even when the bar contrast is * enforced. * * @see android.R.styleable#Window_enforceStatusBarContrast * @see android.R.styleable#Window_enforceNavigationBarContrast */ public static final int FLAG_SUPPRESS_SCRIM = 1; /** * Controls whether the insets frame will be used to move {@link RoundedCorner} inward with the * insets frame size when calculating the rounded corner insets to other windows. * * For example, task bar will draw fake rounded corners above itself, so we need to move the * rounded corner up by the task bar insets size to make other windows see a rounded corner * above the task bar. */ public static final int FLAG_INSETS_ROUNDED_CORNER = 1 << 1; /** * Controls whether the insets provided by this source should be forcibly consumed. */ public static final int FLAG_FORCE_CONSUMING = 1 << 2; /** * Controls whether the insets source will play an animation when resizing. */ public static final int FLAG_ANIMATE_RESIZING = 1 << 3; @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, prefix = "FLAG_", value = { FLAG_SUPPRESS_SCRIM, FLAG_INSETS_ROUNDED_CORNER, FLAG_FORCE_CONSUMING, FLAG_ANIMATE_RESIZING, }) public @interface Flags {} /** * Used when there are no bounding rects to describe an inset, which is only possible when the * insets itself is {@link Insets#NONE}. */ private static final Rect[] NO_BOUNDING_RECTS = new Rect[0]; private @Flags int mFlags; /** * An unique integer to identify this source across processes. */ private final int mId; private final @InsetsType int mType; /** Frame of the source in screen coordinate space */ private final Rect mFrame; private @Nullable Rect mVisibleFrame; private @Nullable Rect[] mBoundingRects; private boolean mVisible; /** * Used to decide which side of the relative frame should receive insets when the frame fully * covers the relative frame. */ private @InternalInsetsSide int mSideHint = SIDE_NONE; private final Rect mTmpFrame = new Rect(); private final Rect mTmpBoundingRect = new Rect(); public InsetsSource(int id, @InsetsType int type) { mId = id; mType = type; mFrame = new Rect(); mVisible = (WindowInsets.Type.defaultVisible() & type) != 0; } public InsetsSource(InsetsSource other) { mId = other.mId; mType = other.mType; mFrame = new Rect(other.mFrame); mVisible = other.mVisible; mVisibleFrame = other.mVisibleFrame != null ? new Rect(other.mVisibleFrame) : null; mFlags = other.mFlags; mSideHint = other.mSideHint; mBoundingRects = other.mBoundingRects != null ? other.mBoundingRects.clone() : null; } public void set(InsetsSource other) { mFrame.set(other.mFrame); mVisible = other.mVisible; mVisibleFrame = other.mVisibleFrame != null ? new Rect(other.mVisibleFrame) : null; mFlags = other.mFlags; mSideHint = other.mSideHint; mBoundingRects = other.mBoundingRects != null ? other.mBoundingRects.clone() : null; } public InsetsSource setFrame(int left, int top, int right, int bottom) { mFrame.set(left, top, right, bottom); return this; } public InsetsSource setFrame(Rect frame) { mFrame.set(frame); return this; } public InsetsSource setVisibleFrame(@Nullable Rect visibleFrame) { mVisibleFrame = visibleFrame != null ? new Rect(visibleFrame) : null; return this; } public InsetsSource setVisible(boolean visible) { mVisible = visible; return this; } public InsetsSource setFlags(@Flags int flags) { mFlags = flags; return this; } public InsetsSource setFlags(@Flags int flags, @Flags int mask) { mFlags = (mFlags & ~mask) | (flags & mask); return this; } /** * Updates the side hint which is used to decide which side of the relative frame should receive * insets when the frame fully covers the relative frame. * * @param bounds A rectangle which contains the frame. It will be used to calculate the hint. */ public InsetsSource updateSideHint(Rect bounds) { mSideHint = getInsetSide( calculateInsets(bounds, mFrame, true /* ignoreVisibility */)); return this; } /** * Set the bounding rectangles of this source. They are expected to be relative to the source * frame. */ public InsetsSource setBoundingRects(@Nullable Rect[] rects) { mBoundingRects = rects != null ? rects.clone() : null; return this; } public int getId() { return mId; } public @InsetsType int getType() { return mType; } public Rect getFrame() { return mFrame; } public @Nullable Rect getVisibleFrame() { return mVisibleFrame; } public boolean isVisible() { return mVisible; } public @Flags int getFlags() { return mFlags; } public boolean hasFlags(int flags) { return (mFlags & flags) == flags; } /** * Returns the bounding rectangles of this source. */ public @Nullable Rect[] getBoundingRects() { return mBoundingRects; } /** * Calculates the insets this source will cause to a client window. * * @param relativeFrame The frame to calculate the insets relative to. * @param ignoreVisibility If true, always reports back insets even if source isn't visible. * @return The resulting insets. The contract is that only one side will be occupied by a * source. */ public Insets calculateInsets(Rect relativeFrame, boolean ignoreVisibility) { return calculateInsets(relativeFrame, mFrame, ignoreVisibility); } /** * Like {@link #calculateInsets(Rect, boolean)}, but will return visible insets. */ public Insets calculateVisibleInsets(Rect relativeFrame) { return calculateInsets(relativeFrame, mVisibleFrame != null ? mVisibleFrame : mFrame, false /* ignoreVisibility */); } private Insets calculateInsets(Rect relativeFrame, Rect frame, boolean ignoreVisibility) { if (!ignoreVisibility && !mVisible) { 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. // However, with the "fake" IME navigation bar treated as a caption bar, we return the // insets with the corresponding height the bottom. if (getType() == WindowInsets.Type.captionBar()) { return getId() == ID_IME_CAPTION_BAR ? Insets.of(0, 0, 0, frame.height()) : Insets.of(0, frame.height(), 0, 0); } // Checks for whether there is shared edge with insets for 0-width/height window. final boolean hasIntersection = relativeFrame.isEmpty() ? getIntersection(frame, relativeFrame, mTmpFrame) : mTmpFrame.setIntersect(frame, relativeFrame); if (!hasIntersection) { return Insets.NONE; } // TODO: Currently, non-floating IME always intersects at bottom due to issues with cutout. // However, we should let the policy decide from the server. if (getType() == WindowInsets.Type.ime()) { return Insets.of(0, 0, 0, mTmpFrame.height()); } if (mTmpFrame.equals(relativeFrame)) { // Covering all sides switch (mSideHint) { default: case SIDE_LEFT: return Insets.of(mTmpFrame.width(), 0, 0, 0); case SIDE_TOP: return Insets.of(0, mTmpFrame.height(), 0, 0); case SIDE_RIGHT: return Insets.of(0, 0, mTmpFrame.width(), 0); case SIDE_BOTTOM: return Insets.of(0, 0, 0, mTmpFrame.height()); } } else if (mTmpFrame.width() == relativeFrame.width()) { // Intersecting at top/bottom if (mTmpFrame.top == relativeFrame.top) { return Insets.of(0, mTmpFrame.height(), 0, 0); } else if (mTmpFrame.bottom == relativeFrame.bottom) { return Insets.of(0, 0, 0, mTmpFrame.height()); } // TODO: remove when insets are shell-customizable. // This is a hack that says "if this is a top-inset (eg statusbar), always apply it // to the top". It is used when adjusting primary split for IME. if (mTmpFrame.top == 0) { return Insets.of(0, mTmpFrame.height(), 0, 0); } } else if (mTmpFrame.height() == relativeFrame.height()) { // Intersecting at left/right if (mTmpFrame.left == relativeFrame.left) { return Insets.of(mTmpFrame.width(), 0, 0, 0); } else if (mTmpFrame.right == relativeFrame.right) { return Insets.of(0, 0, mTmpFrame.width(), 0); } } return Insets.NONE; } /** * Calculates the bounding rects the source will cause to a client window. */ public @NonNull Rect[] calculateBoundingRects(Rect relativeFrame, boolean ignoreVisibility) { if (!ignoreVisibility && !mVisible) { return NO_BOUNDING_RECTS; } final Rect frame = getFrame(); if (mBoundingRects == null) { // No bounding rects set, make a single bounding rect that covers the intersection of // the |frame| and the |relativeFrame|. Also make it relative to the window origin. return mTmpBoundingRect.setIntersect(frame, relativeFrame) ? new Rect[]{ new Rect( mTmpBoundingRect.left - relativeFrame.left, mTmpBoundingRect.top - relativeFrame.top, mTmpBoundingRect.right - relativeFrame.left, mTmpBoundingRect.bottom - relativeFrame.top ) } : NO_BOUNDING_RECTS; } // Special treatment for captionBar inset type. During drag-resizing, the |frame| and // |boundingRects| may not get updated as quickly as |relativeFrame|, so just assume the // |frame| will always be either at the top or bottom of |relativeFrame|. This means some // calculations to make |boundingRects| relative to |relativeFrame| can be skipped or // simplified. // TODO(b/254128050): remove special treatment. if (getType() == WindowInsets.Type.captionBar()) { final ArrayList validBoundingRects = new ArrayList<>(); for (final Rect boundingRect : mBoundingRects) { // Assume that the caption |frame| and |relativeFrame| perfectly align at the top // or bottom, meaning that the provided |boundingRect|, which is relative to the // |frame| either is already relative to |relativeFrame| (for top captionBar()), or // just needs to be made relative to |relativeFrame| for bottom bars. final int frameHeight = frame.height(); mTmpBoundingRect.set(boundingRect); if (getId() == ID_IME_CAPTION_BAR) { mTmpBoundingRect.offset(0, relativeFrame.height() - frameHeight); } validBoundingRects.add(new Rect(mTmpBoundingRect)); } return validBoundingRects.toArray(new Rect[validBoundingRects.size()]); } // Regular treatment for non-captionBar inset types. final ArrayList validBoundingRects = new ArrayList<>(); for (final Rect boundingRect : mBoundingRects) { // |boundingRect| was provided relative to |frame|. Make it absolute to be in the same // coordinate system as |frame|. final Rect absBoundingRect = new Rect( boundingRect.left + frame.left, boundingRect.top + frame.top, boundingRect.right + frame.left, boundingRect.bottom + frame.top ); // Now find the intersection of that |absBoundingRect| with |relativeFrame|. In other // words, whichever part of the bounding rect is inside the window frame. if (!mTmpBoundingRect.setIntersect(absBoundingRect, relativeFrame)) { // It's possible for this to be empty if the frame and bounding rects were larger // than the |relativeFrame|, such as when a system window is wider than the app // window width. Just ignore that rect since it will have no effect on the // window insets. continue; } // At this point, |mTmpBoundingRect| is a valid bounding rect located fully inside the // window, convert it to be relative to the window so that apps don't need to know the // location of the window to understand bounding rects. validBoundingRects.add(new Rect( mTmpBoundingRect.left - relativeFrame.left, mTmpBoundingRect.top - relativeFrame.top, mTmpBoundingRect.right - relativeFrame.left, mTmpBoundingRect.bottom - relativeFrame.top)); } if (validBoundingRects.isEmpty()) { return NO_BOUNDING_RECTS; } return validBoundingRects.toArray(new Rect[validBoundingRects.size()]); } /** * Outputs the intersection of two rectangles. The shared edges will also be counted in the * intersection. * * @param a The first rectangle being intersected with. * @param b The second rectangle being intersected with. * @param out The rectangle which represents the intersection. * @return {@code true} if there is any intersection. */ private static boolean getIntersection(@NonNull Rect a, @NonNull Rect b, @NonNull Rect out) { if (a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom) { out.left = Math.max(a.left, b.left); out.top = Math.max(a.top, b.top); out.right = Math.min(a.right, b.right); out.bottom = Math.min(a.bottom, b.bottom); return true; } out.setEmpty(); return false; } /** * Retrieves the side for a certain {@code insets}. It is required that only one field l/t/r/b * is set in order that this method returns a meaningful result. */ static @InternalInsetsSide int getInsetSide(Insets insets) { if (Insets.NONE.equals(insets)) { return SIDE_NONE; } if (insets.left != 0) { return SIDE_LEFT; } if (insets.top != 0) { return SIDE_TOP; } if (insets.right != 0) { return SIDE_RIGHT; } if (insets.bottom != 0) { return SIDE_BOTTOM; } return SIDE_UNKNOWN; } static String sideToString(@InternalInsetsSide int side) { switch (side) { case SIDE_NONE: return "NONE"; case SIDE_LEFT: return "LEFT"; case SIDE_TOP: return "TOP"; case SIDE_RIGHT: return "RIGHT"; case SIDE_BOTTOM: return "BOTTOM"; default: return "UNKNOWN:" + side; } } /** * Creates an identifier of an {@link InsetsSource}. * * @param owner An object owned by the owner. Only the owner can modify its own sources. * @param index An owner may have multiple sources with the same type. For example, the system * server might have multiple display cutout sources. This is used to identify * which one is which. The value must be in a range of [0, 2047]. * @param type The {@link InsetsType type} of the source. * @return a unique integer as the identifier. */ public static int createId(Object owner, @IntRange(from = 0, to = 2047) int index, @InsetsType int type) { if (index < 0 || index >= 2048) { throw new IllegalArgumentException(); } // owner takes top 16 bits; // index takes 11 bits since the 6th bit; // type takes bottom 5 bits. return ((System.identityHashCode(owner) % (1 << 16)) << 16) + (index << 5) + WindowInsets.Type.indexOf(type); } /** * Gets the index from the ID. * * @see #createId(Object, int, int) */ public static int getIndex(int id) { // start: ????????????????***********????? // & 65535: 0000000000000000***********????? // >> 5: 000000000000000000000*********** return (id & 65535) >> 5; } /** * Gets the {@link InsetsType} from the ID. * * @see #createId(Object, int, int) * @see WindowInsets.Type#indexOf(int) */ public static int getType(int id) { // start: ???????????????????????????***** // & 31: 000000000000000000000000000***** // 1 <<: See WindowInsets.Type#indexOf return 1 << (id & 31); } public static String flagsToString(@Flags int flags) { final StringJoiner joiner = new StringJoiner("|"); if ((flags & FLAG_SUPPRESS_SCRIM) != 0) { joiner.add("SUPPRESS_SCRIM"); } if ((flags & FLAG_INSETS_ROUNDED_CORNER) != 0) { joiner.add("INSETS_ROUNDED_CORNER"); } if ((flags & FLAG_FORCE_CONSUMING) != 0) { joiner.add("FORCE_CONSUMING"); } if ((flags & FLAG_ANIMATE_RESIZING) != 0) { joiner.add("ANIMATE_RESIZING"); } return joiner.toString(); } /** * Export the state of {@link InsetsSource} into a protocol buffer output stream. * * @param proto Stream to write the state to * @param fieldId FieldId of InsetsSource as defined in the parent message */ public void dumpDebug(ProtoOutputStream proto, long fieldId) { final long token = proto.start(fieldId); if (!android.os.Flags.androidOsBuildVanillaIceCream()) { // Deprecated since V. proto.write(TYPE, WindowInsets.Type.toString(mType)); } mFrame.dumpDebug(proto, FRAME); if (mVisibleFrame != null) { mVisibleFrame.dumpDebug(proto, VISIBLE_FRAME); } proto.write(VISIBLE, mVisible); proto.write(TYPE_NUMBER, mType); proto.end(token); } public void dump(String prefix, PrintWriter pw) { pw.print(prefix); pw.print("InsetsSource id="); pw.print(Integer.toHexString(mId)); pw.print(" type="); pw.print(WindowInsets.Type.toString(mType)); pw.print(" frame="); pw.print(mFrame.toShortString()); if (mVisibleFrame != null) { pw.print(" visibleFrame="); pw.print(mVisibleFrame.toShortString()); } pw.print(" visible="); pw.print(mVisible); pw.print(" flags="); pw.print(flagsToString(mFlags)); pw.print(" sideHint="); pw.print(sideToString(mSideHint)); pw.print(" boundingRects="); pw.print(Arrays.toString(mBoundingRects)); pw.println(); } @Override public boolean equals(@Nullable Object o) { return equals(o, false); } /** * @param excludeInvisibleImeFrames If {@link WindowInsets.Type#ime()} frames should be ignored * when IME is not visible. */ public boolean equals(@Nullable Object o, boolean excludeInvisibleImeFrames) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; InsetsSource that = (InsetsSource) o; if (mId != that.mId) return false; if (mType != that.mType) return false; if (mVisible != that.mVisible) return false; if (mFlags != that.mFlags) return false; if (mSideHint != that.mSideHint) return false; if (excludeInvisibleImeFrames && !mVisible && mType == WindowInsets.Type.ime()) return true; if (!Objects.equals(mVisibleFrame, that.mVisibleFrame)) return false; if (!mFrame.equals(that.mFrame)) return false; return Arrays.equals(mBoundingRects, that.mBoundingRects); } @Override public int hashCode() { return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags, mSideHint, Arrays.hashCode(mBoundingRects)); } public InsetsSource(Parcel in) { mId = in.readInt(); mType = in.readInt(); mFrame = Rect.CREATOR.createFromParcel(in); if (in.readInt() != 0) { mVisibleFrame = Rect.CREATOR.createFromParcel(in); } else { mVisibleFrame = null; } mVisible = in.readBoolean(); mFlags = in.readInt(); mSideHint = in.readInt(); mBoundingRects = in.createTypedArray(Rect.CREATOR); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mId); dest.writeInt(mType); mFrame.writeToParcel(dest, 0); if (mVisibleFrame != null) { dest.writeInt(1); mVisibleFrame.writeToParcel(dest, 0); } else { dest.writeInt(0); } dest.writeBoolean(mVisible); dest.writeInt(mFlags); dest.writeInt(mSideHint); dest.writeTypedArray(mBoundingRects, flags); } @Override public String toString() { return "InsetsSource: {" + Integer.toHexString(mId) + " mType=" + WindowInsets.Type.toString(mType) + " mFrame=" + mFrame.toShortString() + " mVisible=" + mVisible + " mFlags=" + flagsToString(mFlags) + " mSideHint=" + sideToString(mSideHint) + " mBoundingRects=" + Arrays.toString(mBoundingRects) + "}"; } public static final @NonNull Creator CREATOR = new Creator<>() { public InsetsSource createFromParcel(Parcel in) { return new InsetsSource(in); } public InsetsSource[] newArray(int size) { return new InsetsSource[size]; } }; }