1 /* 2 * Copyright (C) 2020 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.view.Gravity.BOTTOM; 20 import static android.view.Gravity.LEFT; 21 import static android.view.Gravity.RIGHT; 22 import static android.view.Gravity.TOP; 23 24 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; 25 26 import android.annotation.NonNull; 27 import android.annotation.Nullable; 28 import android.graphics.Insets; 29 import android.graphics.Matrix; 30 import android.graphics.Path; 31 import android.graphics.Rect; 32 import android.graphics.RectF; 33 import android.graphics.Region; 34 import android.text.TextUtils; 35 import android.util.Log; 36 import android.util.PathParser; 37 38 import com.android.internal.annotations.VisibleForTesting; 39 40 import java.util.Locale; 41 import java.util.Objects; 42 43 /** 44 * In order to accept the cutout specification for all of edges in devices, the specification 45 * parsing method is extracted from 46 * {@link android.view.DisplayCutout#fromResourcesRectApproximation(Resources, int, int)} to be 47 * the specified class for parsing the specification. 48 * BNF definition: 49 * <ul> 50 * <li>Cutouts Specification = ([Cutout Delimiter],Cutout Specification) {...}, [Dp] ; </li> 51 * <li>Cutout Specification = [Vertical Position], (SVG Path Element), [Horizontal Position] 52 * [Bind Cutout] ;</li> 53 * <li>Vertical Position = "@bottom" | "@center_vertical" ;</li> 54 * <li>Horizontal Position = "@left" | "@right" ;</li> 55 * <li>Bind Cutout = "@bind_left_cutout" | "@bind_right_cutout" ;</li> 56 * <li>Cutout Delimiter = "@cutout" ;</li> 57 * <li>Dp = "@dp"</li> 58 * </ul> 59 * 60 * <ul> 61 * <li>Vertical position is top by default if there is neither "@bottom" nor "@center_vertical" 62 * </li> 63 * <li>Horizontal position is center horizontal by default if there is neither "@left" nor 64 * "@right".</li> 65 * <li>@bottom make the cutout piece bind to bottom edge.</li> 66 * <li>both of @bind_left_cutout and @bind_right_cutout are use to claim the cutout belong to 67 * left or right edge cutout.</li> 68 * </ul> 69 * 70 * @hide 71 */ 72 @VisibleForTesting(visibility = PACKAGE) 73 public class CutoutSpecification { 74 private static final String TAG = "CutoutSpecification"; 75 private static final boolean DEBUG = false; 76 77 private static final int MINIMAL_ACCEPTABLE_PATH_LENGTH = "H1V1Z".length(); 78 79 private static final char MARKER_START_CHAR = '@'; 80 private static final String DP_MARKER = MARKER_START_CHAR + "dp"; 81 82 private static final String BOTTOM_MARKER = MARKER_START_CHAR + "bottom"; 83 private static final String RIGHT_MARKER = MARKER_START_CHAR + "right"; 84 private static final String LEFT_MARKER = MARKER_START_CHAR + "left"; 85 private static final String CUTOUT_MARKER = MARKER_START_CHAR + "cutout"; 86 private static final String CENTER_VERTICAL_MARKER = MARKER_START_CHAR + "center_vertical"; 87 88 /* By default, it's top bound cutout. That's why TOP_BOUND_CUTOUT_MARKER is not defined */ 89 private static final String BIND_RIGHT_CUTOUT_MARKER = MARKER_START_CHAR + "bind_right_cutout"; 90 private static final String BIND_LEFT_CUTOUT_MARKER = MARKER_START_CHAR + "bind_left_cutout"; 91 92 private final Path mPath; 93 private final Rect mLeftBound; 94 private final Rect mTopBound; 95 private final Rect mRightBound; 96 private final Rect mBottomBound; 97 private Insets mInsets; 98 CutoutSpecification(@onNull Parser parser)99 private CutoutSpecification(@NonNull Parser parser) { 100 mPath = parser.mPath; 101 mLeftBound = parser.mLeftBound; 102 mTopBound = parser.mTopBound; 103 mRightBound = parser.mRightBound; 104 mBottomBound = parser.mBottomBound; 105 mInsets = parser.mInsets; 106 107 applyPhysicalPixelDisplaySizeRatio(parser.mPhysicalPixelDisplaySizeRatio); 108 109 if (DEBUG) { 110 Log.d(TAG, String.format(Locale.ENGLISH, 111 "left cutout = %s, top cutout = %s, right cutout = %s, bottom cutout = %s", 112 mLeftBound != null ? mLeftBound.toString() : "", 113 mTopBound != null ? mTopBound.toString() : "", 114 mRightBound != null ? mRightBound.toString() : "", 115 mBottomBound != null ? mBottomBound.toString() : "")); 116 } 117 } 118 applyPhysicalPixelDisplaySizeRatio(float physicalPixelDisplaySizeRatio)119 private void applyPhysicalPixelDisplaySizeRatio(float physicalPixelDisplaySizeRatio) { 120 if (physicalPixelDisplaySizeRatio == 1f) { 121 return; 122 } 123 124 if (mPath != null && !mPath.isEmpty()) { 125 final Matrix matrix = new Matrix(); 126 matrix.postScale(physicalPixelDisplaySizeRatio, physicalPixelDisplaySizeRatio); 127 mPath.transform(matrix); 128 } 129 130 scaleBounds(mLeftBound, physicalPixelDisplaySizeRatio); 131 scaleBounds(mTopBound, physicalPixelDisplaySizeRatio); 132 scaleBounds(mRightBound, physicalPixelDisplaySizeRatio); 133 scaleBounds(mBottomBound, physicalPixelDisplaySizeRatio); 134 mInsets = scaleInsets(mInsets, physicalPixelDisplaySizeRatio); 135 } 136 scaleBounds(Rect r, float ratio)137 private void scaleBounds(Rect r, float ratio) { 138 if (r != null && !r.isEmpty()) { 139 r.scale(ratio); 140 } 141 } 142 scaleInsets(Insets insets, float ratio)143 private Insets scaleInsets(Insets insets, float ratio) { 144 return Insets.of( 145 (int) (insets.left * ratio + 0.5f), 146 (int) (insets.top * ratio + 0.5f), 147 (int) (insets.right * ratio + 0.5f), 148 (int) (insets.bottom * ratio + 0.5f)); 149 } 150 151 @VisibleForTesting(visibility = PACKAGE) 152 @Nullable getPath()153 public Path getPath() { 154 return mPath; 155 } 156 157 @VisibleForTesting(visibility = PACKAGE) 158 @Nullable getLeftBound()159 public Rect getLeftBound() { 160 return mLeftBound; 161 } 162 163 @VisibleForTesting(visibility = PACKAGE) 164 @Nullable getTopBound()165 public Rect getTopBound() { 166 return mTopBound; 167 } 168 169 @VisibleForTesting(visibility = PACKAGE) 170 @Nullable getRightBound()171 public Rect getRightBound() { 172 return mRightBound; 173 } 174 175 @VisibleForTesting(visibility = PACKAGE) 176 @Nullable getBottomBound()177 public Rect getBottomBound() { 178 return mBottomBound; 179 } 180 181 /** 182 * To count the safe inset according to the cutout bounds and waterfall inset. 183 * 184 * @return the safe inset. 185 */ 186 @VisibleForTesting(visibility = PACKAGE) 187 @NonNull getSafeInset()188 public Rect getSafeInset() { 189 return mInsets.toRect(); 190 } 191 decideWhichEdge(boolean isTopEdgeShortEdge, boolean isShortEdge, boolean isStart)192 private static int decideWhichEdge(boolean isTopEdgeShortEdge, 193 boolean isShortEdge, boolean isStart) { 194 return (isTopEdgeShortEdge) 195 ? ((isShortEdge) ? (isStart ? TOP : BOTTOM) : (isStart ? LEFT : RIGHT)) 196 : ((isShortEdge) ? (isStart ? LEFT : RIGHT) : (isStart ? TOP : BOTTOM)); 197 } 198 199 /** 200 * The CutoutSpecification Parser. 201 */ 202 @VisibleForTesting(visibility = PACKAGE) 203 public static class Parser { 204 private final boolean mIsShortEdgeOnTop; 205 private final float mStableDensity; 206 private final int mPhysicalDisplayWidth; 207 private final int mPhysicalDisplayHeight; 208 private final float mPhysicalPixelDisplaySizeRatio; 209 private final Matrix mMatrix; 210 private Insets mInsets; 211 private int mSafeInsetLeft; 212 private int mSafeInsetTop; 213 private int mSafeInsetRight; 214 private int mSafeInsetBottom; 215 216 private final Rect mTmpRect = new Rect(); 217 private final RectF mTmpRectF = new RectF(); 218 219 private boolean mInDp; 220 221 private Path mPath; 222 private Rect mLeftBound; 223 private Rect mTopBound; 224 private Rect mRightBound; 225 private Rect mBottomBound; 226 227 private boolean mPositionFromLeft = false; 228 private boolean mPositionFromRight = false; 229 private boolean mPositionFromBottom = false; 230 private boolean mPositionFromCenterVertical = false; 231 232 private boolean mBindLeftCutout = false; 233 private boolean mBindRightCutout = false; 234 private boolean mBindBottomCutout = false; 235 236 private boolean mIsTouchShortEdgeStart; 237 private boolean mIsTouchShortEdgeEnd; 238 private boolean mIsCloserToStartSide; 239 240 @VisibleForTesting(visibility = PACKAGE) Parser(float stableDensity, int physicalDisplayWidth, int physicalDisplayHeight)241 public Parser(float stableDensity, int physicalDisplayWidth, 242 int physicalDisplayHeight) { 243 this(stableDensity, physicalDisplayWidth, physicalDisplayHeight, 1f); 244 } 245 246 /** 247 * The constructor of the CutoutSpecification parser to parse the specification of cutout. 248 * @param stableDensity the display density. 249 * @param physicalDisplayWidth the display width. 250 * @param physicalDisplayHeight the display height. 251 * @param physicalPixelDisplaySizeRatio the display size ratio based on stable display size. 252 */ Parser(float stableDensity, int physicalDisplayWidth, int physicalDisplayHeight, float physicalPixelDisplaySizeRatio)253 Parser(float stableDensity, int physicalDisplayWidth, int physicalDisplayHeight, 254 float physicalPixelDisplaySizeRatio) { 255 mStableDensity = stableDensity; 256 mPhysicalDisplayWidth = physicalDisplayWidth; 257 mPhysicalDisplayHeight = physicalDisplayHeight; 258 mPhysicalPixelDisplaySizeRatio = physicalPixelDisplaySizeRatio; 259 mMatrix = new Matrix(); 260 mIsShortEdgeOnTop = mPhysicalDisplayWidth < mPhysicalDisplayHeight; 261 } 262 263 private void computeBoundsRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) { 264 mTmpRectF.setEmpty(); 265 p.computeBounds(mTmpRectF, false /* unused */); 266 mTmpRectF.round(inoutRect); 267 inoutRegion.op(inoutRect, Region.Op.UNION); 268 } 269 270 private void resetStatus(StringBuilder sb) { 271 sb.setLength(0); 272 mPositionFromBottom = false; 273 mPositionFromLeft = false; 274 mPositionFromRight = false; 275 mPositionFromCenterVertical = false; 276 277 mBindLeftCutout = false; 278 mBindRightCutout = false; 279 mBindBottomCutout = false; 280 } 281 282 private void translateMatrix() { 283 final float offsetX; 284 if (mPositionFromRight) { 285 offsetX = mPhysicalDisplayWidth; 286 } else if (mPositionFromLeft) { 287 offsetX = 0; 288 } else { 289 offsetX = mPhysicalDisplayWidth / 2f; 290 } 291 292 final float offsetY; 293 if (mPositionFromBottom) { 294 offsetY = mPhysicalDisplayHeight; 295 } else if (mPositionFromCenterVertical) { 296 offsetY = mPhysicalDisplayHeight / 2f; 297 } else { 298 offsetY = 0; 299 } 300 301 mMatrix.reset(); 302 if (mInDp) { 303 mMatrix.postScale(mStableDensity, mStableDensity); 304 } 305 mMatrix.postTranslate(offsetX, offsetY); 306 } 307 308 private int computeSafeInsets(int gravity, Rect rect) { 309 if (gravity == LEFT && rect.right > 0 && rect.right < mPhysicalDisplayWidth) { 310 return rect.right; 311 } else if (gravity == TOP && rect.bottom > 0 && rect.bottom < mPhysicalDisplayHeight) { 312 return rect.bottom; 313 } else if (gravity == RIGHT && rect.left > 0 && rect.left < mPhysicalDisplayWidth) { 314 return mPhysicalDisplayWidth - rect.left; 315 } else if (gravity == BOTTOM && rect.top > 0 && rect.top < mPhysicalDisplayHeight) { 316 return mPhysicalDisplayHeight - rect.top; 317 } 318 return 0; 319 } 320 321 private void setSafeInset(int gravity, int inset) { 322 if (gravity == LEFT) { 323 mSafeInsetLeft = inset; 324 } else if (gravity == TOP) { 325 mSafeInsetTop = inset; 326 } else if (gravity == RIGHT) { 327 mSafeInsetRight = inset; 328 } else if (gravity == BOTTOM) { 329 mSafeInsetBottom = inset; 330 } 331 } 332 333 private int getSafeInset(int gravity) { 334 if (gravity == LEFT) { 335 return mSafeInsetLeft; 336 } else if (gravity == TOP) { 337 return mSafeInsetTop; 338 } else if (gravity == RIGHT) { 339 return mSafeInsetRight; 340 } else if (gravity == BOTTOM) { 341 return mSafeInsetBottom; 342 } 343 return 0; 344 } 345 346 @NonNull 347 private Rect onSetEdgeCutout(boolean isStart, boolean isShortEdge, @NonNull Rect rect) { 348 final int gravity; 349 if (isShortEdge) { 350 gravity = decideWhichEdge(mIsShortEdgeOnTop, true, isStart); 351 } else { 352 if (mIsTouchShortEdgeStart && mIsTouchShortEdgeEnd) { 353 gravity = decideWhichEdge(mIsShortEdgeOnTop, false, isStart); 354 } else if (mIsTouchShortEdgeStart || mIsTouchShortEdgeEnd) { 355 gravity = decideWhichEdge(mIsShortEdgeOnTop, true, 356 mIsCloserToStartSide); 357 } else { 358 gravity = decideWhichEdge(mIsShortEdgeOnTop, isShortEdge, isStart); 359 } 360 } 361 362 int oldSafeInset = getSafeInset(gravity); 363 int newSafeInset = computeSafeInsets(gravity, rect); 364 if (oldSafeInset < newSafeInset) { 365 setSafeInset(gravity, newSafeInset); 366 } 367 368 return new Rect(rect); 369 } 370 371 private void setEdgeCutout(@NonNull Path newPath) { 372 if (mBindRightCutout && mRightBound == null) { 373 mRightBound = onSetEdgeCutout(false, !mIsShortEdgeOnTop, mTmpRect); 374 } else if (mBindLeftCutout && mLeftBound == null) { 375 mLeftBound = onSetEdgeCutout(true, !mIsShortEdgeOnTop, mTmpRect); 376 } else if (mBindBottomCutout && mBottomBound == null) { 377 mBottomBound = onSetEdgeCutout(false, mIsShortEdgeOnTop, mTmpRect); 378 } else if (!(mBindBottomCutout || mBindLeftCutout || mBindRightCutout) 379 && mTopBound == null) { 380 mTopBound = onSetEdgeCutout(true, mIsShortEdgeOnTop, mTmpRect); 381 } else { 382 return; 383 } 384 385 if (mPath != null) { 386 mPath.addPath(newPath); 387 } else { 388 mPath = newPath; 389 } 390 } 391 392 private void parseSvgPathSpec(Region region, String spec) { 393 if (TextUtils.length(spec) < MINIMAL_ACCEPTABLE_PATH_LENGTH) { 394 Log.e(TAG, "According to SVG definition, it shouldn't happen"); 395 return; 396 } 397 translateMatrix(); 398 399 final Path newPath = PathParser.createPathFromPathData(spec); 400 newPath.transform(mMatrix); 401 computeBoundsRectAndAddToRegion(newPath, region, mTmpRect); 402 403 if (DEBUG) { 404 Log.d(TAG, String.format(Locale.ENGLISH, 405 "hasLeft = %b, hasRight = %b, hasBottom = %b, hasCenterVertical = %b", 406 mPositionFromLeft, mPositionFromRight, mPositionFromBottom, 407 mPositionFromCenterVertical)); 408 Log.d(TAG, "region = " + region); 409 Log.d(TAG, "spec = \"" + spec + "\" rect = " + mTmpRect + " newPath = " + newPath); 410 } 411 412 if (mTmpRect.isEmpty()) { 413 return; 414 } 415 416 if (mIsShortEdgeOnTop) { 417 mIsTouchShortEdgeStart = mTmpRect.top <= 0; 418 mIsTouchShortEdgeEnd = mTmpRect.bottom >= mPhysicalDisplayHeight; 419 mIsCloserToStartSide = mTmpRect.centerY() < mPhysicalDisplayHeight / 2; 420 } else { 421 mIsTouchShortEdgeStart = mTmpRect.left <= 0; 422 mIsTouchShortEdgeEnd = mTmpRect.right >= mPhysicalDisplayWidth; 423 mIsCloserToStartSide = mTmpRect.centerX() < mPhysicalDisplayWidth / 2; 424 } 425 426 setEdgeCutout(newPath); 427 } 428 429 private void parseSpecWithoutDp(@NonNull String specWithoutDp) { 430 Region region = Region.obtain(); 431 StringBuilder sb = null; 432 int currentIndex = 0; 433 int lastIndex = 0; 434 while ((currentIndex = specWithoutDp.indexOf(MARKER_START_CHAR, lastIndex)) != -1) { 435 if (sb == null) { 436 sb = new StringBuilder(specWithoutDp.length()); 437 } 438 sb.append(specWithoutDp, lastIndex, currentIndex); 439 440 if (specWithoutDp.startsWith(LEFT_MARKER, currentIndex)) { 441 if (!mPositionFromRight) { 442 mPositionFromLeft = true; 443 } 444 currentIndex += LEFT_MARKER.length(); 445 } else if (specWithoutDp.startsWith(RIGHT_MARKER, currentIndex)) { 446 if (!mPositionFromLeft) { 447 mPositionFromRight = true; 448 } 449 currentIndex += RIGHT_MARKER.length(); 450 } else if (specWithoutDp.startsWith(BOTTOM_MARKER, currentIndex)) { 451 parseSvgPathSpec(region, sb.toString()); 452 currentIndex += BOTTOM_MARKER.length(); 453 454 /* prepare to parse the rest path */ 455 resetStatus(sb); 456 mBindBottomCutout = true; 457 mPositionFromBottom = true; 458 } else if (specWithoutDp.startsWith(CENTER_VERTICAL_MARKER, currentIndex)) { 459 parseSvgPathSpec(region, sb.toString()); 460 currentIndex += CENTER_VERTICAL_MARKER.length(); 461 462 /* prepare to parse the rest path */ 463 resetStatus(sb); 464 mPositionFromCenterVertical = true; 465 } else if (specWithoutDp.startsWith(CUTOUT_MARKER, currentIndex)) { 466 parseSvgPathSpec(region, sb.toString()); 467 currentIndex += CUTOUT_MARKER.length(); 468 469 /* prepare to parse the rest path */ 470 resetStatus(sb); 471 } else if (specWithoutDp.startsWith(BIND_LEFT_CUTOUT_MARKER, currentIndex)) { 472 mBindBottomCutout = false; 473 mBindRightCutout = false; 474 mBindLeftCutout = true; 475 476 currentIndex += BIND_LEFT_CUTOUT_MARKER.length(); 477 } else if (specWithoutDp.startsWith(BIND_RIGHT_CUTOUT_MARKER, currentIndex)) { 478 mBindBottomCutout = false; 479 mBindLeftCutout = false; 480 mBindRightCutout = true; 481 482 currentIndex += BIND_RIGHT_CUTOUT_MARKER.length(); 483 } else { 484 currentIndex += 1; 485 } 486 487 lastIndex = currentIndex; 488 } 489 490 if (sb == null) { 491 parseSvgPathSpec(region, specWithoutDp); 492 } else { 493 sb.append(specWithoutDp, lastIndex, specWithoutDp.length()); 494 parseSvgPathSpec(region, sb.toString()); 495 } 496 497 region.recycle(); 498 } 499 500 /** 501 * To parse specification string as the CutoutSpecification. 502 * 503 * @param originalSpec the specification string 504 * @return the CutoutSpecification instance 505 */ 506 @VisibleForTesting(visibility = PACKAGE) 507 public CutoutSpecification parse(@NonNull String originalSpec) { 508 Objects.requireNonNull(originalSpec); 509 510 int dpIndex = originalSpec.lastIndexOf(DP_MARKER); 511 mInDp = (dpIndex != -1); 512 final String spec; 513 if (dpIndex != -1) { 514 spec = originalSpec.substring(0, dpIndex) 515 + originalSpec.substring(dpIndex + DP_MARKER.length()); 516 } else { 517 spec = originalSpec; 518 } 519 520 parseSpecWithoutDp(spec); 521 mInsets = Insets.of(mSafeInsetLeft, mSafeInsetTop, mSafeInsetRight, mSafeInsetBottom); 522 return new CutoutSpecification(this); 523 } 524 } 525 } 526