/* * 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.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER; import static android.view.InsetsStateProto.DISPLAY_CUTOUT; import static android.view.InsetsStateProto.DISPLAY_FRAME; import static android.view.InsetsStateProto.SOURCES; import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE; import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsets.Type.displayCutout; import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsets.Type.indexOf; import static android.view.WindowInsets.Type.statusBars; import static android.view.WindowInsets.Type.systemBars; import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN; import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST; import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ERROR; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.WindowConfiguration.ActivityType; import android.graphics.Insets; import android.graphics.Rect; import android.os.Parcel; import android.os.Parcelable; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.proto.ProtoOutputStream; import android.view.InsetsSource.InternalInsetsSide; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowManager.LayoutParams.SoftInputModeFlags; import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; import java.util.Objects; import java.util.StringJoiner; /** * Holder for state of system windows that cause window insets for all other windows in the system. * @hide */ public class InsetsState implements Parcelable { private final SparseArray mSources; /** * The frame of the display these sources are relative to. */ private final Rect mDisplayFrame = new Rect(); /** The area cut from the display. */ private final DisplayCutout.ParcelableWrapper mDisplayCutout = new DisplayCutout.ParcelableWrapper(); /** * The frame that rounded corners are relative to. * * There are 2 cases that will draw fake rounded corners: * 1. In split-screen mode * 2. Devices with a task bar * We need to report these fake rounded corners to apps by re-calculating based on this frame. */ private final Rect mRoundedCornerFrame = new Rect(); /** The rounded corners on the display */ private RoundedCorners mRoundedCorners = RoundedCorners.NO_ROUNDED_CORNERS; /** The bounds of the Privacy Indicator */ private PrivacyIndicatorBounds mPrivacyIndicatorBounds = new PrivacyIndicatorBounds(); /** The display shape */ private DisplayShape mDisplayShape = DisplayShape.NONE; public InsetsState() { mSources = new SparseArray<>(); } public InsetsState(InsetsState copy) { this(copy, false /* copySources */); } public InsetsState(InsetsState copy, boolean copySources) { mSources = new SparseArray<>(copy.mSources.size()); set(copy, copySources); } /** * Calculates {@link WindowInsets} based on the current source configuration. * * @param frame The frame to calculate the insets relative to. * @param ignoringVisibilityState {@link InsetsState} used to calculate * {@link WindowInsets#getInsetsIgnoringVisibility(int)} information, or pass * {@code null} to use this state to calculate that information. * @return The calculated insets. */ public WindowInsets calculateInsets(Rect frame, @Nullable InsetsState ignoringVisibilityState, boolean isScreenRound, int legacySoftInputMode, int legacyWindowFlags, int legacySystemUiFlags, int windowType, @ActivityType int activityType, @Nullable @InternalInsetsSide SparseIntArray idSideMap) { Insets[] typeInsetsMap = new Insets[Type.SIZE]; Insets[] typeMaxInsetsMap = new Insets[Type.SIZE]; boolean[] typeVisibilityMap = new boolean[Type.SIZE]; final Rect relativeFrame = new Rect(frame); final Rect relativeFrameMax = new Rect(frame); @InsetsType int forceConsumingTypes = 0; @InsetsType int suppressScrimTypes = 0; final Rect[][] typeBoundingRectsMap = new Rect[Type.SIZE][]; final Rect[][] typeMaxBoundingRectsMap = new Rect[Type.SIZE][]; for (int i = mSources.size() - 1; i >= 0; i--) { final InsetsSource source = mSources.valueAt(i); final @InsetsType int type = source.getType(); if ((source.getFlags() & InsetsSource.FLAG_FORCE_CONSUMING) != 0) { forceConsumingTypes |= type; } if ((source.getFlags() & InsetsSource.FLAG_SUPPRESS_SCRIM) != 0) { suppressScrimTypes |= type; } processSource(source, relativeFrame, false /* ignoreVisibility */, typeInsetsMap, idSideMap, typeVisibilityMap, typeBoundingRectsMap); // IME won't be reported in max insets as the size depends on the EditorInfo of the IME // target. if (type != WindowInsets.Type.ime()) { InsetsSource ignoringVisibilitySource = ignoringVisibilityState != null ? ignoringVisibilityState.peekSource(source.getId()) : source; if (ignoringVisibilitySource == null) { continue; } processSource(ignoringVisibilitySource, relativeFrameMax, true /* ignoreVisibility */, typeMaxInsetsMap, null /* idSideMap */, null /* typeVisibilityMap */, typeMaxBoundingRectsMap); } } final int softInputAdjustMode = legacySoftInputMode & SOFT_INPUT_MASK_ADJUST; @InsetsType int compatInsetsTypes = systemBars() | displayCutout(); if (softInputAdjustMode == SOFT_INPUT_ADJUST_RESIZE) { compatInsetsTypes |= ime(); } if ((legacyWindowFlags & FLAG_FULLSCREEN) != 0) { compatInsetsTypes &= ~statusBars(); } if (clearsCompatInsets(windowType, legacyWindowFlags, activityType, forceConsumingTypes)) { compatInsetsTypes = 0; } return new WindowInsets(typeInsetsMap, typeMaxInsetsMap, typeVisibilityMap, isScreenRound, forceConsumingTypes, suppressScrimTypes, calculateRelativeCutout(frame), calculateRelativeRoundedCorners(frame), calculateRelativePrivacyIndicatorBounds(frame), calculateRelativeDisplayShape(frame), compatInsetsTypes, (legacySystemUiFlags & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0, typeBoundingRectsMap, typeMaxBoundingRectsMap, frame.width(), frame.height()); } private DisplayCutout calculateRelativeCutout(Rect frame) { final DisplayCutout raw = mDisplayCutout.get(); if (mDisplayFrame.equals(frame)) { return raw; } if (frame == null) { return DisplayCutout.NO_CUTOUT; } final int insetLeft = frame.left - mDisplayFrame.left; final int insetTop = frame.top - mDisplayFrame.top; final int insetRight = mDisplayFrame.right - frame.right; final int insetBottom = mDisplayFrame.bottom - frame.bottom; if (insetLeft >= raw.getSafeInsetLeft() && insetTop >= raw.getSafeInsetTop() && insetRight >= raw.getSafeInsetRight() && insetBottom >= raw.getSafeInsetBottom()) { return DisplayCutout.NO_CUTOUT; } return raw.inset(insetLeft, insetTop, insetRight, insetBottom); } private RoundedCorners calculateRelativeRoundedCorners(Rect frame) { if (frame == null) { return RoundedCorners.NO_ROUNDED_CORNERS; } // If mRoundedCornerFrame is set, we should calculate the new RoundedCorners based on this // frame. final Rect roundedCornerFrame = new Rect(mRoundedCornerFrame); for (int i = mSources.size() - 1; i >= 0; i--) { final InsetsSource source = mSources.valueAt(i); if (source.hasFlags(FLAG_INSETS_ROUNDED_CORNER)) { final Insets insets = source.calculateInsets(roundedCornerFrame, false); roundedCornerFrame.inset(insets); } } if (!roundedCornerFrame.isEmpty() && !roundedCornerFrame.equals(mDisplayFrame)) { return mRoundedCorners.insetWithFrame(frame, roundedCornerFrame); } if (mDisplayFrame.equals(frame)) { return mRoundedCorners; } final int insetLeft = frame.left - mDisplayFrame.left; final int insetTop = frame.top - mDisplayFrame.top; final int insetRight = mDisplayFrame.right - frame.right; final int insetBottom = mDisplayFrame.bottom - frame.bottom; return mRoundedCorners.inset(insetLeft, insetTop, insetRight, insetBottom); } private PrivacyIndicatorBounds calculateRelativePrivacyIndicatorBounds(Rect frame) { if (mDisplayFrame.equals(frame)) { return mPrivacyIndicatorBounds; } if (frame == null) { return null; } final int insetLeft = frame.left - mDisplayFrame.left; final int insetTop = frame.top - mDisplayFrame.top; final int insetRight = mDisplayFrame.right - frame.right; final int insetBottom = mDisplayFrame.bottom - frame.bottom; return mPrivacyIndicatorBounds.inset(insetLeft, insetTop, insetRight, insetBottom); } private DisplayShape calculateRelativeDisplayShape(Rect frame) { if (mDisplayFrame.equals(frame)) { return mDisplayShape; } if (frame == null) { return DisplayShape.NONE; } return mDisplayShape.setOffset(-frame.left, -frame.top); } public Insets calculateInsets(Rect frame, @InsetsType int types, boolean ignoreVisibility) { Insets insets = Insets.NONE; for (int i = mSources.size() - 1; i >= 0; i--) { final InsetsSource source = mSources.valueAt(i); if ((source.getType() & types) == 0) { continue; } insets = Insets.max(source.calculateInsets(frame, ignoreVisibility), insets); } return insets; } public Insets calculateInsets(Rect frame, @InsetsType int types, @InsetsType int requestedVisibleTypes) { Insets insets = Insets.NONE; for (int i = mSources.size() - 1; i >= 0; i--) { final InsetsSource source = mSources.valueAt(i); if ((source.getType() & types & requestedVisibleTypes) == 0) { continue; } insets = Insets.max(source.calculateInsets(frame, true), insets); } return insets; } public Insets calculateVisibleInsets(Rect frame, int windowType, @ActivityType int activityType, @SoftInputModeFlags int softInputMode, int windowFlags) { final int softInputAdjustMode = softInputMode & SOFT_INPUT_MASK_ADJUST; final int visibleInsetsTypes = softInputAdjustMode != SOFT_INPUT_ADJUST_NOTHING ? systemBars() | ime() : systemBars(); @InsetsType int forceConsumingTypes = 0; Insets insets = Insets.NONE; for (int i = mSources.size() - 1; i >= 0; i--) { final InsetsSource source = mSources.valueAt(i); if ((source.getType() & visibleInsetsTypes) == 0) { continue; } if (source.hasFlags(FLAG_FORCE_CONSUMING)) { forceConsumingTypes |= source.getType(); } insets = Insets.max(source.calculateVisibleInsets(frame), insets); } return clearsCompatInsets(windowType, windowFlags, activityType, forceConsumingTypes) ? Insets.NONE : insets; } /** * Calculate which insets *cannot* be controlled, because the frame does not cover the * respective side of the inset. * * If the frame of our window doesn't cover the entire inset, the control API makes very * little sense, as we don't deal with negative insets. */ @InsetsType public int calculateUncontrollableInsetsFromFrame(Rect frame) { int blocked = 0; for (int i = mSources.size() - 1; i >= 0; i--) { final InsetsSource source = mSources.valueAt(i); if (!canControlSource(frame, source)) { blocked |= source.getType(); } } return blocked; } private static boolean canControlSource(Rect frame, InsetsSource source) { final Insets insets = source.calculateInsets(frame, true /* ignoreVisibility */); final Rect sourceFrame = source.getFrame(); final int sourceWidth = sourceFrame.width(); final int sourceHeight = sourceFrame.height(); return insets.left == sourceWidth || insets.right == sourceWidth || insets.top == sourceHeight || insets.bottom == sourceHeight; } private void processSource(InsetsSource source, Rect relativeFrame, boolean ignoreVisibility, Insets[] typeInsetsMap, @Nullable @InternalInsetsSide SparseIntArray idSideMap, @Nullable boolean[] typeVisibilityMap, Rect[][] typeBoundingRectsMap) { Insets insets = source.calculateInsets(relativeFrame, ignoreVisibility); final Rect[] boundingRects = source.calculateBoundingRects(relativeFrame, ignoreVisibility); final int type = source.getType(); processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, typeBoundingRectsMap, insets, boundingRects, type); if (type == Type.MANDATORY_SYSTEM_GESTURES) { // Mandatory system gestures are also system gestures. // TODO: find a way to express this more generally. One option would be to define // Type.systemGestureInsets() as NORMAL | MANDATORY, but then we lose the // ability to set systemGestureInsets() independently from // mandatorySystemGestureInsets() in the Builder. processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, typeBoundingRectsMap, insets, boundingRects, Type.SYSTEM_GESTURES); } if (type == Type.CAPTION_BAR) { // Caption should also be gesture and tappable elements. This should not be needed when // the caption is added from the shell, as the shell can add other types at the same // time. processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, typeBoundingRectsMap, insets, boundingRects, Type.SYSTEM_GESTURES); processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, typeBoundingRectsMap, insets, boundingRects, Type.MANDATORY_SYSTEM_GESTURES); processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, typeBoundingRectsMap, insets, boundingRects, Type.TAPPABLE_ELEMENT); } } private void processSourceAsPublicType(InsetsSource source, Insets[] typeInsetsMap, @InternalInsetsSide @Nullable SparseIntArray idSideMap, @Nullable boolean[] typeVisibilityMap, Rect[][] typeBoundingRectsMap, Insets insets, Rect[] boundingRects, int type) { int index = indexOf(type); // Don't put Insets.NONE into typeInsetsMap. Otherwise, two WindowInsets can be considered // as non-equal while they provide the same insets of each type from WindowInsets#getInsets // if one WindowInsets has Insets.NONE for a type and the other has null for the same type. if (!Insets.NONE.equals(insets)) { Insets existing = typeInsetsMap[index]; if (existing == null) { typeInsetsMap[index] = insets; } else { typeInsetsMap[index] = Insets.max(existing, insets); } } if (typeVisibilityMap != null) { typeVisibilityMap[index] = source.isVisible(); } if (idSideMap != null) { @InternalInsetsSide int insetSide = InsetsSource.getInsetSide(insets); if (insetSide != InsetsSource.SIDE_UNKNOWN) { idSideMap.put(source.getId(), insetSide); } } if (typeBoundingRectsMap != null && boundingRects.length > 0) { final Rect[] existing = typeBoundingRectsMap[index]; if (existing == null) { typeBoundingRectsMap[index] = boundingRects; } else { typeBoundingRectsMap[index] = concatenate(existing, boundingRects); } } } private static Rect[] concatenate(Rect[] a, Rect[] b) { final Rect[] c = new Rect[a.length + b.length]; System.arraycopy(a, 0, c, 0, a.length); System.arraycopy(b, 0, c, a.length, b.length); return c; } /** * Gets the source mapped from the ID, or creates one if no such mapping has been made. */ public InsetsSource getOrCreateSource(int id, int type) { InsetsSource source = mSources.get(id); if (source != null) { return source; } source = new InsetsSource(id, type); mSources.put(id, source); return source; } /** * Gets the source mapped from the ID, or null if no such mapping has been made. */ public @Nullable InsetsSource peekSource(int id) { return mSources.get(id); } /** * Given an index in the range 0...sourceSize()-1, returns the source ID from the * indexth ID-source mapping that this state stores. */ public int sourceIdAt(int index) { return mSources.keyAt(index); } /** * Given an index in the range 0...sourceSize()-1, returns the source from the * indexth ID-source mapping that this state stores. */ public InsetsSource sourceAt(int index) { return mSources.valueAt(index); } /** * Returns the amount of the sources. */ public int sourceSize() { return mSources.size(); } /** * Returns if the source is visible or the type is default visible and the source doesn't exist. * * @param id The ID of the source. * @param type The {@link InsetsType} to see if it is default visible. * @return {@code true} if the source is visible or the type is default visible and the source * doesn't exist. */ public boolean isSourceOrDefaultVisible(int id, @InsetsType int type) { final InsetsSource source = mSources.get(id); return source != null ? source.isVisible() : (type & Type.defaultVisible()) != 0; } public void setDisplayFrame(Rect frame) { mDisplayFrame.set(frame); } public Rect getDisplayFrame() { return mDisplayFrame; } public void setDisplayCutout(DisplayCutout cutout) { mDisplayCutout.set(cutout); } public DisplayCutout getDisplayCutout() { return mDisplayCutout.get(); } public void getDisplayCutoutSafe(Rect outBounds) { outBounds.set( WindowLayout.MIN_X, WindowLayout.MIN_Y, WindowLayout.MAX_X, WindowLayout.MAX_Y); final DisplayCutout cutout = mDisplayCutout.get(); final Rect displayFrame = mDisplayFrame; if (!cutout.isEmpty()) { if (cutout.getSafeInsetLeft() > 0) { outBounds.left = displayFrame.left + cutout.getSafeInsetLeft(); } if (cutout.getSafeInsetTop() > 0) { outBounds.top = displayFrame.top + cutout.getSafeInsetTop(); } if (cutout.getSafeInsetRight() > 0) { outBounds.right = displayFrame.right - cutout.getSafeInsetRight(); } if (cutout.getSafeInsetBottom() > 0) { outBounds.bottom = displayFrame.bottom - cutout.getSafeInsetBottom(); } } } public void setRoundedCorners(RoundedCorners roundedCorners) { mRoundedCorners = roundedCorners; } public RoundedCorners getRoundedCorners() { return mRoundedCorners; } /** * Set the frame that will be used to calculate the rounded corners. * * @see #mRoundedCornerFrame */ public void setRoundedCornerFrame(Rect frame) { mRoundedCornerFrame.set(frame); } public void setPrivacyIndicatorBounds(PrivacyIndicatorBounds bounds) { mPrivacyIndicatorBounds = bounds; } public PrivacyIndicatorBounds getPrivacyIndicatorBounds() { return mPrivacyIndicatorBounds; } public void setDisplayShape(DisplayShape displayShape) { mDisplayShape = displayShape; } public DisplayShape getDisplayShape() { return mDisplayShape; } /** * Removes the source which has the ID from this state, if there was any. * * @param id The ID of the source to remove. */ public void removeSource(int id) { mSources.delete(id); } /** * Removes the source at the specified index. * * @param index The index of the source to remove. */ public void removeSourceAt(int index) { mSources.removeAt(index); } /** * A shortcut for setting the visibility of the source. * * @param id The ID of the source to set the visibility * @param visible {@code true} for visible */ public void setSourceVisible(int id, boolean visible) { final InsetsSource source = mSources.get(id); if (source != null) { source.setVisible(visible); } } /** * Scales the frame and the visible frame (if there is one) of each source. * * @param scale the scale to be applied */ public void scale(float scale) { mDisplayFrame.scale(scale); mDisplayCutout.scale(scale); mRoundedCorners = mRoundedCorners.scale(scale); mRoundedCornerFrame.scale(scale); mPrivacyIndicatorBounds = mPrivacyIndicatorBounds.scale(scale); mDisplayShape = mDisplayShape.setScale(scale); for (int i = mSources.size() - 1; i >= 0; i--) { final InsetsSource source = mSources.valueAt(i); source.getFrame().scale(scale); final Rect visibleFrame = source.getVisibleFrame(); if (visibleFrame != null) { visibleFrame.scale(scale); } } } public void set(InsetsState other) { set(other, false /* copySources */); } public void set(InsetsState other, boolean copySources) { mDisplayFrame.set(other.mDisplayFrame); mDisplayCutout.set(other.mDisplayCutout); mRoundedCorners = other.getRoundedCorners(); mRoundedCornerFrame.set(other.mRoundedCornerFrame); mPrivacyIndicatorBounds = other.getPrivacyIndicatorBounds(); mDisplayShape = other.getDisplayShape(); mSources.clear(); for (int i = 0, size = other.mSources.size(); i < size; i++) { final InsetsSource otherSource = other.mSources.valueAt(i); mSources.append(otherSource.getId(), copySources ? new InsetsSource(otherSource) : otherSource); } } /** * Sets the values from the other InsetsState. But for sources, only specific types of source * would be set. * * @param other the other InsetsState. * @param types the only types of sources would be set. */ public void set(InsetsState other, @InsetsType int types) { mDisplayFrame.set(other.mDisplayFrame); mDisplayCutout.set(other.mDisplayCutout); mRoundedCorners = other.getRoundedCorners(); mRoundedCornerFrame.set(other.mRoundedCornerFrame); mPrivacyIndicatorBounds = other.getPrivacyIndicatorBounds(); mDisplayShape = other.getDisplayShape(); if (types == 0) { return; } for (int i = mSources.size() - 1; i >= 0; i--) { final InsetsSource source = mSources.valueAt(i); if ((source.getType() & types) != 0) { mSources.removeAt(i); } } for (int i = other.mSources.size() - 1; i >= 0; i--) { final InsetsSource otherSource = other.mSources.valueAt(i); if ((otherSource.getType() & types) != 0) { mSources.put(otherSource.getId(), otherSource); } } } public void addSource(InsetsSource source) { mSources.put(source.getId(), source); } public static boolean clearsCompatInsets(int windowType, int windowFlags, @ActivityType int activityType, @InsetsType int forceConsumingTypes) { return (windowFlags & FLAG_LAYOUT_NO_LIMITS) != 0 // For compatibility reasons, this excludes the wallpaper, the system error windows, // and the app windows while any system bar is forcibly consumed. && windowType != TYPE_WALLPAPER && windowType != TYPE_SYSTEM_ERROR // This ensures the app content won't be obscured by compat insets even if the app // has FLAG_LAYOUT_NO_LIMITS. && (forceConsumingTypes == 0 || activityType != ACTIVITY_TYPE_STANDARD); } public void dump(String prefix, PrintWriter pw) { final String newPrefix = prefix + " "; pw.println(prefix + "InsetsState"); pw.println(newPrefix + "mDisplayFrame=" + mDisplayFrame); pw.println(newPrefix + "mDisplayCutout=" + mDisplayCutout.get()); pw.println(newPrefix + "mRoundedCorners=" + mRoundedCorners); pw.println(newPrefix + "mRoundedCornerFrame=" + mRoundedCornerFrame); pw.println(newPrefix + "mPrivacyIndicatorBounds=" + mPrivacyIndicatorBounds); pw.println(newPrefix + "mDisplayShape=" + mDisplayShape); for (int i = 0, size = mSources.size(); i < size; i++) { mSources.valueAt(i).dump(newPrefix + " ", pw); } } void dumpDebug(ProtoOutputStream proto, long fieldId) { final long token = proto.start(fieldId); final InsetsSource source = mSources.get(InsetsSource.ID_IME); if (source != null) { source.dumpDebug(proto, SOURCES); } mDisplayFrame.dumpDebug(proto, DISPLAY_FRAME); mDisplayCutout.get().dumpDebug(proto, DISPLAY_CUTOUT); proto.end(token); } @Override public boolean equals(@Nullable Object o) { return equals(o, false, 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 excludesCaptionBar If {@link Type#captionBar()}} should be ignored. * @param excludesInvisibleIme If {@link WindowInsets.Type#ime()} should be ignored when IME is * not visible. * @return {@code true} if the two InsetsState objects are equal, {@code false} otherwise. */ @VisibleForTesting public boolean equals(@Nullable Object o, boolean excludesCaptionBar, boolean excludesInvisibleIme) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } InsetsState state = (InsetsState) o; if (!mDisplayFrame.equals(state.mDisplayFrame) || !mDisplayCutout.equals(state.mDisplayCutout) || !mRoundedCorners.equals(state.mRoundedCorners) || !mRoundedCornerFrame.equals(state.mRoundedCornerFrame) || !mPrivacyIndicatorBounds.equals(state.mPrivacyIndicatorBounds) || !mDisplayShape.equals(state.mDisplayShape)) { return false; } final SparseArray thisSources = mSources; final SparseArray thatSources = state.mSources; if (!excludesCaptionBar && !excludesInvisibleIme) { return thisSources.contentEquals(thatSources); } else { final int thisSize = thisSources.size(); final int thatSize = thatSources.size(); int thisIndex = 0; int thatIndex = 0; while (thisIndex < thisSize || thatIndex < thatSize) { InsetsSource thisSource = thisIndex < thisSize ? thisSources.valueAt(thisIndex) : null; // Seek to the next non-excluding source of ours. while (thisSource != null && (excludesCaptionBar && thisSource.getType() == captionBar() || excludesInvisibleIme && thisSource.getType() == ime() && !thisSource.isVisible())) { thisIndex++; thisSource = thisIndex < thisSize ? thisSources.valueAt(thisIndex) : null; } InsetsSource thatSource = thatIndex < thatSize ? thatSources.valueAt(thatIndex) : null; // Seek to the next non-excluding source of theirs. while (thatSource != null && (excludesCaptionBar && thatSource.getType() == captionBar() || excludesInvisibleIme && thatSource.getType() == ime() && !thatSource.isVisible())) { thatIndex++; thatSource = thatIndex < thatSize ? thatSources.valueAt(thatIndex) : null; } if (!Objects.equals(thisSource, thatSource)) { return false; } thisIndex++; thatIndex++; } return true; } } @Override public int hashCode() { return Objects.hash(mDisplayFrame, mDisplayCutout, mSources.contentHashCode(), mRoundedCorners, mPrivacyIndicatorBounds, mRoundedCornerFrame, mDisplayShape); } public InsetsState(Parcel in) { mSources = readFromParcel(in); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { mDisplayFrame.writeToParcel(dest, flags); mDisplayCutout.writeToParcel(dest, flags); dest.writeTypedObject(mRoundedCorners, flags); mRoundedCornerFrame.writeToParcel(dest, flags); dest.writeTypedObject(mPrivacyIndicatorBounds, flags); dest.writeTypedObject(mDisplayShape, flags); final int size = mSources.size(); dest.writeInt(size); for (int i = 0; i < size; i++) { dest.writeTypedObject(mSources.valueAt(i), flags); } } public static final @NonNull Creator CREATOR = new Creator<>() { public InsetsState createFromParcel(Parcel in) { return new InsetsState(in); } public InsetsState[] newArray(int size) { return new InsetsState[size]; } }; public SparseArray readFromParcel(Parcel in) { mDisplayFrame.readFromParcel(in); mDisplayCutout.readFromParcel(in); mRoundedCorners = in.readTypedObject(RoundedCorners.CREATOR); mRoundedCornerFrame.readFromParcel(in); mPrivacyIndicatorBounds = in.readTypedObject(PrivacyIndicatorBounds.CREATOR); mDisplayShape = in.readTypedObject(DisplayShape.CREATOR); final int size = in.readInt(); final SparseArray sources; if (mSources == null) { // We are constructing this InsetsState. sources = new SparseArray<>(size); } else { sources = mSources; sources.clear(); } for (int i = 0; i < size; i++) { final InsetsSource source = in.readTypedObject(InsetsSource.CREATOR); sources.append(source.getId(), source); } return sources; } @Override public String toString() { final StringJoiner joiner = new StringJoiner(", "); for (int i = 0, size = mSources.size(); i < size; i++) { joiner.add(mSources.valueAt(i).toString()); } return "InsetsState: {" + "mDisplayFrame=" + mDisplayFrame + ", mDisplayCutout=" + mDisplayCutout + ", mRoundedCorners=" + mRoundedCorners + " mRoundedCornerFrame=" + mRoundedCornerFrame + ", mPrivacyIndicatorBounds=" + mPrivacyIndicatorBounds + ", mDisplayShape=" + mDisplayShape + ", mSources= { " + joiner + " }"; } /** * Traverses sources in two {@link InsetsState}s and calls back when events defined in * {@link OnTraverseCallbacks} happen. This is optimized for {@link SparseArray} that we avoid * triggering the binary search while getting the key or the value. * * This can be used to copy attributes of sources from one InsetsState to the other one, or to * remove sources existing in one InsetsState but not in the other one. * * @param state1 The first {@link InsetsState} to be traversed. * @param state2 The second {@link InsetsState} to be traversed. * @param cb The {@link OnTraverseCallbacks} to call back to the caller. */ public static void traverse(InsetsState state1, InsetsState state2, OnTraverseCallbacks cb) { cb.onStart(state1, state2); final int size1 = state1.sourceSize(); final int size2 = state2.sourceSize(); int index1 = 0; int index2 = 0; while (index1 < size1 && index2 < size2) { int id1 = state1.sourceIdAt(index1); int id2 = state2.sourceIdAt(index2); while (id1 != id2) { if (id1 < id2) { cb.onIdNotFoundInState2(index1, state1.sourceAt(index1)); index1++; if (index1 < size1) { id1 = state1.sourceIdAt(index1); } else { break; } } else { cb.onIdNotFoundInState1(index2, state2.sourceAt(index2)); index2++; if (index2 < size2) { id2 = state2.sourceIdAt(index2); } else { break; } } } if (index1 >= size1 || index2 >= size2) { break; } final InsetsSource source1 = state1.sourceAt(index1); final InsetsSource source2 = state2.sourceAt(index2); cb.onIdMatch(source1, source2); index1++; index2++; } while (index2 < size2) { cb.onIdNotFoundInState1(index2, state2.sourceAt(index2)); index2++; } while (index1 < size1) { cb.onIdNotFoundInState2(index1, state1.sourceAt(index1)); index1++; } cb.onFinish(state1, state2); } /** * Used with {@link #traverse(InsetsState, InsetsState, OnTraverseCallbacks)} to call back when * certain events happen. */ public interface OnTraverseCallbacks { /** * Called at the beginning of the traverse. * * @param state1 same as the state1 supplied to {@link #traverse} * @param state2 same as the state2 supplied to {@link #traverse} */ default void onStart(InsetsState state1, InsetsState state2) { } /** * Called when finding two IDs from two InsetsStates are the same. * * @param source1 the source in state1. * @param source2 the source in state2. */ default void onIdMatch(InsetsSource source1, InsetsSource source2) { } /** * Called when finding an ID in state2 but not in state1. * * @param index2 the index of the ID in state2. * @param source2 the source which has the ID in state2. */ default void onIdNotFoundInState1(int index2, InsetsSource source2) { } /** * Called when finding an ID in state1 but not in state2. * * @param index1 the index of the ID in state1. * @param source1 the source which has the ID in state1. */ default void onIdNotFoundInState2(int index1, InsetsSource source1) { } /** * Called at the end of the traverse. * * @param state1 same as the state1 supplied to {@link #traverse} * @param state2 same as the state2 supplied to {@link #traverse} */ default void onFinish(InsetsState state1, InsetsState state2) { } } }