1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.view; 18 19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; 20 import static android.util.SequenceUtils.getInitSeq; 21 import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; 22 import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; 23 import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER; 24 import static android.view.InsetsStateProto.DISPLAY_CUTOUT; 25 import static android.view.InsetsStateProto.DISPLAY_FRAME; 26 import static android.view.InsetsStateProto.SOURCES; 27 import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE; 28 import static android.view.WindowInsets.Type.captionBar; 29 import static android.view.WindowInsets.Type.displayCutout; 30 import static android.view.WindowInsets.Type.ime; 31 import static android.view.WindowInsets.Type.indexOf; 32 import static android.view.WindowInsets.Type.statusBars; 33 import static android.view.WindowInsets.Type.systemBars; 34 import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN; 35 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; 36 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; 37 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; 38 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST; 39 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ERROR; 40 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; 41 import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS; 42 43 import android.annotation.NonNull; 44 import android.annotation.Nullable; 45 import android.app.WindowConfiguration.ActivityType; 46 import android.graphics.Insets; 47 import android.graphics.Rect; 48 import android.os.Parcel; 49 import android.os.Parcelable; 50 import android.util.SparseArray; 51 import android.util.SparseIntArray; 52 import android.util.proto.ProtoOutputStream; 53 import android.view.InsetsSource.InternalInsetsSide; 54 import android.view.WindowInsets.Type; 55 import android.view.WindowInsets.Type.InsetsType; 56 import android.view.WindowManager.LayoutParams.SoftInputModeFlags; 57 58 import com.android.internal.annotations.VisibleForTesting; 59 60 import java.io.PrintWriter; 61 import java.util.Objects; 62 import java.util.StringJoiner; 63 64 /** 65 * Holder for state of system windows that cause window insets for all other windows in the system. 66 * @hide 67 */ 68 public class InsetsState implements Parcelable { 69 70 private final SparseArray<InsetsSource> mSources; 71 72 /** 73 * The frame of the display these sources are relative to. 74 */ 75 private final Rect mDisplayFrame = new Rect(); 76 77 /** The area cut from the display. */ 78 private final DisplayCutout.ParcelableWrapper mDisplayCutout = 79 new DisplayCutout.ParcelableWrapper(); 80 81 /** 82 * The frame that rounded corners are relative to. 83 * 84 * There are 2 cases that will draw fake rounded corners: 85 * 1. In split-screen mode 86 * 2. Devices with a task bar 87 * We need to report these fake rounded corners to apps by re-calculating based on this frame. 88 */ 89 private final Rect mRoundedCornerFrame = new Rect(); 90 91 /** The rounded corners on the display */ 92 private RoundedCorners mRoundedCorners = RoundedCorners.NO_ROUNDED_CORNERS; 93 94 /** The bounds of the Privacy Indicator */ 95 private PrivacyIndicatorBounds mPrivacyIndicatorBounds = 96 new PrivacyIndicatorBounds(); 97 98 /** The display shape */ 99 private DisplayShape mDisplayShape = DisplayShape.NONE; 100 101 /** To make sure the info update between client and system server is in order. */ 102 private int mSeq = getInitSeq(); 103 InsetsState()104 public InsetsState() { 105 mSources = new SparseArray<>(); 106 } 107 InsetsState(InsetsState copy)108 public InsetsState(InsetsState copy) { 109 this(copy, false /* copySources */); 110 } 111 InsetsState(InsetsState copy, boolean copySources)112 public InsetsState(InsetsState copy, boolean copySources) { 113 mSources = new SparseArray<>(copy.mSources.size()); 114 set(copy, copySources); 115 } 116 117 /** 118 * Calculates {@link WindowInsets} based on the current source configuration. 119 * 120 * @param frame The frame to calculate the insets relative to. 121 * @param ignoringVisibilityState {@link InsetsState} used to calculate 122 * {@link WindowInsets#getInsetsIgnoringVisibility(int)} information, or pass 123 * {@code null} to use this state to calculate that information. 124 * @return The calculated insets. 125 */ calculateInsets(Rect frame, @Nullable InsetsState ignoringVisibilityState, boolean isScreenRound, int legacySoftInputMode, int legacyWindowFlags, int legacySystemUiFlags, int windowType, @ActivityType int activityType, @Nullable @InternalInsetsSide SparseIntArray idSideMap)126 public WindowInsets calculateInsets(Rect frame, @Nullable InsetsState ignoringVisibilityState, 127 boolean isScreenRound, int legacySoftInputMode, int legacyWindowFlags, 128 int legacySystemUiFlags, int windowType, @ActivityType int activityType, 129 @Nullable @InternalInsetsSide SparseIntArray idSideMap) { 130 Insets[] typeInsetsMap = new Insets[Type.SIZE]; 131 Insets[] typeMaxInsetsMap = new Insets[Type.SIZE]; 132 boolean[] typeVisibilityMap = new boolean[Type.SIZE]; 133 final Rect relativeFrame = new Rect(frame); 134 final Rect relativeFrameMax = new Rect(frame); 135 @InsetsType int forceConsumingTypes = 0; 136 boolean forceConsumingOpaqueCaptionBar = false; 137 @InsetsType int suppressScrimTypes = 0; 138 final Rect[][] typeBoundingRectsMap = new Rect[Type.SIZE][]; 139 final Rect[][] typeMaxBoundingRectsMap = new Rect[Type.SIZE][]; 140 for (int i = mSources.size() - 1; i >= 0; i--) { 141 final InsetsSource source = mSources.valueAt(i); 142 final @InsetsType int type = source.getType(); 143 final @InsetsSource.Flags int flags = source.getFlags(); 144 145 if ((flags & InsetsSource.FLAG_FORCE_CONSUMING) != 0) { 146 forceConsumingTypes |= type; 147 } 148 149 if (ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS.isTrue() 150 && (flags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) != 0) { 151 forceConsumingOpaqueCaptionBar = true; 152 } 153 154 if ((flags & InsetsSource.FLAG_SUPPRESS_SCRIM) != 0) { 155 suppressScrimTypes |= type; 156 } 157 158 processSource(source, relativeFrame, false /* ignoreVisibility */, typeInsetsMap, 159 idSideMap, typeVisibilityMap, typeBoundingRectsMap); 160 161 // IME won't be reported in max insets as the size depends on the EditorInfo of the IME 162 // target. 163 if (type != WindowInsets.Type.ime()) { 164 InsetsSource ignoringVisibilitySource = ignoringVisibilityState != null 165 ? ignoringVisibilityState.peekSource(source.getId()) 166 : source; 167 if (ignoringVisibilitySource == null) { 168 continue; 169 } 170 processSource(ignoringVisibilitySource, relativeFrameMax, 171 true /* ignoreVisibility */, typeMaxInsetsMap, null /* idSideMap */, 172 null /* typeVisibilityMap */, typeMaxBoundingRectsMap); 173 } 174 } 175 final int softInputAdjustMode = legacySoftInputMode & SOFT_INPUT_MASK_ADJUST; 176 177 @InsetsType int compatInsetsTypes = systemBars() | displayCutout(); 178 if (softInputAdjustMode == SOFT_INPUT_ADJUST_RESIZE) { 179 compatInsetsTypes |= ime(); 180 } 181 if ((legacyWindowFlags & FLAG_FULLSCREEN) != 0) { 182 compatInsetsTypes &= ~statusBars(); 183 } 184 if (clearsCompatInsets(windowType, legacyWindowFlags, activityType, forceConsumingTypes)) { 185 compatInsetsTypes = 0; 186 } 187 188 return new WindowInsets(typeInsetsMap, typeMaxInsetsMap, typeVisibilityMap, isScreenRound, 189 forceConsumingTypes, forceConsumingOpaqueCaptionBar, suppressScrimTypes, 190 calculateRelativeCutout(frame), 191 calculateRelativeRoundedCorners(frame), 192 calculateRelativePrivacyIndicatorBounds(frame), 193 calculateRelativeDisplayShape(frame), 194 compatInsetsTypes, (legacySystemUiFlags & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0, 195 typeBoundingRectsMap, typeMaxBoundingRectsMap, frame.width(), frame.height()); 196 } 197 calculateRelativeCutout(Rect frame)198 private DisplayCutout calculateRelativeCutout(Rect frame) { 199 final DisplayCutout raw = mDisplayCutout.get(); 200 if (mDisplayFrame.equals(frame)) { 201 return raw; 202 } 203 if (frame == null) { 204 return DisplayCutout.NO_CUTOUT; 205 } 206 final int insetLeft = frame.left - mDisplayFrame.left; 207 final int insetTop = frame.top - mDisplayFrame.top; 208 final int insetRight = mDisplayFrame.right - frame.right; 209 final int insetBottom = mDisplayFrame.bottom - frame.bottom; 210 if (insetLeft >= raw.getSafeInsetLeft() 211 && insetTop >= raw.getSafeInsetTop() 212 && insetRight >= raw.getSafeInsetRight() 213 && insetBottom >= raw.getSafeInsetBottom()) { 214 return DisplayCutout.NO_CUTOUT; 215 } 216 return raw.inset(insetLeft, insetTop, insetRight, insetBottom); 217 } 218 calculateRelativeRoundedCorners(Rect frame)219 private RoundedCorners calculateRelativeRoundedCorners(Rect frame) { 220 if (frame == null) { 221 return RoundedCorners.NO_ROUNDED_CORNERS; 222 } 223 // If mRoundedCornerFrame is set, we should calculate the new RoundedCorners based on this 224 // frame. 225 final Rect roundedCornerFrame = new Rect(mRoundedCornerFrame); 226 for (int i = mSources.size() - 1; i >= 0; i--) { 227 final InsetsSource source = mSources.valueAt(i); 228 if (source.hasFlags(FLAG_INSETS_ROUNDED_CORNER)) { 229 final Insets insets = source.calculateInsets(roundedCornerFrame, false); 230 roundedCornerFrame.inset(insets); 231 } 232 } 233 if (!roundedCornerFrame.isEmpty() && !roundedCornerFrame.equals(mDisplayFrame)) { 234 return mRoundedCorners.insetWithFrame(frame, roundedCornerFrame); 235 } 236 if (mDisplayFrame.equals(frame)) { 237 return mRoundedCorners; 238 } 239 final int insetLeft = frame.left - mDisplayFrame.left; 240 final int insetTop = frame.top - mDisplayFrame.top; 241 final int insetRight = mDisplayFrame.right - frame.right; 242 final int insetBottom = mDisplayFrame.bottom - frame.bottom; 243 return mRoundedCorners.inset(insetLeft, insetTop, insetRight, insetBottom); 244 } 245 calculateRelativePrivacyIndicatorBounds(Rect frame)246 private PrivacyIndicatorBounds calculateRelativePrivacyIndicatorBounds(Rect frame) { 247 if (mDisplayFrame.equals(frame)) { 248 return mPrivacyIndicatorBounds; 249 } 250 if (frame == null) { 251 return null; 252 } 253 final int insetLeft = frame.left - mDisplayFrame.left; 254 final int insetTop = frame.top - mDisplayFrame.top; 255 final int insetRight = mDisplayFrame.right - frame.right; 256 final int insetBottom = mDisplayFrame.bottom - frame.bottom; 257 return mPrivacyIndicatorBounds.inset(insetLeft, insetTop, insetRight, insetBottom); 258 } 259 calculateRelativeDisplayShape(Rect frame)260 private DisplayShape calculateRelativeDisplayShape(Rect frame) { 261 if (mDisplayFrame.equals(frame)) { 262 return mDisplayShape; 263 } 264 if (frame == null) { 265 return DisplayShape.NONE; 266 } 267 return mDisplayShape.setOffset(-frame.left, -frame.top); 268 } 269 calculateInsets(Rect frame, @InsetsType int types, boolean ignoreVisibility)270 public Insets calculateInsets(Rect frame, @InsetsType int types, boolean ignoreVisibility) { 271 Insets insets = Insets.NONE; 272 for (int i = mSources.size() - 1; i >= 0; i--) { 273 final InsetsSource source = mSources.valueAt(i); 274 if ((source.getType() & types) == 0) { 275 continue; 276 } 277 insets = Insets.max(source.calculateInsets(frame, ignoreVisibility), insets); 278 } 279 return insets; 280 } 281 calculateInsets(Rect frame, @InsetsType int types, @InsetsType int requestedVisibleTypes)282 public Insets calculateInsets(Rect frame, @InsetsType int types, 283 @InsetsType int requestedVisibleTypes) { 284 Insets insets = Insets.NONE; 285 for (int i = mSources.size() - 1; i >= 0; i--) { 286 final InsetsSource source = mSources.valueAt(i); 287 if ((source.getType() & types & requestedVisibleTypes) == 0) { 288 continue; 289 } 290 insets = Insets.max(source.calculateInsets(frame, true), insets); 291 } 292 return insets; 293 } 294 calculateVisibleInsets(Rect frame, int windowType, @ActivityType int activityType, @SoftInputModeFlags int softInputMode, int windowFlags)295 public Insets calculateVisibleInsets(Rect frame, int windowType, @ActivityType int activityType, 296 @SoftInputModeFlags int softInputMode, int windowFlags) { 297 final int softInputAdjustMode = softInputMode & SOFT_INPUT_MASK_ADJUST; 298 final int visibleInsetsTypes = softInputAdjustMode != SOFT_INPUT_ADJUST_NOTHING 299 ? systemBars() | displayCutout() | ime() 300 : systemBars() | displayCutout(); 301 @InsetsType int forceConsumingTypes = 0; 302 Insets insets = Insets.NONE; 303 for (int i = mSources.size() - 1; i >= 0; i--) { 304 final InsetsSource source = mSources.valueAt(i); 305 if ((source.getType() & visibleInsetsTypes) == 0) { 306 continue; 307 } 308 if (source.hasFlags(FLAG_FORCE_CONSUMING)) { 309 forceConsumingTypes |= source.getType(); 310 } 311 insets = Insets.max(source.calculateVisibleInsets(frame), insets); 312 } 313 return clearsCompatInsets(windowType, windowFlags, activityType, forceConsumingTypes) 314 ? Insets.NONE 315 : insets; 316 } 317 318 /** 319 * Calculate which insets *cannot* be controlled, because the frame does not cover the 320 * respective side of the inset. 321 * 322 * If the frame of our window doesn't cover the entire inset, the control API makes very 323 * little sense, as we don't deal with negative insets. 324 */ 325 @InsetsType calculateUncontrollableInsetsFromFrame(Rect frame)326 public int calculateUncontrollableInsetsFromFrame(Rect frame) { 327 int blocked = 0; 328 for (int i = mSources.size() - 1; i >= 0; i--) { 329 final InsetsSource source = mSources.valueAt(i); 330 if (!canControlSource(frame, source)) { 331 blocked |= source.getType(); 332 } 333 } 334 return blocked; 335 } 336 canControlSource(Rect frame, InsetsSource source)337 private static boolean canControlSource(Rect frame, InsetsSource source) { 338 final Insets insets = source.calculateInsets(frame, true /* ignoreVisibility */); 339 final Rect sourceFrame = source.getFrame(); 340 final int sourceWidth = sourceFrame.width(); 341 final int sourceHeight = sourceFrame.height(); 342 return insets.left == sourceWidth || insets.right == sourceWidth 343 || insets.top == sourceHeight || insets.bottom == sourceHeight; 344 } 345 processSource(InsetsSource source, Rect relativeFrame, boolean ignoreVisibility, Insets[] typeInsetsMap, @Nullable @InternalInsetsSide SparseIntArray idSideMap, @Nullable boolean[] typeVisibilityMap, Rect[][] typeBoundingRectsMap)346 private void processSource(InsetsSource source, Rect relativeFrame, boolean ignoreVisibility, 347 Insets[] typeInsetsMap, @Nullable @InternalInsetsSide SparseIntArray idSideMap, 348 @Nullable boolean[] typeVisibilityMap, Rect[][] typeBoundingRectsMap) { 349 Insets insets = source.calculateInsets(relativeFrame, ignoreVisibility); 350 final Rect[] boundingRects = source.calculateBoundingRects(relativeFrame, ignoreVisibility); 351 352 final int type = source.getType(); 353 processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, 354 typeBoundingRectsMap, insets, boundingRects, type); 355 356 if (type == Type.MANDATORY_SYSTEM_GESTURES) { 357 // Mandatory system gestures are also system gestures. 358 // TODO: find a way to express this more generally. One option would be to define 359 // Type.systemGestureInsets() as NORMAL | MANDATORY, but then we lose the 360 // ability to set systemGestureInsets() independently from 361 // mandatorySystemGestureInsets() in the Builder. 362 processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, 363 typeBoundingRectsMap, insets, boundingRects, Type.SYSTEM_GESTURES); 364 } 365 if (type == Type.CAPTION_BAR) { 366 // Caption should also be gesture and tappable elements. This should not be needed when 367 // the caption is added from the shell, as the shell can add other types at the same 368 // time. 369 processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, 370 typeBoundingRectsMap, insets, boundingRects, Type.SYSTEM_GESTURES); 371 processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, 372 typeBoundingRectsMap, insets, boundingRects, Type.MANDATORY_SYSTEM_GESTURES); 373 processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap, 374 typeBoundingRectsMap, insets, boundingRects, Type.TAPPABLE_ELEMENT); 375 } 376 } 377 processSourceAsPublicType(InsetsSource source, Insets[] typeInsetsMap, @InternalInsetsSide @Nullable SparseIntArray idSideMap, @Nullable boolean[] typeVisibilityMap, Rect[][] typeBoundingRectsMap, Insets insets, Rect[] boundingRects, int type)378 private void processSourceAsPublicType(InsetsSource source, Insets[] typeInsetsMap, 379 @InternalInsetsSide @Nullable SparseIntArray idSideMap, 380 @Nullable boolean[] typeVisibilityMap, Rect[][] typeBoundingRectsMap, 381 Insets insets, Rect[] boundingRects, int type) { 382 int index = indexOf(type); 383 384 // Don't put Insets.NONE into typeInsetsMap. Otherwise, two WindowInsets can be considered 385 // as non-equal while they provide the same insets of each type from WindowInsets#getInsets 386 // if one WindowInsets has Insets.NONE for a type and the other has null for the same type. 387 if (!Insets.NONE.equals(insets)) { 388 Insets existing = typeInsetsMap[index]; 389 if (existing == null) { 390 typeInsetsMap[index] = insets; 391 } else { 392 typeInsetsMap[index] = Insets.max(existing, insets); 393 } 394 } 395 396 if (typeVisibilityMap != null) { 397 typeVisibilityMap[index] = source.isVisible(); 398 } 399 400 if (idSideMap != null) { 401 @InternalInsetsSide int insetSide = InsetsSource.getInsetSide(insets); 402 if (insetSide != InsetsSource.SIDE_UNKNOWN) { 403 idSideMap.put(source.getId(), insetSide); 404 } 405 } 406 407 if (typeBoundingRectsMap != null && boundingRects.length > 0) { 408 final Rect[] existing = typeBoundingRectsMap[index]; 409 if (existing == null) { 410 typeBoundingRectsMap[index] = boundingRects; 411 } else { 412 typeBoundingRectsMap[index] = concatenate(existing, boundingRects); 413 } 414 } 415 } 416 concatenate(Rect[] a, Rect[] b)417 private static Rect[] concatenate(Rect[] a, Rect[] b) { 418 final Rect[] c = new Rect[a.length + b.length]; 419 System.arraycopy(a, 0, c, 0, a.length); 420 System.arraycopy(b, 0, c, a.length, b.length); 421 return c; 422 } 423 424 /** 425 * Gets the source mapped from the ID, or creates one if no such mapping has been made. 426 */ getOrCreateSource(int id, int type)427 public InsetsSource getOrCreateSource(int id, int type) { 428 InsetsSource source = mSources.get(id); 429 if (source != null) { 430 return source; 431 } 432 source = new InsetsSource(id, type); 433 mSources.put(id, source); 434 return source; 435 } 436 437 /** 438 * Gets the source mapped from the ID, or <code>null</code> if no such mapping has been made. 439 */ peekSource(int id)440 public @Nullable InsetsSource peekSource(int id) { 441 return mSources.get(id); 442 } 443 444 /** 445 * Given an index in the range <code>0...sourceSize()-1</code>, returns the source ID from the 446 * <code>index</code>th ID-source mapping that this state stores. 447 */ sourceIdAt(int index)448 public int sourceIdAt(int index) { 449 return mSources.keyAt(index); 450 } 451 452 /** 453 * Given an index in the range <code>0...sourceSize()-1</code>, returns the source from the 454 * <code>index</code>th ID-source mapping that this state stores. 455 */ sourceAt(int index)456 public InsetsSource sourceAt(int index) { 457 return mSources.valueAt(index); 458 } 459 460 /** 461 * Returns the amount of the sources. 462 */ sourceSize()463 public int sourceSize() { 464 return mSources.size(); 465 } 466 467 /** 468 * Returns if the source is visible or the type is default visible and the source doesn't exist. 469 * 470 * @param id The ID of the source. 471 * @param type The {@link InsetsType} to see if it is default visible. 472 * @return {@code true} if the source is visible or the type is default visible and the source 473 * doesn't exist. 474 */ isSourceOrDefaultVisible(int id, @InsetsType int type)475 public boolean isSourceOrDefaultVisible(int id, @InsetsType int type) { 476 final InsetsSource source = mSources.get(id); 477 return source != null ? source.isVisible() : (type & Type.defaultVisible()) != 0; 478 } 479 setDisplayFrame(Rect frame)480 public void setDisplayFrame(Rect frame) { 481 mDisplayFrame.set(frame); 482 } 483 getDisplayFrame()484 public Rect getDisplayFrame() { 485 return mDisplayFrame; 486 } 487 setDisplayCutout(DisplayCutout cutout)488 public void setDisplayCutout(DisplayCutout cutout) { 489 mDisplayCutout.set(cutout); 490 } 491 getDisplayCutout()492 public DisplayCutout getDisplayCutout() { 493 return mDisplayCutout.get(); 494 } 495 getDisplayCutoutSafe(Rect outBounds)496 public void getDisplayCutoutSafe(Rect outBounds) { 497 outBounds.set( 498 WindowLayout.MIN_X, WindowLayout.MIN_Y, WindowLayout.MAX_X, WindowLayout.MAX_Y); 499 final DisplayCutout cutout = mDisplayCutout.get(); 500 final Rect displayFrame = mDisplayFrame; 501 if (!cutout.isEmpty()) { 502 if (cutout.getSafeInsetLeft() > 0) { 503 outBounds.left = displayFrame.left + cutout.getSafeInsetLeft(); 504 } 505 if (cutout.getSafeInsetTop() > 0) { 506 outBounds.top = displayFrame.top + cutout.getSafeInsetTop(); 507 } 508 if (cutout.getSafeInsetRight() > 0) { 509 outBounds.right = displayFrame.right - cutout.getSafeInsetRight(); 510 } 511 if (cutout.getSafeInsetBottom() > 0) { 512 outBounds.bottom = displayFrame.bottom - cutout.getSafeInsetBottom(); 513 } 514 } 515 } 516 setRoundedCorners(RoundedCorners roundedCorners)517 public void setRoundedCorners(RoundedCorners roundedCorners) { 518 mRoundedCorners = roundedCorners; 519 } 520 getRoundedCorners()521 public RoundedCorners getRoundedCorners() { 522 return mRoundedCorners; 523 } 524 525 /** 526 * Set the frame that will be used to calculate the rounded corners. 527 * 528 * @see #mRoundedCornerFrame 529 */ setRoundedCornerFrame(Rect frame)530 public void setRoundedCornerFrame(Rect frame) { 531 mRoundedCornerFrame.set(frame); 532 } 533 setPrivacyIndicatorBounds(PrivacyIndicatorBounds bounds)534 public void setPrivacyIndicatorBounds(PrivacyIndicatorBounds bounds) { 535 mPrivacyIndicatorBounds = bounds; 536 } 537 getPrivacyIndicatorBounds()538 public PrivacyIndicatorBounds getPrivacyIndicatorBounds() { 539 return mPrivacyIndicatorBounds; 540 } 541 setDisplayShape(DisplayShape displayShape)542 public void setDisplayShape(DisplayShape displayShape) { 543 mDisplayShape = displayShape; 544 } 545 getDisplayShape()546 public DisplayShape getDisplayShape() { 547 return mDisplayShape; 548 } 549 550 /** 551 * Removes the source which has the ID from this state, if there was any. 552 * 553 * @param id The ID of the source to remove. 554 */ removeSource(int id)555 public void removeSource(int id) { 556 mSources.delete(id); 557 } 558 559 /** 560 * Removes the source at the specified index. 561 * 562 * @param index The index of the source to remove. 563 */ removeSourceAt(int index)564 public void removeSourceAt(int index) { 565 mSources.removeAt(index); 566 } 567 568 /** 569 * A shortcut for setting the visibility of the source. 570 * 571 * @param id The ID of the source to set the visibility 572 * @param visible {@code true} for visible 573 */ setSourceVisible(int id, boolean visible)574 public void setSourceVisible(int id, boolean visible) { 575 final InsetsSource source = mSources.get(id); 576 if (source != null) { 577 source.setVisible(visible); 578 } 579 } 580 581 /** 582 * Scales the frame and the visible frame (if there is one) of each source. 583 * 584 * @param scale the scale to be applied 585 */ scale(float scale)586 public void scale(float scale) { 587 mDisplayFrame.scale(scale); 588 mDisplayCutout.scale(scale); 589 mRoundedCorners = mRoundedCorners.scale(scale); 590 mRoundedCornerFrame.scale(scale); 591 mPrivacyIndicatorBounds = mPrivacyIndicatorBounds.scale(scale); 592 mDisplayShape = mDisplayShape.setScale(scale); 593 for (int i = mSources.size() - 1; i >= 0; i--) { 594 final InsetsSource source = mSources.valueAt(i); 595 source.getFrame().scale(scale); 596 final Rect visibleFrame = source.getVisibleFrame(); 597 if (visibleFrame != null) { 598 visibleFrame.scale(scale); 599 } 600 } 601 } 602 getSeq()603 public int getSeq() { 604 return mSeq; 605 } 606 setSeq(int seq)607 public void setSeq(int seq) { 608 mSeq = seq; 609 } 610 set(InsetsState other)611 public void set(InsetsState other) { 612 set(other, false /* copySources */); 613 } 614 set(InsetsState other, boolean copySources)615 public void set(InsetsState other, boolean copySources) { 616 mDisplayFrame.set(other.mDisplayFrame); 617 mDisplayCutout.set(other.mDisplayCutout); 618 mRoundedCorners = other.getRoundedCorners(); 619 mRoundedCornerFrame.set(other.mRoundedCornerFrame); 620 mPrivacyIndicatorBounds = other.getPrivacyIndicatorBounds(); 621 mDisplayShape = other.getDisplayShape(); 622 mSeq = other.mSeq; 623 mSources.clear(); 624 for (int i = 0, size = other.mSources.size(); i < size; i++) { 625 final InsetsSource otherSource = other.mSources.valueAt(i); 626 mSources.append(otherSource.getId(), copySources 627 ? new InsetsSource(otherSource) 628 : otherSource); 629 } 630 } 631 632 /** 633 * Sets the values from the other InsetsState. But for sources, only specific types of source 634 * would be set. 635 * 636 * @param other the other InsetsState. 637 * @param types the only types of sources would be set. 638 */ set(InsetsState other, @InsetsType int types)639 public void set(InsetsState other, @InsetsType int types) { 640 mDisplayFrame.set(other.mDisplayFrame); 641 mDisplayCutout.set(other.mDisplayCutout); 642 mRoundedCorners = other.getRoundedCorners(); 643 mRoundedCornerFrame.set(other.mRoundedCornerFrame); 644 mPrivacyIndicatorBounds = other.getPrivacyIndicatorBounds(); 645 mDisplayShape = other.getDisplayShape(); 646 mSeq = other.mSeq; 647 if (types == 0) { 648 return; 649 } 650 for (int i = mSources.size() - 1; i >= 0; i--) { 651 final InsetsSource source = mSources.valueAt(i); 652 if ((source.getType() & types) != 0) { 653 mSources.removeAt(i); 654 } 655 } 656 for (int i = other.mSources.size() - 1; i >= 0; i--) { 657 final InsetsSource otherSource = other.mSources.valueAt(i); 658 if ((otherSource.getType() & types) != 0) { 659 mSources.put(otherSource.getId(), otherSource); 660 } 661 } 662 } 663 addSource(InsetsSource source)664 public void addSource(InsetsSource source) { 665 mSources.put(source.getId(), source); 666 } 667 clearsCompatInsets(int windowType, int windowFlags, @ActivityType int activityType, @InsetsType int forceConsumingTypes)668 public static boolean clearsCompatInsets(int windowType, int windowFlags, 669 @ActivityType int activityType, @InsetsType int forceConsumingTypes) { 670 return (windowFlags & FLAG_LAYOUT_NO_LIMITS) != 0 671 // For compatibility reasons, this excludes the wallpaper, the system error windows, 672 // and the app windows while any system bar is forcibly consumed. 673 && windowType != TYPE_WALLPAPER && windowType != TYPE_SYSTEM_ERROR 674 // This ensures the app content won't be obscured by compat insets even if the app 675 // has FLAG_LAYOUT_NO_LIMITS. 676 && (forceConsumingTypes == 0 || activityType != ACTIVITY_TYPE_STANDARD); 677 } 678 dump(String prefix, PrintWriter pw)679 public void dump(String prefix, PrintWriter pw) { 680 final String newPrefix = prefix + " "; 681 pw.println(prefix + "InsetsState"); 682 pw.println(newPrefix + "mDisplayFrame=" + mDisplayFrame); 683 pw.println(newPrefix + "mDisplayCutout=" + mDisplayCutout.get()); 684 pw.println(newPrefix + "mRoundedCorners=" + mRoundedCorners); 685 pw.println(newPrefix + "mRoundedCornerFrame=" + mRoundedCornerFrame); 686 pw.println(newPrefix + "mPrivacyIndicatorBounds=" + mPrivacyIndicatorBounds); 687 pw.println(newPrefix + "mDisplayShape=" + mDisplayShape); 688 for (int i = 0, size = mSources.size(); i < size; i++) { 689 mSources.valueAt(i).dump(newPrefix + " ", pw); 690 } 691 } 692 dumpDebug(ProtoOutputStream proto, long fieldId)693 void dumpDebug(ProtoOutputStream proto, long fieldId) { 694 final long token = proto.start(fieldId); 695 final InsetsSource source = mSources.get(InsetsSource.ID_IME); 696 if (source != null) { 697 source.dumpDebug(proto, SOURCES); 698 } 699 mDisplayFrame.dumpDebug(proto, DISPLAY_FRAME); 700 mDisplayCutout.get().dumpDebug(proto, DISPLAY_CUTOUT); 701 proto.end(token); 702 } 703 704 @Override equals(@ullable Object o)705 public boolean equals(@Nullable Object o) { 706 return equals(o, false, false); 707 } 708 709 /** 710 * An equals method can exclude the caption insets. This is useful because we assemble the 711 * caption insets information on the client side, and when we communicate with server, it's 712 * excluded. 713 * @param excludesCaptionBar If {@link Type#captionBar()}} should be ignored. 714 * @param excludesInvisibleIme If {@link WindowInsets.Type#ime()} should be ignored when IME is 715 * not visible. 716 * @return {@code true} if the two InsetsState objects are equal, {@code false} otherwise. 717 */ 718 @VisibleForTesting equals(@ullable Object o, boolean excludesCaptionBar, boolean excludesInvisibleIme)719 public boolean equals(@Nullable Object o, boolean excludesCaptionBar, 720 boolean excludesInvisibleIme) { 721 if (this == o) { return true; } 722 if (o == null || getClass() != o.getClass()) { return false; } 723 724 InsetsState state = (InsetsState) o; 725 726 if (!mDisplayFrame.equals(state.mDisplayFrame) 727 || !mDisplayCutout.equals(state.mDisplayCutout) 728 || !mRoundedCorners.equals(state.mRoundedCorners) 729 || !mRoundedCornerFrame.equals(state.mRoundedCornerFrame) 730 || !mPrivacyIndicatorBounds.equals(state.mPrivacyIndicatorBounds) 731 || !mDisplayShape.equals(state.mDisplayShape)) { 732 // mSeq is for internal bookkeeping only. 733 return false; 734 } 735 736 final SparseArray<InsetsSource> thisSources = mSources; 737 final SparseArray<InsetsSource> thatSources = state.mSources; 738 if (!excludesCaptionBar && !excludesInvisibleIme) { 739 return thisSources.contentEquals(thatSources); 740 } else { 741 final int thisSize = thisSources.size(); 742 final int thatSize = thatSources.size(); 743 int thisIndex = 0; 744 int thatIndex = 0; 745 while (thisIndex < thisSize || thatIndex < thatSize) { 746 InsetsSource thisSource = thisIndex < thisSize 747 ? thisSources.valueAt(thisIndex) 748 : null; 749 750 // Seek to the next non-excluding source of ours. 751 while (thisSource != null 752 && (excludesCaptionBar && thisSource.getType() == captionBar() 753 || excludesInvisibleIme && thisSource.getType() == ime() 754 && !thisSource.isVisible())) { 755 thisIndex++; 756 thisSource = thisIndex < thisSize ? thisSources.valueAt(thisIndex) : null; 757 } 758 759 InsetsSource thatSource = thatIndex < thatSize 760 ? thatSources.valueAt(thatIndex) 761 : null; 762 763 // Seek to the next non-excluding source of theirs. 764 while (thatSource != null 765 && (excludesCaptionBar && thatSource.getType() == captionBar() 766 || excludesInvisibleIme && thatSource.getType() == ime() 767 && !thatSource.isVisible())) { 768 thatIndex++; 769 thatSource = thatIndex < thatSize ? thatSources.valueAt(thatIndex) : null; 770 } 771 772 if (!Objects.equals(thisSource, thatSource)) { 773 return false; 774 } 775 776 thisIndex++; 777 thatIndex++; 778 } 779 return true; 780 } 781 } 782 783 @Override 784 public int hashCode() { 785 return Objects.hash(mDisplayFrame, mDisplayCutout, mSources.contentHashCode(), 786 mRoundedCorners, mPrivacyIndicatorBounds, mRoundedCornerFrame, mDisplayShape); 787 } 788 789 public InsetsState(Parcel in) { 790 mSources = readFromParcel(in); 791 } 792 793 @Override 794 public int describeContents() { 795 return 0; 796 } 797 798 @Override 799 public void writeToParcel(Parcel dest, int flags) { 800 mDisplayFrame.writeToParcel(dest, flags); 801 mDisplayCutout.writeToParcel(dest, flags); 802 dest.writeTypedObject(mRoundedCorners, flags); 803 mRoundedCornerFrame.writeToParcel(dest, flags); 804 dest.writeTypedObject(mPrivacyIndicatorBounds, flags); 805 dest.writeTypedObject(mDisplayShape, flags); 806 dest.writeInt(mSeq); 807 final int size = mSources.size(); 808 dest.writeInt(size); 809 for (int i = 0; i < size; i++) { 810 dest.writeTypedObject(mSources.valueAt(i), flags); 811 } 812 } 813 814 public static final @NonNull Creator<InsetsState> CREATOR = new Creator<>() { 815 816 public InsetsState createFromParcel(Parcel in) { 817 return new InsetsState(in); 818 } 819 820 public InsetsState[] newArray(int size) { 821 return new InsetsState[size]; 822 } 823 }; 824 825 public SparseArray<InsetsSource> readFromParcel(Parcel in) { 826 mDisplayFrame.readFromParcel(in); 827 mDisplayCutout.readFromParcel(in); 828 mRoundedCorners = in.readTypedObject(RoundedCorners.CREATOR); 829 mRoundedCornerFrame.readFromParcel(in); 830 mPrivacyIndicatorBounds = in.readTypedObject(PrivacyIndicatorBounds.CREATOR); 831 mDisplayShape = in.readTypedObject(DisplayShape.CREATOR); 832 mSeq = in.readInt(); 833 final int size = in.readInt(); 834 final SparseArray<InsetsSource> sources; 835 if (mSources == null) { 836 // We are constructing this InsetsState. 837 sources = new SparseArray<>(size); 838 } else { 839 sources = mSources; 840 sources.clear(); 841 } 842 for (int i = 0; i < size; i++) { 843 final InsetsSource source = in.readTypedObject(InsetsSource.CREATOR); 844 sources.append(source.getId(), source); 845 } 846 return sources; 847 } 848 849 @Override 850 public String toString() { 851 final StringJoiner joiner = new StringJoiner(", "); 852 for (int i = 0, size = mSources.size(); i < size; i++) { 853 joiner.add(mSources.valueAt(i).toString()); 854 } 855 return "InsetsState: {" 856 + "mDisplayFrame=" + mDisplayFrame 857 + ", mDisplayCutout=" + mDisplayCutout 858 + ", mRoundedCorners=" + mRoundedCorners 859 + " mRoundedCornerFrame=" + mRoundedCornerFrame 860 + ", mPrivacyIndicatorBounds=" + mPrivacyIndicatorBounds 861 + ", mDisplayShape=" + mDisplayShape 862 + ", mSources= { " + joiner 863 + " }"; 864 } 865 866 /** 867 * Traverses sources in two {@link InsetsState}s and calls back when events defined in 868 * {@link OnTraverseCallbacks} happen. This is optimized for {@link SparseArray} that we avoid 869 * triggering the binary search while getting the key or the value. 870 * 871 * This can be used to copy attributes of sources from one InsetsState to the other one, or to 872 * remove sources existing in one InsetsState but not in the other one. 873 * 874 * @param state1 The first {@link InsetsState} to be traversed. 875 * @param state2 The second {@link InsetsState} to be traversed. 876 * @param cb The {@link OnTraverseCallbacks} to call back to the caller. 877 */ 878 public static void traverse(InsetsState state1, InsetsState state2, OnTraverseCallbacks cb) { 879 cb.onStart(state1, state2); 880 final int size1 = state1.sourceSize(); 881 final int size2 = state2.sourceSize(); 882 int index1 = 0; 883 int index2 = 0; 884 while (index1 < size1 && index2 < size2) { 885 int id1 = state1.sourceIdAt(index1); 886 int id2 = state2.sourceIdAt(index2); 887 while (id1 != id2) { 888 if (id1 < id2) { 889 cb.onIdNotFoundInState2(index1, state1.sourceAt(index1)); 890 index1++; 891 if (index1 < size1) { 892 id1 = state1.sourceIdAt(index1); 893 } else { 894 break; 895 } 896 } else { 897 cb.onIdNotFoundInState1(index2, state2.sourceAt(index2)); 898 index2++; 899 if (index2 < size2) { 900 id2 = state2.sourceIdAt(index2); 901 } else { 902 break; 903 } 904 } 905 } 906 if (index1 >= size1 || index2 >= size2) { 907 break; 908 } 909 final InsetsSource source1 = state1.sourceAt(index1); 910 final InsetsSource source2 = state2.sourceAt(index2); 911 cb.onIdMatch(source1, source2); 912 index1++; 913 index2++; 914 } 915 while (index2 < size2) { 916 cb.onIdNotFoundInState1(index2, state2.sourceAt(index2)); 917 index2++; 918 } 919 while (index1 < size1) { 920 cb.onIdNotFoundInState2(index1, state1.sourceAt(index1)); 921 index1++; 922 } 923 cb.onFinish(state1, state2); 924 } 925 926 /** 927 * Used with {@link #traverse(InsetsState, InsetsState, OnTraverseCallbacks)} to call back when 928 * certain events happen. 929 */ 930 public interface OnTraverseCallbacks { 931 932 /** 933 * Called at the beginning of the traverse. 934 * 935 * @param state1 same as the state1 supplied to {@link #traverse} 936 * @param state2 same as the state2 supplied to {@link #traverse} 937 */ 938 default void onStart(InsetsState state1, InsetsState state2) { } 939 940 /** 941 * Called when finding two IDs from two InsetsStates are the same. 942 * 943 * @param source1 the source in state1. 944 * @param source2 the source in state2. 945 */ 946 default void onIdMatch(InsetsSource source1, InsetsSource source2) { } 947 948 /** 949 * Called when finding an ID in state2 but not in state1. 950 * 951 * @param index2 the index of the ID in state2. 952 * @param source2 the source which has the ID in state2. 953 */ 954 default void onIdNotFoundInState1(int index2, InsetsSource source2) { } 955 956 /** 957 * Called when finding an ID in state1 but not in state2. 958 * 959 * @param index1 the index of the ID in state1. 960 * @param source1 the source which has the ID in state1. 961 */ 962 default void onIdNotFoundInState2(int index1, InsetsSource source1) { } 963 964 /** 965 * Called at the end of the traverse. 966 * 967 * @param state1 same as the state1 supplied to {@link #traverse} 968 * @param state2 same as the state2 supplied to {@link #traverse} 969 */ 970 default void onFinish(InsetsState state1, InsetsState state2) { } 971 } 972 } 973 974