script-astra/Android/Sdk/sources/android-35/android/view/InsetsState.java
localadmin 4380f00a78 init
2025-01-20 18:15:20 +03:00

947 lines
38 KiB
Java

/*
* 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<InsetsSource> 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 <code>null</code> if no such mapping has been made.
*/
public @Nullable InsetsSource peekSource(int id) {
return mSources.get(id);
}
/**
* Given an index in the range <code>0...sourceSize()-1</code>, returns the source ID from the
* <code>index</code>th ID-source mapping that this state stores.
*/
public int sourceIdAt(int index) {
return mSources.keyAt(index);
}
/**
* Given an index in the range <code>0...sourceSize()-1</code>, returns the source from the
* <code>index</code>th 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<InsetsSource> thisSources = mSources;
final SparseArray<InsetsSource> 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<InsetsState> CREATOR = new Creator<>() {
public InsetsState createFromParcel(Parcel in) {
return new InsetsState(in);
}
public InsetsState[] newArray(int size) {
return new InsetsState[size];
}
};
public SparseArray<InsetsSource> 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<InsetsSource> 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) { }
}
}