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 final 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 if (DEBUG) { 108 Log.d(TAG, String.format(Locale.ENGLISH, 109 "left cutout = %s, top cutout = %s, right cutout = %s, bottom cutout = %s", 110 mLeftBound != null ? mLeftBound.toString() : "", 111 mTopBound != null ? mTopBound.toString() : "", 112 mRightBound != null ? mRightBound.toString() : "", 113 mBottomBound != null ? mBottomBound.toString() : "")); 114 } 115 } 116 117 @VisibleForTesting(visibility = PACKAGE) 118 @Nullable getPath()119 public Path getPath() { 120 return mPath; 121 } 122 123 @VisibleForTesting(visibility = PACKAGE) 124 @Nullable getLeftBound()125 public Rect getLeftBound() { 126 return mLeftBound; 127 } 128 129 @VisibleForTesting(visibility = PACKAGE) 130 @Nullable getTopBound()131 public Rect getTopBound() { 132 return mTopBound; 133 } 134 135 @VisibleForTesting(visibility = PACKAGE) 136 @Nullable getRightBound()137 public Rect getRightBound() { 138 return mRightBound; 139 } 140 141 @VisibleForTesting(visibility = PACKAGE) 142 @Nullable getBottomBound()143 public Rect getBottomBound() { 144 return mBottomBound; 145 } 146 147 /** 148 * To count the safe inset according to the cutout bounds and waterfall inset. 149 * 150 * @return the safe inset. 151 */ 152 @VisibleForTesting(visibility = PACKAGE) 153 @NonNull getSafeInset()154 public Rect getSafeInset() { 155 return mInsets.toRect(); 156 } 157 decideWhichEdge(boolean isTopEdgeShortEdge, boolean isShortEdge, boolean isStart)158 private static int decideWhichEdge(boolean isTopEdgeShortEdge, 159 boolean isShortEdge, boolean isStart) { 160 return (isTopEdgeShortEdge) 161 ? ((isShortEdge) ? (isStart ? TOP : BOTTOM) : (isStart ? LEFT : RIGHT)) 162 : ((isShortEdge) ? (isStart ? LEFT : RIGHT) : (isStart ? TOP : BOTTOM)); 163 } 164 165 /** 166 * The CutoutSpecification Parser. 167 */ 168 @VisibleForTesting(visibility = PACKAGE) 169 public static class Parser { 170 private final boolean mIsShortEdgeOnTop; 171 private final float mDensity; 172 private final int mDisplayWidth; 173 private final int mDisplayHeight; 174 private final Matrix mMatrix; 175 private Insets mInsets; 176 private int mSafeInsetLeft; 177 private int mSafeInsetTop; 178 private int mSafeInsetRight; 179 private int mSafeInsetBottom; 180 181 private final Rect mTmpRect = new Rect(); 182 private final RectF mTmpRectF = new RectF(); 183 184 private boolean mInDp; 185 186 private Path mPath; 187 private Rect mLeftBound; 188 private Rect mTopBound; 189 private Rect mRightBound; 190 private Rect mBottomBound; 191 192 private boolean mPositionFromLeft = false; 193 private boolean mPositionFromRight = false; 194 private boolean mPositionFromBottom = false; 195 private boolean mPositionFromCenterVertical = false; 196 197 private boolean mBindLeftCutout = false; 198 private boolean mBindRightCutout = false; 199 private boolean mBindBottomCutout = false; 200 201 private boolean mIsTouchShortEdgeStart; 202 private boolean mIsTouchShortEdgeEnd; 203 private boolean mIsCloserToStartSide; 204 205 /** 206 * The constructor of the CutoutSpecification parser to parse the specification of cutout. 207 * @param density the display density. 208 * @param displayWidth the display width. 209 * @param displayHeight the display height. 210 */ 211 @VisibleForTesting(visibility = PACKAGE) Parser(float density, int displayWidth, int displayHeight)212 public Parser(float density, int displayWidth, int displayHeight) { 213 mDensity = density; 214 mDisplayWidth = displayWidth; 215 mDisplayHeight = displayHeight; 216 mMatrix = new Matrix(); 217 mIsShortEdgeOnTop = mDisplayWidth < mDisplayHeight; 218 } 219 220 private void computeBoundsRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) { 221 mTmpRectF.setEmpty(); 222 p.computeBounds(mTmpRectF, false /* unused */); 223 mTmpRectF.round(inoutRect); 224 inoutRegion.op(inoutRect, Region.Op.UNION); 225 } 226 227 private void resetStatus(StringBuilder sb) { 228 sb.setLength(0); 229 mPositionFromBottom = false; 230 mPositionFromLeft = false; 231 mPositionFromRight = false; 232 mPositionFromCenterVertical = false; 233 234 mBindLeftCutout = false; 235 mBindRightCutout = false; 236 mBindBottomCutout = false; 237 } 238 239 private void translateMatrix() { 240 final float offsetX; 241 if (mPositionFromRight) { 242 offsetX = mDisplayWidth; 243 } else if (mPositionFromLeft) { 244 offsetX = 0; 245 } else { 246 offsetX = mDisplayWidth / 2f; 247 } 248 249 final float offsetY; 250 if (mPositionFromBottom) { 251 offsetY = mDisplayHeight; 252 } else if (mPositionFromCenterVertical) { 253 offsetY = mDisplayHeight / 2f; 254 } else { 255 offsetY = 0; 256 } 257 258 mMatrix.reset(); 259 if (mInDp) { 260 mMatrix.postScale(mDensity, mDensity); 261 } 262 mMatrix.postTranslate(offsetX, offsetY); 263 } 264 265 private int computeSafeInsets(int gravity, Rect rect) { 266 if (gravity == LEFT && rect.right > 0 && rect.right < mDisplayWidth) { 267 return rect.right; 268 } else if (gravity == TOP && rect.bottom > 0 && rect.bottom < mDisplayHeight) { 269 return rect.bottom; 270 } else if (gravity == RIGHT && rect.left > 0 && rect.left < mDisplayWidth) { 271 return mDisplayWidth - rect.left; 272 } else if (gravity == BOTTOM && rect.top > 0 && rect.top < mDisplayHeight) { 273 return mDisplayHeight - rect.top; 274 } 275 return 0; 276 } 277 278 private void setSafeInset(int gravity, int inset) { 279 if (gravity == LEFT) { 280 mSafeInsetLeft = inset; 281 } else if (gravity == TOP) { 282 mSafeInsetTop = inset; 283 } else if (gravity == RIGHT) { 284 mSafeInsetRight = inset; 285 } else if (gravity == BOTTOM) { 286 mSafeInsetBottom = inset; 287 } 288 } 289 290 private int getSafeInset(int gravity) { 291 if (gravity == LEFT) { 292 return mSafeInsetLeft; 293 } else if (gravity == TOP) { 294 return mSafeInsetTop; 295 } else if (gravity == RIGHT) { 296 return mSafeInsetRight; 297 } else if (gravity == BOTTOM) { 298 return mSafeInsetBottom; 299 } 300 return 0; 301 } 302 303 @NonNull 304 private Rect onSetEdgeCutout(boolean isStart, boolean isShortEdge, @NonNull Rect rect) { 305 final int gravity; 306 if (isShortEdge) { 307 gravity = decideWhichEdge(mIsShortEdgeOnTop, true, isStart); 308 } else { 309 if (mIsTouchShortEdgeStart && mIsTouchShortEdgeEnd) { 310 gravity = decideWhichEdge(mIsShortEdgeOnTop, false, isStart); 311 } else if (mIsTouchShortEdgeStart || mIsTouchShortEdgeEnd) { 312 gravity = decideWhichEdge(mIsShortEdgeOnTop, true, 313 mIsCloserToStartSide); 314 } else { 315 gravity = decideWhichEdge(mIsShortEdgeOnTop, isShortEdge, isStart); 316 } 317 } 318 319 int oldSafeInset = getSafeInset(gravity); 320 int newSafeInset = computeSafeInsets(gravity, rect); 321 if (oldSafeInset < newSafeInset) { 322 setSafeInset(gravity, newSafeInset); 323 } 324 325 return new Rect(rect); 326 } 327 328 private void setEdgeCutout(@NonNull Path newPath) { 329 if (mBindRightCutout && mRightBound == null) { 330 mRightBound = onSetEdgeCutout(false, !mIsShortEdgeOnTop, mTmpRect); 331 } else if (mBindLeftCutout && mLeftBound == null) { 332 mLeftBound = onSetEdgeCutout(true, !mIsShortEdgeOnTop, mTmpRect); 333 } else if (mBindBottomCutout && mBottomBound == null) { 334 mBottomBound = onSetEdgeCutout(false, mIsShortEdgeOnTop, mTmpRect); 335 } else if (!(mBindBottomCutout || mBindLeftCutout || mBindRightCutout) 336 && mTopBound == null) { 337 mTopBound = onSetEdgeCutout(true, mIsShortEdgeOnTop, mTmpRect); 338 } else { 339 return; 340 } 341 342 if (mPath != null) { 343 mPath.addPath(newPath); 344 } else { 345 mPath = newPath; 346 } 347 } 348 349 private void parseSvgPathSpec(Region region, String spec) { 350 if (TextUtils.length(spec) < MINIMAL_ACCEPTABLE_PATH_LENGTH) { 351 Log.e(TAG, "According to SVG definition, it shouldn't happen"); 352 return; 353 } 354 spec.trim(); 355 translateMatrix(); 356 357 final Path newPath = PathParser.createPathFromPathData(spec); 358 newPath.transform(mMatrix); 359 computeBoundsRectAndAddToRegion(newPath, region, mTmpRect); 360 361 if (DEBUG) { 362 Log.d(TAG, String.format(Locale.ENGLISH, 363 "hasLeft = %b, hasRight = %b, hasBottom = %b, hasCenterVertical = %b", 364 mPositionFromLeft, mPositionFromRight, mPositionFromBottom, 365 mPositionFromCenterVertical)); 366 Log.d(TAG, "region = " + region); 367 Log.d(TAG, "spec = \"" + spec + "\" rect = " + mTmpRect + " newPath = " + newPath); 368 } 369 370 if (mTmpRect.isEmpty()) { 371 return; 372 } 373 374 if (mIsShortEdgeOnTop) { 375 mIsTouchShortEdgeStart = mTmpRect.top <= 0; 376 mIsTouchShortEdgeEnd = mTmpRect.bottom >= mDisplayHeight; 377 mIsCloserToStartSide = mTmpRect.centerY() < mDisplayHeight / 2; 378 } else { 379 mIsTouchShortEdgeStart = mTmpRect.left <= 0; 380 mIsTouchShortEdgeEnd = mTmpRect.right >= mDisplayWidth; 381 mIsCloserToStartSide = mTmpRect.centerX() < mDisplayWidth / 2; 382 } 383 384 setEdgeCutout(newPath); 385 } 386 387 private void parseSpecWithoutDp(@NonNull String specWithoutDp) { 388 Region region = Region.obtain(); 389 StringBuilder sb = null; 390 int currentIndex = 0; 391 int lastIndex = 0; 392 while ((currentIndex = specWithoutDp.indexOf(MARKER_START_CHAR, lastIndex)) != -1) { 393 if (sb == null) { 394 sb = new StringBuilder(specWithoutDp.length()); 395 } 396 sb.append(specWithoutDp, lastIndex, currentIndex); 397 398 if (specWithoutDp.startsWith(LEFT_MARKER, currentIndex)) { 399 if (!mPositionFromRight) { 400 mPositionFromLeft = true; 401 } 402 currentIndex += LEFT_MARKER.length(); 403 } else if (specWithoutDp.startsWith(RIGHT_MARKER, currentIndex)) { 404 if (!mPositionFromLeft) { 405 mPositionFromRight = true; 406 } 407 currentIndex += RIGHT_MARKER.length(); 408 } else if (specWithoutDp.startsWith(BOTTOM_MARKER, currentIndex)) { 409 parseSvgPathSpec(region, sb.toString()); 410 currentIndex += BOTTOM_MARKER.length(); 411 412 /* prepare to parse the rest path */ 413 resetStatus(sb); 414 mBindBottomCutout = true; 415 mPositionFromBottom = true; 416 } else if (specWithoutDp.startsWith(CENTER_VERTICAL_MARKER, currentIndex)) { 417 parseSvgPathSpec(region, sb.toString()); 418 currentIndex += CENTER_VERTICAL_MARKER.length(); 419 420 /* prepare to parse the rest path */ 421 resetStatus(sb); 422 mPositionFromCenterVertical = true; 423 } else if (specWithoutDp.startsWith(CUTOUT_MARKER, currentIndex)) { 424 parseSvgPathSpec(region, sb.toString()); 425 currentIndex += CUTOUT_MARKER.length(); 426 427 /* prepare to parse the rest path */ 428 resetStatus(sb); 429 } else if (specWithoutDp.startsWith(BIND_LEFT_CUTOUT_MARKER, currentIndex)) { 430 mBindBottomCutout = false; 431 mBindRightCutout = false; 432 mBindLeftCutout = true; 433 434 currentIndex += BIND_LEFT_CUTOUT_MARKER.length(); 435 } else if (specWithoutDp.startsWith(BIND_RIGHT_CUTOUT_MARKER, currentIndex)) { 436 mBindBottomCutout = false; 437 mBindLeftCutout = false; 438 mBindRightCutout = true; 439 440 currentIndex += BIND_RIGHT_CUTOUT_MARKER.length(); 441 } else { 442 currentIndex += 1; 443 } 444 445 lastIndex = currentIndex; 446 } 447 448 if (sb == null) { 449 parseSvgPathSpec(region, specWithoutDp); 450 } else { 451 sb.append(specWithoutDp, lastIndex, specWithoutDp.length()); 452 parseSvgPathSpec(region, sb.toString()); 453 } 454 455 region.recycle(); 456 } 457 458 /** 459 * To parse specification string as the CutoutSpecification. 460 * 461 * @param originalSpec the specification string 462 * @return the CutoutSpecification instance 463 */ 464 @VisibleForTesting(visibility = PACKAGE) 465 public CutoutSpecification parse(@NonNull String originalSpec) { 466 Objects.requireNonNull(originalSpec); 467 468 int dpIndex = originalSpec.lastIndexOf(DP_MARKER); 469 mInDp = (dpIndex != -1); 470 final String spec; 471 if (dpIndex != -1) { 472 spec = originalSpec.substring(0, dpIndex) 473 + originalSpec.substring(dpIndex + DP_MARKER.length()); 474 } else { 475 spec = originalSpec; 476 } 477 478 parseSpecWithoutDp(spec); 479 480 mInsets = Insets.of(mSafeInsetLeft, mSafeInsetTop, mSafeInsetRight, mSafeInsetBottom); 481 return new CutoutSpecification(this); 482 } 483 } 484 } 485